2.7 重写项目历史

LitoMore edited this page Jul 17, 2017 · 4 revisions

BY 童仲毅(geeeeeeeeek@github

这是一篇在原文(BY atlassian)基础上演绎的译文。除非另行注明,页面上所有内容采用知识共享-署名(CC BY 2.5 AU)协议共享。

概述

Git 的主要职责是保证你不会丢失提交的修改。但是,它同样被设计成让你完全掌控开发工作流。这包括了让你自定义你的项目历史,而这也创造了丢失提交的可能性。Git 提供了可以重写项目历史的命令,但也警告你这些命令可能会让你丢失内容。

这份教程讨论了重写提交快照的一些常见原因,并告诉你如何避免不好的影响。

git commit --amend

git commit --amend 命令是修复最新提交的便捷方式。它允许你将缓存的修改和之前的提交合并到一起,而不是提交一个全新的快照。它还可以用来简单地编辑上一次提交的信息而不改变快照。

Git Tutorial: git commit --amend

但是,amend 不只是修改了最新的提交——它进行了一次替换。对于 Git 来说,这看上去像一个全新的提交,即上图中用星号表示的那一个。在公共仓库工作时一定要牢记这一点。

用法

git commit --amend

合并缓存的修改和上一次的提交,用新的快照替换上一个提交。缓存区没有文件时运行这个命令可以用来编辑上次提交的提交信息,而不会更改快照。

讨论

仓促的提交在你日常开发过程中时常会发生。很容易就忘记了缓存一个文件或者弄错了提交信息的格式。--amend 标记是修复这些小意外的便捷方式。

不要修复公共提交

git reset这节中,我们说过永远不要重设和其他开发者共享的提交。对于修复也是一样:永远不要修复一个已经推送到公共仓库中的提交。

修复过的提交事实上是全新的提交,之前的提交会被移除出项目历史。这和重设公共快照的后果是一样的。如果你修复了其他开发者在之后继续开发的一个提交,看上去他们的工作基础从项目历史中消失了一样。对于在这上面的开发者来说这是很困惑的,而且很难恢复。

栗子

下面这个🌰展示了 Git 开发工作流中的一个常见情形。我们编辑了一些希望在同一个快照中提交的文件,但我们忘记添加了其中的一个。修复错误只需要缓存那个文件并且用 --amend 标记提交:

# 编辑 hello.py 和 main.py
git add hello.py
git commit

# 意识到你忘记添加 main.py 的更改
git add main.py
git commit --amend --no-edit

编辑器会弹出上一次提交的信息,加入 --no-edit 标记会修复提交但不修改提交信息。需要的话你可以修改,不然的话就像往常一样保存并关闭文件。完整的提交会替换之前不完整的提交,看上去就像我们在同一个快照中提交了 hello.pymain.py

git rebase

变基(rebase, 事实上这个名字十分诡异, 所以在大多数时候直接用英文术语)是将分支移到一个新的基提交的过程。过程一般如下所示:

Git Tutorial: Rebase to maintain a linear project history.

从内容的角度来看,rebase 只不过是将分支从一个提交移到了另一个。但从内部机制来看,Git 是通过在选定的基上创建新提交来完成这件事的——它事实上重写了你的项目历史。理解这一点很重要,尽管分支看上去是一样的,但它包含了全新的提交。

用法

git rebase <base>

将当前分支 rebase 到 <base>,这里可以是任何类型的提交引用(ID、分支名、标签,或是 HEAD 的相对引用)。

讨论

rebase 的主要目的是为了保持一个线性的项目历史。比如说,当你在 feature 分支工作时 master 分支取得了一些进展:

Git Rebase Branch onto Master

要将你的 feature 分支整合进 master 分支,你有两个选择:直接 merge,或者先 rebase 后 merge。前者会产生一个三路合并(3-way merge)和一个合并提交,而后者产生的是一个快速向前的合并以及完美的线性历史。下图展示了为什么 rebase 到 master 分支会促成一个快速向前的合并。

Git Tutorial: Fast-forward merge

rebase 是将上游更改合并进本地仓库的通常方法。你每次想查看上游进展时,用 git merge 拉取上游更新会导致一个多余的合并提交。在另一方面,rebase 就好像是说「我想将我的更改建立在其他人的进展之上」。

不要 rebase 公共历史

和我们讨论过的 git commit --amendgit reset 一样,你永远不应该 rebase 那些已经推送到公共仓库的提交。rebase 会用新的提交替换旧的提交,你的项目历史会像突然消失了一样。

栗子

下面这个🌰同时使用 git rebase 和 git merge 来保持线性的项目历史。这是一个确认你的合并都是快速向前的方法。

# 开始新的功能分支
git checkout -b new-feature master
# 编辑文件
git commit -a -m "Start developing a feature"

在 feature 分支开发了一半的时候,我们意识到项目中有一个安全漏洞:

# 基于master分支创建一个快速修复分支
git checkout -b hotfix master
# 编辑文件
git commit -a -m "Fix security hole"
# 合并回master
git checkout master
git merge hotfix
git branch -d hotfix

将 hotfix 分支并回之后 master,我们有了一个分叉的项目历史。我们用 rebase 整合 feature 分支以获得线性的历史,而不是使用普通的 git merge。

git checkout new-feature
git rebase master

它将 new-feature 分支移到了 master 分支的末端,现在我们可以在 master 上进行标准的快速向前合并了:

git checkout master
git merge new-feature

git rebase -i

-i 标记运行 git rebase 开始交互式 rebase。交互式 rebase 给你在过程中修改单个提交的机会,而不是盲目地将所有提交都移到新的基上。你可以移除、分割提交,更改提交的顺序。它就像是打了鸡血的 git commit --amend 一样。

用法

git rebase -i <base>

将当前分支 rebase 到 base,但使用可交互的形式。它会打开一个编辑器,你可以为每个将要 rebase 的提交输入命令(见后文)。这些命令决定了每个提交将会怎样被转移到新的基上去。你还可以对这些提交进行排序。

讨论

交互式 rebase 给你了控制项目历史的完全掌控。它给了开发人员很大的自由,因为他们可以提交一个「混乱」的历史而只需专注于写代码,然后回去恢复干净。

大多数开发者喜欢在并入主代码库之前用交互式 rebase 来完善他们的 feature 分支。他们可以将不重要的提交合在一起,删除不需要的,确保所有东西在提交到「正式」的项目历史前都是整齐的。对其他人来说,这个功能的开发看上去是由一系列精心安排的提交组成的。

栗子

下面这个🌰是 非交互式rebae 一节中🌰的可交互升级版本。

# 开始新的功能分支
git checkout -b new-feature master
# 编辑文件
git commit -a -m "Start developing a feature"
# 编辑更多文件
git commit -a -m "Fix something from the previous commit"

# 直接在 master 上添加文件
git checkout master
# 编辑文件
git commit -a -m "Fix security hole"

# 开始交互式 rebase
git checkout new-feature
git rebase -i master

最后的那个命令会打开一个编辑器,包含 new-feature 的两个提交,和一些指示:

pick 32618c4 Start developing a feature
pick 62eed47 Fix something from the previous commit

你可以更改每个提交前的 pick 命令来决定在 rebase 时提交移动的方式。在我们的例子中,我们只需要用 squash 命令把两个提交并在一起就可以了:

pick 32618c4 Start developing a feature
squash 62eed47 Fix something from the previous commit

保存并关闭编辑器以开始 rebase。另一个编辑器会打开,询问你合并后的快照的提交信息。在定义了提交信息之后,rebase 就完成了,你可以在 git log 输出中看到那个提交。整个过程可以用下图可视化:

Git Tutorial: git rebase -i example

注意缩并的提交和原来的两个提交的 ID 都不一样,告诉我们这确实是个新的提交。

最后,你可以执行一个快速向前的合并,来将完善的 feature 分支整合进主代码库:

git checkout master
git merge new-feature

交互式 rebase 强大的能力可以从整合后的 master 分支看出——额外的 62eed47 提交找不到了。对其他人来说,你就像是一个天才,用完美数量的提交完成了 new-feature。这就是交互式提交如何保持项目历史干净和合意。

git reflog

Git 用引用日志这种机制来记录分支顶端的更新。它允许你回到那些不被任何分支或标签引用的更改。在重写历史后,引用日志包含了分支旧状态的信息,有需要的话你可以回到这个状态。

用法

git reflog

显示本地仓库的引用日志。

git reflog --relative-date

用相对的日期显示引用日志。(如 2 周前)。

讨论

每次当前的 HEAD 更新时(如切换分支、拉取新更改、重写历史或只是添加新的提交),引用日志都会添加一个新条目。

栗子

为了理解 git reflog,我们来看一个🌰。

0a2e358 HEAD@{0}: reset: moving to HEAD~2
0254ea7 HEAD@{1}: checkout: moving from 2.2 to master
c10f740 HEAD@{2}: checkout: moving from master to 2.2

上面的引用日志显示了 master 和 2.2 的 branch 之间的相互切换。还有对一个更老的提交的强制重设。最近的活动用 HEAD@{0} 标记在上方显示。

如果事实上你是不小心切换回去的,引用日志包含了你意外地丢掉两个提交之前 master 指向的提交 0254ea7。

git reset --hard 0254ea7

使用 git reset,就有可能能将master变回之前的那个提交。它提供了一张安全网,以防历史发生意外更改。

务必记住,引用日志提供的安全网只对提交到本地仓库的更改有效,而且只有移动操作会被记录。

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.