git子模块的使用

git子模块的使用,第1张

git子模块可以用于项目包含另一个项目的情况,也许是第三方库或被多个项目引用的基础框架。

此次学习子模块就是因为公司的基础框架在多个不同的项目上的使用,同时又想当在开发项目过程中对基础框架有改动时,其他项目也能同时更新到改动的部分,而子模块允许我们将一个git仓库作为另一个git仓库的子目录,同时还能保持提交独立,正好满足原来设想的需求。

git submodule add https://github.com/yangmin1234/test1.git

当我们运行 git submodule update 从子模块仓库中抓取修改时,Git 将会获得这些改动并 更新子目录中的文件,但是会将子仓库留在一个称作 “游离的 HEAD” 的状态。 这意味着没有本 地工作分支(例如 “master”)跟踪改动。 所以你做的任何改动都不会被跟踪。

git checkout stable进入子模块并检出相应的工作分支

git submodule update --remote从上游拉取数据

git submodule update --remote --merge 从上游拉取数据并合并

发布子模块改动

如果我们在主项目中提交并推送但并不推送子模块上的改动,其他尝试检出我们修改的人会遇到 麻烦,因为他们无法得到依赖的子模块改动。 那些改动只存在于我们本地的拷贝中。

为了确保这不会发生,你可以让 Git 在推送到主项目前检查所有子模块是否已推送。 git push 命令接受可以设置为 check 或 on-demand 的 --recurse-submodules 参数。 如果任何提交的子模块改动没有推送那么 check 选项会直接使 push *** 作失败。

提交主项目时自动检测子模块是否有未提交的改动

git push --recurse-submodules=check

提交主项目时,尝试自动推送一改动的子模块

git push --recurse-submodules=on-demand

子模块技巧

子模块遍历

有一个 foreach 子模块命令,它能在每一个子模块中运行任意命令。 如果项目中包含了大量子模块,这会非常有用。

有用的别名

你可能想为其中一些命令设置别名,因为它们可能会非常长而你又不能 设置选项作为它们的默认选项。

子模块的问题

例如在有子模块的项目中切换分支可能会造成麻烦。 如果你创建一个新分支, 在其中添加一个子模块,之后切换到没有该子模块的分支上时,你仍然会有一个还未跟踪的子模块目录。

子模块 *** 作

git submodule add https://github.com/yangmin1234/test.git #将一个git仓库作为本仓库的一个子目录

git clone https://github.com/yangmin1234/test2.git clone带子模块的项目

git submodule init

git submodule update

git push --recurse-submodules=check 提交主项目时自动检测子模块是否有未提交的改动

git push --recurse-submodules=on-demand 提交主项目时,尝试自动推送一改动的子模块

对于一个较大的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 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社区去寻求帮助。

在这篇文章中,我们来讲解一下 git submodule 的实战用法,包括:

假设我们有主仓库 main-module.git ,远程地址 https://github.com/bitmingw/main-module.git ,目录结构

以及仓库 sub-module.git ,远程地址 https://github.com/bitmingw/sub-module.git ,目录结构

如果想要将仓库 sub-module.git 注册成为主仓库 main-module.git 的一个子模块,可以使用如下指令:

git 会自动从远程服务器 clone sub-module.git ,之后 main-module.git 的目录会变成这个样子

由于添加 git 子模块的 *** 作本身也是一个提交,因此它仅仅对 main-module.git 的当前分支有效,另外的分支不会感知到这一变化。

大多数时候, git 子模块 不是凭空创建的,而是从项目中已有的文件拆分出来的。从已有的文件创建 git 子模块 需要做三件事:首先为拆分出来的文件创建新的 git 仓库 ,然后从主仓库中将独立出去的文件移除,最后再注册 git 子模块

例如,假设 main-module.git 的目录结构如下所示

它有 v1.0 和 v2.0 两个分支,在 v2.0 分支中,我们想让 sub-module 文件夹变成 sub-module.git 子模块。

,为 sub-module 创建一个单独的 git 仓库:

,从 main-module.git 中删除 sub-module 文件夹:

,将 sub-module.git 注册为 main-module.git 的子模块

如果你是主仓库的开发者,你可能不想使用最新版本的子模块,而是使用主仓库中指定版本的子模块,此时可以使用下面的指令:

在使用该指令前,主仓库和子模块的 git status 分别是:

使用了 git submodule update 之后,两个仓库的 git status 信息变成了这个样子:

这样,主仓库的开发者就可以从一个干净的空间开始工作了。

还记得我们是从 v2.0 分支引入子模块的么?假如现在要查看 v1.0 分支,会发生什么呢?

还记得我们是从 v2.0 分支引入子模块的么?假如现在要查看 v1.0 分支,会发生什么呢?

难道有了子模块之后我们就回不去了??情况没有那么糟糕,我们可以通过 git checkout -f v1.0 强行回去。

不过这个时候要注意,由于 sub-module 没有被移除,因此切换到 v1.0 分支 以后,你看到的 sub-module 文件夹依然是个子模块。也就是说,子模块穿越了时空来到了 v1.0 分支 …… 嗯这个行为似乎不是我们期望的那个样子。

如果你真的想回到 v1.0 分支 的非子模块的 sub-module ,那你不得不在切换分支前把这个子模块卸载掉:

如果你想从 v1.0 分支 回到 v2.0 ,也会遇到一些问题。

经过了 git checkout -f v1.0 和 git checkout v2.0 之后,子模块的文件竟然被 git 删掉了。而且这个时候无论是 git submodule update 还是 git checkout -- sub-module 都不好使了。到底应该怎么办呢?

答案是在子模块内部使用 git reset 。

从这一部分的演示可以看出, git submodule 的设计对从已有文件拆分出来的子模块来说是非常糟糕的。或许这会成为大家尽量避免使用 git 子模块 的原因之一吧。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/bake/10971302.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-12
下一篇 2023-05-12

发表评论

登录后才能评论

评论列表(0条)

保存