首頁 > 軟體

如何用JGit管理Git子模組

2020-06-16 18:01:16

對於一個較大的Git工程,你可能會想在多個倉庫之間共用程式碼,不管這些程式碼是在多個不同產品間使用的專案共用庫或是一些模板。Git通過子模組來實現這樣的需求。子模組允許將其他程式碼倉庫的克隆作為子目錄放到一個父倉庫(有時候也稱為父專案)中。一個子模組也是一個獨立的倉庫,你可以像其他倉庫一樣執行commit,branch,rebase等等操作。

JGit提供了實現大部分Git子模組命令的API。我將在這兒給大家介紹這些API。

設定

本文中用到的程式碼片段將作為學習測試程式。簡單的測試程式有助於理解第三方庫是如何工作,以及如何使用新的API。你可以將這些測試程式看做是可控制的試驗,幫助你更加直觀地發現第三方程式碼是如何執行的。

除此之外,如果你保持編寫測試程式,可以幫助你檢驗第三方程式碼的新版本。如果你的測試程式涵蓋了如何呼叫這些庫,那麼第三方程式碼中不相容的修改將會儘早展現出來。

回到之前的話題,所有的測試程式共用同一個設定,詳細資訊請檢視原始碼。現在有一個空的倉庫,叫parent,以及另一個倉庫叫library。測試程式中,library將會作為子模組新增到parent倉庫中。library倉庫初始化提交了一個readme.txt檔案。測試程式中有一個setUp方法,用來建立這兩個倉庫,如下所示:

1
Git git = Git.init().setDirectory( "/tmp/path/to/repo" ).call();

這兩個倉庫用型別為Git的parent和library變數表示。該類封裝了一個倉庫並允許存取JGit的所有可用指令。就如較早之前我在這裡中提到,每個Commnad類對應於一條原生的Git pocelain指令。呼叫一個指令需要用到生成器模式。舉個例子,執行Git.commit()的結果實際上相當於一個CommitCommand。你可以提供一些必要的引數去呼叫它的call()方法,從而執行相應的指令。

新增一個子模組

第一步當然是在一個已有的倉庫新增子模組。通過上面提到的setUp步驟,library倉庫應當作為子模組新增到parent倉庫的modules/library目錄下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testAddSubmodule() throws Exception {
  String uri
    = library.getRepository().getDirectory().getCanonicalPath();
  SubmoduleAddCommand addCommand = parent.submoduleAdd();
  addCommand.setURI( uri );
  addCommand.setPath( "modules/library" );
  Repository repository = addCommand.call();
  repository.close();
 
  F‌ile workDir = parent.getRepository().getWorkTree();
  F‌ile readme = new F‌ile( workDir, "modules/library/readme.txt" );
  F‌ile gitmodules = new F‌ile( workDir, ".gitmodules" );
  assertTrue( readme.isF‌ile() );
  assertTrue( gitmodules.isF‌ile() );
}

SubmoduleAddCommand物件需要知道兩件事,第一是子模組從哪裡克隆而來,第二是它應該存放在哪裡。URI屬性表示倉庫庫的克隆地址,這個克隆地址將會傳遞給clone命令。path屬性則指定了相對於parent倉庫根工作目錄的路徑,子模組將被存放在這個路徑。這個指令執行之後,parent倉庫的工作目錄將會變成這樣:

library倉庫存放在modules/library目錄下,而且它的工作目錄樹被檢出。call()方法返回一個Repository物件,你可以把它當做一個常規的倉庫來使用。這也意味著,你必須在程式中明確顯式地關閉返回的倉庫,以避免檔案控制代碼洩露。

從上圖我們可以看到,SubmoduleAddCommand做了一件事,它在parent倉庫的根工作目錄下建立了一個.git模組檔案,並把它新增到索引中。

1
2
3
[submodule "modules/library"]
path = modules/library
url = git@example.com:path/to/lib.git

如果你開啟過Git的組態檔,你會發現以上句法。這個檔案列出了當前倉庫的所有子模組。對於每個模組,檔案中列出了它倉庫URL地址以及本地路徑。一旦commit並push了這個檔案,克隆這個倉庫的一方就知道哪裡可以獲取相應的子模組(稍後會詳細講解)。

列出子模組

當我們新增了一個子模組之後,我們可以會想知道,它是否對於父倉庫來說是可知的。第一項測試中我們做了一個基礎的檢測,驗證了某些檔案和目錄的存在。我們也可以使用一個API來列出一個倉庫的子模組,如下所示:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testListSubmodules() throws Exception {
  addLibrarySubmodule();
 
  Map<String,SubmoduleStatus> submodules
    = parent.submoduleStatus().call();
 
  assertEquals( 1, submodules.size() );
  SubmoduleStatus status = submodules.get( "modules/library" );
  assertEquals( INITIALIZED, status.getType() );
}

SubmoduleStatus命令返回了一個子模組的Map集合,其中鍵是子模組的路徑,值是這個模組的狀態值。通過以上程式碼我們能夠驗證子模組確實已經新增進去,而且它的狀態是INITIALIZED的。這個命令還允許新增一個或多個路徑來限制子模組狀態。

