7.1 Git 工具 – 选择修订版本
现在,你已经学习了管理或者维护 Git 仓库、实现代码控制所需的大多数日常命令和工作流程。 你已经尝试了跟踪和提交文件的基本操作,并且发挥了暂存区和轻量级的分支及合并的威力。
选择修订版本
Git 允许你通过几种方法来指明特定的或者一定范围内的提交。 了解它们并不是必需的,但是了解一下总没坏处。
单个修订版本
你可以通过 Git 给出的 SHA-1 值来获取一次提交,不过还有很多更人性化的方式来做同样的事情。 本节将会介绍获取单个提交的多种方法。
简短的 SHA-1
Git 十分智能,你只需要提供 SHA-1 的前几个字符就可以获得对应的那次提交,当然你提供的 SHA-1 字符数量不得少于 4 个,并且没有歧义——也就是说,当前仓库中只有一个对象以这段 SHA-1 开头。
例如查看一次指定的提交,假设你执行 git log 命令来查看之前新增一个功能的那次提交:
$ 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 这个提交,下面的命令是等价的(假设简短的版本没有歧义):
$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b $ git show 1c002dd4b536e7479f $ git show 1c002d
Git 可以为 SHA-1 值生成出简短且唯一的缩写。 如果你在 git log 后加上 –abbrev-commit 参数,输出结果里就会显示简短且唯一的值;默认使用七个字符,不过有时为了避免 SHA-1 的歧义,会增加字符数:
$ git log --abbrev-commit --pretty=oneline ca82a6d changed the version number 085bb3b removed unnecessary test code a11bef0 first commit
通常 8 到 10 个字符就已经足够在一个项目中避免 SHA-1 的歧义。
比如 Linux 内核这个相当大的 Git 项目,目前有超过 45 万个提交,包含 360 万个对象,也只需要前 11 个字符就能保证唯一性。
NOTE
关于 SHA-1 的简短说明
许多人觉得他们的仓库里有可能出现两个 SHA-1 值相同的对象。 然后呢?
如果你真的向仓库里提交了一个跟之前的某个对象具有相同 SHA-1 值的对象,Git 发现仓库里已经存在了拥有相同 HASH 值的对象,就会认为这个新的提交是已经被写入仓库的。 如果之后你想检出那个对象时,你将得到先前那个对象的数据。
但是这种情况发生的概率十分渺小。 SHA-1 摘要长度是 20 字节,也就是 160 位。 280 个随机哈希对象才有 50% 的概率出现一次冲突 (计算冲突机率的公式是 p = (n(n-1)/2) * (1/2^160)) )。280 是 1.2 x 10^24 也就是一亿亿亿。 那是地球上沙粒总数的 1200 倍。
举例说一下怎样才能产生一次 SHA-1 冲突。 如果地球上 65 亿个人类都在编程,每人每秒都在产生等价于整个 Linux 内核历史(360 万个 Git 对象)的代码,并将之提交到一个巨大的 Git 仓库里面,这样持续两年的时间才会产生足够的对象,使其拥有 50% 的概率产生一次 SHA-1 对象冲突。 这要比你编程团队的成员同一个晚上在互不相干的意外中被狼袭击并杀死的机率还要小。
分支引用
指明一次提交最直接的方法是有一个指向它的分支引用。 这样你就可以在任意一个 Git 命令中使用这个分支名来代替对应的提交对象或者 SHA-1 值。 例如,你想要查看一个分支的最后一次提交的对象,假设topic1 分支指向 ca82a6d ,那么以下的命令是等价的:
$ git show ca82a6dff817ec66f44342007202690a93763949 $ git show topic1
如果你想知道某个分支指向哪个特定的 SHA-1,或者想看任何一个例子中被简写的 SHA-1 ,你可以使用一个叫做 rev-parse 的 Git 探测工具。 你可以在 Git 内部原理 中查看更多关于探测工具的信息。简单来说,rev-parse 是为了底层操作而不是日常操作设计的。 不过,有时你想看 Git 现在到底处于什么状态时,它可能会很有用。 你可以在你的分支上执行 rev-parse
$ git rev-parse topic1 ca82a6dff817ec66f44342007202690a93763949
引用日志
当你在工作时, Git 会在后台保存一个引用日志(reflog),引用日志记录了最近几个月你的 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
每当你的 HEAD 所指向的位置发生了变化,Git 就会将这个信息存储到引用日志这个历史记录里。 通过这些数据,你可以很方便地获取之前的提交历史。 如果你想查看仓库中 HEAD 在五次前的所指向的提交,你可以使用 @{n} 来引用 reflog 中输出的提交记录。
$ git show HEAD@{5}
你同样可以使用这个语法来查看某个分支在一定时间前的位置。 例如,查看你的 master 分支在昨天的时候指向了哪个提交,你可以输入
$ git show master@{yesterday}
就会显示昨天该分支的顶端指向了哪个提交。 这个方法只对还在你引用日志里的数据有用,所以不能用来查好几个月之前的提交。
可以运行 git log -g 来查看类似于 git log 输出格式的引用日志信息:
$ 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 的第二父提交” 这个语法只适用于合并(merge)的提交,因为合并提交会有多个父提交。 第一父提交是你合并时所在分支,而第二父提交是你所合并的分支:
$ 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 代表 “第一父提交的第一父提交”,也就是 “祖父提交” —— Git 会根据你指定的次数获取对应的第一父提交。 例如,在之前的列出的提交历史中,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 选出在一个分支中而不在另一个分支中的提交。 例如,你有如下的提交历史 Figure 7-1
你想要查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。 你可以使用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 来指明你不希望提交被包含其中的分支。 因此下列3个命令是等价的:
$ git log refA..refB $ git log ^refA refB $ git log refB --not refA
这个语法很好用,因为你可以在查询中指定超过两个的引用,这是双点语法无法实现的。 比如,你想查看所有被 refA 或 refB 包含的但是不被 refC 包含的提交,你可以输入下面中的任意一个命令
$ git log refA refB ^refC $ git log refA refB --not refC
这就构成了一个十分强大的修订查询系统,你可以通过它来查看你的分支里包含了哪些东西。
三点
最后一种主要的区间选择语法是三点,这个语法可以选择出被两个引用中的一个包含但又不被两者同时包含的提交。 再看看之前双点例子中的提交历史。 如果你想看 master 或者 experiment 中包含的但不是两者共有的提交,你可以执行
$ git log master...experiment F E D C
这和通常 log 按日期排序的输出一样,仅仅给出了4个提交的信息。
这种情形下,log 命令的一个常用参数是 –left-right,它会显示每个提交到底处于哪一侧的分支。 这会让输出数据更加清晰。
$ git log --left-right master...experiment < F < E > D > C
有了这些工具,你就可以十分方便地查看你 Git 仓库中的提交。
7.2 Git 工具 – 交互式暂存
交互式暂存
Git 自带的一些脚本可以使在命令行下工作更容易。 本节的几个互交命令可以帮助你将文件的特定部分组合成提交。 当你修改一组文件后,希望这些改动能放到若干提交而不是混杂在一起成为一个提交时,这几个工具会非常有用。 通过这种方式,可以确保提交是逻辑上独立的变更集,同时也会使其他开发者在与你工作时很容易地审核。 如果运行 git add 时使用 -i 或者 –interactive 选项,Git 将会进入一个交互式终端模式,显示类似下面的东西:
$ 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(撤消)选项:
*** 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(区别)命令。 它会显示暂存文件的一个列表,可以从中选择想要查看的暂存区别。 这跟你在命令行指定 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(补丁)。 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 来启动同样的脚本。
更进一步地,可以使用 reset –patch 命令的补丁模式来部分重置文件,通过 checkout –patch命令来部分检出文件与 stash save –patch 命令来部分暂存文件。 我们将会在接触这些命令的高级使用方法时了解更多详细信息。
7.3 Git 工具 – 储藏与清理
储藏与清理
有时,当你在项目的一部分上已经工作一段时间后,所有东西都进入了混乱的状态,而这时你想要切换到另一个分支做一点别的事情。 问题是,你不想仅仅因为过会儿回到这一点而为做了一半的工作创建一次提交。 针对这个问题的答案是 git stash 命令。
储藏会处理工作目录的脏的状态 – 即,修改的跟踪文件与暂存改动 – 然后将未完成的修改保存到一个栈上,而你可以在任何时候重新应用这些改动。
储藏工作
为了演示,进入项目并改动几个文件,然后可能暂存其中的一个改动。 如果运行 git status,可以看到有改动的状态:
$ git status Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: index.html Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: lib/simplegit.rb
现在想要切换分支,但是还不想要提交之前的工作;所以储藏修改。 将新的储藏推送到栈上,运行 git stash 或 git stash save:
$ 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 会产生合并冲突。
文件的改动被重新应用了,但是之前暂存的文件却没有重新暂存。 想要那样的话,必须使用 –index选项来运行 git stash apply 命令,来尝试重新应用暂存的修改。 如果已经那样做了,那么你将回到原来的位置:
$ 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 #
应用选项只会尝试应用暂存的工作 – 在堆栈上还有它。 可以运行 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 来应用储藏然后立即从栈上扔掉它。
创造性的储藏
有几个储藏的变种可能也很有用。 第一个非常流行的选项是 stash save 命令的 –keep-index 选项。 它告诉 Git 不要储藏任何你通过 git add 命令已暂存的东西。
当你做了几个改动并只想提交其中的一部分,过一会儿再回来处理剩余改动时,这个功能会很有用。
$ git status -s M index.html M lib/simplegit.rb $ git stash --keep-index Saved working directory and index state WIP on master: 1b65b17 added the index file HEAD is now at 1b65b17 added the index file $ git status -s M index.html
另一个经常使用储藏来做的事情是像储藏跟踪文件一样储藏未跟踪文件。 默认情况下,git stash 只会储藏已经在索引中的文件。 如果指定 –include-untracked 或 -u 标记,Git 也会储藏任何创建的未跟踪文件。
$ git status -s M index.html M lib/simplegit.rb ?? new-file.txt $ git stash -u Saved working directory and index state WIP on master: 1b65b17 added the index file HEAD is now at 1b65b17 added the index file $ git status -s $
最终,如果指定了 –patch 标记,Git 不会储藏所有修改过的任何东西,但是会交互式地提示哪些改动想要储藏、哪些改动需要保存在工作目录中。
$ git stash --patch diff --git a/lib/simplegit.rb b/lib/simplegit.rb index 66d332e..8bb5674 100644 --- a/lib/simplegit.rb +++ b/lib/simplegit.rb @@ -16,6 +16,10 @@ class SimpleGit return `#{git_cmd} 2>&1`.chomp end end + + def show(treeish = 'master') + command("git show #{treeish}") + end end test Stash this hunk [y,n,q,a,d,/,e,?]? y Saved working directory and index state WIP on master: 1b65b17 added the index file
从储藏创建一个分支
如果储藏了一些工作,将它留在那儿了一会儿,然后继续在储藏的分支上工作,在重新应用工作时可能会有问题。 如果应用尝试修改刚刚修改的文件,你会得到一个合并冲突并不得不解决它。 如果想要一个轻松的方式来再次测试储藏的改动,可以运行 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)
这是在新分支轻松恢复储藏工作并继续工作的一个很不错的途径。
清理工作目录
对于工作目录中一些工作或文件,你想做的也许不是储藏而是移除。 git clean 命令会帮你做这些事。
有一些通用的原因比如说为了移除由合并或外部工具生成的东西,或是为了运行一个干净的构建而移除之前构建的残留。
你需要谨慎地使用这个命令,因为它被设计为从工作目录中移除未被追踪的文件。 如果你改变主意了,你也不一定能找回来那些文件的内容。 一个更安全的选项是运行 git stash –all 来移除每一样东西并存放在栈中。
你可以使用git clean命令去除冗余文件或者清理工作目录。 使用git clean -f -d命令来移除工作目录中所有未追踪的文件以及空的子目录。 -f 意味着 强制 或 “确定移除”。
如果只是想要看看它会做什么,可以使用 -n 选项来运行命令,这意味着 “做一次演习然后告诉你 将要移除什么”。
$ git clean -d -n Would remove test.o Would remove tmp/
默认情况下,git clean 命令只会移除没有忽略的未跟踪文件。 任何与 .gitiignore 或其他忽略文件中的模式匹配的文件都不会被移除。 如果你也想要移除那些文件,例如为了做一次完全干净的构建而移除所有由构建生成的 .o 文件,可以给 clean 命令增加一个 -x 选项。
$ git status -s M lib/simplegit.rb ?? build.TMP ?? tmp/ $ git clean -n -d Would remove build.TMP Would remove tmp/ $ git clean -n -d -x Would remove build.TMP Would remove test.o Would remove tmp/
如果不知道 git clean 命令将会做什么,在将 -n 改为 -f 来真正做之前总是先用 -n 来运行它做双重检查。 另一个小心处理过程的方式是使用 -i 或 “interactive” 标记来运行它。
这将会以交互模式运行 clean 命令。
$ git clean -x -i Would remove the following items: build.TMP test.o *** Commands *** 1: clean 2: filter by pattern 3: select by numbers 4: ask each 5: quit 6: help What now>
这种方式下可以分别地检查每一个文件或者交互地指定删除的模式。
7.4 Git 工具 – 签署工作
签署工作
Git 虽然是密码级安全的,但它不是万无一失的。 如果你从因特网上的其他人那里拿取工作,并且想要验证提交是不是真正地来自于可信来源,Git 提供了几种通过 GPG 来签署和验证工作的方式。
GPG 介绍
首先,在开始签名之前你需要先配置 GPG 并安装个人密钥。
$ gpg --list-keys /Users/schacon/.gnupg/pubring.gpg --------------------------------- pub 2048R/0A46826A 2014-06-04 uid Scott Chacon (Git signing key) <schacon@gmail.com> sub 2048R/874529A9 2014-06-04
如果你还没有安装一个密钥,可以使用 gpg –gen-key 生成一个。
gpg --gen-key
一旦你有一个可以签署的私钥,可以通过设置 Git 的 user.signingkey 选项来签署。
git config --global user.signingkey 0A46826A
现在 Git 默认使用你的密钥来签署标签与提交。
签署标签
如果已经设置好一个 GPG 私钥,可以使用它来签署新的标签。 所有需要做的只是使用 -s 代替 -a 即可:
$ git tag -s v1.5 -m 'my signed 1.5 tag' You need a passphrase to unlock the secret key for user: "Ben Straub <ben@straub.cc>" 2048-bit RSA key, ID 800430EB, created 2014-05-04
如果在那个标签上运行 git show,会看到你的 GPG 签名附属在后面:
$ git show v1.5 tag v1.5 Tagger: Ben Straub <ben@straub.cc> Date: Sat May 3 20:29:41 2014 -0700 my signed 1.5 tag -----BEGIN PGP SIGNATURE----- Version: GnuPG v1 iQEcBAABAgAGBQJTZbQlAAoJEF0+sviABDDrZbQH/09PfE51KPVPlanr6q1v4/Ut LQxfojUWiLQdg2ESJItkcuweYg+kc3HCyFejeDIBw9dpXt00rY26p05qrpnG+85b hM1/PswpPLuBSr+oCIDj5GMC2r2iEKsfv2fJbNW8iWAXVLoWZRF8B0MfqX/YTMbm ecorc4iXzQu7tupRihslbNkfvfciMnSDeSvzCpWAHl7h8Wj6hhqePmLm9lAYqnKp 8S5B/1SSQuEAjRZgI4IexpZoeKGVDptPHxLLS38fozsyi0QyDyzEgJxcJQVMXxVi RUysgqjcpT8+iQM1PblGfHR4XAhuOqN5Fx06PSaFZhqvWFezJ28/CLyX5q+oIVk= =EFTF -----END PGP SIGNATURE----- commit ca82a6dff817ec66f44342007202690a93763949 Author: Scott Chacon <schacon@gee-mail.com> Date: Mon Mar 17 21:52:11 2008 -0700 changed the version number
验证标签
要验证一个签署的标签,可以运行 git tag -v [tag-name]。 这个命令使用 GPG 来验证签名。 为了验证能正常工作,签署者的公钥需要在你的钥匙链中。
$ git tag -v v1.4.2.1 object 883653babd8ee7ea23e6a5c392bb739348b1eb61 type commit tag v1.4.2.1 tagger Junio C Hamano <junkio@cox.net> 1158138501 -0700 GIT 1.4.2.1 Minor fixes since 1.4.2, including git-mv and git-http with alternates. gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A gpg: Good signature from "Junio C Hamano <junkio@cox.net>" gpg: aka "[jpeg image of size 1513]" Primary key fingerprint: 3565 2A26 2040 E066 C9A7 4A7D C0C6 D9A4 F311 9B9A
如果没有签署者的公钥,那么你将会得到类似下面的东西:
gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A gpg: Can't check signature: public key not found error: could not verify the tag 'v1.4.2.1'
签署提交
在最新版本的 Git 中(v1.7.9 及以上),也可以签署个人提交。 如果相对于标签而言你对直接签署到提交更感兴趣的话,所有要做的只是增加一个 -S 到 git commit 命令。
$ git commit -a -S -m 'signed commit' You need a passphrase to unlock the secret key for user: "Scott Chacon (Git signing key) <schacon@gmail.com>" 2048-bit RSA key, ID 0A46826A, created 2014-06-04 [master 5c3386c] signed commit 4 files changed, 4 insertions(+), 24 deletions(-) rewrite Rakefile (100%) create mode 100644 lib/git.rb
git log 也有一个 –show-signature 选项来查看及验证这些签名。
$ git log --show-signature -1 commit 5c3386cf54bba0a33a32da706aa52bc0155503c2 gpg: Signature made Wed Jun 4 19:49:17 2014 PDT using RSA key ID 0A46826A gpg: Good signature from "Scott Chacon (Git signing key) <schacon@gmail.com>" Author: Scott Chacon <schacon@gmail.com> Date: Wed Jun 4 19:49:17 2014 -0700 signed commit
另外,也可以配置 git log 来验证任何找到的签名并将它们以 %G? 格式列在输出中。
$ git log --pretty="format:%h %G? %aN %s" 5c3386c G Scott Chacon signed commit ca82a6d N Scott Chacon changed the version number 085bb3b N Scott Chacon removed unnecessary test code a11bef0 N Scott Chacon first commit
这里我们可以看到只有最后一次提交是签署并有效的,而之前的提交都不是。
在 Git 1.8.3 及以后的版本中,“git merge” 与“git pull” 可以使用 –verify-signatures 选项来检查并拒绝没有携带可信 GPG 签名的提交。
如果使用这个选项来合并一个包含未签名或有效的提交的分支时,合并不会生效。
$ git merge --verify-signatures non-verify fatal: Commit ab06180 does not have a GPG signature.
如果合并包含的只有有效的签名的提交,合并命令会提示所有的签名它已经检查过了然后会继续向前。
$ git merge --verify-signatures signed-branch Commit 13ad65e has a good GPG signature by Scott Chacon (Git signing key) <schacon@gmail.com> Updating 5c3386c..13ad65e Fast-forward README | 2 ++ 1 file changed, 2 insertions(+)
也可以给 git merge 命令附加 -S 选项来签署自己生成的合并提交。 下面的例子演示了验证将要合并的分支的每一个提交都是签名的并且签署最后生成的合并提交。
$ git merge --verify-signatures -S signed-branch Commit 13ad65e has a good GPG signature by Scott Chacon (Git signing key) <schacon@gmail.com> You need a passphrase to unlock the secret key for user: "Scott Chacon (Git signing key) <schacon@gmail.com>" 2048-bit RSA key, ID 0A46826A, created 2014-06-04 Merge made by the 'recursive' strategy. README | 2 ++ 1 file changed, 2 insertions(+)
每个人必须签署
签署标签与提交很棒,但是如果决定在正常的工作流程中使用它,你必须确保团队中的每一个人都理解如何这样做。 如果没有,你将会花费大量时间帮助其他人找出并用签名的版本重写提交。 在采用签署成为标准工作流程的一部分前,确保你完全理解 GPG 及签署带来的好处。
7.5 Git 工具 – 搜索
搜索
无论仓库里的代码量有多少,你经常需要查找一个函数是在哪里调用或者定义的,或者一个方法的变更历史。 Git 提供了两个有用的工具来快速地从它的数据库中浏览代码和提交。 我们来简单的看一下。
Git Grep
Git 提供了一个 grep 命令,你可以很方便地从提交历史或者工作目录中查找一个字符串或者正则表达式。 我们用 Git 本身源代码的查找作为例子。
默认情况下 Git 会查找你工作目录的文件。 你可以传入 -n 参数来输出 Git 所找到的匹配行行号。
$ git grep -n gmtime_r compat/gmtime.c:3:#undef gmtime_r compat/gmtime.c:8: return git_gmtime_r(timep, &result); compat/gmtime.c:11:struct tm *git_gmtime_r(const time_t *timep, struct tm *result) compat/gmtime.c:16: ret = gmtime_r(timep, result); compat/mingw.c:606:struct tm *gmtime_r(const time_t *timep, struct tm *result) compat/mingw.h:162:struct tm *gmtime_r(const time_t *timep, struct tm *result); date.c:429: if (gmtime_r(&now, &now_tm)) date.c:492: if (gmtime_r(&time, tm)) { git-compat-util.h:721:struct tm *git_gmtime_r(const time_t *, struct tm *); git-compat-util.h:723:#define gmtime_r git_gmtime_r
grep 命令有一些有趣的选项。
例如,你可以使用 –count 选项来使 Git 输出概述的信息,仅仅包括哪些文件包含匹配以及每个文件包含了多少个匹配。
$ git grep --count gmtime_r compat/gmtime.c:4 compat/mingw.c:1 compat/mingw.h:1 date.c:2 git-compat-util.h:2
如果你想看匹配的行是属于哪一个方法或者函数,你可以传入 -p 选项:
$ git grep -p gmtime_r *.c date.c=static int match_multi_number(unsigned long num, char c, const char *date, char *end, struct tm *tm) date.c: if (gmtime_r(&now, &now_tm)) date.c=static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt) date.c: if (gmtime_r(&time, tm)) {
在这里我们可以看到在 date.c 文件中有 match_multi_number 和 match_digit 两个函数调用了gmtime_r。
你还可以使用 –and 标志来查看复杂的字符串组合,也就是在同一行同时包含多个匹配。 比如,我们要查看在旧版本 1.8.0 的 Git 代码库中定义了常量名包含 “LINK” 或者 “BUF_MAX” 这两个字符串所在的行。
这里我们也用到了 –break 和 –heading 选项来使输出更加容易阅读。
$ git grep --break --heading \ -n -e '#define' --and \( -e LINK -e BUF_MAX \) v1.8.0 v1.8.0:builtin/index-pack.c 62:#define FLAG_LINK (1u<<20) v1.8.0:cache.h 73:#define S_IFGITLINK 0160000 74:#define S_ISGITLINK(m) (((m) & S_IFMT) == S_IFGITLINK) v1.8.0:environment.c 54:#define OBJECT_CREATION_MODE OBJECT_CREATION_USES_HARDLINKS v1.8.0:strbuf.c 326:#define STRBUF_MAXLINK (2*PATH_MAX) v1.8.0:symlinks.c 53:#define FL_SYMLINK (1 << 2) v1.8.0:zlib.c 30:/* #define ZLIB_BUF_MAX ((uInt)-1) */ 31:#define ZLIB_BUF_MAX ((uInt) 1024 * 1024 * 1024) /* 1GB */
相比于一些常用的搜索命令比如 grep 和 ack,git grep 命令有一些的优点。 第一就是速度非常快,第二是你不仅仅可以可以搜索工作目录,还可以搜索任意的 Git 树。 在上一个例子中,我们在一个旧版本的 Git 源代码中查找,而不是当前检出的版本。
Git 日志搜索
或许你不想知道某一项在 哪里 ,而是想知道是什么 时候 存在或者引入的。 git log 命令有许多强大的工具可以通过提交信息甚至是 diff 的内容来找到某个特定的提交。
例如,如果我们想找到 ZLIB_BUF_MAX 常量是什么时候引入的,我们可以使用 -S 选项来显示新增和删除该字符串的提交。
$ git log -SZLIB_BUF_MAX --oneline e01503b zlib: allow feeding more than 4GB in one go ef49a7a zlib: zlib can only process 4GB at a time
如果我们查看这些提交的 diff,我们可以看到在 ef49a7a 这个提交引入了常量,并且在 e01503b 这个提交中被修改了。
如果你希望得到更精确的结果,你可以使用 -G 选项来使用正则表达式搜索。
行日志搜索
行日志搜索是另一个相当高级并且有用的日志搜索功能。 这是一个最近新增的不太知名的功能,但却是十分有用。 在 git log 后加上 -L 选项即可调用,它可以展示代码中一行或者一个函数的历史。
例如,假设我们想查看 zlib.c 文件中git_deflate_bound 函数的每一次变更,我们可以执行 git log -L :git_deflate_bound:zlib.c。 Git 会尝试找出这个函数的范围,然后查找历史记录,并且显示从函数创建之后一系列变更对应的补丁。
$ git log -L :git_deflate_bound:zlib.c commit ef49a7a0126d64359c974b4b3b71d7ad42ee3bca Author: Junio C Hamano <gitster@pobox.com> Date: Fri Jun 10 11:52:15 2011 -0700 zlib: zlib can only process 4GB at a time diff --git a/zlib.c b/zlib.c --- a/zlib.c +++ b/zlib.c @@ -85,5 +130,5 @@ -unsigned long git_deflate_bound(z_streamp strm, unsigned long size) +unsigned long git_deflate_bound(git_zstream *strm, unsigned long size) { - return deflateBound(strm, size); + return deflateBound(&strm->z, size); } commit 225a6f1068f71723a910e8565db4e252b3ca21fa Author: Junio C Hamano <gitster@pobox.com> Date: Fri Jun 10 11:18:17 2011 -0700 zlib: wrap deflateBound() too diff --git a/zlib.c b/zlib.c --- a/zlib.c +++ b/zlib.c @@ -81,0 +85,5 @@ +unsigned long git_deflate_bound(z_streamp strm, unsigned long size) +{ + return deflateBound(strm, size); +} +
如果 Git 无法计算出如何匹配你代码中的函数或者方法,你可以提供一个正则表达式。 例如,这个命令和上面的是等同的:git log -L ‘/unsigned long git_deflate_bound/’,/^}/:zlib.c。 你也可以提供单行或者一个范围的行号来获得相同的输出。