【转】Git详解之六 Git工具

Git 工具

如今,你已经学习了管理或者维护 Git 仓库,实现代码控制所需的大多很多天常命令和工做流程。你已经完成了跟踪和提交文件的基本任务,而且发挥了暂存区和轻量级的特性分支及合并的威力。javascript

接下来你将领略到一些 Git 能够实现的很是强大的功能,这些功能你可能并不会在平常操做中使用,但在某些时候你也许会须要。html

 

6.1  修订版本(Revision)选择

Git 容许你经过几种方法来指明特定的或者必定范围内的提交。了解它们并非必需的,可是了解一下总没坏处。java

单个修订版本

显然你可使用给出的 SHA-1 值来指明一次提交,不过也有更加人性化的方法来作一样的事。本节概述了指明单个提交的诸多方法。ios

简短的SHA

Git 很聪明,它可以经过你提供的前几个字符来识别你想要的那次提交,只要你提供的那部分 SHA-1 不短于四个字符,而且没有歧义——也就是说,当前仓库中只有一个对象以这段 SHA-1 开头。git

例如,想要查看一次指定的提交,假设你运行 git log 命令并找到你增长了功能的那次提交:github

$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    fixed refs handling, added gc auto, updated tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    added some blame and merge stuff

假设是 1c002dd.... 。若是你想 git show 此次提交,下面的命令是等价的(假设简短的版本没有歧义):web

$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b
$ git show 1c002dd4b536e7479f
$ git show 1c002d

Git 能够为你的 SHA-1 值生成出简短且惟一的缩写。若是你传递 --abbrev-commit 给 git log 命令,输出结果里就会使用简短且惟一的值;它默认使用七个字符来表示,不过必要时为了不 SHA-1 的歧义,会增长字符数:shell

$ git log --abbrev-commit --pretty=oneline
ca82a6d changed the version number
085bb3b removed unnecessary test code
a11bef0 first commit

一般在一个项目中,使用八到十个字符来避免 SHA-1 歧义已经足够了。最大的 Git 项目之一,Linux 内核,目前也只须要最长 40 个字符中的 12 个字符来保持惟一性。数据库

关于 SHA-1 的简短说明

许多人可能会担忧一个问题:在随机的偶然状况下,在他们的仓库里会出现两个具备相同 SHA-1 值的对象。那会怎么样呢?编程

若是你真的向仓库里提交了一个跟以前的某个对象具备相同 SHA-1 值的对象,Git 将会发现以前的那个对象已经存在在 Git 数据库中,并认为它已经被写入了。若是何时你想再次检出那个对象时,你会老是获得先前的那个对象的数据。

不过,你应该了解到,这种状况发生的几率是多么微小。SHA-1 摘要长度是 20 字节,也就是 160 位。为了保证有 50% 的几率出现一次冲突,须要 2^80 个随机哈希的对象(计算冲突机率的公式是p = (n(n-1)/2) * (1/2^160))。2^80 是 1.2 x 10^24,也就是一亿亿亿,那是地球上沙粒总数的 1200 倍。

如今举例说一下怎样才能产生一次 SHA-1 冲突。若是地球上 65 亿的人类都在编程,每人每秒都在产生等价于整个 Linux 内核历史(一百万个 Git 对象)的代码,并将之提交到一个巨大的 Git 仓库里面,那将花费 5 年的时间才会产生足够的对象,使其拥有 50% 的几率产生一次 SHA-1 对象冲突。这要比你编程团队的成员同一个晚上在互不相干的意外中被狼袭击并杀死的机率还要小。

分支引用

指明一次提交的最直接的方法要求有一个指向它的分支引用。这样,你就能够在任何须要一个提交对象或者 SHA-1 值的 Git 命令中使用该分支名称了。若是你想要显示一个分支的最后一次提交的对象,例如假设topic1 分支指向 ca82a6d,那么下面的命令是等价的:

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

若是你想知道某个分支指向哪一个特定的 SHA,或者想看任何一个例子中被简写的 SHA-1,你可使用一个叫作 rev-parse的 Git 探测工具。在第 9 章你能够看到关于探测工具的更多信息;简单来讲,rev-parse 是为了底层操做而不是平常操做设计的。不过,有时你想看 Git 如今到底处于什么状态时,它可能会颇有用。这里你能够对你的分支运执行rev-parse

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

引用日志里的简称

在你工做的同时,Git 在后台的工做之一就是保存一份引用日志——一份记录最近几个月你的 HEAD 和分支引用的日志。

你可使用 git reflog 来查看引用日志:

$ git reflog
734713b... HEAD@{0}: commit: fixed refs handling, added gc auto, updated
d921970... HEAD@{1}: merge phedders/rdocs: Merge made by recursive.
1c002dd... HEAD@{2}: commit: added some blame and merge stuff
1c36188... HEAD@{3}: rebase -i (squash): updating HEAD
95df984... HEAD@{4}: commit: # This is a combination of two commits.
1c36188... HEAD@{5}: rebase -i (squash): updating HEAD
7e05da5... HEAD@{6}: rebase -i (pick): updating HEAD

每次你的分支顶端由于某些缘由被修改时,Git 就会为你将信息保存在这个临时历史记录里面。你也可使用这份数据来指明更早的分支。若是你想查看仓库中 HEAD 在五次前的值,你可使用引用日志的输出中的@{n} 引用:

$ git show HEAD@{5}

你也可使用这个语法来查看必定时间前分支指向哪里。例如,想看你的 master 分支昨天在哪,你能够输入

$ git show master@{yesterday}

它就会显示昨天分支的顶端在哪。这项技术只对还在你引用日志里的数据有用,因此不能用来查看比几个月前还早的提交。

想要看相似于 git log 输出格式的引用日志信息,你能够运行 git log -g

$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: commit: fixed refs handling, added gc auto, updated 
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    fixed refs handling, added gc auto, updated tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Reflog: master@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: merge phedders/rdocs: Merge made by recursive.
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

须要注意的是,日志引用信息只存在于本地——这是一个你在仓库里作过什么的日志。其余人的仓库拷贝里的引用和你的相同;而你新克隆一个仓库的时候,引用日志是空的,由于你在仓库里尚未操做。只有你克隆了一个项目至少两个月,git show HEAD@{2.months.ago} 才会有用——若是你是五分钟前克隆的仓库,将不会有结果返回。

祖先引用

另外一种指明某次提交的经常使用方法是经过它的祖先。若是你在引用最后加上一个 ^,Git 将其理解为这次提交的父提交。 假设你的工程历史是这样的:

