这种手段的缺点在于用户推送内容遭到拒绝后几乎无法避免的抱怨。辛辛苦苦写成的代码在最后时刻惨遭拒绝是十分悲剧且具有迷惑性的;更可怜的是他们不得不修改提交历史来解决问题,这怎么也算不上王道。
逃离这种两难境地的法宝是给用户一些客户端的挂钩,在他们作出可能悲剧的事情的时候给以警告。然后呢,用户们就能在提交–问题变得更难修正之前解除隐患。由于挂钩本身不跟随克隆的项目副本分发,所以必须通过其他途径把这些挂钩分发到用户的 .git/hooks 目录并设为可执行文件。虽然可以在相同或单独的项目内 容里加入并分发它们,全自动的解决方案是不存在的。
首先,你应该在每次提交前核查你的提交注释信息,这样你才能确保服务器不会因为不合条件的提交注释信息而拒绝你的更改。为了达到这个目的,你可以增加’commit-msg’挂钩。如果你使用该挂钩来阅读作为第一个参数传递给git的提交注释信息,并且与规定的模式作对比,你就可以使git在提交注释信息不符合条件的情况下,拒绝执行提交。
[code]#!/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[/code]
如果这个脚本放在这个位置 (.git/hooks/commit-msg) 并且是可执行的, 并且你的提交注释信息不是符合要求的,你会看到:
$ git commit -am 'test'
[POLICY] Your message is not formatted correctly
在这个实例中,提交没有成功。然而如果你的提交注释信息是符合要求的,git会允许你提交:
$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
1 files changed, 1 insertions(+), 0 deletions(-)
接下来我们要保证没有修改到 ACL 允许范围之外的文件。加入你的 .git 目录里有前面使用过的 ACL 文件,那么以下的 pre-commit 脚本将把里面的规定执行起来:
[code]#!/usr/bin/env ruby
$user = ENV[‘USER’]
[ insert acl_access_data method from above ]
只允许特定用户修改项目重特定子目录的内容
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[/code]
这和服务端的脚本几乎一样,除了两个重要区别。第一,ACL 文件的位置不同,因为这个脚本在当前工作目录运行,而非 Git 目录。ACL 文件的目录必须从
access = get_acl_access_data('acl')
修改成:
access = get_acl_access_data('.git/acl')
另一个重要区别是获取被修改文件列表的方式。在服务端的时候使用了查看提交纪录的方式,可是目前的提交都还没被记录下来呢,所以这个列表只能从暂存区域获取。和原来的
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
不同,现在要用
files_modified = `git diff-index --cached --name-only HEAD`
不同的就只有这两点——除此之外,该脚本完全相同。一个小陷阱在于它假设在本地运行的账户和推送到远程服务端的相同。如果这二者不一样,则需要手动设置一下 $user 变量。
最后一项任务是检查确认推送内容中不包含非 fast-forward 类型的索引,不过这个需求比较少见。要找出一个非 fast-forward 类型的索引,要么衍合超过某个已经推送过的提交,要么从本地不同分支推送到远程相同的分支上。
既然服务器将给出无法推送非 fast-forward 内容的提示,而且上面的挂钩也能阻止强制的推送,唯一剩下的潜在问题就是衍合一次已经推送过的提交内容。
下面是一个检查这个问题的 pre-rabase 脚本的例子。它获取一个所有即将重写的提交内容的列表,然后检查它们是否在远程的索引里已经存在。一旦发现某个提交可以从远程索引里衍变过来,它就放弃衍合操作:
[code]#!/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}^[url=home.php?mod=space&uid=32730]@[/url] 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[/code]
这个脚本利用了一个第六章"修订版本选择"一节中不曾提到的语法。通过这一句可以获得一个所有已经完成推送的提交的列表:
git rev-list ^#{sha}^[url=home.php?mod=space&uid=32987]@[/url] refs/remotes/#{remote_ref}
SHA^@ 语法解析该次提交的所有祖先。这里我们从检查远程最后一次提交能够衍变获得但从所有我们尝试推送的提交的 SHA 值祖先无法衍变获得的提交内容——也就是 fast-forward 的内容。
这个解决方案的硬伤在于它有可能很慢而且常常没有必要——只要不用 -f 来强制推送,服务器会自动给出警告并且拒绝推送内容。然而,这是个不错的练习而且理论上能帮助用户避免一次将来不得不折回来修改的衍合操作。
REF:http://cwiki.ossez.com/pages/viewpage.action?pageId=7045937