Git详细工作流程的可视化

参考博客: Git 工具 - 重置揭密

新建仓库并提交文件

假设我们进入到一个新目录, 其中有一个文件。 我们称其为该文件的 v1 版本, 将它标记为蓝色。 现在运行 git init, 这会创建一个 Git 仓库, 其中的 HEAD 引用指向未创建的 master 分支。

reset-ex1

此时, 只有工作目录有内容。

现在我们想要提交这个文件, 所以用 git add 来获取工作目录中的内容, 并将其复制到索引中。

reset-ex2

接着运行 git commit, 它会取得索引中的内容并将它保存为一个永久的快照, 然后创建一个指向该快照的提交对象, 最后更新 master 来指向本次提交。

reset-ex3

此时如果我们运行 git status, 会发现没有任何改动, 因为现在三棵树完全相同。

修改文件并提交至仓库

现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的 v2 版本, 并将它标记为红色。

reset-ex4

如果现在运行 git status, 我们会看到文件显示在 “Changes not staged for commit” 下面并被标记为红色, 因为该条目在索引与工作目录之间存在不同。 接着我们运行 git add 来将它暂存到索引中。

reset-ex5

此时, 由于索引和 HEAD 不同, 若运行 git status 的话就会看到 “Changes to be committed” 下的该文件变为绿色 ——也就是说, 现在预期的下一次提交与上一次提交不同。

最后, 我们运行 git commit 来完成提交。

reset-ex6

现在运行 git status 会没有输出, 因为三棵树又变得相同了。

版本回退

参考博客: Git 工具 - 重置揭密

参考博客: git reset 命令 | 菜鸟教程

为了演示这些例子, 假设我们再次修改了 file.txt 文件并第三次提交它。 现在的历史看起来是这样的:

reset-start

让我们跟着 reset 看看它都做了什么。 它以一种简单可预见的方式直接操纵这三棵树。 它做了三个基本操作。

第 1 步:移动 HEAD

reset 做的第一件事是移动 HEAD 的指向。 这与改变 HEAD 自身不同(checkout 所做的);reset 移动 HEAD 指向的分支。 这意味着如果 HEAD 设置为 master 分支(例如, 你正在 master 分支上), 运行 git reset 9e5e6a4 将会使 master 指向 9e5e6a4

reset-soft

无论你调用了何种形式的带有一个提交的 reset, 它首先都会尝试这样做。 使用 reset --soft, 它将仅仅停在那儿。

现在看一眼上图, 理解一下发生的事情:它本质上是撤销了上一次 git commit 命令。 当你在运行 git commit 时, Git 会创建一个新的提交, 并移动 HEAD 所指向的分支来使其指向该提交。 当你将它 resetHEAD~(HEAD 的父结点)时, 其实就是把该分支移动回原来的位置, 而不会改变索引和工作目录。 现在你可以更新索引并再次运行 git commit 来完成 git commit --amend 所要做的事情了(见 修改最后一次提交)。

第 2 步:重置索引(–mixed)

注意, 如果你现在运行 git status 的话, 就会看到新的 HEAD 和以绿色标出的它和索引之间的区别。

接下来, reset 会用 HEAD 指向的当前快照的内容来更新索引。

reset-mixed

如果指定 --mixed 选项, reset 将会在这时停止。 这也是默认行为, 所以如果没有指定任何选项(在本例中只是 git reset HEAD~), 这就是命令将会停止的地方。

现在再看一眼上图, 理解一下发生的事情:它依然会撤销一上次 提交, 但还会 取消暂存 所有的东西。 于是, 我们回滚到了所有 git addgit commit 的命令执行之前。

第 3 步:重置工作目录(–hard)

reset 要做的的第三件事情就是让工作目录看起来像索引。 如果使用 --hard 选项, 它将会继续这一步。

reset-hard

现在让我们回想一下刚才发生的事情。 你撤销了最后的提交、git addgit commit 命令 以及 工作目录中的所有工作。

必须注意, --hard 标记是 reset 命令唯一的危险用法, 它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset 调用都可以轻松撤消, 但是 --hard 选项不能, 因为它强制覆盖了工作目录中的文件。 在这种特殊情况下, 我们的 Git 数据库中的一个提交内还留有该文件的 v3 版本, 我们可以通过 reflog 来找回它。但是若该文件还未提交, Git 仍会覆盖它从而导致无法恢复。

