Git正解 脱水版 【8. 定制Git】

8.1 配置Git

如前所述,使用git config可配置Git,首先需设定用户名和用户邮箱,

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

Git使用了一组配置文件,用于修改Git系统的默认功能,Git首先将查找,系统级配置文件/etc/gitconfig,操作系统的所有用户和所有仓库,都将使用该文件的配置,同时用户可使用git config --system,修改系统级配置。

之后Git将查找,用户级配置文件~/.gitconfig(或~/.config/git/config),这是特定用户所使用的配置文件,同时它可覆盖系统级的同名配置,用户可使用git config --global,修改用户级配置。

然后Git还可查找,仓库级配置文件.git/config,这是特定仓库所使用的配置文件,同时它可覆盖系统级和用户级的同名配置,用户可使用git config --local,修改仓库级配置,不建议用户修改该配置,使用默认配置即可。

注意,Git配置文件是一个纯文本,用户可以选择手工修改,或是使用git config命令。

客户端的基本配置

Git的配置分为两大类,客户端和服务端,客户端配置即用户配置,虽然大多数功能都支持用户配置,但依然有很多琐碎的小功能,只适用于特殊场景,这里只讨论常用功能,如果需要查看所有的配置变量,可运行,

$ man git-config
core.editor

默认情况下,Git将使用,shell环境变量VISUAL或EDITOR所指定的默认文本编辑器,通常是vi编辑器,文本编辑器可用于修改提交和标签的描述,使用core.editor配置变量,用户可设定不同的文本编辑器,比如emacs,

$ git config --global core.editor emacs
commit.template

该配置变量可设定成,一个带路径的文件,当编写提交描述时,该文件可为用户提供一个预置模板,以保证正确的格式,比如常见的模块文件 ~/.gitmessage.txt,

Subject line (try to keep under 50 characters)

Multi-line description of commit,
feel free to be detailed.

[Ticket: X]

Subject line是提交者应当给出的提交简介,即git log --oneline的输出信息,之后两行,则需详细描述提交的细节,Ticket则是问题点或bug的编号。

完成commit.template设定后,用户再运行git commit,Git将调用文本编辑器,并打开提交描述模板,等待用户编辑,

$ git config --global commit.template ~/.gitmessage.txt
$ git commit

当然不同的开发团队可以自定义不同的提交模板。

core.pager

该配置变量可设定Git输出的分页格式(分页器),比如log或diff命令中,默认参数为less,也可设定为more,当然用户也可提供一个空串,失效该配置,这时输出将不会使用分页模式,

$ git config --global core.pager ''
user.signingkey

在创建附注标签的签名时,预设一个GPG签名密钥,可简化操作,如下,

$ git config --global user.signingkey <GPG密钥id>

配置变量被设定后,在创建标签时,则无需指定一个密钥,

$ git tag -s <tag-name>
core.excludesfile

该配置变量可指定.gitignore文件,.gitignore中列出的文件,可视为未跟踪文件,或是在运行git add命令时,不暂存这些文件,比如用户创建了一个~/.gitignore_global文件,内容如下,

*~
.*.swp
.DS_Store

之后,可设定配置,

git config --global core.excludesfile ~/.gitignore_global
help.autocorrect

如果用户输入出现错误,如下,

$ git chekcout master
git: 'chekcout' is not a git command. See 'git --help'.

Did you mean this?
    checkout

Git有能力识别用户的输入错误,只是默认被禁用,如果将help.autocorrect设为1,Git可自动修正用户的输入错误,如下,

$ git chekcout master
WARNING: You called a Git command named 'chekcout', which does not exist.
Continuing under the assumption that you meant 'checkout'
in 0.1 seconds automatically...

0.1秒是等待时间,如果用户的设定值为50,那么等待时间为5秒,即Git自动修正用户的输入错误后,将等待一段时间,以便用户确认修正后的结果,如果用户未给出响应,修正后的命令,将自动执行。

配色

Git支持命令行输出的配色,可协助用户更快更简单地查看相关命令的输出,因此提供了一些配置变量,方便设定不同的配色方案。

color.ui

Git会默认实现命令输出的配色,同时也提供了一个配置变量,用于关闭输出的配色,

$ git config --global color.ui false

配色的默认参数为auto,但只能用于命令行输出,如果输出信息重定向到管道或文件中,配色将失效,当然用户可将配色参数,设定为always,管道输出也将包含配色信息,在大多数情况下,为了在重定向输出中,保留配色信息,可在Git命令中,添加 --color选项,因此默认参数auto,基本能满足用户的大多数需求。

color.*

如果用户期望不同命令使用不同的配色,Git也支持相关设定,每个配置变量都可使用true,false,always参数值,