說到狀態,JGit的StatusCommand並非原生的Git指令。如果在執行指令時新增選項‐‐ignore-submodules=dirty,那麼所有對子模組工作目錄的修改都會被忽略。

更新子模組

子模組通常指向他們所在的倉庫的一次特殊的提交。如果之後有人克隆了父倉庫,他們也會獲得與之完全相同的子模組狀態,即便子模組的上游有新的提交。

為了修改子模組,你像一下程式碼一樣明確地對其進行更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testUpdateSubmodule() throws Exception {
  addLibrarySubmodule();
  ObjectId newHead = library.commit().setMessage( "msg" ).call();
 
  File workDir = parent.getRepository().getWorkTree();
  Git libModule = Git.open( new F‌ile( workDir, "modules/library" ) );
  libModule.pull().call();
  libModule.close();
  parent.add().addF‌ilepattern( "modules/library" ).call();
  parent.commit().setMessage( "Update submodule" ).call();
 
  assertEquals( newHead, getSubmoduleHead( "modules/library" ) );
}

這個較長的程式碼片段中,首先第一件事就是提交一些東西到library倉庫中(第四行),接著將子模組更新到最近的一次提交。

為了讓這種更新持久化儲存下來,子模組必須被提交(第10,11行)。這次提交在子模組的名下(例子中是modules/library)儲存了此次更新的commit-id。最後,通常需要將修改push上去,使得他們對其他倉庫可用。

在父倉庫中更新對子模組的修改

將上游的提交拉取到父倉庫中也會修改子模組的設定。然而子模組本身並不會自動得到更新。

SubmoduleUpdateCommand就是用來解決這個問題。使用這個命令並不需要指定其他引數,它會更新所有已註冊的子模組。該命令會克隆缺失的子模組並檢出其設定中指定的提交。就如其他子模組命令一樣,這裡也有一個addPath()方法,以保證只更新給定路徑下的子模組。

克隆一個包含子模組的倉庫

此時你可能已經掌握一個規律,所有對子模組的操作都是手動的。克隆一個包含子模組設定的倉庫並不會預設克隆它的子模組。但是,CloneCommand命令有一個cloneSubmodules的屬性,如果設定為true,那麼將會克隆所有設定的子模組。從內部看,在對父倉庫進行克隆之後,SubmoduleInitCommand和SubmoduleUpdateCommand命令會被遞回地執行,並且父倉庫的工作目錄會被檢出。

移除一個子模組

如果要移除一個子模組,你會希望可以這樣寫:

1
git.submoduleRm().setPath( ... ).call();

但是很不幸,不管是原生的Git或者JGit都沒有提供內建的移除子模組的指令,希望將來會新增這樣的指令,在這之前,我們必須手動去移除子模組。如果你捲動到removeSubmodule()方法你會發現這並不是一件複雜的事。

首先,各個子模組會從.gitsubmodules和.git/config組態檔中移除。其次,子模組的入口會從索引中被移除。最後,.gitsubmodules檔案以及索引的修改會被提交,並且子模組的內容會從工作目錄中刪除。

遍歷子模組

原生的Git提供了git submodule foreach命令為每個子模組執行一個shell指令。JGit並沒有直接支援這樣的指令,而是提供了SubmoduleWalk。該類可以用來疊代倉庫中子模組。以下範例程式實現了為所有子模組拉取上游的提交。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testSubmoduleWalk() throws Exception {
  addLibrarySubmodule();
 
  int submoduleCount = 0;
  Repository parentRepository = parent.getRepository();
  SubmoduleWalk walk = SubmoduleWalk.forIndex( parentRepository );
  while( walk.next() ) {
    Repository submoduleRepository = walk.getRepository();
    Git.wrap( submoduleRepository ).fetch().call();
    submoduleRepository.close();
    submoduleCount++;
  }
  walk.release();
 
  assertEquals( 1, submoduleCount );
}

通過next()方法walk物件可以指向下一個子模組,如果沒有更多的子模組,該方法會返回false。使用SubmoduleWalk時,通過呼叫release()方法可以釋放子模組相關的資源。再次強調,如果你獲得一個子模組的倉庫範例可別忘了關閉它。

SubmoduleWalk也可以用來獲取子模組的詳細資訊。通過它的大部分getter方法可以存取到當前子模組的屬性,諸如path,head,remote URL等等。

同步遠端URL

從上面我們知道子模組的設定儲存在父倉庫根工作目錄下的.gitsubmodules檔案中。而至少,在.git/config檔案中,我們可以重寫覆蓋子模組的遠端URL。對於每個子模組,它們本身都有一個組態檔。那麼反過來,每個子模組可以有另一個遠端URL。SubmoduleSyncCommand命令可以用來將所有遠端URL重置為.gitmodules中的設定。

綜上所述,JGit對子模組的支援幾乎與原生的Git一致。大部分Git指令都在JGit中實現了,或可以通過一些途徑進行模擬。如果你發現一些操作缺失或實現不了,可以去友好且幫得上忙的JGit社群去尋求幫助。

本文永久更新連結地址http://www.linuxidc.com/Linux/2015-04/116826.htm


IT145.com E-mail:sddin#qq.com