對於一個較大的Git工程,你可能會想在多個倉庫之間共享代碼,不管這些代碼是在多個不同產品間使用的項目共享庫或是一些模板。Git通過子模塊來實現這樣的需求。子模塊允許將其他代碼倉庫的克隆作為子目錄放到一個父倉庫(有時候也稱為父項目)中。一個子模塊也是一個獨立的倉庫,你可以像其他倉庫一樣執行commit,branch,rebase等等操作。
JGit提供了實現大部分Git子模塊命令的API。我將在這兒給大家介紹這些API。
本文中用到的代碼片段將作為學習測試程序。簡單的測試程序有助於理解第三方庫是如何工作,以及如何使用新的API。你可以將這些測試程序看做是可控制的試驗,幫助你更加直觀地發現第三方代碼是如何執行的。
除此之外,如果你保持編寫測試程序,可以幫助你檢驗第三方代碼的新版本。如果你的測試程序涵蓋了如何調用這些庫,那麼第三方代碼中不兼容的修改將會盡早展現出來。
回到之前的話題,所有的測試程序共享同一個設置,詳細信息請查看源代碼。現在有一個空的倉庫,叫parent,以及另一個倉庫叫library。測試程序中,library將會作為子模塊添加到parent倉庫中。library倉庫初始化提交了一個readme.txt文件。測試程序中有一個setUp方法,用來創建這兩個倉庫,如下所示:
1Git 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();
File workDir = parent.getRepository().getWorkTree();
File readme =
new
File( workDir,
"modules/library/readme.txt"
);
File gitmodules =
new
File( workDir,
".gitmodules"
);
assertTrue( readme.isFile() );
assertTrue( gitmodules.isFile() );
}
SubmoduleAddCommand對象需要知道兩件事,第一是子模塊從哪裡克隆而來,第二是它應該存放在哪裡。URI屬性表示倉庫庫的克隆地址,這個克隆地址將會傳遞給clone命令。path屬性則指定了相對於parent倉庫根工作目錄的路徑,子模塊將被存放在這個路徑。這個指令執行之後,parent倉庫的工作目錄將會變成這樣:
library倉庫存放在modules/library目錄下,而且它的工作目錄樹被檢出。call()方法返回一個Repository對象,你可以把它當做一個常規的倉庫來使用。這也意味著,你必須在程序中明確顯式地關閉返回的倉庫,以避免文件句柄洩露。
從上圖我們可以看到,SubmoduleAddCommand做了一件事,它在parent倉庫的根工作目錄下創建了一個.git模塊文件,並把它添加到索引中。
1 2 3[submodule
"modules/library"
]
path = modules
/library
url = [email protected]: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
File( workDir,
"modules/library"
) );
libModule.pull().call();
libModule.close();
parent.add().addFilepattern(
"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命令會被遞歸地執行,並且父倉庫的工作目錄會被檢出。
如果要移除一個子模塊,你會希望可以這樣寫:
1git.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等等。
從上面我們知道子模塊的配置保存在父倉庫根工作目錄下的.gitsubmodules文件中。而至少,在.git/config文件中,我們可以重寫覆蓋子模塊的遠程URL。對於每個子模塊,它們本身都有一個配置文件。那麼反過來,每個子模塊可以有另一個遠程URL。SubmoduleSyncCommand命令可以用來將所有遠程URL重置為.gitmodules中的配置。
綜上所述,JGit對子模塊的支持幾乎與原生的Git一致。大部分Git指令都在JGit中實現了,或可以通過一些途徑進行仿真。如果你發現一些操作缺失或實現不了,可以去友好且幫得上忙的JGit社區去尋求幫助。