$ git log --pretty=format:'%h %s' --graph
* 734713b fixed refs handling, added gc auto, updated tests
*   d921970 Merge commit 'phedders/rdocs'
|\  
| * 35cfb2b Some rdoc changes
* | 1c002dd added some blame and merge stuff
|/  
* 1c36188 ignore *.gem
* 9b29157 add open3_detach to gemspec file list

那么,想看上一次提交,你可使用 HEAD^,意思是“HEAD 的父提交”:

$ git show HEAD^
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

你也能够在 ^ 后添加一个数字——例如,d921970^2 意思是“d921970 的第二父提交”。这种语法只在合并提交时有用,由于合并提交可能有多个父提交。第一父提交是你合并时所在分支,而第二父提交是你所合并的分支:

$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    added some blame and merge stuff

$ git show d921970^2
commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548
Author: Paul Hedderly <paul+git@mjr.org>
Date:   Wed Dec 10 22:22:03 2008 +0000

    Some rdoc changes

另一个指明祖先提交的方法是 ~。这也是指向第一父提交,因此 HEAD~ 和 HEAD^ 是等价的。当你指定数字的时候就明显不同了。HEAD~2 是指“第一父提交的第一父提交”,也就是“祖父提交”——它会根据你指定的次数检索第一父提交。例如,在上面列出的历史记录里面,HEAD~3 会是

$ git show HEAD~3
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    ignore *.gem

也能够写成 HEAD^^^,一样是第一父提交的第一父提交的第一父提交:

$ git show HEAD^^^
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    ignore *.gem

你也能够混合使用这些语法——你能够经过 HEAD~3^2 指明先前引用的第二父提交(假设它是一个合并提交)。

提交范围

如今你已经能够指明单次的提交,让咱们来看看怎样指明必定范围的提交。这在你管理分支的时候尤显重要——若是你有不少分支,你能够指明范围来圈定一些问题的答案,好比:“这个分支上我有哪些工做还没合并到主分支的?”

双点

最经常使用的指明范围的方法是双点的语法。这种语法主要是让 Git 区分出可从一个分支中得到而不能从另外一个分支中得到的提交。例如,假设你有相似于图 6-1 的提交历史。


图 6-1. 范围选择的提交历史实例

你想要查看你的试验分支上哪些没有被提交到主分支,那么你就可使用 master..experiment 来让 Git 显示这些提交的日志——这句话的意思是“全部可从experiment分支中得到而不能从master分支中得到的提交”。为了使例子简单明了,我使用了图标中提交对象的字母来代替真实日志的输出,因此会显示:

$ git log master..experiment
D
C

另外一方面,若是你想看相反的——全部在 master 而不在 experiment 中的分支——你能够交换分支的名字。experiment..master 显示全部可在master 得到而在 experiment 中不能的提交:

$ git log experiment..master
F
E

这在你想保持 experiment 分支最新和预览你将合并的提交的时候特别有用。这个语法的另外一种常见用途是查看你将把什么推送到远程:

$ git log origin/master..HEAD

这条命令显示任何在你当前分支上而不在远程origin 上的提交。若是你运行 git push 而且的你的当前分支正在跟踪origin/master,被git log origin/master..HEAD 列出的提交就是将被传输到服务器上的提交。 你也能够留空语法中的一边来让 Git 来假定它是 HEAD。例如,输入git log origin/master.. 将获得和上面的例子同样的结果—— Git 使用 HEAD 来代替不存在的一边。

多点

双点语法就像速记同样有用;可是你也许会想针对两个以上的分支来指明修订版本,好比查看哪些提交被包含在某些分支中的一个,可是不在你当前的分支上。Git容许你在引用前使用^字符或者--not指明你不但愿提交被包含其中的分支。所以下面三个命令是等同的:

$ git log refA..refB
$ git log ^refA refB
$ git log refB --not refA

这样很好,由于它容许你在查询中指定多于两个的引用,而这是双点语法所作不到的。例如,若是你想查找全部从refArefB包含的可是不被refC包含的提交,你能够输入下面中的一个

$ git log refA refB ^refC
$ git log refA refB --not refC

这创建了一个很是强大的修订版本查询系统,应该能够帮助你解决分支里包含了什么这个问题。

三点

最后一种主要的范围选择语法是三点语法,这个能够指定被两个引用中的一个包含但又不被二者同时包含的分支。回过头来看一下图6-1里所列的提交历史的例子。 若是你想查看master或者experiment中包含的但不是二者共有的引用,你能够运行

$ git log master...experiment
F
E
D
C

这个再次给出你普通的log输出可是只显示那四次提交的信息,按照传统的提交日期排列。

这种情形下,log命令的一个经常使用参数是--left-right,它会显示每一个提交到底处于哪一侧的分支。这使得数据更加有用。

$ git log --left-right master...experiment
< F
< E
> D
> C

有了以上工具,让Git知道你要察看哪些提交就容易得多了。

 

6.2  交互式暂存

Git提供了不少脚原本辅助某些命令行任务。这里,你将看到一些交互式命令,它们帮助你方便地构建只包含特定组合和部分文件的提交。在你修改了一大批文件而后决定将这些变动分布在几个各有侧重的提交而不是单个又大又乱的提交时,这些工具很是有用。用这种方法,你能够确保你的提交在逻辑上划分为相应的变动集,以便于供和你一块儿工做的开发者审阅。若是你运行git add时加上-i或者--interactive选项,Git就进入了一个交互式的shell模式,显示一些相似于下面的信息:

$ git add -i
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:    unchanged        +1/-1 index.html
  3:    unchanged        +5/-1 lib/simplegit.rb

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now>

你会看到这个命令以一个彻底不一样的视图显示了你的暂存区——主要是你经过git status获得的那些信息可是稍微简洁但信息更加丰富一些。它在左侧列出了你暂存的变动,在右侧列出了未被暂存的变动。

在这以后是一个命令区。这里你能够作不少事情,包括暂存文件,撤回文件,暂存部分文件,加入未被追踪的文件,查看暂存文件的差异。

暂存和撤回文件

若是你在What now>的提示后输入2或者u,这个脚本会提示你那些文件你想要暂存:

What now> 2
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:    unchanged        +1/-1 index.html
  3:    unchanged        +5/-1 lib/simplegit.rb
Update>>

若是想暂存TODO和index.html,你能够输入相应的编号:

Update>> 1,2
           staged     unstaged path
* 1:    unchanged        +0/-1 TODO
* 2:    unchanged        +1/-1 index.html
  3:    unchanged        +5/-1 lib/simplegit.rb
