1. Pro Git
Scott Chacon*
2010-03-25
*
This is the PDF file for the Pro Git book contents. It is licensed under the Creative Commons Attribution-
Non Commercial-Share Alike 3.0 license. I hope you enjoy it, I hope it helps you learn Git, and I hope you’ll
support Apress and me by purchasing a print copy of the book at Amazon: http://tinyurl.com/amazonprogit
23. Scott Chacon Pro Git 2.2节 记录每次更新到仓库
2.2.1 检查当前文件状态
要确定哪些文件当前处于什么状态,可以用 git status 命令。如果在克隆仓库之后立即执行此命令,会看
到类似这样的输出:
$ git status
# On branch master
nothing to commit (working directory clean)
这说明你现在的工作目录相当干净。换句话说,当前没有任何跟踪着的文件,也没有任何文件在上次提交后
更改过。此外,上面的信息还表明,当前目录下没有出现任何处于未跟踪的新文件,否则 Git 会在这里列出
来。最后,该命令还显示了当前所在的分支是 master,这是默认的分支名称,实际是可以修改的,现在不必
多虑。下一章我们就会详细讨论分支和引用。
现在让我们用 vim 编辑一个新文件 README,保存退出后运行 git status 会看到该文件出现在未跟踪文件列
表中:
$ vim README
$ git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# README
nothing added to commit but untracked files present (use "git add" to track)
就是在“Untracked files”这行下面。Git 不会自动将之纳入跟踪范围,除非你明明白白地告诉它这么
做,因而不用担心把临时文件什么的也归入版本管理。不过现在我们确实想要跟踪管理 README 这个文件。
2.2.2 跟踪新文件
使用命令 git add 开始跟踪一个新文件。所以,要跟踪 README 文件,运行:
$ git add README
此时再运行 git status 命令,会看到 README 文件已被跟踪,并处于暂存状态:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
只要在 “Changes to be committed” 这行下面的,就说明是已暂存状态。如果此时提交,那么该文件此
时此刻的版本将被留存在历史记录中。你可能会想起之前我们使用 git init 后就运行了 git add 命令,开始跟
踪当前目录下的文件。git add 后可以接要跟踪的文件或目录的路径。如果是目录的话,就说明要递归跟踪所
有该目录下的文件。
13
24. 第2章 Git 基础 Scott Chacon Pro Git
2.2.3 暂存已修改文件
现在我们修改下之前已跟踪过的文件 benchmarks.rb,然后再次运行 status 命令,会看到这样的状态报告:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#
文件 benchmarks.rb 出现在 “Changed but not updated” 这行下面,说明已跟踪文件的内容发生了变
化,但还没有放到暂存区。要暂存这次更新,需要运行 git add 命令(这是个多功能命令,根据目标文件的状
态不同,此命令的效果也不同:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时
把有冲突的文件标记为已解决状态等)。现在让我们运行 git add 将 benchmarks.rb 放到暂存区,然后再看
看 git status 的输出:
$ git add benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
现在两个文件都已暂存,下次提交时就会一并记录到仓库。假设此时,你想要在 benchmarks.rb 里再加条
注释,重新编辑存盘后,准备好提交。不过且慢,再运行 git status 看看:
$ vim benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#
见鬼!benchmarks.rb 文件出现了两次!一次算未暂存,一次算已暂存,这怎么可能呢?好吧,实际上 Git
只不过暂存了你运行 git add 命令时的版本,如果现在提交,那么提交的是添加注释前的版本,而非当前工
14
26. 第2章 Git 基础 Scott Chacon Pro Git
2.2.5 查看已暂存和未暂存的更新
实际上 git status 的显示比较简单,仅仅是列出了修改过的文件,如果要查看具体修改了什么地方,可以用
git diff 命令。稍后我们会详细介绍 git diff,不过现在,它已经能回答我们的两个问题了:当前作的哪些更
新还没有暂存?有哪些更新已经暂存起来准备好了下次提交? git diff 会使用文件补丁的格式显示具体添加和
删除的行。
假如再次修改 README 文件后暂存,然后编辑 benchmarks.rb 文件后先别暂存,运行 status 命令,会看
到:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#
要查看尚未暂存的文件更新了哪些部分,不加参数直接输入 git diff:
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..da65585 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
run_code(x, 'commits 2') do
log = git.commits('master', 15)
log.size
此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内
容。
若要看已经暂存起来的文件和上次提交时的快照之间的差异,可以用 git diff --cached 命令。(Git 1.6.1
及更高版本还允许使用 git diff --staged,效果是相同的,但更好记些。)来看看实际的效果:
$ git diff --cached
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
16
27. Scott Chacon Pro Git 2.2节 记录每次更新到仓库
+++ b/README2
@@ -0,0 +1,5 @@
+grit
+ by Tom Preston-Werner, Chris Wanstrath
+ http://github.com/mojombo/grit
+
+Grit is a Ruby library for extracting information from a Git repository
请注意,单单 git diff 不过是显示还没有暂存起来的改动,而不是这次工作和上次提交之间的差异。所以有
时候你一下子暂存了所有更新过的文件后,运行 git diff 后却什么也没有,就是这个原因。
像之前说的,暂存 benchmarks.rb 后再编辑,运行 git status 会看到暂存前后的两个版本:
$ git add benchmarks.rb
$ echo '# test line' >> benchmarks.rb
$ git status
# On branch master
#
# Changes to be committed:
#
# modified: benchmarks.rb
#
# Changed but not updated:
#
# modified: benchmarks.rb
#
现在运行 git diff 看暂存前后的变化:
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index e445e28..86b2f7c 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -127,3 +127,4 @@ end
main()
##pp Grit::GitRuby.cache_client.stats
+# test line
and git diff --cached to see what you’ve staged so far:
$ git diff --cached
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..e445e28 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
run_code(x, 'commits 2') do
17
28. 第2章 Git 基础 Scott Chacon Pro Git
log = git.commits('master', 15)
log.size
2.2.6 提交更新
现在的暂存区域已经准备妥当可以提交了。在此之前,请一定要确认还有什么修改过的或新建的文件还没有
git add 过,否则提交的时候不会记录这些还没暂存起来的变化。所以,每次准备提交前,先用 git status 看
下,是不是都已暂存起来了,然后再运行提交命令 git commit:
$ git commit
这种方式会启动文本编辑器以便输入本次提交的说明。(默认会启用 shell 的环境变量 $EDITOR 所指定的
软件,一般都是 vim 或 emacs。当然也可以按照第一章介绍的方式,使用 git config --global core.editor 命令
设定你喜欢的编辑软件。)
编辑器会显示类似下面的文本信息(本例选用 Vim 的屏显方式展示):
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
~
~
~
".git/COMMIT_EDITMSG" 10L, 283C
可以看到,默认的提交消息包含最后一次运行 git status 的输出,放在注释行里,另外开头还有一空行,供
你输入提交说明。你完全可以去掉这些注释行,不过留着也没关系,多少能帮你回想起这次更新的内容有哪
些。(如果觉得这还不够,可以用 -v 选项将修改差异的每一行都包含到注释中来。)退出编辑器时,Git 会
丢掉注释行,将说明内容和本次更新提交到仓库。
也可以使用 -m 参数后跟提交说明的方式,在一行命令中提交更新:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master]: created 463dc4f: "Fix benchmarks for speed"
2 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 README
好,现在你已经创建了第一个提交!可以看到,提交后它会告诉你,当前是在哪个分支(master)提交的,
本次提交的完整 SHA-1 校验和是什么(463dc4f),以及在本次提交中,有多少文件修订过,多少行添改和删
改过。
记住,提交时记录的是放在暂存区域的快照,任何还未暂存的仍然保持已修改状态,可以在下次提交时纳入
版本管理。每一次运行提交操作,都是对你项目作一次快照,以后可以回到这个状态,或者进行比较。
18
29. Scott Chacon Pro Git 2.2节 记录每次更新到仓库
2.2.7 跳过使用暂存区域
尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。Git 提供了一个跳过使用
暂存区域的方式,只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂
存起来一并提交,从而跳过 git add 步骤:
$ git status
# On branch master
#
# Changed but not updated:
#
# modified: benchmarks.rb
#
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
1 files changed, 5 insertions(+), 0 deletions(-)
看到了吗?提交之前不再需要 git add 文件 benchmarks.rb 了。
2.2.8 移除文件
要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提
交。可以用 git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪
文件清单中了。
如果只是简单地从工作目录中手工删除文件,运行 git status 时就会在 “Changed but not updated” 部
分(也就是_未暂存_清单)看到:
$ rm grit.gemspec
$ git status
# On branch master
#
# Changed but not updated:
# (use "git add/rm <file>..." to update what will be committed)
#
# deleted: grit.gemspec
#
然后再运行 git rm 记录此次移除文件的操作:
$ git rm grit.gemspec
rm 'grit.gemspec'
$ git status
# On branch master
#
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# deleted: grit.gemspec
#
19
36. 第2章 Git 基础 Scott Chacon Pro Git
图 2.2: gitk 的图形界面
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
上面的三条命令最终得到一个提交,第二个提交命令修正了第一个的提交内容。
2.4.2 取消已经暂存的文件
接下来的两个小节将演示如何取消暂存区域中的文件,以及如何取消工作目录中已修改的文件。不用担心,
查看文件状态的时候就提示了该如何撤消,所以不需要死记硬背。来看下面的例子,有两个修改过的文件,我
们想要分开提交,但不小心用 git add * 全加到了暂存区域。该如何撤消暂存其中的一个文件呢?git status 命
令的输出会告诉你怎么做:
$ git add .
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: README.txt
26
37. Scott Chacon Pro Git 2.4节 撤消操作
# modified: benchmarks.rb
#
就在 “Changes to be committed” 下面,括号中有提示,可以使用 git reset HEAD <file>... 的方式取消暂
存。好吧,我们来试试取消暂存 benchmarks.rb 文件:
$ git reset HEAD benchmarks.rb
benchmarks.rb: locally modified
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: README.txt
#
# 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: benchmarks.rb
#
这条命令看起来有些古怪,先别管,能用就行。现在 benchmarks.rb 文件又回到了之前已修改未暂存的状
态。
2.4.3 取消对文件的修改
如果觉得刚才对 benchmarks.rb 的修改完全没有必要,该如何取消修改,回到之前的状态(也就是修改之
前的版本)呢?git status 同样提示了具体的撤消方法,接着上面的例子,现在未暂存区域看起来像这样:
# 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: benchmarks.rb
#
在第二个括号中,我们看到了抛弃文件修改的命令(至少在 Git 1.6.1 以及更高版本中会这样提示,如果
你还在用老版本,我们强烈建议你升级,以获取最佳的用户体验),让我们试试看:
$ git checkout -- benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: README.txt
#
可以看到,该文件已经恢复到修改前的版本。你可能已经意识到了,这条命令有些危险,所有对文件的修改
都没有了,因为我们刚刚把之前版本的文件复制过来重写了此文件。所以在用这条命令前,请务必确定真的不
27
42. 第2章 Git 基础 Scott Chacon Pro Git
2.6.2 新建标签
Git 使用的标签有两种类型:轻量级的(lightweight)和含附注的(annotated)。轻量级标签就像是个不
会变化的分支,实际上它就是个指向特定提交对象的引用。而含附注标签,实际上是存储在仓库中的一个独立
对象,它有自身的校验和信息,包含着标签的名字,电子邮件地址和日期,以及标签说明,标签本身也允许使
用 GNU Privacy Guard (GPG) 来签署或验证。一般我们都建议使用含附注型的标签,以便保留相关信息;当
然,如果只是临时性加注标签,或者不需要旁注额外信息,用轻量级标签也没问题。
2.6.3 含附注的标签
创建一个含附注类型的标签非常简单,用 -a (译注:取 annotated 的首字母)指定标签名字即可:
$ git tag -a v1.4 -m 'my version 1.4'
$ git tag
v0.1
v1.3
v1.4
而 -m 选项则指定了对应的标签说明,Git 会将此说明一同保存在标签对象中。如果在此选项后没有给出具
体的说明内容,Git 会启动文本编辑软件供你输入。
可以使用 git show 命令查看相应标签的版本信息,并连同显示打标签时的提交对象。
$ git show v1.4
tag v1.4
Tagger: Scott Chacon <schacon@gee-mail.com>
Date: Mon Feb 9 14:45:11 2009 -0800
my version 1.4
commit 15027957951b64cf874c3557a0f3547bd83b3ff6
Merge: 4a447f7... a6b4c97...
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sun Feb 8 19:02:46 2009 -0800
Merge branch 'experiment'
我们可以看到在提交对象信息上面,列出了此标签的提交者和提交时间,以及相应的标签说明。
2.6.4 签署标签
如果你有自己的私钥,还可以用 GPG 来签署标签,只需要把之前的 -a 改为 -s (译注: 取 Signed 的首
字母)即可:
$ git tag -s v1.5 -m 'my signed 1.5 tag'
You need a passphrase to unlock the secret key for
user: "Scott Chacon <schacon@gee-mail.com>"
1024-bit DSA key, ID F721C45A, created 2009-02-09
现在再运行 git show 会看到对应的 GPG 签名也附在其内:
32
43. Scott Chacon Pro Git 2.6节 打标签
$ git show v1.5
tag v1.5
Tagger: Scott Chacon <schacon@gee-mail.com>
Date: Mon Feb 9 15:22:20 2009 -0800
my signed 1.5 tag
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.8 (Darwin)
iEYEABECAAYFAkmQurIACgkQON3DxfchxFr5cACeIMN+ZxLKggJQf0QYiQBwgySN
Ki0An2JeAVUCAiJ7Ox6ZEtK+NvZAj82/
=WryJ
-----END PGP SIGNATURE-----
commit 15027957951b64cf874c3557a0f3547bd83b3ff6
Merge: 4a447f7... a6b4c97...
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sun Feb 8 19:02:46 2009 -0800
Merge branch 'experiment'
稍后我们再学习如何验证已经签署的标签。
2.6.5 轻量级标签
轻量级标签实际上就是一个保存着对应提交对象的校验和信息的文件。要创建这样的标签,一个 -a,-s 或
-m 选项都不用,直接给出标签名字即可:
$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5
现在运行 git show 查看此标签信息,就只有相应的提交对象摘要:
$ git show v1.4-lw
commit 15027957951b64cf874c3557a0f3547bd83b3ff6
Merge: 4a447f7... a6b4c97...
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sun Feb 8 19:02:46 2009 -0800
Merge branch 'experiment'
2.6.6 验证标签
可以使用 git tag -v [tag-name] (译注:取 verify 的首字母)的方式验证已经签署的标签。此命令会调用
GPG 来验证签名,所以你需要有签署者的公钥,存放在 keyring 中,才能验证:
33
44. 第2章 Git 基础 Scott Chacon Pro Git
$ 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'
2.6.7 后期加注标签
你甚至可以在后期对早先的某次提交加注标签。比如在下面展示的提交历史中:
$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment'
a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support
0d52aaab4479697da7686c15f77a3d64d9165190 one more thing
6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment'
0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit function
4682c3261057305bdd616e23b64b0857d832627b added a todo file
166ae0c4d3f420721acbb115cc33848dfcc2121a started write support
9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile
964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo
8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme
我们忘了在提交 “updated rakefile” 后为此项目打上版本号 v1.2,没关系,现在也能做。只要在打标
签的时候跟上对应提交对象的校验和(或前几位字符)即可:
$ git tag -a v1.2 9fceb02
可以看到我们已经补上了标签:
$ git tag
v0.1
v1.2
v1.3
v1.4
v1.4-lw
34
83. Scott Chacon Pro Git 4.7节 权限管理器 Gitosis
$ git clone git://git.kernel.org/pub/scm/git/git.git
$ cd git/
$ make GITWEB_PROJECTROOT="/opt/git"
prefix=/usr gitweb/gitweb.cgi
$ sudo cp -Rf gitweb /var/www/
注意通过指定 GITWEB_PROJECTROOT 变量告诉编译命令 Git 仓库的位置。然后,让 Apache 来提供脚本的 CGI,
为此添加一个 VirtualHost:
<VirtualHost *:80>
ServerName gitserver
DocumentRoot /var/www/gitweb
<Directory /var/www/gitweb>
Options ExecCGI +FollowSymLinks +SymLinksIfOwnerMatch
AllowOverride All
order allow,deny
Allow from all
AddHandler cgi-script cgi
DirectoryIndex gitweb.cgi
</Directory>
</VirtualHost>
不难想象,GitWeb 可以使用任何兼容 CGI 的网页服务来运行;如果偏向使用其他的(译注:这里指
Apache 以外的服务),配置也不会很麻烦。现在,通过 http://gitserver 就可以在线访问仓库了,在
http://git.server 上还可以通过 HTTP 克隆和获取仓库的内容。 Again, GitWeb can be served with any
CGI capable web server; if you prefer to use something else, it shouldn’t be difficult to set up.
At this point, you should be able to visit http://gitserver/ to view your repositories online,
and you can use http://git.gitserver to clone and fetch your repositories over HTTP.
4.7 权限管理器 Gitosis
把所有用户的公钥保存在 authorized_keys 文件的做法只能暂时奏效。当用户数量到了几百人的时候,它会变
成一种痛苦。每一次都必须进入服务器的 shell,而且缺少对连接的限制——文件里的每个人都对所有项目拥
有读写权限。
现在,是时候向广泛使用的软件 Gitosis 求救了。Gitosis 简单的说就是一套用来管理 authorized_keys 文
件和实现简单连接限制的脚本。最有意思的是,该软件用来添加用户和设定权限的界面不是网页,而是一个特
殊的 Git 仓库。你只需要设定好某个项目;然后推送,Gitosis 就会随之改变服务器设定,酷吧?
Gitosis 的安装算不上傻瓜化,不过也不算太难。用 Linux 服务器架设起来最简单——以下例子中的服务
器使用 Ubuntu 8.10 系统。
Gitosis 需要使用部分 Python 工具,所以首先要安装 Python 的 setuptools 包,在 Ubuntu 中名为
python-setuptools:
$ apt-get install python-setuptools
接下来,从项目主页克隆和安装 Gitosis:
73
89. Scott Chacon Pro Git 4.9节 Git 托管服务
而是在 github.com/shacon/grit (译注:作者在 GitHub 上的用户名是 shacon)。不存在所谓某个项目的官方
版本,所以假如第一作者放弃了某个项目,它可以无缝转移到其它用户的旗下。
GitHub 同时也是一个向使用私有仓库的用户收取费用的商业公司,不过所有人都可以快捷的得到一个免费
账户并且在上面托管任意多的开源项目。我们将快速介绍一下该过程。
4.9.2 建立账户
第一个必要必要步骤是注册一个免费的账户。访问 Pricing and Signup (价格与注册)页面 http:
//github.com/plans 并点击 Free acount (免费账户)的 “Sign Up(注册)” 按钮(见图 4.2),进入
注册页面。 The first thing you need to do is set up a free user account. If you visit the Pricing
and Signup page at http://github.com/plans and click the “Sign Up” button on the Free account
(see figure 4-2), you’re taken to the signup page.
图 4.2: GitHub 服务简介页面
这里要求选择一个系统中尚未存在的用户名,提供一个与之相连的电邮地址,以及一个密码(见图 4.3)。
如果事先有准备,可以顺便提供 SSH 公钥。我们在前文中的“小型安装” 一节介绍过生成新公钥的方法。
把生成的钥匙对中的公钥粘贴到 SSH Public Key (SSH 公钥)文本框中。点击 “explain ssh keys” 链
接可以获取在所有主流操作系统上完成该步骤的介绍。 点击 “I agree,sign me up (同意条款,让我注
册)” 按钮就能进入新用户的控制面板(见图 4.4)。
然后就可以建立新仓库了。
4.9.3 建立新仓库
点击用户面板上仓库旁边的 “create a new one(新建)” 连接。进入 Create a New Repository (新
建仓库)表格(见图 4.5)。
唯一必做的仅仅是提供一个项目名称,当然也可以添加一点描述。搞定这些以后,点 “Create Repository
(建立仓库)” 按钮。新仓库就建立起来了(见图4-6)。
由于还没有提交代码,GitHub 会展示如何创建一个新项目,如何推送一个现存项目,以及如何从一个公
共的 Subversion 仓库导入项目(译注:这简直是公开挖 google code 和 sourceforge 的墙角)(见图
4.7)。
该指南和本书前文中的介绍类似。要把一个非 Git 项目变成 Git 项目,运行
79
90. 第4章 服务器上的 Git Scott Chacon Pro Git
图 4.3: The GitHub user signup form
图 4.4: GitHub 用户面板
$ git init
$ git add .
$ git commit -m 'initial commit'
一旦拥有一个本地 Git 仓库,把 GitHub 添加为远程仓库并推送 master 分支:
80
114. 第5章 分布式 Git Scott Chacon Pro Git
$ cat 0001-add-limit-to-log-function.patch
From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith <jessica@example.com>
Date: Sun, 6 Apr 2008 10:17:23 -0700
Subject: [PATCH 1/2] add limit to log function
Limit log functionality to the first 20
---
lib/simplegit.rb | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index 76f47bc..f9815f1 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -14,7 +14,7 @@ class SimpleGit
end
def log(treeish = 'master')
- command("git log #{treeish}")
+ command("git log -n 20 #{treeish}")
end
def ls_tree(treeish = 'master')
--
1.6.2.rc1.20.g8c5b.dirty
如果有额外信息需要补充,但又不想放在提交消息中说明,可以编辑这些补丁文件,在第一个 --- 行之前
添加说明,但不要修改下面的补丁正文,比如例子中的 Limit log functionality to the first 20 部分。这样,其
它开发者能阅读,但在采纳补丁时不会将此合并进来。
你可以用邮件客户端软件发送这些补丁文件,也可以直接在命令行发送。有些所谓智能的邮件客户端软件会
自作主张帮你调整格式,所以粘贴补丁到邮件正文时,有可能会丢失换行符和若干空格。Git 提供了一个通过
IMAP 发送补丁文件的工具。接下来我会演示如何通过 Gmail 的 IMAP 服务器发送。另外,在 Git 源代码中
有个 Documentation/SubmittingPatches 文件,可以仔细读读,看看其它邮件程序的相关导引。
首先在 ~/.gitconfig 文件中配置 imap 项。每个选项都可用 git config 命令分别设置,当然直接编辑文件添
加以下内容更便捷:
[imap]
folder = "[Gmail]/Drafts"
host = imaps://imap.gmail.com
user = user@gmail.com
pass = p4ssw0rd
port = 993
sslverify = false
如果你的 IMAP 服务器没有启用 SSL,就无需配置最后那两行,并且 host 应该以 imap:// 开头而不再是有
s 的 imaps://。保存配置文件后,就能用 git send-email 命令把补丁作为邮件依次发送到指定的 IMAP 服务器上
的文件夹中(译注:这里就是 Gmail 的 [Gmail]/Drafts 文件夹。但如果你的语言设置不是英文,此处的文件夹
Drafts 字样会变为对应的语言。):
104
115. Scott Chacon Pro Git 5.3节 项目的管理
$ git send-email *.patch
0001-added-limit-to-log-function.patch
0002-changed-log-output-to-30-from-25.patch
Who should the emails appear to be from? [Jessica Smith <jessica@example.com>]
Emails will be sent from: Jessica Smith <jessica@example.com>
Who should the emails be sent to? jessica@example.com
Message-ID to be used as In-Reply-To for the first email? y
接下来,Git 会根据每个补丁依次输出类似下面的日志:
(mbox) Adding cc: Jessica Smith <jessica@example.com> from
line 'From: Jessica Smith <jessica@example.com>'
OK. Log says:
Sendmail: /usr/sbin/sendmail -i jessica@example.com
From: Jessica Smith <jessica@example.com>
To: jessica@example.com
Subject: [PATCH 1/2] added limit to log function
Date: Sat, 30 May 2009 13:29:15 -0700
Message-Id: <1243715356-61726-1-git-send-email-jessica@example.com>
X-Mailer: git-send-email 1.6.2.rc1.20.g8c5b.dirty
In-Reply-To: <y>
References: <y>
Result: OK
最后,到 Gmail 上打开 Drafts 文件夹,编辑这些邮件,修改收件人地址为邮件列表地址,另外给要抄送
的人也加到 Cc 列表中,最后发送。
5.2.6 小结
本节主要介绍了常见 Git 项目协作的工作流程,还有一些帮助处理这些工作的命令和工具。接下来我们要
看看如何维护 Git 项目,并成为一个合格的项目管理员,或是集成经理。
5.3 项目的管理
既然是相互协作,在贡献代码的同时,也免不了要维护管理自己的项目。像是怎么处理别人用 format-patch
生成的补丁,或是集成远端仓库上某个分支上的变化等等。但无论是管理代码仓库,还是帮忙审核收到的补
丁,都需要同贡献者约定某种长期可持续的工作方式。
5.3.1 使用特性分支进行工作
如果想要集成新的代码进来,最好局限在特性分支上做。临时的特性分支可以让你随意尝试,进退自如。比
如碰上无法正常工作的补丁,可以先搁在那边,直到有时间仔细核查修复为止。创建的分支可以用相关的主题
关键字命名,比如 ruby_client 或者其它类似的描述性词语,以帮助将来回忆。Git 项目本身还时常把分支名
称分置于不同命名空间下,比如 sc/ruby_client 就说明这是 sc 这个人贡献的。现在从当前主干分支为基础,
新建临时分支:
$ git branch sc/ruby_client master
105
117. Scott Chacon Pro Git 5.3节 项目的管理
Subject: [PATCH 1/2] add limit to log function
Limit log functionality to the first 20
这是 format-patch 命令输出的开头几行,也是一个有效的 mbox 文件格式。如果有人用 git send-email 给你
发了一个补丁,你可以将此邮件下载到本地,然后运行 git am 命令来应用这个补丁。如果你的邮件客户端能
将多封电邮导出为 mbox 格式的文件,就可以用 git am 一次性应用所有导出的补丁。
如果贡献者将 format-patch 生成的补丁文件上传到类似 Request Ticket 一样的任务处理系统,那么可以先
下载到本地,继而使用 git am 应用该补丁:
$ git am 0001-limit-log-function.patch
Applying: add limit to log function
你会看到它被干净地应用到本地分支,并自动创建了新的提交对象。作者信息取自邮件头 From 和 Date,提
交消息则取自 Subject 以及正文中补丁之前的内容。来看具体实例,采纳之前展示的那个 mbox 电邮补丁后,
最新的提交对象为:
$ git log --pretty=fuller -1
commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Author: Jessica Smith <jessica@example.com>
AuthorDate: Sun Apr 6 10:17:23 2008 -0700
Commit: Scott Chacon <schacon@gmail.com>
CommitDate: Thu Apr 9 09:19:06 2009 -0700
add limit to log function
Limit log functionality to the first 20
Commit 部分显示的是采纳补丁的人,以及采纳的时间。而 Author 部分则显示的是原作者,以及创建补丁的
时间。
有时,我们也会遇到打不上补丁的情况。这多半是因为主干分支和补丁的基础分支相差太远,但也可能是因
为某些依赖补丁还未应用。这种情况下,git am 会报错并询问该怎么做:
$ git am 0001-seeing-if-this-helps-the-gem.patch
Applying: seeing if this helps the gem
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
Patch failed at 0001.
When you have resolved this problem run "git am --resolved".
If you would prefer to skip this patch, instead run "git am --skip".
To restore the original branch and stop patching run "git am --abort".
Git 会在有冲突的文件里加入冲突解决标记,这同合并或衍合操作一样。解决的办法也一样,先编辑文件消
除冲突,然后暂存文件,最后运行 git am --resolved 提交修正结果:
$ (fix the file)
$ git add ticgit.gemspec
$ git am --resolved
Applying: seeing if this helps the gem
107
118. 第5章 分布式 Git Scott Chacon Pro Git
如果想让 Git 更智能地处理冲突,可以用 -3 选项进行三方合并。如果当前分支未包含该补丁的基础代码
或其祖先,那么三方合并就会失败,所以该选项默认为关闭状态。一般来说,如果该补丁是基于某个公开的提
交制作而成的话,你总是可以通过同步来获取这个共同祖先,所以用三方合并选项可以解决很多麻烦:
$ git am -3 0001-seeing-if-this-helps-the-gem.patch
Applying: seeing if this helps the gem
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
No changes -- Patch already applied.
像上面的例子,对于打过的补丁我又再打一遍,自然会产生冲突,但因为加上了 -3 选项,所以它很聪明地
告诉我,无需更新,原有的补丁已经应用。
对于一次应用多个补丁时所用的 mbox 格式文件,可以用 am 命令的交互模式选项 -i,这样就会在打每个补
丁前停住,询问该如何操作:
$ git am -3 -i mbox
Commit Body is:
--------------------------
seeing if this helps the gem
--------------------------
Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all
在多个补丁要打的情况下,这是个非常好的办法,一方面可以预览下补丁内容,同时也可以有选择性的接纳
或跳过某些补丁。
打完所有补丁后,如果测试下来新特性可以正常工作,那就可以安心地将当前特性分支合并到长期分支中去
了。
5.3.3 检出远程分支
如果贡献者有自己的 Git 仓库,并将修改推送到此仓库中,那么当你拿到仓库的访问地址和对应分支的名
称后,就可以加为远程分支,然后在本地进行合并。
比如,Jessica 发来一封邮件,说在她代码库中的 ruby-client 分支上已经实现了某个非常棒的新功能,希
望我们能帮忙测试一下。我们可以先把她的仓库加为远程仓库,然后抓取数据,完了再将她所说的分支检出到
本地来测试:
$ git remote add jessica git://github.com/jessica/myproject.git
$ git fetch jessica
$ git checkout -b rubyclient jessica/ruby-client
若是不久她又发来邮件,说还有个很棒的功能实现在另一分支上,那我们只需重新抓取下最新数据,然后检
出那个分支到本地就可以了,无需重复设置远程仓库。
这种做法便于同别人保持长期的合作关系。但前提是要求贡献者有自己的服务器,而我们也需要为每个人建
一个远程分支。有些贡献者提交代码补丁并不是很频繁,所以通过邮件接收补丁效率会更高。同时我们自己也
不会希望建上百来个分支,却只从每个分支取一两个补丁。但若是用脚本程序来管理,或直接使用代码仓库托
管服务,就可以简化此过程。当然,选择何种方式取决于你和贡献者的喜好。
108
119. Scott Chacon Pro Git 5.3节 项目的管理
使用远程分支的另外一个好处是能够得到提交历史。不管代码合并是不是会有问题,至少我们知道该分支的
历史分叉点,所以默认会从共同祖先开始自动进行三方合并,无需 -3 选项,也不用像打补丁那样祈祷存在共
同的基准点。
如果只是临时合作,只需用 git pull 命令抓取远程仓库上的数据,合并到本地临时分支就可以了。一次性的
抓取动作自然不会把该仓库地址加为远程仓库。
$ git pull git://github.com/onetimeguy/project.git
From git://github.com/onetimeguy/project
* branch HEAD -> FETCH_HEAD
Merge made by recursive.
5.3.4 决断代码取舍
现在特性分支上已合并好了贡献者的代码,是时候决断取舍了。本节将回顾一些之前学过的命令,以看清将
要合并到主干的是哪些代码,从而理解它们到底做了些什么,是否真的要并入。
一般我们会先看下,特性分支上都有哪些新增的提交。比如在 contrib 特性分支上打了两个补丁,仅查看这
两个补丁的提交信息,可以用 --not 选项指定要屏蔽的分支 master,这样就会剔除重复的提交历史:
$ git log contrib --not master
commit 5b6235bd297351589efc4d73316f0a68d484f118
Author: Scott Chacon <schacon@gmail.com>
Date: Fri Oct 24 09:53:59 2008 -0700
seeing if this helps the gem
commit 7482e0d16d04bea79d0dba8988cc78df655f16a0
Author: Scott Chacon <schacon@gmail.com>
Date: Mon Oct 22 19:38:36 2008 -0700
updated the gemspec to hopefully work better
还可以查看每次提交的具体修改。请牢记,在 git log 后加 -p 选项将展示每次提交的内容差异。
如果想看当前分支同其他分支合并时的完整内容差异,有个小窍门:
$ git diff master
虽然能得到差异内容,但请记住,结果有可能和我们的预期不同。一旦主干 master 在特性分支创建之后有
所修改,那么通过 diff 命令来比较的,是最新主干上的提交快照。显然,这不是我们所要的。比方在 master
分支中某个文件里添了一行,然后运行上面的命令,简单的比较最新快照所得到的结论只能是,特性分支中删
除了这一行。
这个很好理解:如果 master 是特性分支的直接祖先,不会产生任何问题;如果它们的提交历史在不同的分
叉上,那么产生的内容差异,看起来就像是增加了特性分支上的新代码,同时删除了 master 分支上的新代
码。
实际上我们真正想要看的,是新加入到特性分支的代码,也就是合并时会并入主干的代码。所以,准确地
讲,我们应该比较特性分支和它同 master 分支的共同祖先之间的差异。
我们可以手工定位它们的共同祖先,然后与之比较:
109
122. 第5章 分布式 Git Scott Chacon Pro Git
图 5.23: 特性分支发布后
大项目的合并流程
Git 项目本身有四个长期分支:用于发布的 master 分支、用于合并基本稳定特性的 next 分支、用于合并
仍需改进特性的 pu 分支(pu 是 proposed updates 的缩写),以及用于除错维护的 maint 分支(maint 取
自 maintenance)。维护者可以按照之前介绍的方法,将贡献者的代码引入为不同的特性分支(如图 5.24 所
示),然后测试评估,看哪些特性能稳定工作,哪些还需改进。稳定的特性可以并入 next 分支,然后再推送
到公共仓库,以供其他人试用。
图 5.24: 管理复杂的并行贡献
仍需改进的特性可以先并入 pu 分支。直到它们完全稳定后再并入 master。同时一并检查下 next 分支,将
足够稳定的特性也并入 master。所以一般来说,master 始终是在快进,next 偶尔做下衍合,而 pu 则是频繁衍
合,如图 5.25 所示:
并入 master 后的特性分支,已经无需保留分支索引,放心删除好了。Git 项目还有一个 maint 分支,它是
以最近一次发行版为基础分化而来的,用于维护除错补丁。所以克隆 Git 项目仓库后会得到这四个分支,通
过检出不同分支可以了解各自进展,或是试用前沿特性,或是贡献代码。而维护者则通过管理这些分支,逐步
有序地并入第三方贡献。
衍合与挑拣(cherry-pick)的流程
一些维护者更喜欢衍合或者挑拣贡献者的代码,而不是简单的合并,因为这样能够保持线性的提交历史。如
果你完成了一个特性的开发,并决定将它引入到主干代码中,你可以转到那个特性分支然后执行衍合命令,好
在你的主干分支上(也可能是develop分支之类的)重新提交这些修改。如果这些代码工作得很好,你就可以快
进master分支,得到一个线性的提交历史。
112
123. Scott Chacon Pro Git 5.3节 项目的管理
图 5.25: 将特性并入长期分支
另一个引入代码的方法是挑拣。挑拣类似于针对某次特定提交的衍合。它首先提取某次提交的补丁,然后试
着应用在当前分支上。如果某个特性分支上有多个commits,但你只想引入其中之一就可以使用这种方法。也
可能仅仅是因为你喜欢用挑拣,讨厌衍合。假设你有一个类似图 5.26的工程。
图 5.26: 挑拣(cherry-pick)之前的历史
如果你希望拉取e43a6到你的主干分支,可以这样:
$ git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf
Finished one cherry-pick.
[master]: created a0a41a9: "More friendly message when locking the index fails."
3 files changed, 17 insertions(+), 3 deletions(-)
这将会引入e43a6的代码,但是会得到不同的SHA-1值,因为应用日期不同。现在你的历史看起来像图 5.27.
现在,你可以删除这个特性分支并丢弃你不想引入的那些commit。
5.3.6 给发行版签名
你可以删除上次发布的版本并重新打标签,也可以像第二章所说的那样建立一个新的标签。如果你决定以维
护者的身份给发行版签名,应该这样做:
$ git tag -s v1.5 -m 'my signed 1.5 tag'
You need a passphrase to unlock the secret key for
user: "Scott Chacon <schacon@gmail.com>"
1024-bit DSA key, ID F721C45A, created 2009-02-09
113
124. 第5章 分布式 Git Scott Chacon Pro Git
图 5.27: 挑拣(cherry-pick)之后的历史
完成签名之后,如何分发PGP公钥(public key)是个问题。(译者注:分发公钥是为了验证标签)。还
好,Git的设计者想到了解决办法:可以把key(既公钥)作为blob变量写入Git库,然后把它的内容直接写在
标签里。gpg --list-keys命令可以显示出你所拥有的key:
$ gpg --list-keys
/Users/schacon/.gnupg/pubring.gpg
---------------------------------
pub 1024D/F721C45A 2009-02-09 [expires: 2010-02-09]
uid Scott Chacon <schacon@gmail.com>
sub 2048g/45D02282 2009-02-09 [expires: 2010-02-09]
然后,导出key的内容并经由管道符传递给git hash-object,之后钥匙会以blob类型写入Git中,最后返回这个
blob量的SHA-1值:
$ gpg -a --export F721C45A | git hash-object -w --stdin
659ef797d181633c87ec71ac3f9ba29fe5775b92
现在你的Git已经包含了这个key的内容了,可以通过不同的SHA-1值指定不同的key来创建标签。
$ git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92
在运行git push --tags命令之后,maintainer-pgp-pub标签就会公布给所有人。如果有人想要校验标签,他可以
使用如下命令导入你的key:
$ git show maintainer-pgp-pub | gpg --import
人们可以用这个key校验你签名的所有标签。另外,你也可以在标签信息里写入一个操作向导,用户只需要
运行git show <tag>查看标签信息,然后按照你的向导就能完成校验。
5.3.7 生成内部版本号
因为Git不会为每次提交自动附加类似’v123’的递增序列,所以如果你想要得到一个便于理解的提交号可
以运行git describe命令。Git将会返回一个字符串,由三部分组成:最近一次标定的版本号,加上自那次标定
之后的提交次数,再加上一段SHA-1值of the commit you’re describing:
114
125. Scott Chacon Pro Git 5.3节 项目的管理
$ git describe master
v1.6.2-rc1-20-g8c5b85c
这个字符串可以作为快照的名字,方便人们理解。如果你的Git是你自己下载源码然后编译安装的,你会发
现git --version命令的输出和这个字符串差不多。如果在一个刚刚打完标签的提交上运行describe命令,只会得
到这次标定的版本号,而没有后面两项信息。
git describe命令只适用于有标注的标签(通过-a或者-s选项创建的标签),所以发行版的标签都应该是带有
标注的,以保证git describe能够正确的执行。你也可以把这个字符串作为checkout或者show命令的目标,因为他
们最终都依赖于一个简短的SHA-1值,当然如果这个SHA-1值失效他们也跟着失效。最近Linux内核为了保证
SHA-1值的唯一性,将位数由8位扩展到10位,这就导致扩展之前的git describe输出完全失效了。
5.3.8 准备发布
现在可以发布一个新的版本了。首先要将代码的压缩包归档,方便那些可怜的还没有使用Git的人们。可以
使用git archive:
$ git archive master --prefix='project/' | gzip > `git describe master`.tar.gz
$ ls *.tar.gz
v1.6.2-rc1-20-g8c5b85c.tar.gz
这个压缩包解压出来的是一个文件夹,里面是你项目的最新代码快照。你也可以用类似的方法建立一个zip
压缩包,在git archive加上--format=zip选项:
$ git archive master --prefix='project/' --format=zip > `git describe master`.zip
现在你有了一个tar.gz压缩包和一个zip压缩包,可以把他们上传到你网站上或者用e-mail发给别人。
5.3.9 制作简报
是时候通知邮件列表里的朋友们来检验你的成果了。使用git shortlog命令可以方便快捷的制作一份修改日志
(changelog),告诉大家上次发布之后又增加了哪些特性和修复了哪些bug。实际上这个命令能够统计给定
范围内的所有提交;假如你上一次发布的版本是v1.0.1,下面的命令将给出自从上次发布之后的所有提交的简
介:
$ git shortlog --no-merges master --not v1.0.1
Chris Wanstrath (8):
Add support for annotated tags to Grit::Tag
Add packed-refs annotated tag support.
Add Grit::Commit#to_patch
Update version and History.txt
Remove stray `puts`
Make ls_tree ignore nils
Tom Preston-Werner (4):
fix dates in history
dynamic version method
Version bump to 1.0.2
Regenerated gemspec for version 1.0.2
115
126. 第5章 分布式 Git Scott Chacon Pro Git
这就是自从v1.0.1版本以来的所有提交的简介,内容按照作者分组,以便你能快速的发e-mail给他们。
5.4 小结
你学会了如何使用Git为项目做贡献,也学会了如何使用Git维护你的项目。恭喜!你已经成为一名高效的开
发者。在下一章你将学到更强大的工具来处理更加复杂的问题,之后你会变成一位Git大师。
116
136. 第6章 Git 工具 Scott Chacon Pro Git
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命令。
“‘储藏”“可以获取你工作目录的中间状态——也就是你修改过的被追踪的文件和暂存的变更——并将它
保存到一个未完结变更的堆栈中,随时可以重新应用。
6.3.1 储藏你的工作
为了演示这一功能,你可以进入你的项目,在一些文件上进行工作,有可能还暂存其中一个变更。如果你运
行 git status,你可以看到你的中间状态:
126
137. Scott Chacon Pro Git 6.3节 储藏(Stashing)
$ 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
#
127
138. 第6章 Git 工具 Scott Chacon Pro Git
你可以看到 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 来重新应用储藏,同时立刻将其从堆栈中移走。
6.3.2 从储藏中创建分支
如果你储藏了一些工作,暂时不去理会,然后继续在你储藏工作的分支上工作,你在重新应用工作时可能会
碰到一些问题。如果尝试应用的变更是针对一个你那之后修改过的文件,你会碰到一个归并冲突并且必须去化
解它。如果你想用更方便的方法来重新检验你储藏的变更,你可以运行 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:
128
140. 第6章 Git 工具 Scott Chacon Pro Git
$ 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
130
141. Scott Chacon Pro Git 6.4节 重写历史
Once you’re satisfied with your changes, run
git rebase --continue
这些指示很明确地告诉了你该干什么。输入
$ git commit --amend
修改提交说明,退出编辑器。然后,运行
$ git rebase --continue
这个命令会自动应用其他两次提交,你就完成任务了。如果你将更多行的 pick 改为 edit ,你就能对你想
修改的提交重复这些步骤。Git每次都会停下,让你修正提交,完成后继续运行。
6.4.3 重排提交
你也可以使用交互式的衍合来彻底重排或删除提交。如果你想删除“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”这次提交。
6.4.4 压制(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.
#
131
142. 第6章 Git 工具 Scott Chacon Pro Git
如果不用“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
当你保存之后,你就拥有了一个包含前三次提交的全部变更的单一提交。
6.4.5 拆分提交
拆分提交就是撤销一次提交,然后多次部分地暂存或提交直到结束。例如,假设你想将三次提交中的中
间一次拆分。将“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),你的历史看起来就像这样了:
132
143. Scott Chacon Pro Git 6.4节 重写历史
$ 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 值,所以请确保这个列表里不包含你已经推送到共享仓库的提
交。
6.4.6 核弹级选项: 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 会自动地删除不对这个子目录产生影响的提交。
133
164. 第7章 自定义 Git Scott Chacon Pro Git
*.doc diff=word
当你要看比较结果时,如果文件扩展名是“doc”,Git 调用“word”过滤器。什么是“word”过滤器呢?
其实就是 Git 使用strings 程序,把Word文档转换成可读的文本文件,之后再进行比较:
$ git config diff.word.textconv strings
现在如果在两个快照之间比较以.doc结尾的文件,Git 对这些文件运用“word”过滤器,在比较前把Word文
件转换成文本文件。
下面展示了一个实例,我把此书的第一章纳入 Git 管理,在一个段落中加入了一些文本后保存,之后运
行git diff命令,得到结果如下:
$ git diff
diff --git a/chapter1.doc b/chapter1.doc
index c1c8a0a..b93c9e4 100644
--- a/chapter1.doc
+++ b/chapter1.doc
@@ -8,7 +8,8 @@ re going to cover Version Control Systems (VCS) and Git basics
re going to cover how to get it and set it up for the first time if you don
t already have it on your system.
In Chapter Two we will go over basic Git usage - how to use Git for the 80%
-s going on, modify stuff and contribute changes. If the book spontaneously
+s going on, modify stuff and contribute changes. If the book spontaneously
+Let's see if this works.
Git 成功且简洁地显示出我增加的文本“Let’s see if this works”。虽然有些瑕疵,在末尾显示了一些
随机的内容,但确实可以比较了。如果你能找到或自己写个Word到纯文本的转换器的话,效果可能会更好。
strings可以在大部分Mac和Linux系统上运行,所以它是处理二进制格式的第一选择。
你还能用这个方法比较图像文件。当比较时,对JPEG文件运用一个过滤器,它能提炼出EXIF信息 — 大部分
图像格式使用的元数据。如果你下载并安装了exiftool程序,可以用它参照元数据把图像转换成文本。比较的
不同结果将会用文本向你展示:
$ echo '*.png diff=exif' >> .gitattributes
$ git config diff.exif.textconv exiftool
如果在项目中替换了一个图像文件,运行git diff命令的结果如下:
diff --git a/image.png b/image.png
index 88839c4..4afcb7c 100644
--- a/image.png
+++ b/image.png
@@ -1,12 +1,12 @@
ExifTool Version Number : 7.74
-File Size : 70 kB
-File Modification Date/Time : 2009:04:21 07:02:45-07:00
+File Size : 94 kB
+File Modification Date/Time : 2009:04:21 07:02:43-07:00
154
169. Scott Chacon Pro Git 7.3节 Git挂钩
话,提交的SHA-1校验和。该挂钩对通常的提交来说不是很有用,只在自动产生的默认提交信息的情况下有作
用,如提交信息模板、合并、压缩和修订提交等。可以和提交模板配合使用,以编程的方式插入信息。
commit-msg挂钩接收一个参数,此参数是包含最近提交信息的临时文件的路径。如果该挂钩脚本以非零退出,
Git 放弃提交,因此,可以用来在提交通过前验证项目状态或提交信息。本章上一小节已经展示了使用该挂钩
核对提交信息是否符合特定的模式。
post-commit挂钩在整个提交过程完成后运行,他不会接收任何参数,但可以运行git log -1 HEAD来获得最后的
提交信息。总之,该挂钩是作为通知之类使用的。
提交工作流的客户端挂钩脚本可以在任何工作流中使用,他们经常被用来实施某些策略,但值得注意的是,
这些脚本在clone期间不会被传送。可以在服务器端实施策略来拒绝不符合某些策略的推送,但这完全取决于
开发者在客户端使用这些脚本的情况。所以,这些脚本对开发者是有用的,由他们自己设置和维护,而且在任
何时候都可以覆盖或修改这些脚本。
E-mail工作流挂钩
有3个可用的客户端挂钩用于e-mail工作流。当运行git am命令时,会调用他们,因此,如果你没有在工作
流中用到此命令,可以跳过本节。如果你通过e-mail接收由git format-patch产生的补丁,这些挂钩也许对你有
用。
首先运行的是applypatch-msg挂钩,他接收一个参数:包含被建议提交信息的临时文件名。如果该脚本非零退
出,Git 放弃此补丁。可以使用这个脚本确认提交信息是否被正确格式化,或让脚本编辑信息以达到标准化。
下一个在git am运行期间调用是pre-applypatch挂钩。该挂钩不接收参数,在补丁被运用之后运行,因此,可以
被用来在提交前检查快照。你能用此脚本运行测试,检查工作树。如果有些什么遗漏,或测试没通过,脚本会
以非零退出,放弃此次git am的运行,补丁不会被提交。
最后在git am运行期间调用的是post-applypatch挂钩。你可以用他来通知一个小组或获取的补丁的作者,但无
法阻止打补丁的过程。
其他客户端挂钩
pre- rebase挂钩在衍合前运行,脚本以非零退出可以中止衍合的过程。你可以使用这个挂钩来禁止衍合已经
推送的提交对象,Git pre- rebase挂钩样本就是这么做的。该样本假定next是你定义的分支名,因此,你可能
要修改样本,把next改成你定义过且稳定的分支名。
在git checkout成功运行后,post-checkout挂钩会被调用。他可以用来为你的项目环境设置合适的工作目录。例
如:放入大的二进制文件、自动产生的文档或其他一切你不想纳入版本控制的文件。
最后,在merge命令成功执行后,post-merge挂钩会被调用。他可以用来在 Git 无法跟踪的工作树中恢复数
据,诸如权限数据。该挂钩同样能够验证在 Git 控制之外的文件是否存在,因此,当工作树改变时,你想这
些文件可以被复制。
7.3.3 服务器端挂钩
除了客户端挂钩,作为系统管理员,你还可以使用两个服务器端的挂钩对项目实施各种类型的策略。这些挂
钩脚本可以在提交对象推送到服务器前被调用,也可以在推送到服务器后被调用。推送到服务器前调用的挂钩
可以在任何时候以非零退出,拒绝推送,返回错误消息给客户端,还可以如你所愿设置足够复杂的推送策略。
pre-receive 和 post-receive
The first script to run when handling a push from a client is pre-receive. It takes a list of
references that are being pushed from stdin; if it exits non-zero, none of them are accepted. You
can use this hook to do things like make sure none of the updated references are non-fast-forwards;
159
170. 第7章 自定义 Git Scott Chacon Pro Git
or to check that the user doing the pushing has create, delete, or push access or access to push
updates to all the files they’re modifying with the push.
The post-receive hook runs after the entire process is completed and can be used to update other
services or notify users. It takes the same stdin data as the pre-receive hook. Examples include
e-mailing a list, notifying a continuous integration server, or updating a ticket-tracking system
— you can even parse the commit messages to see if any tickets need to be opened, modified, or
closed. This script can’t stop the push process, but the client doesn’t disconnect until it has
completed; so, be careful when you try to do anything that may take a long time.
update
The update script is very similar to the pre-receive script, except that it’s run once for each
branch the pusher is trying to update. If the pusher is trying to push to multiple branches, pre-
receive runs only once, whereas update runs once per branch they’re pushing to. Instead of reading
from stdin, this script takes three arguments: the name of the reference (branch), the SHA-1 that
reference pointed to before the push, and the SHA-1 the user is trying to push. If the update
script exits non-zero, only that reference is rejected; other references can still be updated.
7.4 An Example Git-Enforced Policy
In this section, you’ll use what you’ve learned to establish a Git workflow that checks for a
custom commit message format, enforces fast-forward-only pushes, and allows only certain users to
modify certain subdirectories in a project. You’ll build client scripts that help the developer
know if their push will be rejected and server scripts that actually enforce the policies.
I used Ruby to write these, both because it’s my preferred scripting language and because I
feel it’s the most pseudocode-looking of the scripting languages; thus you should be able to
roughly follow the code even if you don’t use Ruby. However, any language will work fine. All
the sample hook scripts distributed with Git are in either Perl or Bash scripting, so you can also
see plenty of examples of hooks in those languages by looking at the samples.
7.4.1 Server-Side Hook
All the server-side work will go into the update file in your hooks directory. The update file
runs once per branch being pushed and takes the reference being pushed to, the old revision where
that branch was, and the new revision being pushed. You also have access to the user doing the
pushing if the push is being run over SSH. If you’ve allowed everyone to connect with a single
user (like “git”) via public-key authentication, you may have to give that user a shell wrapper
that determines which user is connecting based on the public key, and set an environment variable
specifying that user. Here I assume the connecting user is in the $USER environment variable, so
your update script begins by gathering all the information you need:
#!/usr/bin/env ruby
$refname = ARGV[0]
$oldrev = ARGV[1]
$newrev = ARGV[2]
$user = ENV['USER']
160
171. Scott Chacon Pro Git 7.4节 An Example Git-Enforced Policy
puts "Enforcing Policies... n(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"
Yes, I’m using global variables. Don’t judge me — it’s easier to demonstrate in this manner.
Enforcing a Specific Commit-Message Format
Your first challenge is to enforce that each commit message must adhere to a particular format.
Just to have a target, assume that each message has to include a string that looks like “ref:
1234” because you want each commit to link to a work item in your ticketing system. You must
look at each commit being pushed up, see if that string is in the commit message, and, if the
string is absent from any of the commits, exit non-zero so the push is rejected.
You can get a list of the SHA-1 values of all the commits that are being pushed by taking the
$newrev and $oldrev values and passing them to a Git plumbing command called git rev-list. This is
basically the git log command, but by default it prints out only the SHA-1 values and no other
information. So, to get a list of all the commit SHAs introduced between one commit SHA and
another, you can run something like this:
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
You can take that output, loop through each of those commit SHAs, grab the message for it, and
test that message against a regular expression that looks for a pattern.
You have to figure out how to get the commit message from each of these commits to test. To get
the raw commit data, you can use another plumbing command called git cat-file. I’ll go over all
these plumbing commands in detail in Chapter 9; but for now, here’s what that command gives you:
$ git cat-file commit ca82a6
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
changed the version number
A simple way to get the commit message from a commit when you have the SHA-1 value is to go to
the first blank line and take everything after that. You can do so with the sed command on Unix
systems:
$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number
You can use that incantation to grab the commit message from each commit that is trying to be
pushed and exit if you see anything that doesn’t match. To exit the script and reject the push,
exit non-zero. The whole method looks like this:
161
172. 第7章 自定义 Git Scott Chacon Pro Git
$regex = /[ref: (d+)]/
# enforced custom commit message format
def check_message_format
missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("n")
missed_revs.each do |rev|
message = `git cat-file commit #{rev} | sed '1,/^$/d'`
if !$regex.match(message)
puts "[POLICY] Your message is not formatted correctly"
exit 1
end
end
end
check_message_format
Putting that in your update script will reject updates that contain commits that have messages
that don’t adhere to your rule.
Enforcing a User-Based ACL System
Suppose you want to add a mechanism that uses an access control list (ACL) that specifies which
users are allowed to push changes to which parts of your projects. Some people have full access,
and others only have access to push changes to certain subdirectories or specific files. To
enforce this, you’ll write those rules to a file named acl that lives in your bare Git repository
on the server. You’ll have the update hook look at those rules, see what files are being introduced
for all the commits being pushed, and determine whether the user doing the push has access to
update all those files.
The first thing you’ll do is write your ACL. Here you’ll use a format very much like the CVS
ACL mechanism: it uses a series of lines, where the first field is avail or unavail, the next field
is a comma-delimited list of the users to which the rule applies, and the last field is the path
to which the rule applies (blank meaning open access). All of these fields are delimited by a
pipe (|) character.
In this case, you have a couple of administrators, some documentation writers with access to
the doc directory, and one developer who only has access to the lib and tests directories, and your
ACL file looks like this:
avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests
You begin by reading this data into a structure that you can use. In this case, to keep the
example simple, you’ll only enforce the avail directives. Here is a method that gives you an
associative array where the key is the user name and the value is an array of paths to which the
user has write access:
def get_acl_access_data(acl_file)
# read in ACL data
162
173. Scott Chacon Pro Git 7.4节 An Example Git-Enforced Policy
acl_file = File.read(acl_file).split("n").reject { |line| line == '' }
access = {}
acl_file.each do |line|
avail, users, path = line.split('|')
next unless avail == 'avail'
users.split(',').each do |user|
access[user] ||= []
access[user] << path
end
end
access
end
On the ACL file you looked at earlier, this get_acl_access_data method returns a data structure
that looks like this:
{"defunkt"=>[nil],
"tpw"=>[nil],
"nickh"=>[nil],
"pjhyett"=>[nil],
"schacon"=>["lib", "tests"],
"cdickens"=>["doc"],
"usinclair"=>["doc"],
"ebronte"=>["doc"]}
Now that you have the permissions sorted out, you need to determine what paths the commits being
pushed have modified, so you can make sure the user who’s pushing has access to all of them.
You can pretty easily see what files have been modified in a single commit with the --name-only
option to the git log command (mentioned briefly in Chapter 2):
$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rb
If you use the ACL structure returned from the get_acl_access_data method and check it against the
listed files in each of the commits, you can determine whether the user has access to push all of
their commits:
# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
access = get_acl_access_data('acl')
# see if anyone is trying to push something they can't
new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("n")
new_commits.each do |rev|
files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("n")
files_modified.each do |path|
next if path.size == 0
has_file_access = false
163
174. 第7章 自定义 Git Scott Chacon Pro Git
access[$user].each do |access_path|
if !access_path # user has access to everything
|| (path.index(access_path) == 0) # access to this path
has_file_access = true
end
end
if !has_file_access
puts "[POLICY] You do not have access to push to #{path}"
exit 1
end
end
end
end
check_directory_perms
Most of that should be easy to follow. You get a list of new commits being pushed to your
server with git rev-list. Then, for each of those, you find which files are modified and make sure
the user who’s pushing has access to all the paths being modified. One Rubyism that may not be
clear is path.index(access_path) == 0, which is true if path begins with access_path — this ensures that
access_path is not just in one of the allowed paths, but an allowed path begins with each accessed
path.
Now your users can’t push any commits with badly formed messages or with modified files outside
of their designated paths.
Enforcing Fast-Forward-Only Pushes
The only thing left is to enforce fast-forward-only pushes. In Git versions 1.6 or newer, you
can set the receive.denyDeletes and receive.denyNonFastForwards settings. But enforcing this with a hook
will work in older versions of Git, and you can modify it to do so only for certain users or
whatever else you come up with later.
The logic for checking this is to see if any commits are reachable from the older revision
that aren’t reachable from the newer one. If there are none, then it was a fast-forward push;
otherwise, you deny it:
# enforces fast-forward only pushes
def check_fast_forward
missed_refs = `git rev-list #{$newrev}..#{$oldrev}`
missed_ref_count = missed_refs.split("n").size
if missed_ref_count > 0
puts "[POLICY] Cannot push a non fast-forward reference"
exit 1
end
end
check_fast_forward
Everything is set up. If you run chmod u+x .git/hooks/update, which is the file you into which
you should have put all this code, and then try to push a non-fast-forwarded reference, you get
something like this:
164
175. Scott Chacon Pro Git 7.4节 An Example Git-Enforced Policy
$ git push -f origin master
Counting objects: 5, done.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 323 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
Enforcing Policies...
(refs/heads/master) (8338c5) (c5b616)
[POLICY] Cannot push a non-fast-forward reference
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
There are a couple of interesting things here. First, you see this where the hook starts running.
Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)
Notice that you printed that out to stdout at the very beginning of your update script. It’s
important to note that anything your script prints to stdout will be transferred to the client.
The next thing you’ll notice is the error message.
[POLICY] Cannot push a non fast-forward reference
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
The first line was printed out by you, the other two were Git telling you that the update script
exited non-zero and that is what is declining your push. Lastly, you have this:
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
You’ll see a remote rejected message for each reference that your hook declined, and it tells
you that it was declined specifically because of a hook failure.
Furthermore, if the ref marker isn’t there in any of your commits, you’ll see the error message
you’re printing out for that.
[POLICY] Your message is not formatted correctly
Or if someone tries to edit a file they don’t have access to and push a commit containing it,
they will see something similar. For instance, if a documentation author tries to push a commit
modifying something in the lib directory, they see
165
176. 第7章 自定义 Git Scott Chacon Pro Git
[POLICY] You do not have access to push to lib/test.rb
That’s all. From now on, as long as that update script is there and executable, your repository
will never be rewound and will never have a commit message without your pattern in it, and your
users will be sandboxed.
7.4.2 Client-Side Hooks
The downside to this approach is the whining that will inevitably result when your users’
commit pushes are rejected. Having their carefully crafted work rejected at the last minute can
be extremely frustrating and confusing; and furthermore, they will have to edit their history to
correct it, which isn’t always for the faint of heart.
The answer to this dilemma is to provide some client-side hooks that users can use to notify them
when they’re doing something that the server is likely to reject. That way, they can correct any
problems before committing and before those issues become more difficult to fix. Because hooks
aren’t transferred with a clone of a project, you must distribute these scripts some other way
and then have your users copy them to their .git/hooks directory and make them executable. You can
distribute these hooks within the project or in a separate project, but there is no way to set
them up automatically.
To begin, you should check your commit message just before each commit is recorded, so you know
the server won’t reject your changes due to badly formatted commit messages. To do this, you can
add the commit-msg hook. If you have it read the message from the file passed as the first argument
and compare that to the pattern, you can force Git to abort the commit if there is no match:
#!/usr/bin/env ruby
message_file = ARGV[0]
message = File.read(message_file)
$regex = /[ref: (d+)]/
if !$regex.match(message)
puts "[POLICY] Your message is not formatted correctly"
exit 1
end
If that script is in place (in .git/hooks/commit-msg) and executable, and you commit with a message
that isn’t properly formatted, you see this:
$ git commit -am 'test'
[POLICY] Your message is not formatted correctly
No commit was completed in that instance. However, if your message contains the proper pattern,
Git allows you to commit:
$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
1 files changed, 1 insertions(+), 0 deletions(-)
166
177. Scott Chacon Pro Git 7.4节 An Example Git-Enforced Policy
Next, you want to make sure you aren’t modifying files that are outside your ACL scope. If your
project’s .git directory contains a copy of the ACL file you used previously, then the following
pre-commit script will enforce those constraints for you:
#!/usr/bin/env ruby
$user = ENV['USER']
# [ insert acl_access_data method from above ]
# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
access = get_acl_access_data('.git/acl')
files_modified = `git diff-index --cached --name-only HEAD`.split("n")
files_modified.each do |path|
next if path.size == 0
has_file_access = false
access[$user].each do |access_path|
if !access_path || (path.index(access_path) == 0)
has_file_access = true
end
if !has_file_access
puts "[POLICY] You do not have access to push to #{path}"
exit 1
end
end
end
check_directory_perms
This is roughly the same script as the server-side part, but with two important differences.
First, the ACL file is in a different place, because this script runs from your working directory,
not from your Git directory. You have to change the path to the ACL file from this
access = get_acl_access_data('acl')
to this:
access = get_acl_access_data('.git/acl')
The other important difference is the way you get a listing of the files that have been changed.
Because the server-side method looks at the log of commits, and, at this point, the commit hasn’t
been recorded yet, you must get your file listing from the staging area instead. Instead of
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
you have to use
167
178. 第7章 自定义 Git Scott Chacon Pro Git
files_modified = `git diff-index --cached --name-only HEAD`
But those are the only two differences — otherwise, the script works the same way. One caveat
is that it expects you to be running locally as the same user you push as to the remote machine.
If that is different, you must set the $user variable manually.
The last thing you have to do is check that you’re not trying to push non-fast-forwarded
references, but that is a bit less common. To get a reference that isn’t a fast-forward, you
either have to rebase past a commit you’ve already pushed up or try pushing a different local
branch up to the same remote branch.
Because the server will tell you that you can’t push a non-fast-forward anyway, and the hook
prevents forced pushes, the only accidental thing you can try to catch is rebasing commits that
have already been pushed.
Here is an example pre-rebase script that checks for that. It gets a list of all the commits
you’re about to rewrite and checks whether they exist in any of your remote references. If it
sees one that is reachable from one of your remote references, it aborts the rebase:
#!/usr/bin/env ruby
base_branch = ARGV[0]
if ARGV[1]
topic_branch = ARGV[1]
else
topic_branch = "HEAD"
end
target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("n")
remote_refs = `git branch -r`.split("n").map { |r| r.strip }
target_shas.each do |sha|
remote_refs.each do |remote_ref|
shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
if shas_pushed.split(“n”).include?(sha)
puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}"
exit 1
end
end
end
This script uses a syntax that wasn’t covered in the Revision Selection section of Chapter 6.
You get a list of commits that have already been pushed up by running this:
git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}
The SHAˆ@ syntax resolves to all the parents of that commit. You’re looking for any commit that
is reachable from the last commit on the remote and that isn’t reachable from any parent of any
of the SHAs you’re trying to push up — meaning it’s a fast-forward.
The main drawback to this approach is that it can be very slow and is often unnecessary — if you
don’t try to force the push with -f, the server will warn you and not accept the push. However,
168
179. Scott Chacon Pro Git 7.5节 Summary
it’s an interesting exercise and can in theory help you avoid a rebase that you might later have
to go back and fix.
7.5 Summary
You’ve covered most of the major ways that you can customize your Git client and server to
best fit your workflow and projects. You’ve learned about all sorts of configuration settings,
file-based attributes, and event hooks, and you’ve built an example policy-enforcing server. You
should now be able to make Git fit nearly any workflow you can dream up.
169
184. 第8章 Git 与其他系统 Scott Chacon Pro Git
这里有两个远程服务器:一个名为 gitserver ,具有一个 master分支;另一个叫 origin,具有 master 和
testing 两个分支。
注意本例中通过 git svn 导入的远程引用,(Subversion 的)标签是当作远程分支添加的,而不是真正的
Git 标签。导入的 Subversion 仓库仿佛是有一个带有不同分支的 tags 远程服务器。
8.1.4 提交到 Subversion
有了可以开展工作的(本地)仓库以后,你可以开始对该项目做出贡献并向上游仓库提交内容了,Git
这时相当于一个 SVN 客户端。假如编辑了一个文件并进行提交,那么这次提交仅存在于本地的 Git 而非
Subversion 服务器上。
$ git commit -am 'Adding git-svn instructions to the README'
[master 97031e5] Adding git-svn instructions to the README
1 files changed, 1 insertions(+), 1 deletions(-)
接下来,可以将作出的修改推送到上游。值得注意的是,Subversion 的使用流程也因此改变了——你可以
在离线状态下进行多次提交然后一次性的推送到 Subversion 的服务器上。向 Subversion 服务器推送的命令
是 git svn dcommit:
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M README.txt
Committed r79
M README.txt
r79 = 938b1a547c2cc92033b74d32030e86468294a5c8 (trunk)
No changes between current HEAD and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
所有在原 Subversion 数据基础上提交的 commit 会一一提交到 Subversion,然后你本地 Git 的 commit
将被重写,加入一个特别标识。这一步很重要,因为它意味着所有 commit 的 SHA-1 指都会发生变化。这也是
同时使用 Git 和 Subversion 两种服务作为远程服务不是个好主意的原因之一。检视以下最后一个 commit,
你会找到新添加的 git-svn-id (译注:即本段开头所说的特别标识):
$ git log -1
commit 938b1a547c2cc92033b74d32030e86468294a5c8
Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029>
Date: Sat May 2 22:06:44 2009 +0000
Adding git-svn instructions to the README
git-svn-id: file:///tmp/test-svn/trunk@79 4c93b258-373f-11de-be05-5f7a86268029
注意看,原本以 97031e5 开头的 SHA-1 校验值在提交完成以后变成了 938b1a5 。如果既要向 Git 远程服务
器推送内容,又要推送到 Subversion 远程服务器,则必须先向 Subversion 推送(dcommit),因为该操作会
改变所提交的数据内容。
8.1.5 拉取最新进展
如果要与其他开发者协作,总有那么一天你推送完毕之后,其他人发现他们推送自己修改的时候(与你推送
的内容)产生冲突。这些修改在你合并之前将一直被拒绝。在 git svn 里这种情况形似:
174
185. Scott Chacon Pro Git 8.1节 Git 与 Subversion
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
Merge conflict during commit: Your file or directory 'README.txt' is probably
out-of-date: resource out of date; try updating at /Users/schacon/libexec/git-
core/git-svn line 482
为了解决该问题,可以运行 git svn rebase ,它会拉取服务器上所有最新的改变,再次基础上衍合你的修
改:
$ git svn rebase
M README.txt
r80 = ff829ab914e8775c7c025d741beb3d523ee30bc4 (trunk)
First, rewinding head to replay your work on top of it...
Applying: first user change
现在,你做出的修改都发生在服务器内容之后,所以可以顺利的运行 dcommit :
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M README.txt
Committed r81
M README.txt
r81 = 456cbe6337abe49154db70106d1836bc1332deed (trunk)
No changes between current HEAD and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
需要牢记的一点是,Git 要求我们在推送之前先合并上游仓库中最新的内容,而 git svn 只要求存在冲突的
时候才这样做。假如有人向一个文件推送了一些修改,这时你要向另一个文件推送一些修改,那么 dcommit 将
正常工作:
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M configure.ac
Committed r84
M autogen.sh
r83 = 8aa54a74d452f82eee10076ab2584c1fc424853b (trunk)
M configure.ac
r84 = cdbac939211ccb18aa744e581e46563af5d962d0 (trunk)
W: d2f23b80f67aaaa1f6f5aaef48fce3263ac71a92 and refs/remotes/trunk differ,
using rebase:
:100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18
015e4c98c482f0fa71e4d5434338014530b37fa6 M autogen.sh
First, rewinding head to replay your work on top of it...
Nothing to do.
这一点需要牢记,因为它的结果是推送之后项目处于一个不完整存在与任何主机上的状态。如果做出的修改
无法兼容但没有产生冲突,则可能造成一些很难确诊的难题。这和使用 Git 服务器是不同的——在 Git 世界
里,发布之前,你可以在客户端系统里完整的测试项目的状态,而在 SVN 永远都没法确保提交前后项目的状
态完全一样。
175
186. 第8章 Git 与其他系统 Scott Chacon Pro Git
及时还没打算进行提交,你也应该用这个命令从 Subversion 服务器拉取最新修改。sit svn fetch 能获取最
新的数据,不过 git svn rebase 才会在获取之后在本地进行更新 。
$ git svn rebase
M generate_descriptor_proto.sh
r82 = bd16df9173e424c6f52c337ab6efa7f7643282f1 (trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/trunk.
不时地运行一下 git svn rebase 可以确保你的代码没有过时。不过,运行该命令时需要确保工作目录的整
洁。如果在本地做了修改,则必须在运行 git svn rebase 之前或暂存工作,或暂时提交内容——否则,该命令
会发现衍合的结果包含着冲突因而终止。
8.1.6 Git 分支问题
习惯了 Git 的工作流程以后,你可能会创建一些特性分支,完成相关的开发工作,然后合并他们。如果要
用 git svn 向 Subversion 推送内容,那么最好是每次用衍合来并入一个单一分支,而不是直接合并。使用
衍合的原因是 Subversion 只有一个线性的历史而不像 Git 那样处理合并,所以 Git svn 在把快照转换为
Subversion 的 commit 时只能包含第一个祖先。
假设分支历史如下:创建一个 experiment 分支,进行两次提交,然后合并到 master 。在 dcommit 的时候会得
到如下输出:
$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
M CHANGES.txt
Committed r85
M CHANGES.txt
r85 = 4bfebeec434d156c36f2bcd18f4e3d97dc3269a2 (trunk)
No changes between current HEAD and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
COPYING.txt: locally modified
INSTALL.txt: locally modified
M COPYING.txt
M INSTALL.txt
Committed r86
M INSTALL.txt
M COPYING.txt
r86 = 2647f6b86ccfcaad4ec58c520e369ec81f7c283c (trunk)
No changes between current HEAD and refs/remotes/trunk
Resetting to the latest refs/remotes/trunk
在一个包含了合并历史的分支上使用 dcommit 可以成功运行,不过在 Git 项目的历史中,它没有重写你在
experiment 分支中的两个 commit ——另一方面,这些改变却出现在了 SVN 版本中同一个合并 commit 中。
在别人克隆该项目的时候,只能看到这个合并 commit 包含了所有发生过的修改;他们无法获知修改的作者
和时间等提交信息。
8.1.7 Subversion 分支
Subversion 的分支和 Git 中的不尽相同;避免过多的使用可能是最好方案。不过,用 git svn 创建和提
交不同的 Subversion 分支仍是可行的。
176
187. Scott Chacon Pro Git 8.1节 Git 与 Subversion
创建新的 SVN 分支
要在 Subversion 中建立一个新分支,需要运行 git svn branch [分支名] To create a new branch in
Subversion, you run git svn branch [branchname]:
$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r87 to file:///tmp/test-svn/branches/opera...
Found possible branch point: file:///tmp/test-svn/trunk =>
file:///tmp/test-svn/branches/opera, 87
Found branch parent: (opera) 1f6bfe471083cbca06ac8d4176f7ad4de0d62e5f
Following parent with do_switch
Successfully followed parent
r89 = 9b6fe0b90c5c9adf9165f700897518dbc54a7cbf (opera)
相当于在 Subversion 中的 svn copy trunk branches/opera 命令并且对 Subversion 服务器进行了相关操作。
值得提醒的是它没有检出和转换到那个分支;如果现在进行提交,将提交到服务器上的 trunk, 而非 opera。
8.1.8 切换当前分支
Git 通过搜寻提交历史中 Subversion 分支的头部来决定 dcommit 的目的地——而它应该只有一个,那就
是当前分支历史中最近一次包含 git-svn-id 的提交。
如果需要同时在多个分支上提交,可以通过导入 Subversion 上某个其他分支的 commit 来建立以该分支为
dcommit 目的地的本地分支。比如你想拥有一个并行维护的 opera 分支,可以运行
$ git branch opera remotes/opera
然后,如果要把 opera 分支并入 trunk (本地的 master 分支),可以使用普通的 git merge。不过最好提供
一条描述提交的信息(通过 -m),否则这次合并的记录是 Merge branch opera ,而不是任何有用的东西。
记住,虽然使用了 git merge 来进行这次操作,并且合并过程可能比使用 Subversion 简单一些(因为 Git
会自动找到适合的合并基础),这并不是一次普通的 Git 合并提交。最终它将被推送回 commit 无法包含多
个祖先的 Subversion 服务器上;因而在推送之后,它将变成一个包含了所有在其他分支上做出的改变的单
一 commit。把一个分支合并到另一个分支以后,你没法像在 Git 中那样轻易的回到那个分支上继续工作。提
交时运行的 dcommit 命令擦除了全部有关哪个分支被并入的信息,因而以后的合并基础计算将是不正确的——
dcommit 让 git merge 的结果变得类似于 git merge --squash。不幸的是,我们没有什么好办法来避免该情况——
Subversion 无法储存这个信息,所以在使用它作为服务器的时候你将永远为这个缺陷所困。为了不出现这种
问题,在把本地分支(本例中的 opera)并入 trunk 以后应该立即将其删除。
8.1.9 对应 Subversion 的命令
git svn 工具集合了若干个与 Subversion 类似的功能,对应的命令可以简化向 Git 的转化过程。下面这些
命令能实现 Subversion 的这些功能。
SVN 风格的历史
习惯了 Subversion 的人可能想以 SVN 的风格显示历史,运行 git svn log 可以让提交历史显示为 SVN 格
式:
177
188. 第8章 Git 与其他系统 Scott Chacon Pro Git
$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009) | 2 lines
autogen change
------------------------------------------------------------------------
r86 | schacon | 2009-05-02 16:00:21 -0700 (Sat, 02 May 2009) | 2 lines
Merge branch 'experiment'
------------------------------------------------------------------------
r85 | schacon | 2009-05-02 16:00:09 -0700 (Sat, 02 May 2009) | 2 lines
updated the changelog
关于 git svn log ,有两点需要注意。首先,它可以离线工作,不像 svn log 命令,需要向 Subversion 服
务器索取数据。其次,它仅仅显示已经提交到 Subversion 服务器上的 commit。在本地尚未 dcommit 的 Git
数据不会出现在这里;其他人向 Subversion 服务器新提交的数据也不会显示。等于说是显示了最近已知
Subversion 服务器上的状态。
SVN 日志
类似 git svn log 对 git log 的模拟,svn annotate 的等效命令是 git svn blame [文件名]。其输出如下:
$ git svn blame README.txt
2 temporal Protocol Buffers - Google's data interchange format
2 temporal Copyright 2008 Google Inc.
2 temporal http://code.google.com/apis/protocolbuffers/
2 temporal
22 temporal C++ Installation - Unix
22 temporal =======================
2 temporal
79 schacon Committing in git-svn.
78 schacon
2 temporal To build and install the C++ Protocol Buffer runtime and the Protocol
2 temporal Buffer compiler (protoc) execute the following:
2 temporal
同样,它不显示本地的 Git 提交以及 Subversion 上后来更新的内容。
SVN 服务器信息
还可以使用 git svn info 来获取与运行 svn info 类似的信息:
$ git svn info
Path: .
URL: https://schacon-test.googlecode.com/svn/trunk
Repository Root: https://schacon-test.googlecode.com/svn
Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029
Revision: 87
178