color.branch
color.diff
color.interactive
color.status

同时每个配置变量还包含了子变量,用于设定更复杂的局部配色效果,以下命令将设定,diff输出的meta信息,前景色为蓝色,背景色为黑色,文字加粗,

$ git config --global color.diff.meta "blue black bold"

用户还可使用不同的颜色值,normal/black/red/green/yellow/blue/magenta/cyan/white,同时也可设置不同的文字风格,bold/dim/ul/blink/reverse。

合并和比较的外部工具

虽然Git自建了比较(diff)工具,但可以替换成外部工具,同时也可设定一个合并工具(包含图形界面),方便用户手动处理合并冲突,这个工具即为Perforce Visual Merge Tool(P4Merge),另外P4Merge支持主流平台,macOS/Linux/Windows,首先下载P4Merge,之后配置它的封装脚本,不同的操作平台,请参考相关的操作指南,这里以Linux平台为主,在合并脚本extMerge中,可调用P4Merge的二进制执行文件,并提供相应的执行参数,

$ cat /usr/local/bin/extMerge
#!/bin/sh
/Applications/p4merge.app/Contents/MacOS/p4merge $*

diff封装脚本可支持7个执行参数,同时可将其中的两个参数,传递给合并脚本,Git会将以下执行参数,传递给diff工具,

path old-file old-hex old-mode new-file new-hex new-mode

由于合并脚本只需要old-file和new-file参数,传递之,

