凌云的博客

行胜于言

跟我一起学Git (十二) 合并项目

分类:Git| 发布时间:2014-05-06 07:13:52


本文主要介绍了如何组织需要依赖其他项目的代码库。

旧方法:部分签出

这种方法在CVS或Subversion中很流行。 简单来说,就是将所有子项目放到一个代码库中, 然后根据工作需要只签出需要的部分子项目。 不过这种方法不适用于Git,目前的Git架构不支持部分签出。

显而易见的方法:导入代码到你的项目中

最简单的方法是将需要的库复制到你的项目,然后提交。

这种方法有以下优点:

  • 你不会使用到错误版本的库
  • 简单易懂
  • 无论这个库是有Git管理还是其他的版本管理系统管理又或者完全没有版本管理系统,你需要做的事都一样
  • 你的代码库是自包含的,也就是说使用git clone克隆你的代码库就包含了所有需要的东西
  • 容易在你的代码库中对这个库进行patch
  • 当你对代码库创建分支时,也会对这个库创建分支
  • 如果你使用subtree合并策略更新这个库会变得很简单

不幸的是,这种方法有如下缺点:

  • 每个导入这个库的程序都会有这个库的文件,当一个软件中多个子项目都依赖同一个库时会导致重复
  • 当你对这个库作出了改变而你想共享这些改变的唯一方法是创建diff然后在这个库的代码库中进行应用,这无疑是乏味的

通过复制导入子项目

这种方法很简单就不演示了

通过git pull -s subtree导入子项目

为了演示,首先创建一个名为myapp的代码库

$ cd myapp/

$ git init
Initialized empty Git repository in /tmp/myapp/.git/

$ ls

$ echo hello > hello.txt

$ git add hello.txt

$ git commit -m 'first commit'
[master (root-commit) ab46c32] first commit
1 file changed, 1 insertion(+)
create mode 100644 hello.txt

将v1.6.0的git导入我们的代码库

$ ls
hello.txt
$ mkdir git
$ cd git
$ (cd ~/git.git && git archive v1.6.0) | tar -xf -
$ cd ..
$ ls
git/  hello.txt
$ git add git
$ git commit -m 'imported git v1.6.0'

接下来将Git从开始到v1.6.0的历史导入到我们的代码库

$ git pull -s ours ~/git.git refs/tags/v1.6.0
warning: no common commits
remote: Counting objects: 67034, done.
remote: Compressing objects: 100% (19135/19135), done.
remote: Total 67034 (delta 47938), reused 65706 (delta 46656)
Receiving objects: 100% (67034/67034), 14.33 MiB | 12587 KiB/s, done.
Resolving deltas: 100% (47938/47938), done.
From ~/git.git
* tag               v1.6.0     -> FETCH_HEAD
Merge made by ours.

现在对git子项目作修改

$ cd git
$ echo 'I am a git contributor!' > contribution.txt
$ git add contribution.txt
$ git commit -m 'My first contribution to git'
Created commit 6c9fac5: My first contribution to git
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 git/contribution.txt

最后,更新我们的git到版本v1.6.0.1

$ git pull -s subtree ~/git.git refs/tags/v1.6.0.1
remote: Counting objects: 179, done.
remote: Compressing objects: 100% (72/72), done.
remote: Total 136 (delta 97), reused 100 (delta 61)
Receiving objects: 100% (136/136), 25.24 KiB, done.
Resolving deltas: 100% (97/97), completed with 40 local objects.
From ~/git.git
* tag               v1.6.0.1   -> FETCH_HEAD
Merge made by subtree.

自动化解决方案:用脚本签出子项目

这种方法有以下优点:

  • 子模块不需要在Git中,它可以在任何的版本管理系统,或者只是在tar或zip文件中
  • 主程序的历史不用和子模块的历史混在一起,这还会导致主程序的历史简短
  • 如果你要修改子程序,你只需要在子程序中工作

当然这种方法有以下问题:

  • 向其他人解释如何签出所有子项目会令人厌烦
  • 你需要确保获取每个子项目的正确版本
  • 当你切换到其他分支或者使用了git pull,子项目不会自动更新
  • 当你修改了子项目,你需要记住要在子项目中使用git push
  • 如果你没有权限对子项目进行贡献,你没有办法简单地进行应用程序特有的修改

