|
| 1 | +## 重写项目历史 |
| 2 | + |
| 3 | +> BY 童仲毅([geeeeeeeeek@github](https://github.com/geeeeeeeeek/git-recipes/)) |
| 4 | +> |
| 5 | +> 这是一篇在[原文(BY atlassian)](https://www.atlassian.com/git/tutorials/rewriting-history)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名([CC BY 2.5 AU](http://creativecommons.org/licenses/by/2.5/au/deed.zh))协议共享。 |
| 6 | +
|
| 7 | +## 概述 |
| 8 | + |
| 9 | +GIt的主要职责是保证你不会丢失提交的修改。但是,它同样被设计成让你完全掌控开发工作流。这包括了让你自定义你的项目历史,而这也创造了丢失提交的可能性。Git提供了可以重写项目历史的命令,但也警告你这些命令可能会让你丢失内容。 |
| 10 | + |
| 11 | +这份教程讨论了重写提交快照的一些常见原因,并告诉你如何避免不好的影响。 |
| 12 | + |
| 13 | +## git commit --amend |
| 14 | + |
| 15 | +`git commit --amend`命令是修复最新提交的便捷方式。它允许你将缓存的修改和之前的提交合并到一起,而不是提交一个全新的快照。它还可以用来简单地编辑上一次提交的信息而不改变快照。 |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +但是,amend不只是修改了最新的提交——它进行了一次替换。对于Git来说,这看上去像一个全新的提交,即上图中用星号表示的那一个。在公共仓库工作时一定要牢记这一点。 |
| 20 | + |
| 21 | +### 用法 |
| 22 | + |
| 23 | +``` |
| 24 | +git commit --amend |
| 25 | +``` |
| 26 | + |
| 27 | +合并缓存的修改和上一次的提交,用新的快照替换上一个提交。缓存区没有文件时运行这个命令可以用来编辑上次提交的提交信息,而不会更改快照。 |
| 28 | + |
| 29 | +### 讨论 |
| 30 | + |
| 31 | +仓促的提交在你日常开发过程中时常会发生。很容易就忘记了缓存一个文件或者弄错了提交信息的格式。`--amend`标记是修复这些小意外的便捷方式。 |
| 32 | + |
| 33 | +#### 不要修复公共提交 |
| 34 | + |
| 35 | +在[`git reset`](https://github.com/geeeeeeeeek/git-recipes/wiki/2.6-%E5%9B%9E%E6%BB%9A%E9%94%99%E8%AF%AF%E7%9A%84%E4%BF%AE%E6%94%B9#git-reset)这节中,我们说过永远不要重设和其他开发者共享的提交。对于修复也是一样:永远不要修复一个已经推送到公共仓库中的提交。 |
| 36 | + |
| 37 | +修复过的提交事实上是全新的提交,之前的提交会被移除出项目历史。这和重设公共快照的后果是一样的。如果你修复了其他开发者在之后继续开发的一个提交,看上去他们的工作基础从项目历史中消失了一样。对于在这上面的开发者来说这是很困惑的,而且很难恢复。 |
| 38 | + |
| 39 | +### 栗子 |
| 40 | + |
| 41 | +下面这个🌰展示了Git开发工作流中的一个常见情形。我们编辑了一些希望在同一个快照中提交的文件,但我们忘记添加了其中的一个。修复错误只需要缓存那个文件并且用`--amend`标记提交: |
| 42 | + |
| 43 | +``` |
| 44 | +# 编辑 hello.py 和 main.py |
| 45 | +git add hello.py |
| 46 | +git commit |
| 47 | +
|
| 48 | +# 意识到你忘记添加 main.py 的更改 |
| 49 | +git add main.py |
| 50 | +git commit --amend --no-edit |
| 51 | +``` |
| 52 | + |
| 53 | +编辑器会弹出上一次提交的信息,加入`--no-edit`标记会修复提交但不修改提交信息。需要的话你可以修改,不然的话就像往常一样保存并关闭文件。完整的提交会替换之前不完整的提交,看上去就像我们在同一个快照中提交了`hello.py`和`main.py`。 |
| 54 | + |
| 55 | +## git rebase |
| 56 | + |
| 57 | +变基(rebase, 事实上这个名字十分诡异, 所以在大多数时候直接用英文术语)是将分支移到一个新的基提交的过程。过程一般如下所示: |
| 58 | + |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | +从内容的角度来看,rebase只不过是将分支从一个提交移到了另一个。但从内部机制来看,Git是通过在选定的基上创建新提交来完成这件事的——它事实上重写了你的项目历史。理解这一点很重要,尽管分支看上去是一样的,但它包含了全新的提交。 |
| 66 | + |
| 67 | +### 用法 |
| 68 | + |
| 69 | +``` |
| 70 | +git rebase <base> |
| 71 | +``` |
| 72 | + |
| 73 | +将当前分支rebase到`<base>`,这里可以是任何类型的提交引用(ID、分支名、标签,或是`HEAD`的相对引用)。 |
| 74 | + |
| 75 | +### 讨论 |
| 76 | + |
| 77 | +rebase的主要目的是为了保持一个线性的项目历史。比如说,当你在feature分支工作时master分支取得了一些进展: |
| 78 | + |
| 79 | + |
| 80 | + |
| 81 | + |
| 82 | + |
| 83 | + |
| 84 | + |
| 85 | +要将你的feature分支整合进`master`分支,你有两个选择:直接merge,或者先rebase后merge。前者会产生一个三路合并(3-way merge)和一个合并提交,而后者产生的是一个快速向前的合并以及完美的线性历史。下图展示了为什么rebase到`master`分支会促成一个快速向前的合并。 |
| 86 | + |
| 87 | + |
| 88 | + |
| 89 | +rebase是将上游更改合并进本地仓库的通常方法。你每次想查看上游进展时,用`git merge`拉取上游更新会导致一个多余的合并提交。在另一方面,rebase就好像是说:“我想将我的更改建立在其他人的进展之上。” |
| 90 | + |
| 91 | +#### 不要rebase公共历史 |
| 92 | + |
| 93 | +和我们讨论过的`git commit --amend`和`git reset`一样,你永远不应该rebase那些已经推送到公共仓库的提交。rebase会用新的提交替换旧的提交,你的项目历史会像突然消失了一样。 |
| 94 | + |
| 95 | +### 栗子 |
| 96 | + |
| 97 | +下面这个🌰同时使用git rebase和git merge来保持线性的项目历史。这是一个确认你的合并都是快速向前的方法。 |
| 98 | + |
| 99 | +``` |
| 100 | +# 开始新的功能分支 |
| 101 | +git checkout -b new-feature master |
| 102 | +# 编辑文件 |
| 103 | +git commit -a -m "Start developing a feature" |
| 104 | +``` |
| 105 | + |
| 106 | +在feature分支开发了一半的时候,我们意识到项目中有一个安全漏洞: |
| 107 | + |
| 108 | +``` |
| 109 | +# 基于master分支创建一个快速修复分支 |
| 110 | +git checkout -b hotfix master |
| 111 | +# 编辑文件 |
| 112 | +git commit -a -m "Fix security hole" |
| 113 | +# 合并回master |
| 114 | +git checkout master |
| 115 | +git merge hotfix |
| 116 | +git branch -d hotfix |
| 117 | +``` |
| 118 | + |
| 119 | +将hotfix分支并回之后master,我们有了一个分叉的项目历史。我们用rebase整合feature分支以获得线性的历史,而不是使用普通的git merge。 |
| 120 | + |
| 121 | +``` |
| 122 | +git checkout new-feature |
| 123 | +git rebase master |
| 124 | +``` |
| 125 | + |
| 126 | +它将new-feature分支移到了master分支的末端,现在我们可以在master上进行标准的快速向前合并了: |
| 127 | + |
| 128 | +``` |
| 129 | +git checkout master |
| 130 | +git merge new-feature |
| 131 | +``` |
| 132 | + |
| 133 | +## git rebase -i |
| 134 | + |
| 135 | +用`-i`标记运行`git rebase`开始交互式rebase。交互式rebase给你在过程中修改单个提交的机会,而不是盲目地将所有提交都移到新的基上。你可以移除、分割提交,更改提交的顺序。它就像是打了鸡血的`git commit --amendy`一样。 |
| 136 | + |
| 137 | +### 用法 |
| 138 | + |
| 139 | +``` |
| 140 | +git rebase -i <base> |
| 141 | +``` |
| 142 | + |
| 143 | +将当前分支rebase到`base`,但使用可交互的形式。它会打开一个编辑器,你可以为每个将要rebase的提交输入命令(见后文)。这些命令决定了每个提交将会怎样被转移到新的基上去。你还可以对这些提交进行排序。 |
| 144 | + |
| 145 | +### 讨论 |
| 146 | + |
| 147 | +交互式rebase给你了控制项目历史的完全掌控。它给了开发人员很大的自由,因为他们可以提交一个“混乱”的历史而只需专注于写代码,然后回去恢复干净。 |
| 148 | + |
| 149 | +大多数开发者喜欢在并入主代码库之前用交互式rebase来完善他们的feature分支。他们可以将不重要的提交合在一起,删除不需要的,确保所有东西在提交到“正式”的项目历史前都是整齐的。对其他人来说,这个功能的开发看上去是由一系列精心安排的提交组成的。 |
| 150 | + |
| 151 | +### 例子 |
| 152 | + |
| 153 | +下面这个🌰是`非交互式rebae`一节中🌰的可交互升级版本。 |
| 154 | + |
| 155 | +``` |
| 156 | +# 开始新的功能分支 |
| 157 | +git checkout -b new-feature master |
| 158 | +# 编辑文件 |
| 159 | +git commit -a -m "Start developing a feature" |
| 160 | +# 编辑更多文件 |
| 161 | +git commit -a -m "Fix something from the previous commit" |
| 162 | +
|
| 163 | +# 直接在master上添加文件 |
| 164 | +git checkout master |
| 165 | +# 编辑文件 |
| 166 | +git commit -a -m "Fix security hole" |
| 167 | +
|
| 168 | +# 开始交互式rebase |
| 169 | +git checkout new-feature |
| 170 | +git rebase -i master |
| 171 | +``` |
| 172 | + |
| 173 | +最后的那个命令会打开一个编辑器,包含new-feature的两个提交,和一些指示: |
| 174 | + |
| 175 | +``` |
| 176 | +pick 32618c4 Start developing a feature |
| 177 | +pick 62eed47 Fix something from the previous commit |
| 178 | +``` |
| 179 | + |
| 180 | +你可以更改每个提交前的pick命令来决定在rebase时提交移动的方式。在我们的例子中,我们只需要用squash命令把两个提交并在一起就可以了: |
| 181 | + |
| 182 | +``` |
| 183 | +pick 32618c4 Start developing a feature |
| 184 | +squash 62eed47 Fix something from the previous commit |
| 185 | +``` |
| 186 | + |
| 187 | +保存并关闭编辑器以开始rebase。另一个编辑器会打开,询问你合并后的快照的提交信息。在定义了提交信息之后,rebase就完成了,你可以在`git log`输出中看到那个提交。整个过程可以用下图可视化: |
| 188 | + |
| 189 | + |
| 190 | + |
| 191 | + |
| 192 | + |
| 193 | +注意缩并的提交和原来的两个提交的ID都不一样,告诉我们这确实是个新的提交。 |
| 194 | + |
| 195 | +最后,你可以执行一个快速向前的合并,来将完善的feature分支整合进主代码库: |
| 196 | + |
| 197 | +``` |
| 198 | +git checkout master |
| 199 | +git merge new-feature |
| 200 | +``` |
| 201 | + |
| 202 | +交互式rebase强大的能力可以从整合后的master分支看出——额外的`62eed47`提交找不到了。对其他人来说,你就像是一个天才,用完美数量的提交完成了`new-feature`。这就是交互式提交如何保持项目历史干净和合意。 |
| 203 | + |
| 204 | +## git reflog |
| 205 | + |
| 206 | +Git用引用日志这种机制来记录分支顶端的更新。它允许你回到那些不被任何分支或标签引用的更改。在重写历史后,引用日志包含了分支旧状态的信息,有需要的话你可以回到这个状态。 |
| 207 | + |
| 208 | +### 用法 |
| 209 | + |
| 210 | +``` |
| 211 | +git reflog |
| 212 | +``` |
| 213 | + |
| 214 | +显示本地仓库的引用日志。 |
| 215 | + |
| 216 | +``` |
| 217 | +git reflog --relative-date |
| 218 | +``` |
| 219 | + |
| 220 | +用相对的日期显示引用日志。(如2周前)。 |
| 221 | + |
| 222 | +### 讨论 |
| 223 | + |
| 224 | +每次当前的HEAD更新时(如切换分支、拉取新更改、重写历史或只是添加新的提交),引用日志都会添加一个新条目。 |
| 225 | + |
| 226 | +### 栗子 |
| 227 | + |
| 228 | +为了理解`git reflog`,我们来看一个🌰。 |
| 229 | + |
| 230 | +``` |
| 231 | +0a2e358 HEAD@{0}: reset: moving to HEAD~2 |
| 232 | +0254ea7 HEAD@{1}: checkout: moving from 2.2 to master |
| 233 | +c10f740 HEAD@{2}: checkout: moving from master to 2.2 |
| 234 | +``` |
| 235 | + |
| 236 | +上面的引用日志显示了master和2.2 branch之间的相互切换。还有对一个更老的提交的强制重设。最近的活动用`HEAD@{0}`标记在上方显示。 |
| 237 | + |
| 238 | +如果事实上你是不小心切换回去的,引用日志包含了你意外地丢掉两个提交之前master指向的提交0254ea7。 |
| 239 | + |
| 240 | +``` |
| 241 | +git reset --hard 0254ea7 |
| 242 | +``` |
| 243 | + |
| 244 | +使用[`git reset`](https://github.com/geeeeeeeeek/git-recipes/wiki/2.6-%E5%9B%9E%E6%BB%9A%E9%94%99%E8%AF%AF%E7%9A%84%E4%BF%AE%E6%94%B9#git-reset),就有可能能将master变回之前的那个提交。它提供了一张安全网,以防历史发生意外更改。 |
| 245 | + |
| 246 | +务必记住,引用日志提供的安全网只对提交到本地仓库的更改有效,而且只有移动操作会被记录。 |
| 247 | + |
| 248 | + |
| 249 | + |
| 250 | +> 这篇文章是[**『git-recipes』**](https://github.com/geeeeeeeeek/git-recipes/)的一部分,点击[**目录**](https://github.com/geeeeeeeeek/git-recipes/wiki/)查看所有章节。 |
| 251 | +> |
| 252 | +> 如果你觉得文章对你有帮助,欢迎点击右上角的***Star***:star2:或***Fork***:fork_and_knife:。 |
| 253 | +> |
| 254 | +> 如果你发现了错误,或是想要加入协作,请参阅[Wiki协作说明](https://github.com/geeeeeeeeek/git-recipes/issues/1)。 |
0 commit comments