$ cat /usr/local/bin/extDiff
#!/bin/sh
[ $# -eq 7 ] && /usr/local/bin/extMerge "$2" "$5"

开启两个脚本的可执行权限,

$ sudo chmod +x /usr/local/bin/extMerge
$ sudo chmod +x /usr/local/bin/extDiff

这时用户可使用配置变量,设定上述脚本,merge.tool可设定合并工具,mergetool.<工具名>.cmd可设定,使用合并工具的默认命令,mergetool.<工具名>.trustExitCode可设定默认命令的返回值,可标记默认命令的执行结果,diff.external可设定外部的比较工具,

$ git config --global merge.tool extMerge
$ git config --global mergetool.extMerge.cmd 'extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"'
$ git config --global mergetool.extMerge.trustExitCode false
$ git config --global diff.external extDiff

用户也可打开~/.gitconfig文件,手动添加上述设置,

[merge]
  tool = extMerge
[mergetool "extMerge"]
  cmd = extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"
  trustExitCode = false
[diff]
  external = extDiff

完成设置后,可运行比较命令,

$ git diff 32d1776b1^ 32d1776b1

在这里插入图片描述
从上图可知,比较输出并未显示在命令行,而是出现在P4Merge的图形界面中,如果两条分支的合并存在冲突,用户可运行git mergetool,并在P4Merge的图形界面中,手动处理合并冲突,由于存在封装脚本extDiff和extMerge,用户可随意变更不同的外部工具,比如KDiff3,

$ cat /usr/local/bin/extMerge
#!/bin/sh
/Applications/kdiff3.app/Contents/MacOS/kdiff3 $*

Git也提供了一些不同的合并工具,并且无需设置cmd配置变量,以下是合并工具的支持列表,

$ git mergetool --tool-help
'git mergetool --tool=<tool>' may be set to one of the following:
    emerge
    gvimdiff
    gvimdiff2
    opendiff
    p4merge
    vimdiff
    vimdiff2

The following tools are valid, but not currently available:
    araxis
    bc3
    codecompare
    deltawalker
    diffmerge
    diffuse
    ecmerge
    kdiff3
    meld
    tkdiff
    tortoisemerge
    xxdiff
Some of the tools listed above only work in a windowed
environment. If run in a terminal-only session, they will fail.

如果用户不想使用KDiff3进行差异比较,而是用于合并冲突的处理,可进行以下配置,

$ git config --global merge.tool kdiff3

之后Git将合并冲突处理中,使用KDiff3,并使用git diff进行差异比较。

文件格式与空格

在项目协作过程中,大多数开发者都会遇到,因文件格式和空格所引发的小问题,尤其是在跨平台的环境下,这些问题可能来自于补丁,或者编辑器,又或是不同的操作系统,因此Git提供一些配置变量,帮助用户处理这些问题。

core.autocrlf

由于Windows下,一行文本的行尾使用了回车符(CR)和换行符(LF),而macOS和Linux的行尾只使用了换行符(LF),因此不同的操作系统将引入不同的规则,Windows的大多数编辑器,会将文本行尾的LF标识,替换成CRLF标识,当用户点击回车键时,会插入两个CR标识,配置core.autocrlf变量,可使Git将文件放入暂存区之前,自动将CRLF标识,替换成LF标识,当用户查看工作区的代码时,基于不同的操作系统,又可将LF标识,替换成CRLF标识,因此在Windows系统下,需将core.autocrlf设为true,

$ git config --global core.autocrlf true

如果用户工作在macOS或Linux系统下,则无需将LF替换成CRLF,如果文件本身已包含CRLF标识,依然需要Git将CRLF替换成LF,如下,

$ git config --global core.autocrlf input

如果用户工作在Windows系统下,并且开发项目只支持Windows系统,这时可失效该配置变量,

$ git config --global core.autocrlf false
core.whitespace

Git可检查和修复一些空格问题,并提供了6种选项,3种默认有效,但可以失效,3种默认无效,但可以激活,3种默认有效的选项,分别是blank-at-eol, 检查行尾的空格,blank-at-eof, 检查文件末尾的空白行,space-before-tab, 检查行首tab符号之前的空格,3种默认无效的选项,分别是indent-with-non-tab,检查行首中替换tab符号的空格,同时tab符号的空格数,受控于tabwidth,tab-in-indent,检查缩进所包含的tab符号数,cr-at-eol,表示行尾的回车符合法。

在core.whitespace配置中,可添加上述选项,并使用逗号分割,如果用户需失效某个选项,只需在选项名之前,添加-符号,未给出的选项将继续保持默认状态,列出的默认失效的选项将被激活,比如下例中,失效space-before-tab,应当注意,trailing-space同时包含了blank-at-eol和blank-at-eof的功能,

$ git config --global core.whitespace \
    trailing-space,-space-before-tab,indent-with-non-tab,tab-in-indent,cr-at-eol

当用户运行git diff时,Git将检测空格问题,同时出现空格问题的地方,将被着色,以方便用户在提交前,修正空格问题,当用户使用git apply应用补丁文件时,上述配置也可帮助用户规避空格问题,即Git将发出警告信息,

$ git apply --whitespace=warn <补丁文件名>

或者在应用补丁文件之前,由Git自动修复空格问题,

$ git apply --whitespace=fix <补丁文件名>

上述选项也能用于git rebase命令,如果用户提交并未推送的远程仓库,同时包含了空格问题,可运行git rebase --whitespace=fix,由Git自动修复。

服务端配置

虽然服务端的配置变量不多,但用户必须了解一些常用配置。

receive.fsckObjects

服务端可在每次推送时,检查推送包含的所有对象的SHA-1校验值是否正确,但这并不是默认操作,因为开销太大,会导致推送的传输速度变低,尤其是面对大型仓库或大量推送的场景,如果用户需要检查每次推送包含的所有对象,可使用以下设置,

$ git config --system receive.fsckObjects true

此时Git可基于每次推送的检查,获得整个仓库的完整性,并防止故障或恶意客户端,引入错误数据。

receive.denyNonFastForwards

如果用户衍合了已推送提交,并提交了生成的衍合提交,该提交将被拒,如果用户推送了一个远程分支无法识别的提交,该推送操作也将被拒,该策略的优点毋庸置疑,在衍合场景下,如果用户很清楚推送的最终结果,可使用强制推送,为了禁止强制推送,可使用以下配置,

$ git config --system receive.denyNonFastForwards true

但是用户依然可以利用,服务端的接收hook脚本(稍后将介绍),实现强制推送,当然这类操作会更加复杂。

receive.denyDeletes

denyNonFastForwards的另一种实现方式,用户可删除远程分支,并重新推送整条分支,为了禁止该操作,可使用以下配置,

$ git config --system receive.denyDeletes true

该配置可禁止用户删除分支或标签,为了删除远程分支,用户必须登录服务器,手动删除远程分支。

8.2 Git属性

Git可为子目录或文件子集,配置不同的设置,而这些与路径相关的配置,被称为Git属性,Git属性可放入仓库目录(通常是根目录)的.gitattributes文件,如果用户不希望提交属性文件,Git属性也可放入.git/info/attributes文件。

利用Git属性,用户可实现一些特殊功能,比如针对个别文件或目录的特殊合并策略,如何比较非文本文件,关键标识的扩展。

二进制文件

Git属性的一个常见用法,标识二进制文件(没有其他方法可识别二进制文件),并给出特殊的处理指令,在当下的环境中,依然存在不可比较的文本文件,以及可比较的二进制文件,因此Git无法提供一种通用的识别方法,必须由用户指定。

标识二进制文件

有些文件看上去是文本文件,但实际上保存了二进制数据,比如macOS的Xcode项目文件 .pbxproj,实际上是一个JSON数据集合,可记录项目的构建参数,以及与项目关联的信息,虽然它是一个文本,同时它又是一个轻量级的数据库,如果该文件产生变更,也无需合并,同时查找该文件的变更细节,也没有太大的帮助,因此该文件其实是面对机器,而不是面对用户,所以应当将其视为二进制文件,在.gitattributes文件中,添加一行文本,将pbxproj文件,标识为二进制文件,

*.pbxproj binary

这时Git将不会修改该文件出现的空格问题,git show或git diff也不会显示该文件出现的变更。

比较二进制文件

用户可使用Git属性,实现二进制文件的高效比较,也就是将二进制数据,转换成文本格式,再使用diff进行比较。用户可借助该功能,处理一些人尽皆知的常见问题,比如ms word文档的版本控制,大家都知道word是一个怪异而恐怖的编辑器,但依然有很多人在使用它,如果用户需要控制word文档的版本,同样需要提交到Git仓库,如果直接运行git diff,将看到以下输出,

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 88839c4..4afcb7c 100644
Binary files a/chapter1.docx and b/chapter1.docx differ

此时无法实现word文档的直接比较,必须借助Git属性,首先在.gitattributes文件中,插入一行,

*.docx diff=word

这时扩展名为docx的任意文件,在比较时,Git都将调用word过滤器,也就是使用docx2txt工具,将word文档转换成可读的文本文件,再使用diff进行比较。

首先用户需要安装docx2txt工具,可从 https://sourceforge.net/projects/docx2txt 页面,下载该工具,软件包的INSTALL文件中,将给出安装指令,遵照指令完成安装,之后用户需编写一个docx2txt封装脚本,脚本可命令为docx2txt,如下,

#!/bin/bash
docx2txt.pl "$1" -

不要忘记打开脚本文件的权限,即chmod a+x,最后设定Git配置变量,

$ git config diff.word.textconv docx2txt

这时diff在比较时,将自动调用docx2txt,实现word文档的转换,下例的word文档中,添加了一行文本,

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 0b013ca..ba25db5 100644
--- a/chapter1.docx
+++ b/chapter1.docx
@@ -2,6 +2,7 @@
 This chapter will be about getting started with Git. We will begin at the beginning
by explaining some background on version control tools, then move on to how to get Git
running on your system and finally how to get it setup to start working with. At the
end of this chapter you should understand why Git is around, why you should use it and
you should be all setup to do so.
 1.1. About Version Control
 What is "version control", and why should you care? Version control is a system that
records changes to a file or set of files over time so that you can recall specific
versions later. For the examples in this book you will use software source code as the
files being version controlled, though in reality you can do this with nearly any type
of file on a computer.
+Testing: 1, 2, 3.
 If you are a graphic or web designer and want to keep every version of an image or
layout (which you would most certainly want to), a Version Control System (VCS) is a
very wise thing to use. It allows you to revert files back to a previous state, revert
the entire project back to a previous state, compare changes over time, see who last
modified something that might be causing a problem, who introduced an issue and when,
and more. Using a VCS also generally means that if you screw things up or lose files,
you can easily recover. In addition, you get all this for very little overhead.
 1.1.1. Local Version Control Systems
 Many people's version-control method of choice is to copy files into another
directory (perhaps a time-stamped directory, if they're clever). This approach is very
common because it is so simple, but it is also incredibly error prone. It is easy to
forget which directory you're in and accidentally write to the wrong file or copy over
files you don't mean to.

从diff输出可知,word文档中,增加了Testing: 1, 2, 3.行,虽然格式不完美,但结果是正确的。

这类Git属性的另一个用法,图片文件的比较,从图片文件中,提取EXIF数据,即大多数图片格式所保存的元数据(metadata),再比较EXIF,首先用户需要下载和安装exiftool工具,它可将图片元数据转换成文本,首先在.gitattributes文件中,插入一行,

*.png diff=exif

在Git配置变量中,设定图片转换工具,

$ 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
 File Type                       : PNG
 MIME Type                       : image/png
-Image Width                     : 1058
-Image Height                    : 889
+Image Width                     : 1056
+Image Height                    : 827
 Bit Depth                       : 8
 Color Type                      : RGB with Alpha

从上述输出可知,文件大小和图片尺寸都发生了变化。

关键字扩展

在SVN或CVS系统中,开发者经常使用关键字扩展,实现标准信息的通用接口,比如提交日期,提交版本,提交者等,但在Git中,用户无法修改已提交的文件,因为每个文件都附带了一个校验值,文件一旦修改,校验值也将发生变化,这时用户可在切换分支时,将上述扩展信息,添加到文件中,并在暂存文件之前,删除掉扩展信息,因此Git属性为用户,提供了两种实现方法,

比如需要将文件包含的 I d Id Id标记,自动替换成SHA-1校验码,用户可为特定文件或文件类型,配置对应的Git属性,当切换分支时,Git将自动实现上述替换,应当注意,这不是提交的校验码,在.gitattributes文件中,插入一行,

*.txt ident

在测试文件中,添加$Id$标记,

$ echo '$Id$' > test.txt

之前的介绍中并未提及,git checkout除了具有切换分支的功能,还具备恢复工作区文件的功能,因此以下命令才不会让人奇怪,

$ rm test.txt
$ git checkout -- test.txt
$ cat test.txt
$Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

上述用法存在缺陷,因为CVS或Subversion的关键字替换,会包含一个时间戳,而在Git中,只有SHA-1校验值,无法区分时间的先后,因此用户需要自定义一个专用过滤器,通常命名为clean(清除关键字替换)和smudge(生成关键字替换),并在.gitattributes文件中,配置该专用过滤器,以便在文件恢复(smudge),或是文件暂存(clean)时,调用不同的过滤器脚本,这些过滤器脚本的用法如下,
在这里插入图片描述
在这里插入图片描述

这里假定关键字扩展过滤器为indent,并且只处理C源码文件,在.gitattributes文件中,关联*.c文件和indent过滤器,

*.c filter=indent

同时设定indent过滤器在清除关键字替换,与生成关键字替换时,所使用的命令工具,

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

如果暂存文件中包含了c文件,Git将自动调用indent工具,如果暂存文件被退回工作区,Git也将自动调用cat工具,当然cat只是一个通用工具,无法完成关键字替换的清除任务,下例将实现$Date$关键字的扩展,类似于本地版本控制系统,这时需要一个简单脚本,获取文件名,项目的最新提交日期,再将这些信息,写入到文件中,如下,

#! /usr/bin/env ruby
data = STDIN.read
last_date = `git log --pretty=format:"%ad" -1`
puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')

用户可将上述脚本,命名为expand_date,重新配置一个dater过滤器,添加expand_date脚本,清除任务则交给Perl表达式,

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

测试上述过滤器,在.gitattributes文件中,插入一行,

date*.txt filter=dater

创建测试文件,查看测试结果,

$ echo '# $Date$' > date_test.txt
$ 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
# $Date: Tue Apr 21 07:26:52 2009 -0700$

用户应当小心处理上述应用,因为.gitattributes会推送到远程仓库,而过滤器的处理工具却不会,所以用户需要考虑过滤器失效的情况下,项目也能正常工作。

导出仓库

在项目打包时,Git属性也可实现不同的功能。

export-ignore

使用该属性,可在打包项目时,忽略一些文件和目录,比如test目录中,只包含一些可忽略的测试文件,可在.gitattributes文件中,插入一行,

test/ export-ignore

执行git archive打包命令时,将不会包含test目录。

export-subst

如果需要在打包文件中附带开发信息,可利用export-subst属性,配置关键字扩展功能(基于git log的输出格式),比如可在项目中包含一个LAST_COMMIT文件,并为该文件,配置export-subst属性,当运行git archive命令时,最新提交的信息将自动替换掉LAST_COMMIT文件所包含的关键字,首先在.gitattributes文件中,插入一行,

LAST_COMMIT export-subst

创建测试文件,并查看打包结果,

$ echo 'Last commit date: $Format:%cd by %aN$' > LAST_COMMIT
$ git add LAST_COMMIT .gitattributes
$ git commit -am 'adding LAST_COMMIT file for archives'

$ git archive HEAD | tar xCf ../deployment-testing -
$ cat ../deployment-testing/LAST_COMMIT
Last commit date: Tue Apr 21 08:38:48 2009 -0700 by Scott Chacon

上述替换还可用于提交描述和对象注解(git note),同时git log也能因此获得更简单的自动换行,

$ echo '$Format:Last commit: %h by %aN at %cd%n%+w(76,6,9)%B$' > LAST_COMMIT
$ git commit -am 'export-subst uses git log'\''s custom formatter

git archive uses git log'\''s `pretty=format:` processor
directly, and strips the surrounding `$Format:` and `$`
markup from the output.
'

$ git archive @ | tar xfO - LAST_COMMIT
Last commit: 312ccc8 by Jim Hill at Fri May 8 09:14:04 2015 -0700
    export-subst uses git log's custom formatter
    
    git archive uses git log's `pretty=format:` processor directly, and
    strips the surrounding `$Format:` and `$` markup from the output.

虽然最终的打包结果,能满足开发的需要,但是这类打包操作未必符合将来的需求。

合并策略

Git属性也可设定特定项目文件的合并策略,一个常见用法,当特定文件出现合并冲突,用户可以不合并这些文件,而是使用自己的本地文件进行覆盖,这有利于保持项目分支的独立性,但用户可以选择在合并中回滚文件,或是直接忽略某些文件,比如在两条分支中,存在一个不同的数据库配置文件database.xml,当两条分支需要合并时,应该忽略此数据库配置文件,首先在.gitattributes文件中,插入一行,

database.xml merge=ours

之后可配置ours(从分支)的配置策略,

$ git config --global merge.ours.driver true

那么在主分支中,上述配置可消除从分支database.xml,所引发的合并冲突,执行以下命令后,database.xml将继续使用主分支的原有文件,

$ git merge topic
Auto-merging database.xml
Merge made by recursive.

8.3 Git hook

与大多数的版本控制系统一样,在某些重要的操作中,Git可以调用自定义脚本,这些脚本(hook)可分为两大类,客户端hook和服务端hook,客户端hook可触发一些操作,比如提交和合并,服务端hook可调用网络操作,比如接收提交的推送。

安装hook

所有的hook脚本都保存在仓库的hooks子目录中,在大多数项目中,hook目录的路径为.git/hooks,当git init完成仓库的初始化之后,Git将在hooks目录下,放置一组示例脚本,用户可参考这些脚本,同时每个脚本都有对应的文档,这些脚本都是shell脚本,有些使用了Perl语言,当然用户也可选择Ruby,Python或其他语言,来编写shell脚本,如果用户需要运行这些示例脚本,则需要改名,因为这些脚本的文件名末尾,都附带.sample。

一个hook脚本必须放置在hooks目录下,并且命令正确(文件名不包含扩展名),同时赋予执行权限。

客户端hook

以下将介绍不同的客户端hook,应当注意,在仓库克隆的默认状态下,将不会复制客户端hook,用户必须在服务端配置这些hook脚本,才可实现hook脚本的本地复制。

提交工作流hook

在提交中,通常会使用4种hook。

在用户输入提交描述之前,pre-commit hook就将运行,即提交的预备处理,常用于检查即将提交的快照,确认提交内容是否完整,确认相关测试是否通过,检查相关代码,当hook脚本返回非零值,提交操作将中止,虽然用户可使用git commit --no-verify,忽略此hook脚本,但一些基础测试必须完成,比如代码风格的检查(类似于lint工具),文件的空格问题(hook的默认操作),新建类方法的文档是否存在。

在提交描述的编辑器调用之前(此时默认提交描述已创建),将运行prepare-commit-msg hook,即提交的预处理,此时hook脚本可编辑,默认的提交描述,同时该hook脚本可附带一些选项,比如保存提交描述的文件路径,提交的类型,提交校验码(提交已被重新修改),但是在正常提交中,该hook用处不大,只适用于附带默认提交描述的提交,比如使用模板的提交描述,合并提交,重组提交,已重新修改的提交,用户可基于提交模板,插入一些专属信息。

commit-msg hook可附带一个参数,即包含提交描述的临时文件的路径,如果该hook脚本返回非零值,提交操作将中止,因此该hook脚本可用于验证项目的当前状态,或是提交描述是否满足预设的需求。

当提交完成后,可运行post-commit hook,该hook脚本无参数,但可使用git log -1 HEAD,获取最新提交的信息,该hook脚本常用于通告提交的完成状态。

邮件工作流hook

在邮件工作流中,可配置3种hook,同时也可被git am调用,以及应用补丁的git format-patch命令。

首先可使用applypatch-msg hook,它可附带一个参数,即包含补丁提交描述的临时文件名,如果该hook脚本返回非零值,Git将中止补丁的应用,使用该hook脚本,可保证提交描述的格式正确,或是自动修改提交描述的风格。

使用git am应用补丁文件时,可首先运行pre-applypatch hook,这里的名称会产生误解,当补丁应用已启动,但提交并未完成时,将运行该hook脚本,脚本可检查需提交的快照,以及运行测试和工作区检查,如果工作区存在文件丢失,或是测试未通过,该hook脚本将返回非零值,git am的执行将被中止。

当提交完成后,git am将运行post-applypatch hook,该hook脚本可通知开发组或补丁生成者,补丁已被应用,应当注意,用户无法停止该脚本的执行。

其他客户端hook

在分支衍合之前,可运行pre-rebase hook,如果该脚本返回非零值,衍合操作将中止,使用该脚本,可拒绝衍合已推送到远程仓库的提交,除此之外,该脚本的一些预设条件,并不适合某些工作流。

替换提交的命令,可运行post-rewrite hook,比如git commit --amend和git rebase(未使用 git filter-branch),该hook脚本可附带一个参数,即触发重新写入操作的命令,它可从stdin中,接收一个重新写入的列表,这一类hook都可完成上述相同的任务,比如post-checkout和post-merge。

成功运行git checkout之后,可运行post-checkout hook,该脚本可正确清理项目的工作区,即移除超大二进制文件,自动生成的文档,以及无需管理的其他文件。

成功运行merge之后,可运行post-merge hook,该脚本可恢复工作区的数据,即未被Git跟踪的数据,比如权限数据,如果工作区产生变化,该hook脚本可验证,用户复制的未跟踪文件是否存在。

git push可运行pre-push hook,也就是远程仓库的引用已被更新,但推送数据并未传输时,可运行该hook脚本,它可接收远程仓库的名称和地址,并能从stdin,获取到远程仓库引用的一个更新列表,因此该脚本可在推送开启之前,验证远程仓库引用的更新结果,如果该脚本返回非零值,推送操作将中止。

Git偶尔会运行git gc --auto命令,进行垃圾收集,在垃圾收集之前,可运行pre-auto-gc hook,该脚本常用于垃圾收集的通告,或者提醒用户,立刻中止垃圾收集,并不明智。

服务端hook

除了客户端hook,系统管理员还可使用一组重要的服务端hook,这些脚本可强制开发项目,实现某些策略,并能运行在推送操作的前后,如果推送之前的hook脚本,返回非零值,推送操作将被拒,并会向客户端,发送一条出错消息,当然配置一个理想的推送策略,其实相当复杂。

pre-receive

当处理客户端推送时,首先将运行pre-receive hook,该hook脚本可从stdin,获取到一个即将被推送的引用列表,同时系统管理员可利用该脚本,完成一些管理任务,比如引用更新是否存在冲突,或者推送是否修改了引用和文件的访问权限。

update

类似于pre-receive hook,两者的区别在于,如果实现多条分支的推送,pre-receive hook只执行一次,而每条分支的推送,都执行一次update hook,该hook脚本不会读取stdin,而是附带了三个参数,引用(即分支)名,推送前的引用校验码,当前推送的引用校验码,若update hook返回非零值,只有当前引用的推送被拒,其他引用依然可更新。

post-receive

整个推送完成后,可运行post-receive hook,用于告知用户,推送已完成,或者更新其他服务,该hook脚本可读取stdin,并实现不同功能,比如基于邮件列表的邮件群发,通告一个持续集成服务器,或者更新一个问题跟踪系统,甚至在问题点开启,修改,关闭时,脚本都可解析提交描述,以获得问题点的状态信息,应当注意,该脚本无法被中止,由于在推送结束前,客户端都不会断开连接,因此系统管理员应当小心处理,以免推送操作花费过多的时间。

8.4 强制策略

经过之前的学习,已了解Git的常见工作方式,比如检查提交描述的格式,只允许特定用户修改项目的特定目录,同时还可以创建客户端脚本,帮助开发者了解推送被拒的原因,以及创建服务端脚本,强制实现一些服务端策略。这些脚本可使用Ruby语言,由于Ruby具有更好的可读性,其他语言的开发者也能轻松阅读,当然用户也可使用其他语言来编写这类脚本,Git包含的示例脚本,都采用了Perl和Bash。

服务端hook

所有服务端的任务,都可放入hook目录的update文件中,当每条分支的推送开启时,都将运行update hook,该hook脚本可附带三个参数,需推送的引用(分支)名,分支旧版本的校验码,即将推送的分支新版本的校验码。

如果推送操作基于ssh协议,系统管理员可获取用户的操作信息,如果管理员允许所有用户,基于公钥验证,使用一个公共账号(比如git),这时管理员可为该公共账号,配置一个shell封装器,并设定相关的环境变量,假设可登录用户都包含在$USER环境变量中,同时update脚本在启动时,可获取所需的传入信息,

#!/usr/bin/env ruby
 
$refname = ARGV[0]
$oldrev  = ARGV[1]
$newrev  = ARGV[2]
$user    = ENV['USER']
 
puts "Enforcing Policies..."
puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"
强制限定提交描述的格式

如果每个提交描述中,都必须包含一个特征字串,例如ref: 1234,因为在问题跟踪系统中,管理员希望每个提交都能关联到特定的开发组,所以需检查每个推送提交的描述中,是否包含了该特征字串,如果未包含,update脚本将返回非零值,推送将被拒。

在update脚本中,首先需获取所有推送提交的校验值列表,可将$newrev和$oldrev变量值,传递给git rev-list命令,该命令可显示新旧分支版本之间,包含的所有提交校验值,即所有需推送的提交,

$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475

之后可逐个检查,首先需获取每个提交的提交描述,这时可使用git cat-file命令,

$ 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

利用sed工具,基于命令输出的空白行,可提取提交描述,如下,

$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number

使用Ruby,编写update脚本,完成上述检查,

$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
强制使用ACL(用户访问权限列表)

在ACL中,可限定用户的推送范围,一些用户可推送整个项目,而另一些用户只能推送,特定子目录或文件,为了配置这些规则,需在仓库中,创建一个acl文件,同时在update脚本中,可应用这些规则,查找登录用户是否具备推送权限。首先需创建acl文件,如下,

avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests

每个配置段使用|符号进行分割,首段可设为avail或unavail,类似于白名单和黑名单,如果未提供末尾段,则表示这组用户拥有整个项目的推送权限,doc/lib/tests则表示用户只拥有doc/lib/tests目录的推送权限,这时可编写Ruby脚本,解析acl文件,

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

解析完成,可返回以下数据结构,

{"defunkt"=>[nil],
"tpw"=>[nil],
"nickh"=>[nil],
"pjhyett"=>[nil],
"schacon"=>["lib", "tests"],
"cdickens"=>["doc"],
"usinclair"=>["doc"],
"ebronte"=>["doc"]}

获取到用户权限后,还需查找每个提交的提交者,这时可使用git log命令,将其加入到Ruby脚本中,确认用户的提交是否合法,

# 特定用户只允许修改特定目录
def 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
测试

修改update脚本的执行权限,chmod u+x .git/hooks/update,当用户推送的提交中,存在不规范的提交描述时,将出现以下错误,

$ 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] Your message is not formatted correctly
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) (8338c5) (c5b616)

