标签归档:版本控制

git hooks

git允许用户在执行一条git命令的前后通过脚本的方式进行一些预处理或善后工作,这些脚本称为hook,也就是钩子。钩子文件统一存放在在.git/hooks目录下,一些常用的钩子介绍在这里可以找到。git hooks配合git config,可以构建一个工作流,非常方便。

比如有个场景,一个仓库下有多个分支,每个分支依赖这不同的配置文件,里面的配置项在不同的开发机上不同(IP,MAC地址等),当有多个开发人员协作开发时,会因为别人的一次提交覆盖自己本地仓库里的配置(加.gitignore也解决不了,git clone时会缺失这个文件)。这时候就可以利用post-checkout这个钩子,每次执行git checkout命令后,post-checkout中的shell脚本会读取本机配置,写入项目配置文件。

一个简单的钩子例子:

1
2
3
#!/bin/sh
 
echo $(git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* .*/\1/')

将上述脚本保存为post-checkout,放在.git/hooks目录下,今后每次执行git-checkout命令都会在控制台显示当前分支名称。

--EOF--

批量git-cherry-pick

Git从1.7.2版本开始支持批量cherry-pick,就是一次可以cherry-pick一个区间的commit。

用法如下:

1
2
3
$ git cherry-pick <start-commit-id>..<end-commit-id>
或
$ git cherry-pick <start-commit-id>^..<end-commit-id>

前者表示把<start-commit-id>到<end-commit-id>之间(左开右闭,不包含start-commit-id)的提交cherry-pick到当前分支;后者表示把<start-commit-id>到<end-commit-id>之间(闭区间,包含start-commit-id)的提交cherry-pick到当前分支。其中,<start-commit-id>到<end-commit-id>只需要commit-id的前6位即可,并且<start-commit-id>在时间上必须早于<end-commit-id>。

下面这个场景可以使用批量cherry-pick:
有两个分支,master和develop,develop有一部分功能比较紧急,需要优先上线,所以develop的一部分提交要合并到master分支。通过git-merge命令,忽略一些commit也可以临时满足要求,说“临时”是因为develop分支上剩下的提交迟早也要合到master上去,所以通过git-merge忽略分支的做法不大可行。这时候使用git-chrry-pick进行批量操作比较方便。

举个栗子:
1. master分支下只有1次提交。

1
2
(master)$ git log --pretty=oneline
e8759e501529b9eea04d5717aeb13e2fa83a7a2d base

2. develop分支下有10次提交。

1
2
3
4
5
6
7
8
9
10
11
(develop)$ git log --pretty=oneline
91b8696560ce3c2fe4b282d4fa814447c0d93cd1 10
e97cc3c92634c8278e1c60b9053d735033db956a 9
58febd437b20391611f90ebeedea0d024345655f 8
562130942862e44a11009b60bb9bddbaa23dc21f 7
dd627ee763bdc3190f6ebd199f74b9a67978b30c 6
9b68ddf74e50573e4cdc6542fb17c182c12e3e07 5
b1d63e7181bb9ef996a90368a9c5a840f201ae80 4
793cd10cbab4ba147b0011d4bdf882d050ff6435 3
1739e414db9709821ae253187a7ce38c56bf6f2d 2
e8759e501529b9eea04d5717aeb13e2fa83a7a2d base

3. 现在要把develop分支上的3-4,7-10次提交合并到master分支,2、5和6暂时不合。

1
2
(master)$ git cherry-pick 1739e4..b1d63e //(2, 4]
(master)$ git cherry-pick 562130^..91b869 //[7,10]

4. 检查master分支log,所需提交已经合并完成。

1
2
3
4
5
6
7
8
(master)$ git log --pretty=oneline
b195b40627f333f2e8f2fa0429d099364b50dd65 10
66ca86797023d05eda47804b325c575ee976d2f0 9
e677961d84dc77771086e3cca0b04fe35bd00046 8
e0dcac8a3786e4b96455a5d1ddac1e30631ee8af 7
87fad31564e801d143ea0a88329a174127a9b477 4
1dbd37e4728b8a92117f2070d057aaff426ce3fa 3
e8759e501529b9eea04d5717aeb13e2fa83a7a2d base

--EOF--

git回滚

git-reset命令可以让本地仓库回滚到之前的任意版本。例如:

1
git reset commit-id

如果加--hard参数,会抹掉当前工作区、暂存区乃至仓库中所有在命令中commit-id之后提交的修改。这样,在本地仓库通过git-log命令就看不见被回滚掉的commit了。

假如一份代码需要多人协作,托管在远程服务器(例如GitHub)上,本地仓库回滚后还要push到服务器上,如果直接执行:

1
$ git push origin

服务器会返回:

1
2
3
4
5
6
To https://github.com/fengchj/test.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'https://github.com/fengchj/test.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.

这时候要给git-push命令加-f参数,表示强制push。如:

1
2
3
4
$ git push -f origin
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/fengchj/test.git
 + d1e6f05...087898e master -> master (forced update)

服务器上的HEAD指针也被回滚到之前的某个版本。如果此时客户端git-clone一个仓库,便看不到被回滚掉的commit。

但是假如在强制push之前,有客户端已经克隆了一个仓库,看到了本来应该被回滚掉的commit,此时即使他执行git-fetch或git-pull命令,也无法将本地仓库的HEAD指针与服务器上的同步起来,本地HEAD会领先服务器HEAD好几个commit。这时需要这个客户端执行git-reset命令,同步本地仓库的HEAD指针与服务器HEAD,使他们指向同一个commit。如:

1
2
3
$ git reset origin/master
或
$ git reset --hard origin/master

执行过后git-log命令就看不到被其他客户端回滚掉的commit了。

--EOF--

git reflog

今天git提交代码的时候,因为提交了一个错误的文件,于是蛋疼地用刚学到的git reset --hard命令来取消提交。按我的理解,git reset只是移除仓库中的commit记录,使所有相关文件回到暂存区,也就是git add之后的状态。结果就悲剧了。

还好有个git-reflog命令,可以详细记录仓库中各个分支的状态变更情况,比如checkout、merge、reset、cherry-pick,以及最重要的commit操作。从git-reflog命令输出的列表中可以看见每次操作的commit-id,如:

1
2
3
4
5
MacBook-Pro:pf fengchj$ git reflog
ac86a08 HEAD@{0}: reset: moving to ac86a080cc0859662e727de625fc414a211ba6c2
92f3986 HEAD@{1}: commit: update configuration. 
ac86a08 HEAD@{2}: commit: misc: 解决接口中文乱码问题。
1ccb4a3 HEAD@{3}: commit: misc: 修改获取列表接口配置(兼容状态推送)。

从上面可以看出,因为执行了git reset --hard ac86a080cc0859662e727de625fc414a211ba6c2导致当前分支回到了“ac86a08 HEAD@{2}: commit: misc: 解决接口中文乱码问题”提交后的状态,也就是说丢失了“92f3986 HEAD@{1}: commit: update configuration. ”这次提交的所有信息(git log中看不到)。

git有多种机制保障只要知道丢失的commit-id,就可以还原因误操作丢失的commit记录。

例如,执行git reset --hard 92f3986可以将分支强制回复到92f3986提交后的状态。也可以用『git cherry-pick』一文提到的git-cherry-pick命令来还原:git cherry-pick 92f3986。此外,还可以用merge命令来还原:git merge 92f3986。

所以只要有了git-reflog命令,就不用怕因误操作而丢失commit记录。另外,如果不进行额外配置,git-reflog默认保存30天的操作记录。

References:
[1] reflog, your safety net. http://gitready.com/intermediate/2009/02/09/reflog-your-safety-net.html .
[2] git-reflog(1) Manual Page. https://www.kernel.org/pub/software/scm/git/docs/git-reflog.html .

--EOF--

git cherry-pick

版本控制中有时候会遇到这样的情况:
线上版本对应git仓库中的master分支,当前迭代对应develop分支,develop分支中已经加了很多新特性,此时发现一个小bug,需要及时修复到master分支上去,按照标准流程,应该从master开一个hotfixes分支,等bug修复后再分别merge到master和develop分支。

有个更快速的办法是使用git cherry-pick命令(man page),cherry-pick允许将一个分支上的一次(或多次)commit合并到另一个分支上。回到上面场景,将bug修复的补丁提交到develop分支时,会得到一个commit-id,这时可以git checkout master切换到master分支,执行git cherry-pick commit-id将develop分支上的bug补丁提交到master上。

--EOF--