Update>>

每一个文件旁边的*表示选中的文件将被暂存。若是你在update>>提示后直接敲入回车,Git会替你把全部选中的内容暂存:

Update>> 
updated 2 paths

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now> 1
           staged     unstaged path
  1:        +0/-1      nothing TODO
  2:        +1/-1      nothing index.html
  3:    unchanged        +5/-1 lib/simplegit.rb

如今你能够看到TODO和index.html文件被暂存了同时simplegit.rb文件仍然未被暂存。若是这时你想要撤回TODO文件,就使用3或者r(表明revert,恢复)选项:

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now> 3
           staged     unstaged path
  1:        +0/-1      nothing TODO
  2:        +1/-1      nothing index.html
  3:    unchanged        +5/-1 lib/simplegit.rb
Revert>> 1
           staged     unstaged path
* 1:        +0/-1      nothing TODO
  2:        +1/-1      nothing index.html
  3:    unchanged        +5/-1 lib/simplegit.rb
Revert>> [enter]
reverted one path

再次查看Git的状态,你会看到你已经撤回了TODO文件

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now> 1
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:        +1/-1      nothing index.html
  3:    unchanged        +5/-1 lib/simplegit.rb

要查看你暂存内容的差别,你可使用6或者d(表示diff)命令。它会显示你暂存文件的列表,你能够选择其中的几个,显示其被暂存的差别。这跟你在命令行下指定git diff --cached很是类似:

*** Commands ***
  1: status     2: update      3: revert     4: add untracked
  5: patch      6: diff        7: quit       8: help
What now> 6
           staged     unstaged path
  1:        +1/-1      nothing index.html
Review diff>> 1
diff --git a/index.html b/index.html
index 4d07108..4335f49 100644
--- a/index.html
+++ b/index.html
@@ -16,7 +16,7 @@ Date Finder

 <p id="out">...</p>

-<div id="footer">contact : support@github.com</div>
+<div id="footer">contact : email.support@github.com</div>

 <script type="text/javascript">

经过这些基本命令,你可使用交互式增长模式更加方便地处理暂存区。

暂存补丁

只让Git暂存文件的某些部分而忽略其余也是有可能的。例如,你对simplegit.rb文件做了两处修改可是只想暂存其中一个而忽略另外一个,在Git中实现这一点很是容易。在交互式的提示符下,输入5或者p(表示patch,补丁)。Git会询问哪些文件你但愿部分暂存;而后对于被选中文件的每一节,他会逐个显示文件的差别区块并询问你是否但愿暂存他们:

diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index dd5ecc4..57399e0 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -22,7 +22,7 @@ class SimpleGit
   end

   def log(treeish = 'master')
-    command("git log -n 25 #{treeish}")
+    command("git log -n 30 #{treeish}")
   end

   def blame(path)
Stage this hunk [y,n,a,d,/,j,J,g,e,?]?

此处你有不少选择。输入?能够显示列表:

Stage this hunk [y,n,a,d,/,j,J,g,e,?]? ?
y - stage this hunk
n - do not stage this hunk
a - stage this and all the remaining hunks in the file
d - do not stage this hunk nor any of the remaining hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
k - leave this hunk undecided, see previous undecided hunk
K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks
e - manually edit the current hunk
? - print help

若是你想暂存各个区块,一般你会输入y或者n,可是暂存特定文件里的所有区块或者暂时跳过对一个区块的处理一样也颇有用。若是你暂存了文件的一个部分而保留另一个部分不被暂存,你的状态输出看起来会是这样:

What now> 1
           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:        +1/-1      nothing index.html
  3:        +1/-1        +4/-0 lib/simplegit.rb

simplegit.rb的状态很是有意思。它显示有几行被暂存了,有几行没有。你部分地暂存了这个文件。在这时,你能够退出交互式脚本而后运行git commit来提交部分暂存的文件。

最后你也能够不经过交互式增长的模式来实现部分文件暂存——你能够在命令行下经过git add -p或者git add --patch来启动一样的脚本。

 

6.3  储藏(Stashing)

常常有这样的事情发生,当你正在进行项目中某一部分的工做,里面的东西处于一个比较杂乱的状态,而你想转到其余分支上进行一些工做。问题是,你不想提交进行了一半的工做,不然之后你没法回到这个工做点。解决这个问题的办法就是git stash命令。

“‘储藏”“能够获取你工做目录的中间状态——也就是你修改过的被追踪的文件和暂存的变动——并将它保存到一个未完结变动的堆栈中,随时能够从新应用。

储藏你的工做

为了演示这一功能,你能够进入你的项目,在一些文件上进行工做,有可能还暂存其中一个变动。若是你运行 git status,你能够看到你的中间状态:

$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#

如今你想切换分支,可是你还不想提交你正在进行中的工做;因此你储藏这些变动。为了往堆栈推送一个新的储藏,只要运行 git stash

$ git stash
Saved working directory and index state \
  "WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file
(To restore them type "git stash apply")

你的工做目录就干净了:

$ git status
# On branch master
nothing to commit (working directory clean)

这时,你能够方便地切换到其余分支工做;你的变动都保存在栈上。要查看现有的储藏,你可使用 git stash list

$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051... Revert "added file_size"
stash@{2}: WIP on master: 21d80a5... added number to log

在这个案例中,以前已经进行了两次储藏,因此你能够访问到三个不一样的储藏。你能够从新应用你刚刚实施的储藏,所采用的命令就是以前在原始的 stash 命令的帮助输出里提示的:git stash apply。若是你想应用更早的储藏,你能够经过名字指定它,像这样:git stash apply stash@{2}。若是你不指明,Git 默认使用最近的储藏并尝试应用它:

$ git stash apply
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   index.html
#      modified:   lib/simplegit.rb
#

你能够看到 Git 从新修改了你所储藏的那些当时还没有提交的文件。在这个案例里,你尝试应用储藏的工做目录是干净的,而且属于同一分支;可是一个干净的工做目录和应用到相同的分支上并非应用储藏的必要条件。你能够在其中一个分支上保留一份储藏,随后切换到另一个分支,再从新应用这些变动。在工做目录里包含已修改和未提交的文件时,你也能够应用储藏——Git 会给出归并冲突若是有任何变动没法干净地被应用。

对文件的变动被从新应用,可是被暂存的文件没有从新被暂存。想那样的话,你必须在运行 git stash apply 命令时带上一个 --index 的选项来告诉命令从新应用被暂存的变动。若是你是这么作的,你应该已经回到你原来的位置:

$ git stash apply --index
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#

apply 选项只尝试应用储藏的工做——储藏的内容仍然在栈上。要移除它,你能够运行 git stash drop,加上你但愿移除的储藏的名字:

$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051... Revert "added file_size"
stash@{2}: WIP on master: 21d80a5... added number to log
$ git stash drop stash@{0}
Dropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43)

你也能够运行 git stash pop 来从新应用储藏,同时马上将其从堆栈中移走。

Un-applying a Stash

In some use case scenarios you might want to apply stashed changes, do some work, but then un-apply those changes that originally came form the stash. Git does not provide such astash unapply command, but it is possible to achieve the effect by simply retrieving the patch associated with a stash and applying it in reverse:

$ git stash show -p stash@{0} | git apply -R

Again, if you don’t specify a stash, Git assumes the most recent stash:

$ git stash show -p | git apply -R

You may want to create an alias and effectively add a stash-unapply command to your git. For example:

$ git config --global alias.stash-unapply '!git stash show -p | git apply -R'
$ git stash
$ #... work work work
$ git stash-unapply

从储藏中建立分支

若是你储藏了一些工做,暂时不去理会,而后继续在你储藏工做的分支上工做,你在从新应用工做时可能会碰到一些问题。若是尝试应用的变动是针对一个你那以后修改过的文件,你会碰到一个归并冲突而且必须去化解它。若是你想用更方便的方法来从新检验你储藏的变动,你能够运行git stash branch,这会建立一个新的分支,检出你储藏工做时的所处的提交,从新应用你的工做,若是成功,将会丢弃储藏。

$ git stash branch testchanges
Switched to a new branch "testchanges"
# On branch testchanges
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      modified:   index.html
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#      modified:   lib/simplegit.rb
#
Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359)

这是一个很棒的捷径来恢复储藏的工做而后在新的分支上继续当时的工做。

 

6.4  重写历史

不少时候,在 Git 上工做的时候,你也许会因为某种缘由想要修订你的提交历史。Git 的一个卓越之处就是它容许你在最后可能的时刻再做决定。你能够在你即将提交暂存区时决定什么文件纳入哪一次提交,你可使用 stash 命令来决定你暂时搁置的工做,你能够重写已经发生的提交以使它们看起来是另一种样子。这个包括改变提交的次序、改变说明或者修改提交中包含的文件,将提交归并、拆分或者彻底删除——这一切在你还没有开始将你的工做和别人共享前都是能够的。

在这一节中,你会学到如何完成这些颇有用的任务以使你的提交历史在你将其共享给别人以前变成你想要的样子。

改变最近一次提交

改变最近一次提交也许是最多见的重写历史的行为。对于你的最近一次提交,你常常想作两件基本事情:改变提交说明,或者改变你刚刚经过增长,改变,删除而记录的快照。

若是你只想修改最近一次提交说明,这很是简单:

$ git commit --amend

这会把你带入文本编辑器,里面包含了你最近一次提交说明,供你修改。当你保存并退出编辑器,这个编辑器会写入一个新的提交,里面包含了那个说明,而且让它成为你的新的最近一次提交。

若是你完成提交后又想修改被提交的快照,增长或者修改其中的文件,可能由于你最初提交时,忘了添加一个新建的文件,这个过程基本上同样。你经过修改文件而后对其运行git add或对一个已被记录的文件运行git rm,随后的git commit --amend会获取你当前的暂存区并将它做为新提交对应的快照。

使用这项技术的时候你必须当心,由于修正会改变提交的SHA-1值。这个很像是一次很是小的rebase——不要在你最近一次提交被推送后还去修正它。

修改多个提交说明

要修改历史中更早的提交,你必须采用更复杂的工具。Git没有一个修改历史的工具,可是你可使用rebase工具来衍合一系列的提交到它们原来所在的HEAD上而不是移到新的上。依靠这个交互式的rebase工具,你就能够停留在每一次提交后,若是你想修改或改变说明、增长文件或任何其余事情。你能够经过给git rebase增长-i选项来以交互方式地运行rebase。你必须经过告诉命令衍合到哪次提交,来指明你须要重写的提交的回溯深度。

例如,你想修改最近三次的提交说明,或者其中任意一次,你必须给git rebase -i提供一个参数,指明你想要修改的提交的父提交,例如HEAD~2或者HEAD~3。可能记住~3更加容易,由于你想修改最近三次提交;可是请记住你事实上所指的是四次提交以前,即你想修改的提交的父提交。

$ git rebase -i HEAD~3

再次提醒这是一个衍合命令——HEAD~3..HEAD范围内的每一次提交都会被重写,不管你是否修改说明。不要涵盖你已经推送到中心服务器的提交——这么作会使其余开发者产生混乱,由于你提供了一样变动的不一样版本。

运行这个命令会为你的文本编辑器提供一个提交列表,看起来像下面这样

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

很重要的一点是你得注意这些提交的顺序与你一般经过log命令看到的是相反的。若是你运行log,你会看到下面这样的结果:

$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d added cat-file
310154e updated README formatting and added blame
f7f3f6d changed my name a bit

请注意这里的倒序。交互式的rebase给了你一个即将运行的脚本。它会从你在命令行上指明的提交开始(HEAD~3)而后自上至下重播每次提交里引入的变动。它将最先的列在顶上而不是最近的,由于这是第一个须要重播的。

你须要修改这个脚原本让它停留在你想修改的变动上。要作到这一点,你只要将你想修改的每一次提交前面的pick改成edit。例如,只想修改第三次提交说明的话,你就像下面这样修改文件:

edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

当你保存并退出编辑器,Git会倒回至列表中的最后一次提交,而后把你送到命令行中,同时显示如下信息:

$ git rebase -i HEAD~3
Stopped at 7482e0d... updated the gemspec to hopefully work better
You can amend the commit now, with

       git commit --amend

Once you’re satisfied with your changes, run

       git rebase --continue

这些指示很明确地告诉了你该干什么。输入

$ git commit --amend

修改提交说明,退出编辑器。而后,运行

$ git rebase --continue

这个命令会自动应用其余两次提交,你就完成任务了。若是你将更多行的 pick 改成 edit ,你就能对你想修改的提交重复这些步骤。Git每次都会停下,让你修正提交,完成后继续运行。

重排提交

你也可使用交互式的衍合来完全重排或删除提交。若是你想删除”added cat-file”这个提交而且修改其余两次提交引入的顺序,你将rebase脚本从这个

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

改成这个:

pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit

