回滚错误的修改

BY 童仲毅(geeeeeeeeek@github

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

这章教程提供了和项目旧版本打交道所需要的所有技巧。首先,你会知道如何浏览旧的提交,然后了解回滚项目历史中的公有提交和回滚本地机器上的私有更改之间的区别。

git checkout

见上一章「2.5 检出之前的提交」

git revert

git revert 命令用来撤销一个已经提交的快照。但是,它是通过搞清楚如何撤销这个提交引入的更改,然后在最后加上一个撤销了更改的 提交,而不是从项目历史中移除这个提交。这避免了Git丢失项目历史,这一点对于你的版本历史和协作的可靠性来说是很重要的。

Git Tutorial: git revert.svg)

用法

  1. git revert <commit>

生成一个撤消了 <commit> 引入的修改的新提交,然后应用到当前分支。

讨论

撤销(revert)应该用在你想要在项目历史中移除一整个提交的时候。比如说,你在追踪一个 bug,然后你发现它是由一个提交造成的,这时候撤销就很有用。与其说自己去修复它,然后提交一个新的快照,不如用 git revert,它帮你做了所有的事情。

撤销(revert)和重设(reset)对比

理解这一点很重要。git revert 回滚了「单独一个提交」,它没有移除后面的提交,然后回到项目之前的状态。在 Git 中,后者实际上被称为 reset,而不是 revert

Git Tutorial: Revert vs Reset.svg)

撤销和重设相比有两个重要的优点。首先,它不会改变项目历史,对那些已经发布到共享仓库的提交来说这是一个安全的操作。至于为什么改变共享的历史是危险的,请参阅 git reset 一节。

其次,git revert 可以针对历史中任何一个提交,而 git reset 只能从当前提交向前回溯。比如,你想用 git reset 重设一个旧的提交,你不得不移除那个提交后的所有提交,再移除那个提交,然后重新提交后面的所有提交。不用说,这并不是一个优雅的回滚方案。

栗子

下面的这个栗子是 git revert 一个简单的演示。它提交了一个快照,然后立即撤销这个操作。

  1. # 编辑一些跟踪的文件
  2. # 提交一份快照
  3. git commit -m "Make some changes that will be undone"
  4. # 撤销刚刚的提交
  5. git revert HEAD

这个操作可以用下图可视化:

Git Tutorial: git revert Example

注意第四个提交在撤销后依然在项目历史中。git revert 在后面增加了一个提交来撤销修改,而不是删除它。 因此,第三和第五个提交表示同样的代码,而第四个提交依然在历史中,以备以后我们想要回到这个提交。

git reset

如果说 git revert 是一个撤销更改安全的方式,你可以将 git reset 看做一个 危险 的方式。当你用 git reset 来重设更改时(提交不再被任何引用或引用日志所引用),我们无法获得原来的样子——这个撤销是永远的。使用这个工具的时候务必要小心,因为这是少数几个可能会造成工作丢失的命令之一。

git checkout 一样,git reset 有很多种用法。它可以被用来移除提交快照,尽管它通常被用来撤销缓存区和工作目录的修改。不管是哪种情况,它应该只被用于 本地 修改——你永远不应该重设和其他开发者共享的快照。

用法

  1. git reset <file>

从缓存区移除特定文件,但不改变工作目录。它会取消这个文件的缓存,而不覆盖任何更改。

  1. git reset

重设缓冲区,匹配最近的一次提交,但工作目录不变。它会取消 所有 文件的缓存,而不会覆盖任何修改,给你了一个重设缓存快照的机会。

  1. git reset --hard

重设缓冲区和工作目录,匹配最近的一次提交。除了取消缓存之外,--hard 标记告诉 Git 还要重写所有工作目录中的更改。换句话说:它清除了所有未提交的更改,所以在使用前确定你想扔掉你所有本地的开发。

  1. git reset <commit>

将当前分支的末端移到 <commit>,将缓存区重设到这个提交,但不改变工作目录。所有 <commit> 之后的更改会保留在工作目录中,这允许你用更干净、原子性的快照重新提交项目历史。

  1. git reset --hard <commit>

将当前分支的末端移到 <commit>,将缓存区和工作目录都重设到这个提交。它不仅清除了未提交的更改,同时还清除了 <commit> 之后的所有提交。

讨论

上面所有的调用都是用来移除仓库中的修改。没有 --hard 标记时 git reset 通过取消缓存或取消一系列的提交,然后重新构建提交来清理仓库。而加上 --hard 标记对于作了大死之后想要重头再来尤其方便。

撤销(revert)被设计为撤销 公开 的提交的安全方式,git reset被设计为重设 本地 更改。因为两个命令的目的不同,它们的实现也不一样:重设完全地移除了一堆更改,而撤销保留了原来的更改,用一个新的提交来实现撤销。

Git Tutorial: Revert vs Reset

不要重设公共历史

当有 <commit> 之后的提交被推送到公共仓库后,你绝不应该使用 git reset。发布一个提交之后,你必须假设其他开发者会依赖于它。

