写点什么

git(7) 自定义 Git

  • 2022 年 4 月 17 日
  • 本文字数:10638 字

    阅读完需:约 35 分钟



这里举一个简单的例子:在暂存前,用indent(缩进)程序过滤所有 C 源代码。在.gitattributes文件中设置"indent"过滤器过滤*.c文件:


*.c filter=indent


然后,通过以下配置,让 Git 知道"indent"过滤器在遇到"smudge"和"clean"时分别该做什么:


$ git config --global filter.indent.clean indent


$ git config --global filter.indent.smudge cat


于是,当你暂存*.c文件时,indent程序会被触发,在把它们签出之前,cat程序会被触发。但cat程序在这里没什么实际作用。这样的组合,使 C 源代码在暂存前被indent程序过滤,非常有效。


另一个例子是类似 RCS 的$Date$关键字扩展。为了演示,需要一个小脚本,接受文件名参数,得到项目的最新提交日期,最后把日期写入该文件。下面用 Ruby 脚本来实现:


#! /usr/bin/env ruby


data = STDIN.read


last_date = git log --pretty=format:"%ad" -1


puts data.gsub('', '')


该脚本从git log命令中得到最新提交日期,找到文件中的所有$Date$字符串,最后把该日期填充到$Date$字符串中 — 此脚本很简单,你可以选择你喜欢的编程语言来实现。把该脚本命名为expand_date,放到正确的路径中,之后需要在 Git 中设置一个过滤器(dater),让它在签出文件时调用expand_d **《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》开源** ate,在暂存文件时用 Perl 清除之:


$ git config filter.dater.smudge expand_date


git config filter.dater.clean 'perl -pe "s/\Date[^\]*\/\Date\/"'


这个 Perl 小程序会删除$Date$字符串里多余的字符,恢复$Date$原貌。到目前为止,你的过滤器已经设置完毕,可以开始测试了。打开一个文件,在文件中输入$Date$关键字,然后设置 Git 属性:


echo '# Date$' > date_test.txt


$ echo 'date*.txt filter=dater' >> .gitattributes


如果暂存该文件,之后再签出,你会发现关键字被替换了:


$ git add date_test.txt .gitattributes


$ git commit -m "Testing date expansion in Git"


$ rm date_test.txt


$ git checkout date_test.txt


$ cat date_test.txt


虽说这项技术对自定义应用来说很有用,但还是要小心,因为.gitattributes文件会随着项目一起提交,而过滤器(例如:dater)不会,所以,过滤器不会在所有地方都生效。当你在设计这些过滤器时要注意,即使它们无法正常工作,也要让整个项目运作下去。

导出仓库

Git 属性在导出项目归档时也能发挥作用。

export-ignore

当产生一个归档时,可以设置 Git 不导出某些文件和目录。如果你不想在归档中包含一个子目录或文件,但想他们纳入项目的版本管理中,你能对应地设置export-ignore属性。


例如,在test/子目录中有一些测试文件,在项目的压缩包中包含他们是没有意义的。因此,可以增加下面这行到 Git 属性文件中:


test/ export-ignore


现在,当运行 git archive 来创建项目的压缩包时,那个目录不会在归档中出现。

export-subst

还能对归档做一些简单的关键字替换。在第 2 章中已经可以看到,可以以--pretty=format形式的简码在任何文件中放入$Format:$ 字符串。例如,如果想在项目中包含一个叫作LAST_COMMIT的文件,当运行git archive时,最后提交日期自动地注入进该文件,可以这样设置:


Format:%cd$' > LAST_COMMIT


$ echo "LAST_COMMIT export-subst" >> .gitattributes


$ git add LAST_COMMIT .gitattributes


$ git commit -am 'adding LAST_COMMIT file for archives'


运行git archive后,打开该文件,会发现其内容如下:


$ cat LAST_COMMIT


Last commit date:

合并策略

通过 Git 属性,还能对项目中的特定文件使用不同的合并策略。一个非常有用的选项就是,当一些特定文件发生冲突,Git 会尝试合并他们,而使用你这边的合并。