当你保存并退出编辑器,Git 将分支倒回至这些提交的父提交,应用310154e,而后f7f3f6d,接着中止。你有效地修改了这些提交的顺序而且完全删除了”added cat-file”此次提交。

压制(Squashing)提交

交互式的衍合工具还能够将一系列提交压制为单一提交。脚本在 rebase 的信息里放了一些有用的指示:

#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

若是不用”pick”或者”edit”,而是指定”squash”,Git 会同时应用那个变动和它以前的变动并将提交说明归并。所以,若是你想将这三个提交合并为单一提交,你能够将脚本修改为这样:

pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file

当你保存并退出编辑器,Git 会应用所有三次变动而后将你送回编辑器来归并三次提交说明。

# This is a combination of 3 commits.
# The first commit's message is:
changed my name a bit

# This is the 2nd commit message:

updated README formatting and added blame

# This is the 3rd commit message:

added cat-file

当你保存以后,你就拥有了一个包含前三次提交的所有变动的单一提交。

拆分提交

拆分提交就是撤销一次提交,而后屡次部分地暂存或提交直到结束。例如,假设你想将三次提交中的中间一次拆分。将”updated README formatting and added blame”拆分红两次提交:第一次为”updated README formatting”,第二次为”added blame”。你能够在rebase -i脚本中修改你想拆分的提交前的指令为”edit”:

pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

而后,这个脚本就将你带入命令行,你重置那次提交,提取被重置的变动,从中建立屡次提交。当你保存并退出编辑器,Git 倒回到列表中第一次提交的父提交,应用第一次提交(f7f3f6d),应用第二次提交(310154e),而后将你带到控制台。那里你能够用git reset HEAD^对那次提交进行一次混合的重置,这将撤销那次提交而且将修改的文件撤回。此时你能够暂存并提交文件,直到你拥有屡次提交,结束后,运行git rebase --continue

$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue

Git在脚本中应用了最后一次提交(a5f4a0d),你的历史看起来就像这样了:

$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit

再次提醒,这会修改你列表中的提交的 SHA 值,因此请确保这个列表里不包含你已经推送到共享仓库的提交。

核弹级选项: filter-branch

若是你想用脚本的方式修改大量的提交,还有一个重写历史的选项能够用——例如,全局性地修改电子邮件地址或者将一个文件从全部提交中删除。这个命令是filter-branch,这个会大面积地修改你的历史,因此你颇有可能不应去用它,除非你的项目还没有公开,没有其余人在你准备修改的提交的基础上工做。尽管如此,这个能够很是有用。你会学习一些常见用法,借此对它的能力有所认识。

从全部提交中删除一个文件

这个常常发生。有些人不经思考使用git add .,意外地提交了一个巨大的二进制文件,你想将它从全部地方删除。也许你不当心提交了一个包含密码的文件,而你想让你的项目开源。filter-branch大概会是你用来清理整个历史的工具。要从整个历史中删除一个名叫password.txt的文件,你能够在filter-branch上使用--tree-filter选项:

$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten

--tree-filter选项会在每次检出项目时先执行指定的命令而后从新提交结果。在这个例子中,你会在全部快照中删除一个名叫 password.txt 的文件,不管它是否存在。若是你想删除全部不当心提交上去的编辑器备份文件,你能够运行相似git filter-branch --tree-filter 'rm -f *~' HEAD的命令。

你能够观察到 Git 重写目录树而且提交,而后将分支指针移到末尾。一个比较好的办法是在一个测试分支上作这些而后在你肯定产物真的是你所要的以后,再 hard-reset 你的主分支。要在你全部的分支上运行filter-branch的话,你能够传递一个--all给命令。

将一个子目录设置为新的根目录

假设你完成了从另一个代码控制系统的导入工做,获得了一些没有意义的子目录(trunk, tags等等)。若是你想让trunk子目录成为每一次提交的新的项目根目录,filter-branch也能够帮你作到:

$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten

如今你的项目根目录就是trunk子目录了。Git 会自动地删除不对这个子目录产生影响的提交。

全局性地更换电子邮件地址

另外一个常见的案例是你在开始时忘了运行git config来设置你的姓名和电子邮件地址,也许你想开源一个项目,把你全部的工做电子邮件地址修改成我的地址。不管哪一种状况你均可以用filter-branch来更换屡次提交里的电子邮件地址。你必须当心一些,只改变属于你的电子邮件地址,因此你使用--commit-filter

$ git filter-branch --commit-filter '
        if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
        then
                GIT_AUTHOR_NAME="Scott Chacon";
                GIT_AUTHOR_EMAIL="schacon@example.com";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

这个会遍历并重写全部提交使之拥有你的新地址。由于提交里包含了它们的父提交的SHA-1值,这个命令会修改你的历史中的全部提交,而不只仅是包含了匹配的电子邮件地址的那些。

 

6.5  使用 Git 调试

Git 一样提供了一些工具来帮助你调试项目中遇到的问题。因为 Git 被设计为可应用于几乎任何类型的项目,这些工具是通用型,可是在遇到问题时能够常常帮助你查找缺陷所在。

文件标注

若是你在追踪代码中的缺陷想知道这是何时为何被引进来的,文件标注会是你的最佳工具。它会显示文件中对每一行进行修改的最近一次提交。所以,若是你发现本身代码中的一个方法存在缺陷,你能够用git blame来标注文件,查看那个方法的每一行分别是由谁在哪一天修改的。下面这个例子使用了-L选项来限制输出范围在第12至22行:

$ git blame -L 12,22 simplegit.rb 
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 12)  def show(tree = 'master')
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 13)   command("git show #{tree}")
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 14)  end
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 15)
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 16)  def log(tree = 'master')
79eaf55d (Scott Chacon  2008-04-06 10:15:08 -0700 17)   command("git log #{tree}")
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 18)  end
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 19) 
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 20)  def blame(path)
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 21)   command("git blame #{path}")
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 22)  end

请注意第一个域里是最后一次修改该行的那次提交的 SHA-1 值。接下去的两个域是从那次提交中抽取的值——做者姓名和日期——因此你能够方便地获知谁在何时修改了这一行。在这后面是行号和文件的内容。请注意^4832fe2提交的那些行,这些指的是文件最初提交的那些行。那个提交是文件第一次被加入这个项目时存在的,自那之后未被修改过。这会带来小小的困惑,由于你已经至少看到了Git使用^来修饰一个提交的SHA值的三种不一样的意义,但这里确实就是这个意思。

