修改文章的组织方式,增加了Wiki。

This commit is contained in:
ZhongyiTong
2015-11-02 15:54:28 +08:00
parent 6f61ba42fc
commit e87c34928c
10 changed files with 128 additions and 106 deletions

View File

@ -0,0 +1,273 @@
Git log高级用法
===
> BY 童仲毅(geeeeeeeeek@github)
>
> 这是一篇在[原文](https://www.atlassian.com/git/tutorials/git-log)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名([CC BY 2.5 AU](http://creativecommons.org/licenses/by/2.5/au/deed.zh))协议共享。
每一个版本控制系统的出现都是为了让你记录代码的变化。你可以看到项目的历史记录——谁贡献了什么、bug是什么时候引入的还可以撤回有问题的更改。但是首先你得知道如何来使用它。这也就是为什么会有`git log` 这个命令。
到现在为止,你应该已经知道如何用`git log` 命令来显示最基本的提交信息。但除此之外,你还可以传入各种不同的参数来获得不一样的输出。
`git log` 有两个高级用法一是自定义commit的输出格式二是过滤哪些commit要输出。这两个用法合二为一你就可以找到你项目中你需要的任何信息。
格式化Log输出
---
首先,这篇文章会展示几种`git log` 格式化输出的例子。大多数例子只是通过标记来向`git log` 请求或多或少的信息。
如果你不喜欢默认的`git log` 格式,你可以用`git config` 的别名功能来给你想要的格式创建一个快捷方式。
### Oneline
`--oneline` 标记把每一个commit压缩到了一行中。它默认只显示commit ID和commit信息的第一行。一般来说`git log --oneline` 的输出是这样的:
```
0e25143 Merge branch 'feature'
ad8621a Fix a bug in the feature
16b36c6 Add a new feature
23ad9ad Add the initial code base
```
它对于获得你项目的大致情况很有帮助。
### Decorate
很多时候知道每个commit关联的分支或者标签很有用。`--decorate` 标记让`git log` 显示指向这个commit的所有引用比如说分支、标签等
这可以和另一个配置项一起使用。比如,执行`git log --oneline --decorate` 会将commit历史格式化成这样
```
0e25143 (HEAD, master) Merge branch 'feature'
ad8621a (feature) Fix a bug in the feature
16b36c6 Add a new feature
23ad9ad (tag: v0.9) Add the initial code base
```
在这个例子中你可以看到最上面那个commit已经被checkout了通过HEAD标记而且它还是master分支的尾端。第二个commit有另一个feature分支指向它以及最后那个commit带有v0.9标签。
分支、标签、HEAD还有commit历史是你Git仓库中包含的所有信息。因此这个命令让你更完整地观察项目结构。
### Diff
`git log` 提供了很多选项来显示两个commit之间的差异。其中最常用的两个是`--stat``-p`
`--stat` 选项显示每次commit的文件增删数量注意修改一行记作增加一行且删去一行当你想要查看commit引入的变化时这会非常有用。比如说下面这个commit在hello.py文件中增加了67行删去了38行。
```
commit f2a238924e89ca1d4947662928218a06d39068c3
Author: John <john@example.com>
Date: Fri Jun 25 17:30:28 2014 -0500
Add a new feature
hello.py | 105 ++++++++++++++++++++++++-----------------
1 file changed, 67 insertion(+), 38 deletions(-)
```
文件名后面+和-的数量是这个commit造成的更改中增删的相对比例。它给你一个直观的感觉关于这次commit有多少改动。如果你想知道每次commit删改的绝对数量你可以将`-p` 选项传入`git log` 。这样commit所有的删改都会被输出
```
commit 16b36c697eb2d24302f89aa22d9170dfe609855b
Author: Mary <mary@example.com>
Date: Fri Jun 25 17:31:57 2014 -0500
Fix a bug in the feature
diff --git a/hello.py b/hello.py
index 18ca709..c673b40 100644
--- a/hello.py
+++ b/hello.py
@@ -13,14 +13,14 @@ B
-print("Hello, World!")
+print("Hello, Git!")
```
对于改动很多的commit来说这个输出会变得又长又大。一般来说当你输出所有删改的时候你应该是想要查找某一具体的改动这时你就要用到`pickaxe` 选项。
### Shortlog
`git shortlog` 是一种特别的`git log` 它是为创建发布声明设计的。它把每个commit按作者分类显示commit信息的第一行。这样可以容易地看到谁做了什么。
比如说两个开发者为项目贡献了5个commit那么`git shortlog` 输出会是这样的:
```
Mary (2):
Fix a bug in the feature
Fix a serious security hole in our framework
John (3):
Add the initial code base
Add a new feature
Merge branch 'feature'
```
默认情况下,`git shortlog` 把输出按作者名字排序,但你可以传入`-n` 选项来按每个作者commit数量排序。
### Graph
`--graph` 选项绘制一个ASCII图像来展示commit历史的分支结构。它经常和 `--oneline``--decorate` 两个命令一起使用这样会更容易查看哪个commit属于哪个分支
```
git log --graph --oneline --decorate
For a simple repository with just 2 branches, this will produce the following:
* 0e25143 (HEAD, master) Merge branch 'feature'
|\
| * 16b36c6 Fix a bug in the new feature
| * 23ad9ad Start a new feature
* | ad8621a Fix a critical security issue
|/
* 400e4b7 Fix typos in the documentation
* 160e224 Add the initial code base
```
星号表明这个commit所在的分支所以上面这个图的意思是`23ad9ad``16b36c6` 这两个commit在topic分之上其余的在master分支上。
虽然这对简单的项目来说是个很好用的选择但你可能会更喜欢gitk或SourceTree这些更强大的可视化工具来分析大型项目。
### 自定义格式
对于其他的`git log` 格式需求,你都可以使用`--pretty=format:"<string>"` 选项。它允许你使用像printf一样的占位符来输出commit。
比如,下面命令中的`%cn``%h``%cd` 这三种占位符会被分别替换为作者名字、缩略标识和提交日期。
```
git log --pretty=format:"%cn committed %h on %cd"
This results in the following format for each commit:
John committed 400e4b7 on Fri Jun 24 12:30:04 2014 -0500
John committed 89ab2cf on Thu Jun 23 17:09:42 2014 -0500
Mary committed 180e223 on Wed Jun 22 17:21:19 2014 -0500
John committed f12ca28 on Wed Jun 22 13:50:31 2014 -0500
```
完整的占位符清单可以在文档中找到。
除了让你只看到关注的信息,这个`--pretty=format:"<string>"` 选项在你想要在另一个命令中使用日志内容是尤为有用的。
过滤提交历史
---
格式化commit输出只是`git log` 其中的一个用途。另一半是理解如何浏览整个提交历史。接下来的文章会介绍如何用`git log` 选择项目历史中的特定的commit。所有的用法都可以和上面讨论过的格式化选项结合起来。
### 按数量
`git log` 最基础的过滤选项是限制显示的commit数量。当你只对最近几次commit感兴趣时它可以节省你一页一页查看的时间。
你可以在后面加上`-<n>`选项。比如说下面这个命令会显示最新的3次commit
```
git log -3
```
### 按日期
如果你想要查看某一特定时间段内的commit你可以使用`--after``--before` 标记来按日期筛选。它们都接受好几种日期格式作为参数。比如说下面的命令会显示2014年7月1日后的commit
```
git log --after="2014-7-1"
```
你也可以传入相对的日期,比如一周前("`1 week ago`")或者昨天("`yesterday`"
```
get log --after="yesterday"
```
你可以同时提供`--before``--after` 来检索两个日期之间的commit。比如为了显示2014年7月1日到2014年7月4日之间的commit你可以这么写
```
git log --after="2014-7-1" --before="2014-7-4"
```
注意`--since``--until` 标记和`--after``--before` 标记分别是等价的。
### 按作者
当你只想看某一特定作者的commit的时候你可以使用`--author` 标记。它接受正则表达式返回所有作者名字满足这个规则的commit。如果你知道那个作者的确切名字你可以直接传入文本字符串
```
git log --author="John"
```
它会显示所有作者叫John的commit。作者名不一定是全匹配只要包含那个子串就会匹配。
你也可以用正则表达式来创建更复杂的检索。比如下面这个命令检索名叫Mary或John的作者的commit。
```
git log --author="John\|Mary"
```
注意作者的邮箱地址也算作是作者的名字,所以你也可以用这个选项来按邮箱检索。
如果你的工作流区分提交者和作者,`--committer` 也能以相同的方式使用。
### 按提交信息
按提交信息来过滤commit你可以使用`--grep` 标记。它和上面的`--author` 标记差不多,只不过它搜索的是提交信息而不是作者。
比如说你的团队规范要求在提交信息中包括相关的issue编号你可以用下面这个命令来显示这个issue相关的所有commit
```
git log --grep="JRA-224:"
```
你也可以传入`-i` 参数来忽略大小写匹配。
### 按文件
很多时候,你只对某个特定文件的更改感兴趣。为了显示某个特定文件的历史,你只需要传入文件路径。比如说,下面这个命令返回所有和`foo.py``bar.py` 文件相关的commit
```
git log -- foo.py bar.py
```
`--` 告诉`git log` 接下来的参数是文件路径而不是分支名。如果分支名和文件名不可能冲突,你可以省略`--`
### 按内容
我们还可以根据源代码中某一行的增加和删除来搜索commit。这被称为pickaxe它接受形如`-S"<string>"` 的参数。比如说,当你想要知道`Hello, World!` 字符串是什么时候加到项目中哪个文件中去的,你可以使用下面这个命令:
```
git log -S "Hello, World!"
```
如果你想用正则表达式而不是字符串来搜索,你可以使用`-G"<regex>"` 标记。
这是一个非常强大的调试工具它能让你定位到所有影响代码中特定一行的commit。它甚至可以让你看到某一行是什么时候复制或者移动到另一个文件中去的。
### 按范围
你可以传入范围来筛选commit。这个范围由下面这样的格式指定其中< since >< until >是commit的引用
```
git log <since>..<until>
```
这个命令在你使用分支引用作为参数时特别有用。这是一个显示两个分支之间区别最简单的方式。看看下面这个命令:
```
git log master..feature
```
其中的master..feature范围包含了在feature分支而不在feature分支中所有的commit。换句话说这个命令可以看出从master分支Fork到feature分支后发生了哪些变化。它可以这样可视化
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/git-log/01.svg)
注意如果你更改范围的前后顺序(feature..master)你会获取到master分支而非feature分支上的所有commit。如果`git log` 输出了全部两个分支的commit这说明你的提交历史已经分叉了。
### 过滤出merge commit
`git log` 输出时默认包括merge commit。但是如果你的团队采用强制合并策略意思是merge上游修改你的分支而不是将你的分支rebase到上游分支你的项目历史中会有很多外来的commit。
你可以通过`--no-merges` 标记来排除这些commit
```
git log --no-merges
```
另一方面如果你只对merge commit感兴趣你可以使用`--merges` 标记:
```
git log --merges
```
它会返回所有有两个父节点的commit。
总结
---
你现在应该对使用`git log` 来格式化输出和选择你要显示的commit的用法比较熟悉了。它允许你查看你项目历史中任何需要的内容。
这些技巧是你Git工具箱中重要的部分不过注意`git log` 往往和其他Git命令连着使用。当你找到了你要的commit你把它传给`git checkout``git revert` 或是其他控制你提交历史的工具。所以请继续坚持Git高级用法的学习。

113
sources/Git图解.md Normal file
View File

@ -0,0 +1,113 @@
Git图解
========
> BY 童仲毅(geeeeeeeeek@github)
>
> 这是一篇在[原文](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)基础上演绎的文章。原作者[Mark Lodato](lodatom@gmail.com),译者[wych](ellrywych@gmail.com)。原文采用[创用CC 姓名标示-非商业性-相同方式分享3.0 美国授权条款](https://creativecommons.org/licenses/by-nc-sa/3.0/us/)授权。
此页图解git中的最常用命令。如果你稍微理解git的工作原理这篇文章能够让你理解的更透彻。
基本用法
---------
![enter image description here](http://marklodato.github.io/visual-git-guide/basic-usage.svg)
上面的四条命令在工作目录、stage缓存(也叫做索引)和commit历史之间复制文件。
* `git add files` 把工作目录中的文件加入stage缓存
* `git commit` 把stage缓存生成一次commit并加入commit历史
* `git reset -- files` 撤销最后一次`git add files`,你也可以用`git reset` 撤销所有stage缓存文件
* `git checkout -- files` 把文件从stage缓存复制到工作目录用来丢弃本地修改
你可以用 `git reset -p``git checkout -p``git add -p`进入交互模式也可以跳过stage缓存直接从commit历史取出文件或者直接提交代码。
![enter image description here](http://marklodato.github.io/visual-git-guide/basic-usage-2.svg)
* `git commit -a ` 相当于运行`git add`把所有当前目录下的文件加入stage缓存再运行`git commit`
* `git commit files` 进行一次包含最后一次提交加上工作目录中文件快照的提交并且文件被添加到stage缓存。
* `git checkout HEAD -- files` 回滚到复制最后一次提交。
约定
--------
后文中以下面的形式使用图片:
![enter image description here](http://marklodato.github.io/visual-git-guide/conventions.svg)
绿色的5位字符表示提交的ID分别指向父节点。分支用橙色显示分别指向特定的提交。当前分支由附在其上的_HEAD_标识。
这张图片里显示最后5次提交_ed489_是最新提交。 _master_分支指向此次提交另一个_maint_分支指向祖父提交节点。
命令详解
---------
### Diff
有许多种方法查看两次提交之间的变动,下面是其中一些例子。
![enter image description here](http://marklodato.github.io/visual-git-guide/diff.svg)
### Commit
提交时git用stage缓存中的文件创建一个新的提交并把此时的节点设为父节点。然后把当前分支指向新的提交节点。下图中当前分支是_master_。
在运行命令之前_master_指向_ed489_提交后_master_指向新的节点_f0cec_并以_ed489_作为父节点。
![enter image description here](http://marklodato.github.io/visual-git-guide/commit-master.svg)
即便当前分支是某次提交的祖父节点git会同样操作。下图中在_master_分支的祖父节点_maint_分支进行一次提交生成了_1800b_。
这样_maint_分支就不再是_master_分支的祖父节点。此时[merge](#merge) 或者 [rebase](#rebase) 是必须的。
![enter image description here](http://marklodato.github.io/visual-git-guide/commit-maint.svg)
如果想更改一次提交,使用 `git commit --amend`。git会使用与当前提交相同的父节点进行一次新提交旧的提交会被取消。
![enter image description here](http://marklodato.github.io/visual-git-guide/commit-amend.svg)
另一个例子是[分离HEAD提交](#detached),在后面的章节中介绍。
### Checkout
checkout命令用于从历史提交或者stage缓存中拷贝文件到工作目录也可用于切换分支。
当给定某个文件名(或者打开-p选项或者文件名和-p选项同时打开git会从指定的提交中拷贝文件到stage缓存和工作目录。比如`git checkout HEAD~ foo.c`会将提交节点_HEAD~_即当前提交节点的父节点中的`foo.c`复制到工作目录并且加到stage缓存中。如果命令中没有指定提交节点则会从stage缓存中拷贝内容。注意当前分支不会发生变化。
![enter image description here](http://marklodato.github.io/visual-git-guide/checkout-files.svg)
当不指定文件名而是给出一个本地分支时那么_HEAD_标识会移动到那个分支也就是说我们“切换”到那个分支了然后stage缓存和工作目录中的内容会和_HEAD_对应的提交节点一致。新提交节点下图中的a47c3中的所有文件都会被复制到stage缓存和工作目录中只存在于老的提交节点ed489中的文件会被删除不属于上述两者的文件会被忽略不受影响。
![enter image description here](http://marklodato.github.io/visual-git-guide/checkout-branch.svg)
如果既没有指定文件名也没有指定分支名而是一个标签、远程分支、SHA-1值或者是像_master~3_类似的东西就得到一个匿名分支称作_detached HEAD_被分离的_HEAD_标识。这样可以很方便地在历史版本之间互相切换。比如说你想要编译1.6.6.1版本的git你可以运行`git checkout v1.6.6.1`(这是一个标签,而非分支名),编译,安装,然后切换回另一个分支,比如说`git checkout master`。然而当提交操作涉及到“分离的HEAD”时其行为会略有不同详情见在[下面](#detached)。
![enter image description here](http://marklodato.github.io/visual-git-guide/checkout-detached.svg)
### HEAD标识处于分离状态时的提交操作
当_HEAD_处于分离状态不依附于任一分支提交操作可以正常进行但是不会更新任何已命名的分支。你可以认为这是在更新一个匿名分支。
![enter image description here](http://marklodato.github.io/visual-git-guide/commit-detached.svg)
一旦此后你切换到别的分支比如说_master_那么这个提交节点可能再也不会被引用到然后就会被丢弃掉了。注意这个命令之后就不会有东西引用_2eecb_。
![enter image description here](http://marklodato.github.io/visual-git-guide/checkout-after-detached.svg)
但是,如果你想保存这个状态,可以用命令`git checkout -b name`来创建一个新的分支。
![enter image description here](http://marklodato.github.io/visual-git-guide/checkout-b-detached.svg)
### Reset
reset命令把当前分支指向另一个位置并且有选择的变动工作目录和索引。也用来在从历史commit历史中复制文件到索引而不动工作目录。
如果不给选项,那么当前分支指向到那个提交。如果用`--hard`选项,那么工作目录也更新,如果用`--soft`选项,那么都不变。
![enter image description here](http://marklodato.github.io/visual-git-guide/reset-commit.svg)
如果没有给出提交点的版本号那么默认用_HEAD_。这样分支指向不变但是索引会回滚到最后一次提交如果用`--hard`选项,工作目录也同样。
![enter image description here](http://marklodato.github.io/visual-git-guide/reset.svg)
如果给了文件名(或者 `-p`选项), 那么工作效果和带文件名的[checkout](#checkout)差不多,除了索引被更新。
![enter image description here](http://marklodato.github.io/visual-git-guide/reset-files.svg)
### Merge
merge 命令把不同分支合并起来。合并前,索引必须和当前提交相同。如果另一个分支是当前提交的祖父节点,那么合并命令将什么也不做。
另一种情况是如果当前提交是另一个分支的祖父节点就导致_fast-forward_合并。指向只是简单的移动并生成一个新的提交。
![enter image description here](http://marklodato.github.io/visual-git-guide/merge-ff.svg)
否则就是一次真正的合并。默认把当前提交(_ed489_ 如下所示)和另一个提交(_33104_)以及他们的共同祖父节点(_b325c_)进行一次[三方合并](http://en.wikipedia.org/wiki/Three-way_merge)。结果是先保存当前目录和索引然后和父节点_33104_一起做一次新提交。
![enter image description here](http://marklodato.github.io/visual-git-guide/merge.svg)
### Cherry Pick
cherry-pick命令"复制"一个提交节点并在当前分支做一次完全一样的新提交。
![enter image description here](http://marklodato.github.io/visual-git-guide/cherry-pick.svg)
### Rebase
rebase是合并命令的另一种选择。合并把两个父分支合并进行一次提交提交历史不是线性的。rebase在当前分支上重演另一个分支的历史提交历史是线性的。
本质上,这是线性化的自动的 [cherry-pick](#cherry-pick)。
![enter image description here](http://marklodato.github.io/visual-git-guide/rebase.svg)
上面的命令都在_topic_分支中进行而不是_master_分支在_master_分支上重演并且把分支指向新的节点。注意旧提交没有被引用将被回收。
要限制回滚范围,使用`--onto`选项。下面的命令在_master_分支上重演当前分支从_169a6_以来的最近几个提交即_2c33a_。
![enter image description here](http://marklodato.github.io/visual-git-guide/rebase-onto.svg)
同样有`git rebase --interactive`让你更方便的完成一些复杂操作,比如丢弃、重排、修改、合并提交。

View File

@ -0,0 +1,76 @@
Git简易指南(上)
===========
> BY 童仲毅(geeeeeeeeek@github)
>
> 除非另行注明,页面上所有内容采用知识共享-署名([CC BY 2.5 AU](http://creativecommons.org/licenses/by/2.5/au/deed.zh))协议共享。[git-guide](https://github.com/rogerdudler/git-guide/) 项目对本文亦有贡献。
这节是完全面向入门者的我假设你从零开始创建一个项目并且想用Git来进行版本控制因此本文会避开分支这些相对复杂的概念。
在这节中我会介绍如何在你的个人项目中使用Git我们会讨论Git最基本的操作——如何初始化你的项目如何管理新的或者已有的文件如何在远端仓库中储存你的代码。
安装Git
---------
- Mac用户Xcode Command Line Tools自带Git (`xcode-select --install` )
- Linux用户`sudo apt-get install git`
- Windows用户下载[Git SCM](git-for-windows.github.io)
- 对于Windows用户安装后如果希望在全局的cmd中使用git需要把git.exe加入PATH环境变量中或在Git Bash中使用Git。
检出仓库
----
执行如下命令以创建一个本地仓库的克隆版本:
`git clone /path/to/repository`
如果是远端服务器上的仓库,你的命令会是这个样子:
`git clone username@host:/path/to/repository` 通过SSH
或者:
`git clone https:/path/to/repository.git` 通过https
比如说`git clone https://github.com/geeeeeeeeek/git-recipes.git`可以将git教程clone到你指定的目录。
创建新仓库
--------
创建新文件夹,打开,然后执行 `git init`以创建新的 git 仓库。
> 下面每一步中,你都可以通过`git status`来查看你的git仓库状态。
工作流
---
你的本地仓库由 git 维护的三棵“树”组成。第一个是你的 `工作目录`,它持有实际文件;第二个是 `缓存区(Index)`,它像个缓存区域,临时保存你的改动;最后是 `HEAD`,指向你最近一次提交后的结果。
![enter image description here](http://www.bootcss.com/p/git-guide/img/trees.png)
> 事实上第三个阶段是commit history的图。HEAD一般是指向最新一次commit的引用。现在暂时不必究其细节。
添加与提交
----
你可以计划改动(把它们添加到缓存区),使用如下命令:
```
git add < filename >
git add *
```
这是 git 基本工作流程的第一步。使用如下命令以实际提交改动:
```
git commit -m "代码提交信息"
```
现在,你的改动已经提交到了 HEAD但是还没到你的远端仓库。
> 在开发时良好的习惯是根据工作进度及时commit并务必注意附上有意义的commit message。创建完项目目录后第一次提交的commit message一般为"Initial commit."。
推送改动
---
你的改动现在已经在本地仓库的 HEAD 中了。执行如下命令以将这些改动提交到远端仓库:
```
git push origin master
```
可以把 master 换成你想要推送的任何分支。
如果你还没有克隆现有仓库,并欲将你的仓库连接到某个远程服务器,你可以使用如下命令添加:
```
git remote add origin <server>
```
如此你就能够将你的改动推送到所添加的服务器上去了。
> - 这里origin是< server >的别名取什么名字都可以你也可以在push时将< server >替换为origin。但为了以后push方便我们第一次一般都会先remote add。
> - 如果你还没有git仓库可以在Github等代码托管平台上创建一个空(不要自动生成README.md)的repository然后将代码push到远端仓库。
##### 至此,你应该可以顺利地提交你的项目了。在下一节中,我们将涉及更多的命令,来完成更有用的操作。比如从远端的仓库拉取更新并且合并到你的本地,如何通过分支多人协作,如何处理不同分支的冲突等等。

451
sources/Git钩子.md Normal file
View File

@ -0,0 +1,451 @@
Git钩子
===
> BY 童仲毅(geeeeeeeeek@github)
>
> 这是一篇在[原文](https://www.atlassian.com/git/tutorials/git-hooks)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名([CC BY 2.5 AU](http://creativecommons.org/licenses/by/2.5/au/deed.zh))协议共享。
Git钩子是在Git仓库中特定事件发生时自动运行的脚本。它可以让你自定义Git内部的行为在开始周期中的关键点触发自定义的行为。
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/git-hooks/01.svg)
Git钩子最常见的使用场景包括了推行提交规范根据仓库状态改变项目环境和接入持续集成工作流。但是因为脚本可以完全定制你可以用Git钩子来自动化或者优化你开发工作流中任意部分。
在这篇文章中我们会先简要介绍Git钩子是如何工作的。然后我们会审视一些本地和远端仓库使用最流行的钩子。
概述
===
所有Git钩子都是仓库中特定事件发生时Git自动运行的普通脚本。因此Git钩子安装和配置也非常容易。
钩子在本地或服务端仓库都可以部署,而且最会在仓库中事件发生时被执行。在文章后面我们会具体地研究各种钩子。接下来所讲的配置对本地和服务端钩子都起作用。
### 安装钩子
钩子存在于每个Git仓库的`.git/hooks` 目录中。当你初始化仓库时Git自动生成这个目录和一些示例脚本。当你观察`.git/hooks` 时,你会看到下面这些文件:
```
applypatch-msg.sample pre-push.sample
commit-msg.sample pre-rebase.sample
post-update.sample prepare-commit-msg.sample
pre-applypatch.sample update.sample
pre-commit.sample
```
这里已经包含了大部分可用的钩子了,但是`.sample` 拓展名防止它们默认被执行。为了安装一个钩子,你只需要去掉`.sample` 拓展名。或者你要写一个新的脚本,你只需添加一个文件名和上述匹配的新文件,去掉`.sample` 拓展名。
比如说,试试安装一个`prepare-commit-msg` 钩子。去掉脚本的`.sample` 拓展名,在文件中加上下面这两行:
```
#!/bin/sh
echo "# Please include a useful commit message!" > $1
```
钩子需要能被执行,所以如果你创建了一个新的脚本文件,你需要修改它的文件权限。比如说,为了确保`prepare-commit-msg` 可执行,运行下面这个命令:
```
chmod +x prepare-commit-msg
```
接下来你每次运行`git commit`你会看到默认的提交信息都被替换了。我们会在“准备提交信息”一节中细看它是如何工作的。现在我们已经可以定制Git的内部功能你只要开心就好。
内置的样例脚本是非常有用的参考资料,因为每个钩子传入的参数都有非常详细的说明(不同钩子不一样)。
### 脚本语言
内置的脚本大多是shell和PERL语言的但你可以使用任何脚本语言只要它们最后能编译到可执行文件。每次脚本中的`#!/bin/sh` 定义了你的文件将被如何解释。比如使用其他语言时你只需要将path改为你的解释器的路径。
比如说,你可以在`prepare-commit-msg` 中写一个可执行的Python脚本。下面这个钩子和上一节的shell脚本做的事完全一样。
```
#!/usr/bin/env python
import sys, os
commit_msg_filepath = sys.argv[1]
with open(commit_msg_filepath, 'w') as f:
f.write("# Please include a useful commit message!")
```
注意第一行改成了Python解释器的路径。此外这里用`sys.argv[1]` 而不是`$1` 来获取第一个参数(这个也后面再讲)。
这个特性非常强大因为你可以用任何你喜欢的语言来编写Git钩子。
### 钩子的作用域
对于任何Git仓库来说钩子都是本地的而且它不会随着`git clone` 一起复制到新的仓库。而且,因为钩子是本地的,任何能接触得到仓库的人都可以修改。
对于开发团队来说,这有很大的影响。首先,你要确保你们成员之间的钩子都是最新的。其次,你也不能强行让其他人用你喜欢的方式提交——你只能鼓励他们这样做。
在开发团队中维护钩子是比较复杂的,因为`.git/hooks` 目录不随你的项目一起拷贝,也不受版本控制影响。一个简单的解决办法是把你的钩子存在项目的实际目录中(在`.git` 外)。这样你就可以像其他文件一样进行版本控制。为了安装钩子,你可以在`.git/hooks` 中创建一个符号链接,或者简单地在更新后把它们复制到`.git/hooks` 目录下。
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/git-hooks/02.svg)
作为备选方案Git同样提供了一个模板目录机制来更简单地自动安装钩子。每次你使用`git init``git clone` 时,模板目录文件夹下的所有文件和目录都会被复制到`.git` 文件夹。
所有的下面讲到的本地钩子都可以被更改或者彻底删除只要你是项目的所有者。这完全取决于你的团队成员想不想用这个钩子。所以记住最好把Git钩子当成一个方便的开发者工具而不是一个严格强制的开发规范。
也就是说,用服务端钩子来拒绝没有遵守规范的提交是完全可行的。后面我们会再讨论这个问题。
本地钩子
---
本地钩子只影响它们所在的仓库。当你在读这一节的时候,记住开发者可以修改他们本地的钩子,所以不要用它们来推行强制的提交规范。不过,它们确实可以让开发者更易于接受这些规范。
在这一节中我们会探讨6个最有用的本地钩子
- pre-commit
- prepare-commit-msg
- commit-msg
- post-commit
- post-checkout
- pre-rebase
前4个钩子让你介入完整的提交生命周期后2个允许你执行一些额外的操作分别为`git checkout``git rebase` 的安全检查。
所有带`pre-` 的钩子允许你修改即将发生的操作,而带`post-` 的钩子只能用于通知。
我们也会看到处理钩子的参数和用底层Git命令获取仓库信息的实用技巧。
### pre-commit
`pre-commit` 脚本在每次你运行`git commit` 命令时Git向你询问提交信息或者生产提交对象时被执行。你可以用这个钩子来检查即将被提交的代码快照。比如说你可以运行一些自动化测试保证这个提交不会破坏现有的功能。
`pre-commit` 不需要任何参数以非0状态退出时将放弃整个提交。让我们看一个简化了的和更详细的内置`pre-commit` 钩子。只要检测到不一致时脚本就放弃这个提交,就像`git diff-index` 命令定义的那样只要词尾有空白字符、只有空白字符的行、行首一个tab后紧接一个空格就被认为错误
```
#!/bin/sh
# Check if this is the initial commit
if git rev-parse --verify HEAD >/dev/null 2>&1
then
echo "pre-commit: About to create a new commit..."
against=HEAD
else
echo "pre-commit: About to create the first commit..."
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# Use git diff-index to check for whitespace errors
echo "pre-commit: Testing for whitespace errors..."
if ! git diff-index --check --cached $against
then
echo "pre-commit: Aborting commit due to whitespace errors"
exit 1
else
echo "pre-commit: No whitespace errors :)"
exit 0
fi
```
为了使用`git diff-index` 我们需要指出我们正在和那次提交比较。一般来说是HEAD但HEAD在创建第一次提交时不存在所以我们的第一个任务是解决这个极端情形。我们通过`git rev-parse --verify` 来检查HEAD是否是一个合法的引用。`>/dev/null 2>&1` 这部分屏蔽了`git rev-parse` 任何输出。HEAD或者一个新的提交对象被储存在`against` 变量中供`git diff-index` 使用。`4b825d...` 这个哈希值是代表一个空白提交的ID。
`git diff-index --cached` 命令将提交和缓存区比较。通过传入`-check` 选项我们要求它在更改引入空白字符错误时警告我们。如果它这么做了我们返回状态1来放弃这次提交否则返回状态0提交工作流正常进行。
这只是`pre-commit` 的其中一个例子。它恰好使用了已有的Git命令来根据提交带来的更改进行测试但你可以在`pre-commit` 中做任何你想做的事比如执行其它脚本、运行第三方测试集、用Lint检查代码风格。
### prepare-commit-msg
`prepare-commit-msg` 钩子在`pre-commit` 钩子在文本编辑器中生成提交信息之后被调用。这被用来方便地修改自动生成的squash或merge提交。
`prepare-commit-msg` 脚本的参数可以是下列三个:
- 包含提交信息的文件名。你可以在原地更改提交信息。
- 提交类型。可以是信息(`-m``-F` 选项),模板(`-t` 选项merge如果是个merge提交或squash如果这个提交插入了其他提交
- 相关提交的SHA1哈希值。只有当`-c`, `-C`, or `--amend` 选项出现时才会出现。
`pre-commit` 一样以非0状态退出会放弃提交。
我们已经看过一个修改提交信息的简单例子现在我们来看一个更有用的脚本。使用issue跟踪器时我们通常在单独的分支上处理issue。如果你在分支名中包含了issue编号你可以使用`prepare-commit-msg` 钩子来自动地将它包括在那个分支的每个提交信息中。
```
#!/usr/bin/env python
import sys, os, re
from subprocess import check_output
# Collect the parameters
commit_msg_filepath = sys.argv[1]
if len(sys.argv) > 2:
commit_type = sys.argv[2]
else:
commit_type = ''
if len(sys.argv) > 3:
commit_hash = sys.argv[3]
else:
commit_hash = ''
print "prepare-commit-msg: File: %s\nType: %s\nHash: %s" % (commit_msg_filepath, commit_type, commit_hash)
# Figure out which branch we're on
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
print "prepare-commit-msg: On branch '%s'" % branch
# Populate the commit message with the issue #, if there is one
if branch.startswith('issue-'):
print "prepare-commit-msg: Oh hey, it's an issue branch."
result = re.match('issue-(.*)', branch)
issue_number = result.group(1)
with open(commit_msg_filepath, 'r+') as f:
content = f.read()
f.seek(0, 0)
f.write("ISSUE-%s %s" % (issue_number, content))
```
首先,上面的`prepare-commit-msg` 钩子告诉你如何收集传入脚本的所有参数。接下来,它调用了`git symbolic-ref --short HEAD` 来获取对应HEAD的分支名。如果分支名以`issue-` 开头它会重写提交信息文件在第一行加上issue编号。比如你的分支名`issue-224` ,下面的提交信息将会生成:
```
ISSUE-224
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch issue-224
# Changes to be committed:
# modified: test.txt
```
有一点要记住的是即使用户用`-m` 传入提交信息时,`prepare-commit-msg` 也会运行。也就是说,上面这个脚本会自动插入`ISSUE-[#]` 字符串,而用户无法更改。你可以检查第二个参数是否是提交类型来处理这个情况。
但是,如果没有`-m` 选项,`prepare-commit-msg` 钩子允许用户修改生成后的提交信息。所以脚本的目的是为了方便,而不是推行强制的提交信息规范。如果你要这么做,你需要下一节所讲的`commit-msg` 钩子。
### commit-msg
`commit-msg` 钩子和`prepare-commit-msg` 钩子很像,但它会在用户输入提交信息之后被调用。这适合用来提醒开发者他们的提交信息不符合你团队的规范。
传入这个钩子唯一的参数是包含提交信息的文件名。如果它不喜欢用户输入的提交信息,它可以在原地修改这个文件(和`prepare-commit-msg` 一样或者它会以非0状态退出放弃这个提交。
比如说,下面这个脚本确认用户没有删除`prepare-commit-msg` 脚本自动生成的`ISSUE-[#]` 字符串。
```
#!/usr/bin/env python
import sys, os, re
from subprocess import check_output
# Collect the parameters
commit_msg_filepath = sys.argv[1]
# Figure out which branch we're on
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip()
print "commit-msg: On branch '%s'" % branch
# Check the commit message if we're on an issue branch
if branch.startswith('issue-'):
print "commit-msg: Oh hey, it's an issue branch."
result = re.match('issue-(.*)', branch)
issue_number = result.group(1)
required_message = "ISSUE-%s" % issue_number
with open(commit_msg_filepath, 'r') as f:
content = f.read()
if not content.startswith(required_message):
print "commit-msg: ERROR! The commit message must start with '%s'" % required_message
sys.exit(1)
```
虽然用户每次创建提交时,这个脚本都会运行。但你还是应该避免做检查提交信息之外的事情。如果你需要通知其他服务一个快照已经被提交了,你应该使用`post-commit` 这个钩子。
### post-commit
`post-commit` 钩子在`commit-msg` 钩子之后立即被运行 。它不能更改`git commit` 的结果了,所以这主要用于通知用途。
这个脚本没有参数,而且退出状态不会影响提交。对于大多数`post-commit` 脚本来说,你只是想访问你刚刚创建的提交。你可以用`git rev-parse HEAD` 来获得最近一次提交的SHA1哈希值或者你可以用`git log -l HEAD` 获取完整的信息。
比如说,如果你需要每次提交快照时向老板发封邮件(也许对于大多数工作流来事不是个好的想法),你可以加上下面这个`post-commit` 钩子。
```
#!/usr/bin/env python
import smtplib
from email.mime.text import MIMEText
from subprocess import check_output
# Get the git log --stat entry of the new commit
log = check_output(['git', 'log', '-1', '--stat', 'HEAD'])
# Create a plaintext email message
msg = MIMEText("Look, I'm actually doing some work:\n\n%s" % log)
msg['Subject'] = 'Git post-commit hook notification'
msg['From'] = 'mary@example.com'
msg['To'] = 'boss@example.com'
# Send the message
SMTP_SERVER = 'smtp.example.com'
SMTP_PORT = 587
session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
session.ehlo()
session.starttls()
session.ehlo()
session.login(msg['From'], 'secretPassword')
session.sendmail(msg['From'], msg['To'], msg.as_string())
session.quit()
```
你虽然可以用`post-commit` 来触发本地的持续集成系统,但大多数时候你想用的是`post-receive` 这个钩子。它运行在服务端而不是用户的本地机器,它同样在任何开发者推送代码时运行。那里更适合你进行持续集成。
### post-checkout
`post-checkout` 钩子和`post-commit` 钩子很像,但它是在你用`git checkout` 查看引用的时候被调用的。这是用来清理你的工作目录中可能会令人困惑的生成文件。This is nice for clearing out your working directory of generated files that would otherwise cause confusion.
这个钩子接受三个参数,它的返回状态不影响`git checkout` 命令。
- HEAD前一次提交的引用
- 新的HEAD的引用
- 1或0分别代表是分支checkout还是文件checkout。
Python程序员经常遇到的问题是切换分支后那些之前生成的`.pyc` 文件。解释器有时使用`.pyc` 而不是`.py` 文件。为了避免歧义,你可以在每次用`post-checkout` 切换到新的分支的时候,删除所有`.pyc` 文件。
```
#!/usr/bin/env python
import sys, os, re
from subprocess import check_output
# Collect the parameters
previous_head = sys.argv[1]
new_head = sys.argv[2]
is_branch_checkout = sys.argv[3]
if is_branch_checkout == "0":
print "post-checkout: This is a file checkout. Nothing to do."
sys.exit(0)
print "post-checkout: Deleting all '.pyc' files in working directory"
for root, dirs, files in os.walk('.'):
for filename in files:
ext = os.path.splitext(filename)[1]
if ext == '.pyc':
os.unlink(os.path.join(root, filename))
```
钩子脚本当前的工作目录总是在仓库的根目录下,所以`os.walk('.')` 调用遍历了仓库中所有文件。接下来,我们检查它的拓展名,如果是`.pyc` 就删除它。
通过`post-checkout` 钩子你还可以根据你切换的分支来来更改工作目录。比如说你可以在代码库外面使用一个插件分支来储存你所有的插件。如果这些插件需要很多二进制文件而其他分支不需要你可以选择只在插件分支上build。
### pre-rebase
`pre-rebase` 钩子在`git rebase` 发生更改之前运行,确保不会有什么糟糕的事情发生。
这个钩子有两个参数frok之前的上游分支将要rebase的下游分支。如果是rebase当前分支的话第二个参数是空的。以非0状态退出会放弃这次rebase。
比如说如果你想彻底禁用rebase操作你可以使用下面的`pre-rebase` 脚本:
```
#!/bin/sh
# Disallow all rebasing
echo "pre-rebase: Rebasing is dangerous. Don't do it."
exit 1
```
每次运行`git rebase` ,你都会看到下面的信息:
```
pre-rebase: Rebasing is dangerous. Don't do it.
The pre-rebase hook refused to rebase.
```
内置的`pre-rebase.sample` 脚本是一个更复杂的例子。它在什么时候阻止rebase这方面更加智能。它会检查你当前的分支是否已经合并到了下一个分支中去也就是主分支。如果是的话rebase可能会遇到问题脚本会放弃这次rebase。
服务端钩子
===
服务端钩子和本地钩子几乎一样,只不过它们存在于服务端的仓库中(比如说中心仓库,或者开发者的公共仓库)。当和官方仓库连接时,其中一些可以用来拒绝一些不符合规范的提交。
这节中我们要讨论下面三个服务端钩子:
- pre-receive
- update
- post-receive
这些钩子都允许你对`git push` 的不同阶段做出响应。
服务端钩子的输出会传送到客户端的控制台中,所以给开发者发送信息是很容易的。但你要记住这些脚本在结束完之前都不会返回控制台的控制权,所以你要小心那些长时间运行的操作。
### pre-receive
`pre-receive` 钩子在有人用`git push` 向仓库推送代码时被执行。它只存在于远端仓库中,而不是原来的仓库中。
这个钩子在任意引用被更新钱被执行,所以这是强制推行开发规范的好地方。如果你不喜欢推送的那个人(多大仇= =),提交信息的格式,或者提交的更改,你都可以拒绝这次提交。虽然你不能阻止开发者写出糟糕的代码,但你可以用`pre-receive` 防止这些代码流入官方的代码库。
这个脚本没有参数,但每一个推送上来的引用都会以下面的格式传入脚本的单独一行:
```
<old-value> <new-value> <ref-name>
```
你可以看到这个钩子做了非常简单的事,就是读取推送上来的引用并且把它们打印出来。
```
#!/usr/bin/env python
import sys
import fileinput
# Read in each ref that the user is trying to update
for line in fileinput.input():
print "pre-receive: Trying to push ref: %s" % line
# Abort the push
# sys.exit(1)
```
这和其它钩子相比略微有些不同,因为信息是通过标准输入而不是命令行传入的。在远端仓库的`.git/hooks` 中加上这个脚本推送到master分支你会看到下面这些信息打印出来
```
b6b36c697eb2d24302f89aa22d9170dfe609855b 85baa88c22b52ddd24d71f05db31f4e46d579095 refs/heads/master
```
你可以用SHA1哈希值或者底层的Git命令来检查将要引入的更改。一些常见的使用包括
- 拒绝将上游分支rebase的更改
- 防止错综复杂的合并(非快速向前,会造成项目历史非线性)
- 检查用户是否有正确的权限来做这些更改大多用于中心化的Git工作流中
- 如果多个引用被推送,在`pre-receive` 中返回非0状态拒绝所有提交。如果你想一个个接受或拒绝分支你需要使用`update` 钩子
### update
`update` 钩子在`pre-receive` 之后被调用用法也差不多。它也是在实际更新前被调用的但它可以分别被每个推送上来的引用分别调用。也就是说如果用户尝试推送到4个分支`update` 会被执行4次。和`pre-receive` 不一样,这个钩子不需要读取标准输入。事实上,它接受三个参数:
- 更新的引用名称
- 引用中存放的旧的对象名称
- 引用中存放的新的对象名称
这些信息和`pre-receive` 相同,但因为每次引用都会分别触发更新,你可以拒绝一些引用而接受另一些。
```
#!/usr/bin/env python
import sys
branch = sys.argv[1]
old_commit = sys.argv[2]
new_commit = sys.argv[3]
print "Moving '%s' from %s to %s" % (branch, old_commit, new_commit)
# Abort pushing only this branch
# sys.exit(1)
```
上面这个钩子简单地输出了分支和新旧提交的哈希值。当你向远程仓库推送超过一个分支时,你可以看到每个分支都有输出。
### post-receive
`post-receive` 钩子在成功推送后被调用,适合用于发送通知。对很多工作流来说,这比`post-commit` 都是一个更好的发送通知的地方,因为这些更改在公共的服务器而不是用户的本地机器上。给其他开发者发送邮件或者触发一个持续集成系统都是`post-receive` 常用的操作。
这个脚本没有参数,但和`pre-receive` 一样通过标准输入读取。
总结
---
在这篇文章中我们学习了如果用Git钩子来修改内部行为当仓库中特定的事件发生时接受消息。钩子是存在于`git/hooks` 仓库中的普通脚本,因此也非常容易安装和定制。
我们还看了一些常用的本地和服务端的钩子。这使得我们能够介入到整个开发生命周期中去。我们现在知道了如何在创建提交或推送的每个阶段执行自定义的操作。有了这些简单的脚本知识你就可以对Git仓库为所欲为了 : )

View File

@ -0,0 +1,212 @@
代码合并Merge还是Rebase
===
> BY 童仲毅(geeeeeeeeek@github)
>
> 这是一篇在[原文](https://www.atlassian.com/git/tutorials/merging-vs-rebasing)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名([CC BY 2.5 AU](http://creativecommons.org/licenses/by/2.5/au/deed.zh))协议共享。
`git rebase` 这个命令经常被人认为是一种Git巫术初学者应该避而远之。但如果使用得当的话它能给你的团队开发省去太多烦恼。在这篇文章中我们会比较`git rebase` 和类似的`git merge` 命令找到Git工作流中rebase的所有用法。
概述
---
你要知道的第一件事是,`git rebase``git merge` 做的事其实是一样的。它们都被设计来将一个分支的更改并入另一个分支,只不过方式有些不同。
想象一下你刚创建了一个专门的分支开发新功能然后团队中另一个成员在master分支上添加了新的commit。这就会造成提交历史被Fork一份用Git来协作的开发者应该都很清楚。
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/01.svg)
现在如果master中新的commit和你的工作是相关的。为了将新的commit并入你的分支你有两个选择merge或rebase。
### Merge
将master分支合并到feature分支最简单的办法就是用下面这些命令
```
git checkout feature
git merge master
```
或者。你也可以把它们压缩在一行里。
```
git merge master feature
```
feature分支中新的 “merge commit” 将两个分支中的历史连在了一起。你会得到下面这样的分支结构:
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/02.svg)
Merge之所以好因为它是一个安全的操作。现有的分支不会被更改。这就避免后rebase潜在的缺点后面会说
另一方面着同样意味着每次合并上游更改的时候feature分支都会引入一个外来的merge commit。如果master非常活跃的话这或多或少会污染你的分支历史。虽然用高级的`git log` 选项可以环节这个问题,但对于开发者来说,还是会增加理解项目历史的难度。
### Rebase
作为merge的替代选择你可以像下面这样将feature分支并入master分支
```
git checkout feature
git rebase master
```
它会把整个feature分支移动到master分支的后面有效地把所有master分支上新的commit并入过来。但是rebase为原分支上的每一个commit创建全新的commit重写了项目历史并且不会带来merge commit。
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/03.svg)
rebase最大的好处是你的项目历史会非常整洁。首先它不像`git merge` 那样引入不必要的merge commit。其次如上图所示rebase导致最后的项目历史呈现出完美的线性——你可以从项目终点到起点浏览而不需要任何的Fork。这让你更容易使用`git log``git bisect``gitk` 来查看项目历史。
不过这种简单的commit历史会带来两个后果安全性和可跟踪性。如果你违反了Rebase黄金法则重写项目历史可能会给你的协作工作流带来灾难性的影响。此外rebase不会有merge commit中附带的信息——你看不到feature分支中并入了上游的哪些更改。
### 交互式的rebase
交互式的rebase允许你更改并入新分支的commit。这比自动的rebase更加强大因为它提供了对分支上提交历史完整的控制。一般来说这被用来在把feature分支并入master分支之前清理混乱的历史。
`-i` 传入`git rebase` 选项来开始一个交互式的rebase过程
```
git checkout feature
git rebase -i master
```
它会打开一个文本编辑器显示所有将被移动的commit
```
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
```
这个列表定义了rebase将被执行后分支会是什么样的。更改`pick` 命令或者重新排序这个分支的历史就能如你所愿了。比如说如果第二个commit修复了第一个commit中的小问题你可以用`fixup` 命令把它们压缩到一行:
```
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
```
保存后关闭文件Git会根据你的指令来执行rebase这会导致项目历史看上去是这样的
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/04.svg)
忽略不重要的commit会让你的feature分支的历史更清晰易读。这是`git merge` 做不到的。
Rebase的黄金法则
---
当你理解rebase是什么的时候最重要的事情就是什么时候不能用rebase。`git rebase` 的黄金法则便是,绝不要在公共的分支上使用它。
比如说如果你把master分支rebase到你的feature分支上会发生什么
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/05.svg)
这次rebase将master分支上的所有commit都移到了feature分支后面。问题是它只发生在你的代码仓库中其他所有的开发者还在原来的master上工作。因为rebase导致了全新的commitGit会认为你的master分支和其他人的master已经分叉了。
同步两个master分支的唯一办法是把它们merge到一起导致一个额外的merge commit和两堆包含同样更改的commit。不用说这会让人非常困惑。
所以,在你运行`git rebase` 之前,一定要问问你自己“有没有别人正在这个分支上工作?”。如果答案是有,那么把你的爪子放回去,重新找到一个无害的方式(如`git revert`)来提交你的更改。不如,你可以随心所欲地重写历史。
### 强制push
如果你想把rebase之后的master分支push到远程仓库中去的话Git会阻止你这么做因为两个分支有冲突。但你可以传入`--force` 标记来强行push。就像下面一样
```
# Be very careful with this command!
git push --force
```
它会重写远程的master分支来匹配你仓库中rebase之后的master对于团队中其他成员来说这看上去很诡异。所以务必小心这个命令只有当你知道你在做什么的时候再使用。
仅有的几个强制push的使用场景之一是当你在想向远程仓库push了一个私有分支之后执行了一个本地的清理比如说为了回滚。这就像是在说“哦其实我并不想push之前那个feature分支的。用我现在的版本替换掉吧。”。同样的你要注意没有人正在这个feature分支上工作。
工作流
---
你的团队或多或少都可以在现在Git工作流中使用rebase。在这一节中我们来看看在feature分支开发的各个阶段中rebase有哪些好处。
第一步是在任何和`git rebase` 有关的工作流中为每一个feature专门创建一个分支。它会给你带来安全使用rebase的分支结构
The first step in any workflow that leverages git rebase is to create a dedicated branch for each feature. This gives you the necessary branch structure to safely utilize rebasing:
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/06.svg)
### 本地清理
在你工作流中使用rebase最好的用法之一就是清理本地正在开发的分支。隔一段时间执行一次交互式rebase你可以保证你feature分支中的每一个commit都是专注和有意义的。你在写代码时不用担心造成孤立的commit——因为你后面一定能修复。
调用`git rebase` 的时候你有两个base可以选择上游分支比如master或者你feature分支中早先的一个commit。我们在“交互式rebase”一节看到了第一种的例子。后一种在当你只需要修改最新几次commit的时候也很有用。比如说下面的命令对最新的3次commit进行了交互式rebase
```
git checkout feature
git rebase -i HEAD~3
```
通过指定`HEAD~3`作为新的base你实际上没有移动分支——你只是将之后的3次commit重写了。注意它不会把上游分支的更改并入到feature分支中。
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/07.svg)
如果你想用这个方法重写整个feature分支`git merge-base` 命令非常方便地找出feature分支最开始的base。下面这段命令返回原base commit的ID你可以接下来将它传给`git rebase`
```
git merge-base feature master
```
交互式rebase是在你工作流中引入`git rebase` 的的好办法,因为它只影响本地分支。其他开发者只能看到你已经完成的结果,那就是一个非常整洁、易于追踪的分支历史。
但同样的这只能用在私有分支上。如果你在同一个feature分支和其他开发者合作的话这个分支是公开的你不能重写这个历史。
用带有交互式的rebase清理本地commit这个用法你无法用`git merge` 命令来完成。
### 将上游分支上的更改并入feature分支
在概览一节我们看到了feature分支如何通过`git merge``git rebase` 来并入上游分支。merge是保留你完整历史的安全选择rebase将你的feature分支移动到master分支后面创建一个线性的历史。
`git rebase` 的用法和本地清理非常类似而且可以同时使用但之间并入了master上的上游更改。
记住rebase到远程分支而不是master也是完全合法的。当你和另一个开发者在同一个feature分之上协作的时候你会用到这个用法将他们的更改并入你的项目。
比如说如果你和另一个开发者——John——往feature分支上添加了几个commit在从John的仓库中fetch之后你的仓库可能会像下面这样
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/08.svg)
就和并入master上的上游更改一样你可以这样解决这个Fork要么merge你的本地分支和John的分支要不把你的本地分支rebase到John的分支后面。
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/09.svg)
注意这里的rebase没有违反Rebase黄金法则因为只有你的本地分支上的commit被移动了之前的所有东西都没有变。这就像是在说“把我的改动加到John已经做的后面去”。在大多数情况下这比通过merge commit来同步远程分支来的更符合直觉。
默认情况下,`git pull` 命令会执行一次merge但你可以传入`--rebase` 来强制它通过rebase来整合远程分支。
By default, the git pull command performs a merge, but you can force it to integrate the remote branch with a rebase by passing it the --rebase option.
### 用Pull Request进行审查
如果你将pull request作为你代码审查过程中的一环你需要避免在创建pull request之后使用`git rebase`。只要你发起了pull request其他开发者能看到你的代码也就是说这个分支变成了公共分支。重写历史会造成Git和你的同事难以找到这个分支接下来的任何commit。
来自其他开发者的任何更改都应该用`git merge` 而不是`git rebase` 来并入。
因此在提交pull request钱用交互式的rebase进行代码清理通常是一个好的做法。
### 并入通过的功能分支
如果某个功能被你们团队通过了你可以选择将这个分支rebase到master分支之后或是使用`git merge` 来将这个功能并入主代码库中。
这和将上游改动并入feature分支很相似但是你不可以在master分支重写commit你最后需要用`git merge` 来并入这个feature。但是在merge之前执行一次rebase你可以确保merge是一直向前的最后生成的是一个完全线性的提交历史。这样你还可以加入pull request之后的commit。
![enter image description here](https://www.atlassian.com/git/images/tutorials/advanced/merging-vs-rebasing/10.svg)
如果你还没有完全熟悉`git rebase`你还可以在一个临时分支中执行rebase。这样的话如果你意外地弄乱了你feature分支的历史你还可以查看原来的分支然后重试。
比如说:
```
git checkout feature
git checkout -b temporary-branch
git rebase -i master
# [Clean up the history]
git checkout master
git merge temporary-branch
```
总结
----
你使用rebase之前需要知道的知识点都在这了。如果你想要一个干净的、线性的提交历史没有不必要的merge commit你应该使用`git rebase` 而不是`git merge` 来并入其他分支上的更改。
另一方面如果你想要保存项目完整的历史并且避免重写公共分支上的commit 你可以使用`git merge `。两种选择都是完全有效的,但至少你现在可以想到`git rebase` 的好处了。

View File

@ -0,0 +1,124 @@
Reset、Checkout和Revert
=============
> BY 童仲毅(geeeeeeeeek@github)
>
> 这是一篇在[原文](https://www.atlassian.com/git/tutorials/resetting-checking-out-and-reverting)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名([CC BY 2.5 AU](http://creativecommons.org/licenses/by/2.5/au/deed.zh))协议共享。
`git reset``git checkout``git revert`是你的Git工具箱中最有用的命令。它们都用来撤销代码仓库中的某些更改而前两个命令不仅可以作用于commit还可以作用于特定文件。
因为它们非常相似,所以我们经常会搞混,不知道什么场景下该用哪个命令。在这篇文章中,我们会比较`git reset``git checkout``git revert`最常见的用法。希望你在看完后能游刃有余地使用这些命令来管理你的仓库。
![Git repo的主要组成](https://www.atlassian.com/git/images/tutorials/advanced/resetting-checking-out-and-reverting/01.svg)
Git仓库有三个主要组成——工作目录stage缓存和提交历史。这张图有助于理解每个命令到底产生了哪些影响。当你阅读的时候牢记这张图。
Commit层面的操作
--------------
你传给`git reset``git checkout`的参数决定了它们的作用域。如果你没有包含文件路径这些操作对所有commit生效。我们这一节要探讨的就是commit层面的操作。注意`git revert`没有文件层面的操作。
###Reset
在commit层面上reset将一个分支的末端指向另一个commit。这可以用来移除当前分支的一些commit。比如下面这两条命令让hotfix分支向后回退了两个commit。
```
git checkout hotfix
git reset HEAD~2
```
hotfix分支末端的两个commit现在变成了悬挂commit。也就是说下次Git执行垃圾回收的时候这两个commit会被删除。换句话说如果你想扔掉这两个commit你可以这么做。reset操作如下图所示
![把hotfix分支reset到HEAD~2](https://www.atlassian.com/git/images/tutorials/advanced/resetting-checking-out-and-reverting/02.svg)
如果你的更改还没有共享给别人,`git reset`是撤销这些更改的简单方法。当你开发一个功能的时候发现“糟糕我做了什么我应该重新来过”这时候reset就像是go-to命令一样。
除了在当前分支上操作你还可以通过传入这些标记来修改你的stage缓存或工作目录
- --soft stage缓存和工作目录不会被改变
- --mixed 默认选项。stage缓存和你指定的commit同步但工作目录不受影响
- --hard stage缓存和工作目录都同步到你指定的commit
把这些标记想成定义`git reset`操作的作用域就容易理解多了。
![git rese的定义域](https://www.atlassian.com/git/images/tutorials/advanced/resetting-checking-out-and-reverting/03.svg)
这些标记往往和HEAD作为参数一起使用。比如`git reset --mixed HEAD` 将你当前的改动从stage缓存中移除但是这些改动还留在工作目录中。利益方面如果你想完全舍弃你没有commit的改动你可以使用`git reset --hard HEAD`。这是`git reset` 最常用的两种用法。
当你传入HEAD以外的其他commit的时候要格外小心因为reset操作会重写当前分支的历史。正如Rebase黄金法则所说的在公共分支上这样做可能会引起严重的后果。
###Checkout
你应该已经非常熟悉commit层面的`git checkout`。当传入分支名时,可以切换到那个分支。
```
git checkout hotfix
```
上面这个命令做的不过是将HEAD移到一个新的分支然后更新工作目录。因为这可能会覆盖本地的修改Git强制你commit或者stash工作目录中的所有更改不如在checkout的时候这些更改都会丢失。不像`git reset``git checkout`没有移动这些分支。
![将 HEAD 从 master 移到 hotfix](https://www.atlassian.com/git/images/tutorials/advanced/resetting-checking-out-and-reverting/04.svg)
除了分支之外你还可以传入commit的引用来checkout到任意的commit。这和checkout到另一个分支是完全一样的把HEAD移动到特定的commit。比如下面这个命令会checkout到当前commit的祖父commit。
```
git checkout HEAD~2
```
![将HEAD移动到任意commit](https://www.atlassian.com/git/images/tutorials/advanced/resetting-checking-out-and-reverting/05.svg)
这对于快速查看项目旧版本来说非常有用。但是你当前的HEAD没有任何分支引用这会造成HEAD分离。这是非常危险的如果你接着添加新的commit然后切换到别的分支之后就没办法回到之前添加的这些commit。因此在为分离的HEAD添加新的commit的时候你应该创建一个新的分支。
###Revert
Revert撤销一个commit的同时会创建一个新的commit。这是一个安全的方法因为它不会重写commit历史。比如下面的命令会找出倒数第二个commit然后创建一个新的commit来撤销这些更改然后把这个commit加入项目中。
```
git checkout hotfix
git revert HEAD~2
```
如下图所示:
![revert到倒数第二个commit](https://www.atlassian.com/git/images/tutorials/advanced/resetting-checking-out-and-reverting/06.svg)
相比`git reset`它不会改变现在的commit历史。因此`git revert`可以用在公共分支上,`git reset`应该用在私有分支上。
你也可以把`git revert`当作撤销已经commit的更改`git reset HEAD`用来撤销没有commit的更改。
就像`git checkout` 一样,`git revert` 也有可能会重写文件。所以Git会在你执行revert之前要求你commit或者stash你工作目录中的更改。
文件层面的操作
-----
`git reset``git checkout` 命令也接受文件路径作为参数。这时它的行为就大为不同了。它不会作用于整份commit参数限制它作用于特定文件。
###Reset
当检测到文件路径时,`git reset` 将stage缓存同步到你指定的那个commit。比如下面这个命令会将倒数第二个commit中的foo.py加入到stage缓存中供下一个commit使用。
```
git reset HEAD~2 foo.py
```
和commit层面的`git reset`一样通常我们使用HEAD而不是某个特定的commit。运行`git reset HEAD foo.py` 会将当前的foo.py从stage缓存中移除出去而不会影响工作目录中对foo.py的更改。
![将一个文件从commit历史中移动到stage缓存中](https://www.atlassian.com/git/images/tutorials/advanced/resetting-checking-out-and-reverting/07.svg)
--soft、--mixed和--hard对文件层面的`git reset` 毫无作用因为stage缓存中的文件一定会变化而工作目录中的文件一定不变。
###Checkout
Checkout一个文件和带文件路径`git reset` 非常像除了它更改的是工作目录而不是stage缓存。不像commit层面的checkout命令它不会移动HEAD引用也就是你不会切换到别的分支上去。
![将文件从commit历史移动到工作目录中](https://www.atlassian.com/git/images/tutorials/advanced/resetting-checking-out-and-reverting/08.svg)
比如下面这个命令将工作目录中的foo.py同步到了倒数第二个commit中的foo.py。
```
git checkout HEAD~2 foo.py
```
和commit层面相同的是它可以用来检查项目的旧版本但作用域被限制到了特定文件。
如果你stage并且commit了checkout的文件它具备将某个文件回撤到之前版本的效果。注意它撤销了这个文件后面所有的更改`git revert` 命令只撤销某个特定commit的更改。
`gie reset` 一样这个命令通常和HEAD一起使用。比如`git checkout HEAD foo.py` 的作用等同于舍弃foo.py没有stage的更改。这个行为和`git reset HEAD --hard` 很像,但只影响特定文件。
总结
-------
你现在已经掌握了Git仓库中撤销更改的所有工具。`git reset``git checkout`、和 `git revert` 命令比较容易混淆但当你想起它们工作目录、stage缓存和commit历史分别的影响就会容易判断现在的情况下应该用那个命令。
下面这个表格总结了这些命令最常用的使用场景。记得经常对照这个表格你使用Git时一定会经常用到。
|命令|作用域|常用情景|
|---|---|---|
|git reset| Commit层面| 在私有分支上舍弃一些没有commit的更改|
|git reset| 文件层面| 将文件从stage中移除|
|git checkout| Commit层面|切换分支或查看旧版本|
|git checkout| 文件层面| 舍弃工作目录中的更改|
|git revert| Commit层面|在公共分支上回撤更改|
|git revert| 文件层面|(没有)|