如果项目的一个分支有歧义或比较特别,但你想从该分支合并,而且需要忽略其中某些文件,这样的合并策略是有用的。例如,你有一个数据库设置文件 database.xml,在 2 个分支中他们是不同的,你想合并一个分支到另一个,而不弄乱该数据库文件,可以设置属性如下:


database.xml merge=ours


如果合并到另一个分支,database.xml 文件不会有合并冲突,显示如下:


$ git merge topic


Auto-merging database.xml


Merge made by recursive.


这样,database.xml 会保持原样。


7.3 Git挂钩




和其他版本控制系统一样,当某些重要事件发生时,Git 以调用自定义脚本。有两组挂钩:客户端和服务器端。客户端挂钩用于客户端的操作,如提交和合并。服务器端挂钩用于 Git 服务器端的操作,如接收被推送的提交。你可以随意地使用这些挂钩,下面会讲解其中一些。

安装一个挂钩

挂钩都被存储在 Git 目录下的hooks子目录中,即大部分项目中的.git/hooks。 Git 默认会放置一些脚本样本在这个目录中,除了可以作为挂钩使用,这些样本本身是可以独立使用的。所有的样本都是 shell 脚本,其中一些还包含了 Perl 的脚本,不过,任何正确命名的可执行脚本都可以正常使用 — 可以用 Ruby 或 Python,或其他。在 Git 1.6 版本之后,这些样本名都是以.sample 结尾,因此,你必须重新命名。在 Git 1.6 版本之前,这些样本名都是正确的,但这些样本不是可执行文件。


把一个正确命名且可执行的文件放入 Git 目录下的hooks子目录中,可以激活该挂钩脚本,因此,之后他一直会被 Git 调用。随后会讲解主要的挂钩脚本。

客户端挂钩

有许多客户端挂钩,以下把他们分为:提交工作流挂钩、电子邮件工作流挂钩及其他客户端挂钩。

提交工作流挂钩

有 4 个挂钩被用来处理提交的过程。pre-commit挂钩在键入提交信息前运行,被用来检查即将提交的快照,例如,检查是否有东西被遗漏,确认测试是否运行,以及检查代码。当从该挂钩返回非零值时,Git 放弃此次提交,但可以用git commit --no-verify来忽略。该挂钩可以被用来检查代码错误(运行类似 lint 的程序),检查尾部空白(默认挂钩是这么做的),检查新方法(译注:程序的函数)的说明。


prepare-commit-msg挂钩在提交信息编辑器显示之前,默认信息被创建之后运行。因此,可以有机会在提交作者看到默认信息前进行编辑。该挂钩接收一些选项:拥有提交信息的文件路径,提交类型,如果是一次修订的话,提交的 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 控制之外的文件是否存在,因此,当工作树改变时,你想这些文件可以被复制。

服务器端挂钩

除了客户端挂钩,作为系统管理员,你还可以使用两个服务器端的挂钩对项目实施各种类型的策略。这些挂钩脚本可以在提交对象推送到服务器前被调用,也可以在推送到服务器后被调用。推送到服务器前调用的挂钩可以在任何时候以非零退出,拒绝推送,返回错误消息给客户端,还可以如你所愿设置足够复杂的推送策略。

pre-receive 和 post-receive

处理来自客户端的推送(push)操作时最先执行的脚本就是 pre-receive 。它从标准输入(stdin)获取被推送引用的列表;如果它退出时的返回值不是 0,所有推送内容都不会被接受。利用此挂钩脚本可以实现类似保证最新的索引中不包含非 fast-forward 类型的这类效果;抑或检查执行推送操作的用户拥有创建,删除或者推送的权限或者他是否对将要修改的每一个文件都有访问权限。


post-receive 挂钩在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。它接受与 pre-receive 相同的标准输入数据。应用实例包括给某邮件列表发信,通知实时整合数据的服务器,或者更新软件项目的问题追踪系统 —— 甚至可以通过分析提交信息来决定某个问题是否应该被开启,修改或者关闭。该脚本无法组织推送进程,不过客户端在它完成运行之前将保持连接状态;所以在用它作一些消耗时间的操作之前请三思。