另外一件很酷的事情是在 Git 中你不须要显式地记录文件的重命名。它会记录快照而后根据现实尝试找出隐式的重命名动做。这其中有一个颇有意思的特性就是你可让它找出全部的代码移动。若是你在git blame后加上-C,Git会分析你在标注的文件而后尝试找出其中代码片断的原始出处,若是它是从其余地方拷贝过来的话。最近,我在将一个名叫GITServerHandler.m的文件分解到多个文件中,其中一个是GITPackUpload.m。经过对GITPackUpload.m执行带-C参数的blame命令,我能够看到代码块的原始出处:

$ git blame -C -L 141,153 GITPackUpload.m 
f344f58d GITServerHandler.m (Scott 2009-01-04 141) 
f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC
f344f58d GITServerHandler.m (Scott 2009-01-04 143) {
70befddd GITServerHandler.m (Scott 2009-03-22 144)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 145)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 146)         NSString *parentSha;
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 147)         GITCommit *commit = [g
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 148)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 149)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 150)
56ef2caf GITServerHandler.m (Scott 2009-01-05 151)         if(commit) {
56ef2caf GITServerHandler.m (Scott 2009-01-05 152)                 [refDict setOb
56ef2caf GITServerHandler.m (Scott 2009-01-05 153)

这真的很是有用。一般,你会把你拷贝代码的那次提交做为原始提交,由于这是你在这个文件中第一次接触到那几行。Git能够告诉你编写那些行的原始提交,即使是在另外一个文件里。

二分查找

标注文件在你知道问题是哪里引入的时候会有帮助。若是你不知道,而且自上次代码可用的状态已经经历了上百次的提交,你可能就要求助于bisect命令了。bisect会在你的提交历史中进行二分查找来尽快地肯定哪一次提交引入了错误。

例如你刚刚推送了一个代码发布版本到产品环境中,对代码为何会表现成那样百思不得其解。你回到你的代码中,还好你能够重现那个问题,可是找不到在哪里。你能够对代码执行bisect来寻找。首先你运行git bisect start启动,而后你用git bisect bad来告诉系统当前的提交已经有问题了。而后你必须告诉bisect已知的最后一次正常状态是哪次提交,使用git bisect good [good_commit]

$ git bisect start
$ git bisect bad
$ git bisect good v1.0
Bisecting: 6 revisions left to test after this
[ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo

Git 发如今你标记为正常的提交(v1.0)和当前的错误版本之间有大约12次提交,因而它检出中间的一个。在这里,你能够运行测试来检查问题是否存在于此次提交。若是是,那么它是在这个中间提交以前的某一次引入的;若是否,那么问题是在中间提交以后引入的。假设这里是没有错误的,那么你就经过git bisect good来告诉 Git 而后继续你的旅程:

$ git bisect good
Bisecting: 3 revisions left to test after this
[b047b02ea83310a70fd603dc8cd7a6cd13d15c04] secure this thing

如今你在另一个提交上了,在你刚刚测试经过的和一个错误提交的中点处。你再次运行测试而后发现此次提交是错误的,所以你经过git bisect bad来告诉Git:

$ git bisect bad
Bisecting: 1 revisions left to test after this
[f71ce38690acf49c1f3c9bea38e09d82a5ce6014] drop exceptions table

此次提交是好的,那么 Git 就得到了肯定问题引入位置所需的全部信息。它告诉你第一个错误提交的 SHA-1 值而且显示一些提交说明以及哪些文件在那次提交里修改过,这样你能够找出缺陷被引入的根源:

$ git bisect good
b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit
commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04
Author: PJ Hyett <pjhyett@example.com>
Date:   Tue Jan 27 14:48:32 2009 -0800

    secure this thing

:040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730
f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M  config

当你完成以后,你应该运行git bisect reset来重设你的HEAD到你开始前的地方,不然你会处于一个诡异的地方:

$ git bisect reset

这是个强大的工具,能够帮助你检查上百的提交,在几分钟内找出缺陷引入的位置。事实上,若是你有一个脚本会在工程正常时返回0,错误时返回非0的话,你能够彻底自动地执行git bisect。首先你须要提供已知的错误和正确提交来告诉它二分查找的范围。你能够经过bisect start命令来列出它们,先列出已知的错误提交再列出已知的正确提交:

$ git bisect start HEAD v1.0
$ git bisect run test-error.sh

这样会自动地在每个检出的提交里运行test-error.sh直到Git找出第一个破损的提交。你也能够运行像make或者make tests或者任何你所拥有的来为你执行自动化的测试。

 

6.6  子模块

常常有这样的事情,当你在一个项目上工做时,你须要在其中使用另一个项目。也许它是一个第三方开发的库或者是你独立开发和并在多个父项目中使用的。这个场景下一个常见的问题产生了:你想将两个项目单独处理可是又须要在其中一个中使用另一个。

这里有一个例子。假设你在开发一个网站,为之建立Atom源。你不想编写一个本身的Atom生成代码,而是决定使用一个库。你可能不得不像CPAN install或者Ruby gem同样包含来自共享库的代码,或者将代码拷贝到你的项目树中。若是采用包含库的办法,那么无论用什么办法都很难去定制这个库,部署它就更加困难了,由于你必须确保每一个客户都拥有那个库。把代码包含到你本身的项目中带来的问题是,当上游被修改时,任何你进行的定制化的修改都很难归并。

Git 经过子模块处理这个问题。子模块容许你将一个 Git 仓库看成另一个Git仓库的子目录。这容许你克隆另一个仓库到你的项目中而且保持你的提交相对独立。

子模块初步

假设你想把 Rack 库(一个 Ruby 的 web 服务器网关接口)加入到你的项目中,可能既要保持你本身的变动,又要延续上游的变动。首先你要把外部的仓库克隆到你的子目录中。你经过git submodule add将外部项目加为子模块:

$ git submodule add git://github.com/chneukirchen/rack.git rack
Initialized empty Git repository in /opt/subtest/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Receiving objects: 100% (3181/3181), 675.42 KiB | 422 KiB/s, done.
Resolving deltas: 100% (1951/1951), done.

如今你就在项目里的rack子目录下有了一个 Rack 项目。你能够进入那个子目录,进行变动,加入你本身的远程可写仓库来推送你的变动,从原始仓库拉取和归并等等。若是你在加入子模块后马上运行git status,你会看到下面两项:

$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#      new file:   .gitmodules
#      new file:   rack
#

首先你注意到有一个.gitmodules文件。这是一个配置文件,保存了项目 URL 和你拉取到的本地子目录

$ cat .gitmodules 
[submodule "rack"]
      path = rack
      url = git://github.com/chneukirchen/rack.git

若是你有多个子模块,这个文件里会有多个条目。很重要的一点是这个文件跟其余文件同样也是处于版本控制之下的,就像你的.gitignore文件同样。它跟项目里的其余文件同样能够被推送和拉取。这是其余克隆此项目的人获知子模块项目来源的途径。

git status的输出里所列的另外一项目是 rack 。若是你运行在那上面运行git diff,会发现一些有趣的东西:

$ git diff --cached rack
diff --git a/rack b/rack
new file mode 160000
index 0000000..08d709f
--- /dev/null
+++ b/rack
@@ -0,0 +1 @@
+Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433

尽管rack是你工做目录里的子目录,但 Git 把它视做一个子模块,当你不在那个目录里时并不记录它的内容。取而代之的是,Git 将它记录成来自那个仓库的一个特殊的提交。当你在那个子目录里修改并提交时,子项目会通知那里的 HEAD 已经发生变动并记录你当前正在工做的那个提交;经过那样的方法,当其余人克隆此项目,他们能够从新建立一致的环境。

这是关于子模块的重要一点:你记录他们当前确切所处的提交。你不能记录一个子模块的master或者其余的符号引用。

当你提交时,会看到相似下面的:

$ git commit -m 'first commit with submodule rack'
[master 0550271] first commit with submodule rack
 2 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 .gitmodules
 create mode 160000 rack

注意 rack 条目的 160000 模式。这在Git中是一个特殊模式,基本意思是你将一个提交记录为一个目录项而不是子目录或者文件。

你能够将rack目录看成一个独立的项目,保持一个指向子目录的最新提交的指针而后反复地更新上层项目。全部的Git命令都在两个子目录里独立工做:

$ git log -1
commit 0550271328a0038865aad6331e620cd7238601bb
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Apr 9 09:03:56 2009 -0700

    first commit with submodule rack
$ cd rack/
$ git log -1
commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433
Author: Christian Neukirchen <chneukirchen@gmail.com>
Date:   Wed Mar 25 14:49:04 2009 +0100

    Document version change

克隆一个带子模块的项目

这里你将克隆一个带子模块的项目。当你接收到这样一个项目,你将获得了包含子项目的目录,但里面没有文件:

$ git clone git://github.com/schacon/myproject.git
Initialized empty Git repository in /opt/myproject/.git/
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.
$ cd myproject
$ ls -l
total 8
-rw-r--r--  1 schacon  admin   3 Apr  9 09:11 README
drwxr-xr-x  2 schacon  admin  68 Apr  9 09:11 rack
$ ls rack/
$

rack目录存在了,可是是空的。你必须运行两个命令:git submodule init来初始化你的本地配置文件,git submodule update来从那个项目拉取全部数据并检出你上层项目里所列的合适的提交:

$ git submodule init
Submodule 'rack' (git://github.com/chneukirchen/rack.git) registered for path 'rack'
$ git submodule update
Initialized empty Git repository in /opt/myproject/rack/.git/
remote: Counting objects: 3181, done.
remote: Compressing objects: 100% (1534/1534), done.
remote: Total 3181 (delta 1951), reused 2623 (delta 1603)
Receiving objects: 100% (3181/3181), 675.42 KiB | 173 KiB/s, done.
Resolving deltas: 100% (1951/1951), done.
Submodule path 'rack': checked out '08d709f78b8c5b0fbeb7821e37fa53e69afcf433'

如今你的rack子目录就处于你先前提交的确切状态了。若是另一个开发者变动了 rack 的代码并提交,你拉取那个引用而后归并之,将获得稍有点怪异的东西:

$ git merge origin/master
Updating 0550271..85a3eee
Fast forward
 rack |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)
[master*]$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#      modified:   rack
#

你归并来的仅仅上是一个指向你的子模块的指针;可是它并不更新你子模块目录里的代码,因此看起来你的工做目录处于一个临时状态:

$ git diff
diff --git a/rack b/rack
index 6c5e70b..08d709f 160000
--- a/rack
+++ b/rack
@@ -1 +1 @@
-Subproject commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
+Subproject commit 08d709f78b8c5b0fbeb7821e37fa53e69afcf433

事情就是这样,由于你所拥有的子模块的指针并对应于子模块目录的真实状态。为了修复这一点,你必须再次运行git submodule update

$ git submodule update
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 2 (delta 0)
Unpacking objects: 100% (3/3), done.
From git@github.com:schacon/rack
   08d709f..6c5e70b  master     -> origin/master
Submodule path 'rack': checked out '6c5e70b984a60b3cecd395edd5b48a7575bf58e0'

每次你从主项目中拉取一个子模块的变动都必须这样作。看起来很怪可是管用。

一个常见问题是当开发者对子模块作了一个本地的变动可是并无推送到公共服务器。而后他们提交了一个指向那个非公开状态的指针而后推送上层项目。当其余开发者试图运行git submodule update,那个子模块系统会找不到所引用的提交,由于它只存在于第一个开发者的系统中。若是发生那种状况,你会看到相似这样的错误:

$ git submodule update
fatal: reference isn’t a tree: 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Unable to checkout '6c5e70b984a60b3cecd395edd5ba7575bf58e0' in submodule path 'rack'

你不得不去查看谁最后变动了子模块

$ git log -1 rack
commit 85a3eee996800fcfa91e2119372dd4172bf76678
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Apr 9 09:19:14 2009 -0700

    added a submodule reference I will never make public. hahahahaha!

而后,你给那个家伙发电子邮件说他一通。

上层项目

有时候,开发者想按照他们的分组获取一个大项目的子目录的子集。若是你是从 CVS 或者 Subversion 迁移过来的话这个很常见,在那些系统中你已经定义了一个模块或者子目录的集合,而你想延续这种类型的工做流程。

在 Git 中实现这个的一个好办法是你将每个子目录都作成独立的 Git 仓库,而后建立一个上层项目的 Git 仓库包含多个子模块。这个办法的一个优点是你能够在上层项目中经过标签和分支更为明确地定义项目之间的关系。

子模块的问题

使用子模块并不是没有任何缺点。首先,你在子模块目录中工做时必须相对当心。当你运行git submodule update,它会检出项目的指定版本,可是不在分支内。这叫作得到一个分离的头——这意味着 HEAD 文件直接指向一次提交,而不是一个符号引用。问题在于你一般并不想在一个分离的头的环境下工做,由于太容易丢失变动了。若是你先执行了一次submodule update,而后在那个子模块目录里不建立分支就进行提交,而后再次从上层项目里运行git submodule update同时不进行提交,Git会毫无提示地覆盖你的变动。技术上讲你不会丢失工做,可是你将失去指向它的分支,所以会很难取到。

为了不这个问题,当你在子模块目录里工做时应使用git checkout -b work建立一个分支。当你再次在子模块里更新的时候,它仍然会覆盖你的工做,可是至少你拥有一个能够回溯的指针。

切换带有子模块的分支一样也颇有技巧。若是你建立一个新的分支,增长了一个子模块,而后切换回不带该子模块的分支,你仍然会拥有一个未被追踪的子模块的目录

$ git checkout -b rack
Switched to a new branch "rack"
$ git submodule add git@github.com:schacon/rack.git rack
Initialized empty Git repository in /opt/myproj/rack/.git/
...
Receiving objects: 100% (3184/3184), 677.42 KiB | 34 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
$ git commit -am 'added rack submodule'
[rack cc49a69] added rack submodule
 2 files changed, 4 insertions(+), 0 deletions(-)
 create mode 100644 .gitmodules
 create mode 160000 rack
$ git checkout master
Switched to branch "master"
$ git status
# On branch master
# Untracked files:
#   (use "git add <file>..." to include in what will be committed)
#
#      rack/

你将不得不将它移走或者删除,这样的话当你切换回去的时候必须从新克隆它——你可能会丢失你未推送的本地的变动或分支。

最后一个须要引发注意的是关于从子目录切换到子模块的。若是你已经跟踪了你项目中的一些文件可是想把它们移到子模块去,你必须很是当心,不然Git会生你的气。假设你的项目中有一个子目录里放了 rack 的文件,而后你想将它转换为子模块。若是你删除子目录而后运行submodule add,Git会向你大吼:

$ rm -Rf rack/
$ git submodule add git@github.com:schacon/rack.git rack
'rack' already exists in the index

你必须先将rack目录撤回。而后你才能加入子模块:

$ git rm -r rack
$ git submodule add git@github.com:schacon/rack.git rack
Initialized empty Git repository in /opt/testsub/rack/.git/
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 88 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.

如今假设你在一个分支里那样作了。若是你尝试切换回一个仍然在目录里保留那些文件而不是子模块的分支时——你会获得下面的错误:

$ git checkout master
error: Untracked working tree file 'rack/AUTHORS' would be overwritten by merge.

你必须先移除rack子模块的目录才能切换到不包含它的分支:

$ mv rack /tmp/
$ git checkout master
Switched to branch "master"
$ ls
README	rack

而后,当你切换回来,你会获得一个空的rack目录。你能够运行git submodule update从新克隆,也能够将/tmp/rack目录从新移回空目录。

 

6.7  子树合并

如今你已经看到了子模块系统的麻烦之处,让咱们来看一下解决相同问题的另外一途径。当 Git 归并时,它会检查须要归并的内容而后选择一个合适的归并策略。若是你归并的分支是两个,Git使用一个_递归_策略。若是你归并的分支超过两个,Git采用_章鱼_策略。这些策略是自动选择的,由于递归策略能够处理复杂的三路归并状况——好比多于一个共同祖先的——可是它只能处理两个分支的归并。章鱼归并能够处理多个分支可是但必须更加当心以免冲突带来的麻烦,所以它被选中做为归并两个以上分支的默认策略。

实际上,你也能够选择其余策略。其中的一个就是_子树_归并,你能够用它来处理子项目问题。这里你会看到如何换用子树归并的方法来实现前一节里所作的 rack 的嵌入。

子树归并的思想是你拥有两个工程,其中一个项目映射到另一个项目的子目录中,反过来也同样。当你指定一个子树归并,Git能够聪明地探知其中一个是另一个的子树从而实现正确的归并——这至关神奇。

首先你将 Rack 应用加入到项目中。你将 Rack 项目看成你项目中的一个远程引用,而后将它检出到它自身的分支:

$ git remote add rack_remote git@github.com:schacon/rack.git
$ git fetch rack_remote
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From git@github.com:schacon/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

如今在你的rack_branch分支中就有了Rack项目的根目录,而你本身的项目在master分支中。若是你先检出其中一个而后另一个,你会看到它们有不一样的项目根目录:

$ ls
AUTHORS	       KNOWN-ISSUES   Rakefile      contrib	       lib
COPYING	       README         bin           example	       test
$ git checkout master
Switched to branch "master"
$ ls
README

要将 Rack 项目看成子目录拉取到你的master项目中。你能够在 Git 中用git read-tree来实现。你会在第9章学到更多与read-tree和它的朋友相关的东西,当前你会知道它读取一个分支的根目录树到当前的暂存区和工做目录。你只要切换回你的master分支,而后拉取rack分支到你主项目的master分支的rack子目录:

$ git read-tree --prefix=rack/ -u rack_branch

当你提交的时候,看起来就像你在那个子目录下拥有Rack的文件——就像你从一个tarball里拷贝的同样。有意思的是你能够比较容易地归并其中一个分支的变动到另一个。所以,若是 Rack 项目更新了,你能够经过切换到那个分支并执行拉取来得到上游的变动:

$ git checkout rack_branch
$ git pull

而后,你能够将那些变动归并回你的 master 分支。你可使用git merge -s subtree,它会工做的很好;可是 Git 同时会把历史归并到一块儿,这可能不是你想要的。为了拉取变动并预置提交说明,须要在-s subtree策略选项的同时使用--squash--no-commit选项。

$ git checkout master
$ git merge --squash -s subtree --no-commit rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

全部 Rack 项目的变动都被归并能够进行本地提交。你也能够作相反的事情——在你主分支的rack目录里进行变动而后归并回rack_branch分支,而后将它们提交给维护者或者推送到上游。

为了获得rack子目录和你rack_branch分支的区别——以决定你是否须要归并它们——你不能使用通常的diff命令。而是对你想比较的分支运行git diff-tree

$ git diff-tree -p rack_branch

或者,为了比较你的rack子目录和服务器上你拉取时的master分支,你能够运行

$ git diff-tree -p rack_remote/master


6.8  总结

你已经看到了不少高级的工具,容许你更加精确地操控你的提交和暂存区。当你碰到问题时,你应该能够很容易找出是哪一个分支何时由谁引入了它们。若是你想在项目中使用子项目,你也已经学会了一些方法来知足这些需求。到此,你应该可以完成平常里你须要用命令行在 Git 下作的大部分事情,而且感到比较顺手。

相关文章
相关标签/搜索