原生解决方案:gitlinks和git submodule

gitlinks

gitlinks是一个tree对象对commit对象的引用,下面来演示下如何创建gitlinks。

首先来创建一个代码库

$mkdir myapp
$cd myapp/
$git init
Initialized empty Git repository in /private/tmp/myapp/.git/
$echo hello > hello.txt
$git add hello.txt
$git commit -m 'first commit'
[master (root-commit) 804b31a] first commit
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt

接下来,我们导入git项目,我们会直接导入,而不是使用git archive命令

$git clone ~/src/git/ git
Cloning into 'git'...
done.
$cd git/
$git checkout v1.6.0
Note: checking out 'v1.6.0'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b new_branch_name

HEAD is now at ea02eef... GIT 1.6.0
$cd ..
$ls
git       hello.txt
$git add git
$git commit -m 'imported git v1.6.0'
[master 414b04d] imported git v1.6.0
 1 file changed, 1 insertion(+)
 create mode 160000 git

通常来说git add git和git add git/的效果是一样的, 但是当git本身是一个代码库时,这两个命令的效果时不一样的。 git add git表示创建一个gitlink而git add git/表示将git目录的文件都加到当前代码库下。

 $git ls-tree HEAD
 160000 commit ea02eef096d4bfcbb83e76cfab0fcb42dbcad35e     git
 100644 blob ce013625030ba8dba906f756967f9e9ca394464a     hello.txt

git子目录的类型是commit。这里创建了一个gitlink。 Git会将gitlinks当成一个简单的指向其他代码库的指针。 大多数的Git操作,不会解引用gitlinks然后对子模块进行操作,比如:git clone。

 $cd /tmp
 $git clone myapp/ app2/
 Cloning into 'app2'...
 done.
 $cd app2/
 $ls
 git       hello.txt
 $ls git/
 $du git/
 0     git/

git submodule命令

git submodule命令并不是Git核心的一部分,实际上它就是一个成为git-submodule.sh的脚本。 可以通过git submodule update命令来更新子模块,这个命令实际上相当于以下操作:

$cd /tmp/app2/
$git ls-files --stage -- git
160000 ea02eef096d4bfcbb83e76cfab0fcb42dbcad35e 0     git
$rmdir git/
$git clone ~/src/git git
Cloning into 'git'...
done.
$cd git/
$git checkout ea02eef
Note: checking out 'ea02eef'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

 git checkout -b new_branch_name

HEAD is now at ea02eef... GIT 1.6.0

不幸的是在使用git submodule update之前需要先做一下配置:

我们需要提供.gitmodules文件:

$cd /tmp/app2/
$cat > .gitmodules << EOF
> [submodule "git"]
>     path = git
>     url = /Users/chenjianlong/src/git
> EOF
(以上内容也可以通过命令git submodule add /Users/chenjianlong/src/git git来实现)

下一步,通过git submodule init来将.gitmodules文件的内容设置到.git/config文件中

$git submodule init
Submodule 'git' (/Users/chenjianlong/src/git) registered for path 'git'
$cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
url = /tmp/myapp/
[branch "master"]
remote = origin
merge = refs/heads/master
[submodule "git"]
url = /Users/chenjianlong/src/git

现在可以用git submodule update命令来更新子模块了

$rm -rf git/
$git submodule update
Cloning into 'git'...
done.
Submodule path 'git': checked out 'ea02eef096d4bfcbb83e76cfab0fcb42dbcad35e'

这是你需要注意的事:

  • 当你使用git pull你需要运行git submodule update来更新你的子模块
  • 当你签出其他分支,你需要调用git submodule update来更新子模块,否则Git会认为你要修改你的子模块到一个“新”的提交。 如果你之后调用git commit -a你会修改了gitlink。小心!
  • 如果你要更新gtilink,你只需要切换子模块到正确的版本然后调用git add添加子模块的目录然后调用git commit提交即可。
  • 如果你在你的分支中更新了gitlink然后调用git pull或者git merge而其他的分支也更新了同一个gitlink。 Git会认为这是一个冲突,然后保留其中一个版本,你需要手动解决这个冲突。

如上可见,git submodule其实是相当复杂的,而且它是一个单独的命令。根据你的实际情况来决定要不要使用它。