本文使用 Zhihu On VSCode 创作并发布

引言

很多新人用 Git 非常别扭,似乎只有 -f 才能拯救世界。秉承“操作 Git 的失误就要用 Git 的正常命令层层回退”的原则,这里整理一下 Git 存储和分支的原理,帮助理解 git 所提供的丰富的命令。

参考

Git 分支 - 分支简介

存储

首先要明确,Git 保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。

在暂存文件时(git add),Git 会计算这些文件的校验和,然后把这些文件的快照以 blob 对象的形式保存到 Git 仓库中。

在提交时(git commit),Git 会计算每一个子目录的校验和,然后把目录和文件的校验和组织成一个树对象。之后,创建一个提交对象,这个对象里存储着:

  • 指向文件树对象的指针。
  • 作者姓名和邮箱。
  • 这次提交的父对象(可能有零个、一个或多个)。

假设一开始我们的工作区里只有三个文件,那么执行 git addgit commit 后会生成如下结构:

db0b8aab2ed96bae31564921dc2edc26.png
git-commit-and-tree.png

如果我们多次修改、提交,那么后一次的提交对象就会包含上一次提交的指针(作为父对象):

f09824fba70aedd03620e442b0201620.png
git-commits-and-parents.png

分支

Git 的分支,其实本质上仅仅是指向提交对象的可变指针。每次的提交操作中,当前分支的指针都会自动向前移动。

bceb3a34d2818f3c3964ab948bd0527c.png
git-branch-and-history.png

所以很容易理解,创建分支(git branch)其实就是新建了一个指针,指向当前提交记录。

那么 Git 是如何知道当前在哪个分支上的呢?也很简单,Git 有一个名为 HEAD 的特殊指针,它指向谁,就代表当前在哪个分支上。分支的切换(git checkout)就是在移动 HEAD 的指向。

5c9d6768bb0b929db17664577b9f1295.png
git-head-to-master.png

三棵树

在初步理解指针的移动之后,一个新的问题出现了,Git 是如何修改磁盘上实际的文件的?比如 git checkoutgit reset

为了解释这一过程,Git 提出了三棵树的模型,注意表格中的“用途”一栏仅用来帮助理解:

fa9dc014ee652af94e899e5f5ab72f38.png
git-three-trees.png

一般而言,文件在三棵树的切换方式如下:

28ddedc823213bc1aa80164c40c657ed.png
git-reset-workflow.png

我们可以这样理解:

  • 我们只能直接编辑 WD 中文件的内容。
  • git add 即把文件从 WD 复制到 Index 列表里。
  • git commit 即生成 Index 列表里文件的快照,保存到 commit 对象里。
  • git checkout 即执行如下流程:
    • 移动 HEAD 指向你所指定的分支
    • 将这次提交的快照填充到 Index 中
    • 将 Index 中的内容复制到 WD
  • git reset 即执行如下流程:
    • 移动 HEAD 指向你所指定的分支(若指定了 --soft,则到此停止)
    • 使 Index 看起来像 HEAD(若未指定 --hard,则到此停止)
    • 使 WD 看起来像 Index

git restore

Git 2.23 新加入的实验性命令,可以从 Index 或者其他 commit 中将文件还原到 WD 中,其实就是对 git checkoutgit reset 的简化,方便新手理解,有兴趣的可自行尝试。

总结

总之,对于以上命令的操作,若只考虑操作 commit,那么仅有 git reset --hard 可能会对 WD 造成不可挽回的修改,其他操作我们都可以用适当的命令做修正。

记住,不要惊慌,遇事不决 git reflog :joy:

关于

  • 作者:张乐萌(Ian Zhang),群脉交付团队工程师。
  • 编辑:王永浩(Aaron Wang),群脉首席架构师。
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