update

update 脚本和 pre-receive 脚本十分类似。不同之处在于它会为推送者更新的每一个分支运行一次。假如推送者同时向多个分支推送内容,pre-receive 只运行一次,相比之下 update 则会为每一个更新的分支运行一次。它不会从标准输入读取内容,而是接受三个参数:索引的名字(分支),推送前索引指向的内容的 SHA-1 值,以及用户试图推送内容的 SHA-1 值。如果 update 脚本以退出时返回非零值,只有相应的那一个索引会被拒绝;其余的依然会得到更新。


7.4 Git 强制策略实例




在本节中,我们应用前面学到的知识建立这样一个 Git 工作流程:检查提交信息的格式,只接受纯 fast-forward 内容的推送,并且指定用户只能修改项目中的特定子目录。我们将写一个客户端角本来提示开发人员他们推送的内容是否会被拒绝,以及一个服务端脚本来实际执行这些策略。


这些脚本使用 Ruby 写成,一半由于它是作者倾向的脚本语言,另外作者觉得它是最接近伪代码的脚本语言;因而即便你不使用 Ruby 也能大致看懂。不过任何其他语言也一样适用。所有 Git 自带的样例脚本都是用 Perl 或 Bash 写的。所以从这些脚本中能找到相当多的这两种语言的挂钩样例。

服务端挂钩

所有服务端的工作都在 hooks(挂钩)目录的 update(更新)脚本中制定。update 脚本为每一个得到推送的分支运行一次;它接受推送目标的索引,该分支原来指向的位置,以及被推送的新内容。如果推送是通过 SSH 进行的,还可以获取发出此次操作的用户。如果设定所有操作都通过公匙授权的单一帐号(比如"git")进行,就有必要通过一个 shell 包装依据公匙来判断用户的身份,并且设定环境变量来表示该用户的身份。下面假设尝试连接的用户储存在 $USER 环境变量里,我们的 update 脚本首先搜集一切需要的信息:


#!/usr/bin/env ruby


$refname = ARGV[0]


$oldrev = ARGV[1]


$newrev = ARGV[2]


$user = ENV['USER']


puts "Enforcing Policies... \n(#{refname}) (#{oldrev[0,6]}) (#{$newrev[0,6]})"


没错,我在用全局变量。别鄙视我——这样比较利于演示过程。

指定特殊的提交信息格式

我们的第一项任务是指定每一条提交信息都必须遵循某种特殊的格式。作为演示,假定每一条信息必须包含一条形似 "ref: 1234" 这样的字符串,因为我们需要把每一次提交和项目的问题追踪系统。我们要逐一检查每一条推送上来的提交内容,看看提交信息是否包含这么一个字符串,然后,如果该提交里不包含这个字符串,以非零返回值退出从而拒绝此次推送。


$newrev$oldrev 变量的值传给一个叫做 git rev-list 的 Git plumbing 命令可以获取所有提交内容的 SHA-1 值列表。git rev-list 基本类似 git log 命令,但它默认只输出 SHA-1 值而已,没有其他信息。所以要获取由 SHA 值表示的从一次提交到另一次提交之间的所有 SHA 值,可以运行:


$ git rev-list 538c33..d14fc7


d14fc7c847ab946ec39590d87783c69b031bdfb7


9f585da4401b0a3999e84113824d15245c13f0be


234071a1be950e2a8d078e6141f5cd20c1e61ad3


dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a


17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475


截取这些输出内容,循环遍历其中每一个 SHA 值,找出与之对应的提交信息,然后用正则表达式来测试该信息包含的格式话的内容。


下面要搞定如何从所有的提交内容中提取出提交信息。使用另一个叫做 git cat-file 的 Git plumbing 工具可以获得原始的提交数据。我们将在第九章了解到这些 plumbing 工具的细节;现在暂时先看一下这条命令的输出:


$ 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