移除一个其他团队成员在上面继续开发的提交在协作时会引发严重的问题。当他们试着和你的仓库同步时,他们会发现项目历史的一部分突然消失了。下面的序列展示了如果你尝试重设公共提交时会发生什么。origin/master 是你本地 master 分支对应的中央仓库中的分支。

Git Tutorial: Resetting an Public Commit.svg)

一旦你在重设之后又增加了新的提交,Git 会认为你的本地历史已经和 origin/master 分叉了,同步你的仓库时的合并提交(merge commit)会使你的同事困惑。

重点是,确保你只对本地的修改使用 git reset,而不是公共更改。如果你需要修复一个公共提交,git revert 命令正是被设计来做这个的。

栗子

取消文件缓存

git reset 命令在准备缓存快照时经常被用到。下面的例子假设你有两个文件,hello.pymain.py它们已经被加入了仓库中。

  1. # 编辑了hello.py和main.py
  2. # 缓存了目录下所有文件
  3. git add .
  4. # 意识到hello.py和main.py中的修改
  5. # 应该在不同的快照中提交
  6. # 取消main.py缓存
  7. git reset main.py
  8. # 只提交hello.py
  9. git commit -m "Make some changes to hello.py"
  10. # 在另一份快照中提交main.py
  11. git add main.py
  12. git commit -m "Edit main.py"

如你所见,git reset 帮助你取消和这次提交无关的修改,让提交能够专注于某一特定的范围。

移除本地修改

下面的这个栗子显示了一个更高端的用法。它展示了你作了大死之后应该如何扔掉那几个更新。

  1. # 创建一个叫`foo.py`的新文件,增加代码
  2. # 提交到项目历史
  3. git add foo.py
  4. git commit -m "Start developing a crazy feature"
  5. # 再次编辑`foo.py`,修改其他文件
  6. # 提交另一份快照
  7. git commit -a -m "Continue my crazy feature"
  8. # 决定废弃这个功能,并删除相关的更改
  9. git reset --hard HEAD~2

git reset HEAD~2 命令将当前分支向前倒退两个提交,相当于在项目历史中移除刚创建的这两个提交。记住,这种重设只能用在 非公开 的提交中。绝不要在将提交推送到共享仓库之后执行上面的操作。

git clean

git clean 命令将未跟踪的文件从你的工作目录中移除。它只是提供了一条捷径,因为用 git status 查看哪些文件还未跟踪然后手动移除它们也很方便。和一般的 rm 命令一样,git clean 是无法撤消的,所以在删除未跟踪的文件之前想清楚,你是否真的要这么做。

git clean 命令经常和 git reset --hard 一起使用。记住,reset 只影响被跟踪的文件,所以还需要一个单独的命令来清理未被跟踪的文件。这个两个命令相结合,你就可以将工作目录回到之前特定提交时的状态。

用法

  1. git clean -n

执行一次git clean的『演习』。它会告诉你那些文件在命令执行后会被移除,而不是真的删除它。

  1. git clean -f

移除当前目录下未被跟踪的文件。-f(强制)标记是必需的,除非 clean.requireForce 配置项被设为了 false(默认为 true)。它 不会 删除 .gitignore 中指定的未跟踪的文件。

  1. git clean -f <path>

移除未跟踪的文件,但限制在某个路径下。

  1. git clean -df

移除未跟踪的文件,以及目录。

  1. git clean -xf

移除当前目录下未跟踪的文件,以及 Git 一般忽略的文件。

讨论

如果你在本地仓库中作死之后想要毁尸灭迹,git reset --hardgit clean -f 是你最好的选择。运行这两个命令使工作目录和最近的提交相匹配,让你在干净的状态下继续工作。

git clean 命令对于 build 后清理工作目录十分有用。比如,它可以轻易地删除 C 编译器生成的 .o.exe 二进制文件。这通常是打包发布前需要的一步。-x 命令在这种情况下特别方便。

请牢记,和 git reset 一样, git clean 是仅有的几个可以永久删除提交的命令之一,所以要小心使用。事实上,它太容易丢掉重要的修改了,以至于 Git 厂商 强制 你用 -f 标志来进行最基本的操作。这可以避免你用一个 git clean 就不小心删除了所有东西。

栗子

下面的栗子清除了工作目录中的所有更改,包括新建还没加入缓存的文件。它假设你已经提交了一些快照,准备开始一些新的实验。

  1. # 编辑了一些文件
  2. # 新增了一些文件
  3. # 『糟糕』
  4. # 将跟踪的文件回滚回去
  5. git reset --hard
  6. # 移除未跟踪的文件
  7. git clean -df

在执行了 reset/clean 的流程之后,工作目录和缓存区和最近一次提交看上去一模一样,而 git status会认为这是一个干净的工作目录。你可以重新来过了。

注意,不像 git reset 的第二个栗子,新的文件没有被加入到仓库中。因此,它们不会受到 git reset --hard 的影响,需要 git clean 来删除它们。

这篇文章是「git-recipes」的一部分,点击 目录 查看所有章节。

如果你觉得文章对你有帮助,欢迎点击右上角的 Star :star2: 或 Fork :fork_and_knife:。

如果你发现了错误,或是想要加入协作,请参阅 Wiki 协作说明