版本回退流程总结

reset 命令会以特定的顺序重写这三棵树, 在你指定以下选项时停止:

  1. 移动 HEAD 分支的指向 (若指定了 --soft, 则到此停止)
  2. 使index看起来像 HEAD (若指定 --mixed或未指定参数, 则到此停止)
  3. 使工作目录看起来像index (若指定了--hard, 则到此停止)

通过路径来重置

前面讲述了 reset 基本形式的行为, 不过你还可以给它提供一个作用路径。 若指定了一个路径, reset 将会跳过第 1 步, 并且将它的作用范围限定为指定的文件或文件集合。 这样做自然有它的道理, 因为 HEAD 只是一个指针, 你无法让它同时指向两个提交中各自的一部分。 不过索引和工作目录 可以部分更新, 所以重置会继续进行第 2、3 步。

现在, 假如我们运行 git reset file.txt (这其实是 git reset --mixed HEAD file.txt 的简写形式, 因为你既没有指定一个提交的 SHA-1 或分支, 也没有指定 --soft--hard), 它会:

  1. 移动 HEAD 分支的指向 (已跳过)
  2. 让索引看起来像 HEAD (到此处停止)

所以它本质上只是将 file.txt 从 HEAD 复制到索引中。

reset-path1

它还有 取消暂存文件 的实际效果。 如果我们查看该命令的示意图, 然后再想想 git add 所做的事, 就会发现它们正好相反。

reset-path2

我们可以不让 Git 从 HEAD 拉取数据, 而是通过具体指定一个提交来拉取该文件的对应版本。 我们只需运行类似于 git reset eb43bf file.txt 的命令即可。

reset-path3

它其实做了同样的事情, 也就是把工作目录中的文件恢复到 v1 版本, 运行 git add 添加它, 然后再将它恢复到 v3 版本(只是不用真的过一遍这些步骤)。 如果我们现在运行 git commit, 它就会记录一条“将该文件恢复到 v1 版本”的更改, 尽管我们并未在工作目录中真正地再次拥有它。

版本回退总结

  • git reset命令用于回退版本, 可以指定回退到某一次提交的版本

    1
    $ git reset [--soft | --mixed | --hard] [HEAD]
  • --soft参数只改变HEAD的指向

    即本地仓库中的版本会被回退, 而暂存区和工作区的内容不变

    1
    2
    3
    $ git reset --soft HEAD~
    # 这个命令将HEAD指向当前版本的上一个版本
    # 暂存区和工作区的内容不会改变
  • --mixed参数将改变HEAD和index的指向

    即本地仓库和暂存区的版本会被回退, 而工作目录的内容不会改变

    1
    2
    3
    4
    5
    $ git reset [--mixed] HEAD~
    # 该命令会先将HEAD指向上一个版本的快照
    # 然后将index的内容回退到HEAD所指向版本的内容
    # 本地仓库和暂存区的内容会改变,并且它们的版本一致,工作区的内容不会改变
    # --mixed为默认参数, 可以不用指定
  • --hard参数将改变HEAD和index的指向, 并改变工作区的内容

    1
    2
    3
    4
    $ git reset --hard HEAD~
    # 这个命令将会回退本地仓库+暂存区+工作区到上一个提交版本
    # 注意,对于工作区未提交的修改,使用这个命令会将其覆盖,无法找回
    # 已提交的文件可以通过reflog命令找回
  • 不指定HEAD回退的版本号和回退次数可以跳过移动HEAD的步骤

    1
    2
    3
    4
    $ git reset file.txt
    $ git reset --mixed HEAD file.txt
    # 这两个命令是一样的,--mixed是默认参数,HEAD后没有跟版本号
    # 相当于只是撤销了一次add,并不改变HEAD的指向
  • ^和~

    1
    2
    3
    4
    5
    HEAD     #表示当前版本
    HEAD^ #上一版本
    HEAD^^ #上上版本
    HEAD^^^ #上上上一版本
    ...
    1
    2
    3
    4
    5
    HEAD~0  #当前版本
    HEAD~1 #上一版本
    HEAD~2 #上两版本
    HEAD~3 #上三版本
    ...
  • 回到未来版本

    1
    $ git reset [--soft | --mixed | --hard] 67bfc
  • 查看被reset的提交日志

    1
    $ git reflog