通过 SHA-1 值获得提交内容中的提交信息的一个简单办法是找到提交的第一行,然后取从它往后的所有内容。可以使用 Unix 系统的 sed 命令来实现该效果:


git cat-file commit ca82a6 | sed '1,/^/d'


changed the version number


这条咒语从每一个待提交内容里提取提交信息,并且会在提取信息不符合要求的情况下退出。为了退出脚本和拒绝此次推送,返回一个非零值。整个脚本大致如下:


$regex = /[ref: (\d+)]/

指定提交信息格式

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


把这一段放在 update 脚本里,所有包含不符合指定规则的提交都会遭到拒绝。

实现基于用户的访问权限控制列表(ACL)系统

假设你需要添加一个使用访问权限控制列表的机制来指定哪些用户对项目的哪些部分有推送权限。某些用户具有全部的访问权,其他人只对某些子目录或者特定的文件具有推送权限。要搞定这一点,所有的规则将被写入一个位于服务器的原始 Git 仓库的 acl 文件。我们让 update 挂钩检阅这些规则,审视推送的提交内容中需要修改的所有文件,然后决定执行推送的用户是否对所有这些文件都有权限。


我们首先要创建这个列表。这里使用的格式和 CVS 的 ACL 机制十分类似:它由若干行构成,第一项内容是 avail 或者 unavail,接着是逗号分隔的规则生效用户列表,最后一项是规则生效的目录(空白表示开放访问)。这些项目由 | 字符隔开。


下例中,我们指定几个管理员,几个对 doc 目录具有权限的文档作者,以及一个对 libtests 目录具有权限的开发人员,相应的 ACL 文件如下:


avail|nickh,pjhyett,defunkt,tpw


avail|usinclair,cdickens,ebronte|doc


avail|schacon|lib


avail|schacon|tests


首先把这些数据读入你编写的数据结构。本例中,为保持简洁,我们暂时只实现 avail 的规则(译注:也就是省略了 unavail 部分)。下面这个方法生成一个关联数组,它的主键是用户名,值是一个该用户有写权限的所有目录组成的数组:


def get_acl_access_data(acl_file)

read in ACL data

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


针对之前给出的 ACL 规则文件,这个 get_acl_access_data 方法返回的数据结构如下:


{"defunkt"=>[nil],


"tpw"=>[nil],


"nickh"=>[nil],


"pjhyett"=>[nil],


"schacon"=>["lib", "tests"],


"cdickens"=>["doc"],


"usinclair"=>["doc"],


"ebronte"=>["doc"]}


搞定了用户权限的数据,下面需要找出哪些位置将要被提交的内容修改,从而确保试图推送的用户对这些位置有全部的权限。


使用 git log--name-only 选项(在第二章里简单的提过)我们可以轻而易举的找出一次提交里修改的文件:


$ git log -1 --name-only --pretty=format:'' 9f585d


README


lib/test.rb


使用 get_acl_access_data 返回的 ACL 结构来一一核对每一次提交修改的文件列表,就能找出该用户是否有权限推送所有的提交内容:

仅允许特定用户修改项目中的特定子目录

def Java 开源项目【ali1024.coding.net/public/P7/Java/git】 check_directory_perms


access = get_acl_access_data('acl')

检查是否有人在向他没有权限的地方推送内容

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


access[$user].each do |access_path|


if !access_path || # 用户拥有完全访问权限


(path.index(access_path) == 0) # 或者对此位置有访问权限


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


以上的大部分内容应该都比较容易理解。通过 git rev-list 获取推送到服务器内容的提交列表。然后,针对其中每一项,找出它试图修改的文件然后确保执行推送的用户对这些文件具有权限。一个不太容易理解的 Ruby 技巧石 path.index(access_path) ==0 这句,它的返回真值如果路径以 access_path 开头——这是为了确保 access_path 并不是只在允许的路径之一,而是所有准许全选的目录都在该目录之下。


现在你的用户没法推送带有不正确的提交信息的内容,也不能在准许他们访问范围之外的位置做出修改。

只允许 Fast-Forward 类型的推送