这时列出了推送分支的新旧版本,

[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master

update脚本返回非零值,当前提送已中止

To git@gitserver:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'

列出远程分支和用户本地分支,并告知推送已中止,原因是hook脚本出现错误。

如果用户修复提交描述之后,重新提交,不过当前用户不具备lib目录的访问权限,这时将产生以下错误,

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

客户端hook

当用户提交被拒,不可避免会引发用户的抱怨,这就要求用户能够正确处理自己的提交历史,以免错误不断发生,这类错误的预防措施,就是配置客户端hook脚本,利用脚本来检查用户操作,以便在提交和问题出现之前,就能修正这些潜在问题,以免问题不断扩大,变得难以修复,由于默认情况下,项目克隆无法复制客户端hook,用户必须使用其他方法,发布这些脚本,同时其他用户必须手动复制.git/hooks目录,并保证目录中的脚本能正常运行,当然用户也可选择,hook脚本能和项目一同发布,但是Git并未给出类似的默认配置。

首先用户需要在提交保存之前,检查提交描述,这可避免因为提交描述的格式问题,导致服务端拒收用户提交,这时可添加commit-msg hook,保存提交描述的文件,可传入hook脚本,之后脚本可检查提交描述的格式,如果格式不符,提交操作将中止,

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

如果上述hook脚本的保存路径为git/hooks/commit-msg,并且开启的执行权限,在提交操作中,可实现提交描述的自动检查,如下,

$ git commit -am 'test'
[POLICY] Your message is not formatted correctly

在提交描述中,添加正确的信息,

$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
 1 files changed, 1 insertions(+), 0 deletions(-)

为了确保用户无法修改ACL(具备权限才可访问)文件,如果项目.git目录中,保存了已修改文件(可能包含ACL文件)的原有副本,可使用pre-commit hook脚本,恢复用户的错误行为,

#!/usr/bin/env ruby
 
$user    = ENV['USER']
 
# 插入之前的acl_access_data方法
 
# 只允许特定用户修改特定子目录
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文件放置在不同位置,因为hook脚本将运行在工作区,而非.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的设置是否正确。

为了确保用户不会推送non-fast-forwarded引用,首先需确定引用的类型,并非non-fast-forwarded,即存在内容覆盖的衍合提交(衍合了已推送的提交),或是向远程分支,推送一条不同的本地分支(远程分支无法识别的分支),当然系统管理员也可使用 receive.denyDeletes和receive.denyNonFastForwards配置变量,禁止上述的推送操作,但是hook脚本可查找到,存在问题的衍合提交。

以下是衍合操作的预处理脚本,它可检查已推送分支的衍合,该脚本可遍历需衍合的提交,并逐个检查这些提交是否为已推送提交,如果存在已推送提交,衍合操作将中止,

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

上述实现的最大缺陷在于,速度巨慢并且作用不大,只要用户不尝试强制推送(-f选项),如果出现non-fast-forwarded推送,服务端都会发出警告,并拒收推送,这里只是介绍相关问题的产生机理,以便在实际操作中,能够轻松修正这类问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值