剩下的最后一项任务是指定只接受 fast-forward 的推送。在 Git 1.6 或者更新版本里,只需要设定 receive.denyDeletesreceive.denyNonFastForwards 选项就可以了。但是通过挂钩的实现可以在旧版本的 Git 上工作,并且通过一定的修改它它可以做到只针对某些用户执行,或者更多以后可能用的到的规则。


检查这一项的逻辑是看看提交里是否包含从旧版本里能找到但在新版本里却找不到的内容。如果没有,那这是一次纯 fast-forward 的推送;如果有,那我们拒绝此次推送:

只允许纯 fast-forward 推送

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


一切都设定好了。如果现在运行 chmod u+x .git/hooks/update —— 修改包含以上内容文件的权限,然后尝试推送一个包含非 fast-forward 类型的索引,会得到一下提示:


$ 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'


这里有几个有趣的信息。首先,我们可以看到挂钩运行的起点:


Enforcing Policies...


(refs/heads/master) (fb8c72) (c56860)


注意这是从 update 脚本开头输出到标准你输出的。所有从脚本输出的提示都会发送到客户端,这点很重要。


下一个值得注意的部分是错误信息。


[POLICY] Cannot push a non fast-forward reference


error: hooks/update exited with error code 1


error: hook declined to update refs/heads/master


第一行是我们的脚本输出的,在往下是 Git 在告诉我们 update 脚本退出时返回了非零值因而推送遭到了拒绝。最后一点:


To git@gitserver:project.git


! [remote rejected] master -> master (hook declined)


error: failed to push some refs to 'git@gitserver:project.git'


我们将为每一个被挂钩拒之门外的索引受到一条远程信息,解释它被拒绝是因为一个挂钩的原因。


而且,如果那个 ref 字符串没有包含在任何的提交里,我们将看到前面脚本里输出的错误信息:


[POLICY] Your message is not formatted correctly


又或者某人想修改一个自己不具备权限的文件然后推送了一个包含它的提交,他将看到类似的提示。比如,一个文档作者尝试推送一个修改到 lib 目录的提交,他会看到


[POLICY] You do not have access to push to lib/test.rb


全在这了。从这里开始,只要 update 脚本存在并且可执行,我们的仓库永远都不会遭到回转或者包含不符合要求信息的提交内容,并且用户都被锁在了沙箱里面。

客户端挂钩

这种手段的缺点在于用户推送内容遭到拒绝后几乎无法避免的抱怨。辛辛苦苦写成的代码在最后时刻惨遭拒绝是十分悲剧切具迷惑性的;更可怜的是他们不得不修改提交历史来解决问题,这怎么也算不上王道。


逃离这种两难境地的法宝是给用户一些客户端的挂钩,在他们作出可能悲剧的事情的时候给以警告。然后呢,用户们就能在提交--问题变得更难修正之前解除隐患。由于挂钩本身不跟随克隆的项目副本分发,所以必须通过其他途径把这些挂钩分发到用户的 .git/hooks 目录并设为可执行文件。虽然可以在相同或单独的项目内 容里加入并分发它们,全自动的解决方案是不存在的。


首先,你应该在每次提交前核查你的提交注释信息,这样你才能确保服务器不会因为不合条件的提交注释信息而拒绝你的更改。为了达到这个目的,你可以增加'commit-msg'挂钩。如果你使用该挂钩来阅读作为第一个参数传递给 git 的提交注释信息,并且与规定的模式作对比,你就可以使 git 在提交注释信息不符合条件的情况下,拒绝执行提交。


#!/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


如果这个脚本放在这个位置 (.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 脚本将把里面的规定执行起来:


#!/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


这和服务端的脚本几乎一样,除了两个重要区别。第一,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 脚本的例子。它获取一个所有即将重写的提交内容的列表,然后检查它们是否在远程的索引里已经存在。一旦发现某个提交可以从远程索引里衍变过来,它就放弃衍合操作:

最后

由于篇幅限制,小编在此截出几张知识讲解的图解







用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
git(7)自定义 Git_Java_爱好编程进阶_InfoQ写作平台