自底向上了解 Git 工作原理

头图

本文介绍了 Git 的工作原理。假定您对 Git 有足够的了解,可以使用它对项目进行版本控制。

本文着重介绍支撑 Git 的图形结构以及该图形的属性指示 Git 行为的方式。您的思维模型是建立在事实之上,而不是根据在尝试 API 时收集的证据构建的假设。这个更真实的模型使您可以更好地了解 Git 所做的事情,正在做的事情以及它将要做的事情。

本文是由在单个项目上运行的一系列 Git 命令构成。不时就会观察到有关构建 Git 的图形数据结构的信息。这些观察结果说明了图形的属性以及该属性产生的行为。

阅读之后,如果您想更深入地研究 Git,可以查看我用 JavaScript 实现的 Git 项目 带大量注释的源代码

创建项目

~ $ mkdir alpha
~ $ cd alpha

用户创建了 alpha, 作为项目的目录。

~/alpha $ mkdir data
~/alpha $ printf 'a' > data/letter.txt

进入 alpha 目录并在其中创建一个名为 data 的目录。在 data 下, 创建一个名为 letter.txt 的文件,并为其写入一个字母 a。 现在 alpha 目录结构如下:

 alpha
 └── data
     └── letter.txt

初始化版本库

~/alpha $ git init
          Initialized empty Git repository

git init 将当前目录初始化为 Git 仓库。执行该命令,会创建一个 .git 目录并往其中写入了一些文件。这些文件定义了关于 Git 配置和项目历史的所有内容。它们只是普通的文件。并没有什么魔法。用户可以使用文本编辑器或 shell 来读取和编辑它们。也就是说:用户可以像对他们的项目文件一样,轻松地读取和编辑项目的历史记录。

现在 alpha 目录结构看起来如下:

 alpha
 ├── data
 |   └── letter.txt
 └── .git
     ├── objects
     etc...

.git 目录及其内容属于 Git 。所有的其他文件统称为工作副本。它们是用户的。

添加一些文件

~/alpha $ git add data/letter.txt

用户对 data/letter.txt 文件执行了 git add 。这会产生两个结果。

首先,它会在 .git/objects/ 目录下创建一个新的 blob(块) 文件。

这个 blob 文件包含了 data/letter.txt 的压缩内容。它的名称是通过对内容进行哈希处理而得出的。散列一段文字意味着在其上运行一个程序,将其转换为更小的1 唯一可识别2 的文字。例如,Git将 a 散列为 2e65efe2a145dda7ee51d1741299f848e5bf752e。前两个字符用作对象数据库中目录的名称:.git/objects/2e/。哈希的其余部分用作保存所添加文件内容的 Blob 文件的名称: .git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e.

请注意,只需要将文件添加到 Git 如何将其内容保存到 objects 目录。如果用户从工作副本中删除 data/letter.txt,其内容在 Git 中仍然是安全的。

其次,git add 将文件添加到索引。索引是一个列表,其中包含 Git 已被告知要跟踪的每一个文件。它以文件形式存储在 .git/index 目录中。它的每一行都会在添加文件时将跟踪的文件映射到其内容的哈希。这是运行 git add 命令后的索引:

data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e

用户创建一个名为 data/number.txt ,其中包含 1234 的文件。

~/alpha $ printf '1234' > data/number.txt

工作副本看起来如下所示:

alpha
└── data
    └── letter.txt
    └── number.txt

用户将文件添加到 Git。

~/alpha $ git add data

git add 命令创建一个 blob 对象,该对象包含 data/number.txt 的内容。它为 data/number.txt 添加指向该 blob 的索引条目。这是第二次运行 git add 命令之后的索引:

data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3

请注意,尽管用户运行了 git add data,但索引中仅列出了 data目录中的文件。data 目录并未单独列出。

~/alpha $ printf '1' > data/number.txt
~/alpha $ git add data

假如用户当初创建 data/number.txt 文件时,想的是输入 1 而不是 1234。用户进行更正并再次将这个文件添加到索引。此时,这个命令就会跟据新的内容创建一个新的 blob 对象。并且会把 data/number.txt 的索引条目指向它。

提交

~/alpha $ git commit -m 'a1'
          [master (root-commit) 774b54a] a1

用户对 a1 进行提交。Git 会打印出有关提交的一些数据。你很快就会知道这些数据的具体含义。

commit 命令包含三个步骤。
它创建一个树形图来表示要提交的项目版本的内容。
它创建一个提交对象 (commit object)。
它将当前分支指向新的提交对象。

创建树形图

Git 通过从索引创建树形图来记录项目的当前状态。该树形图记录了项目中每个文件的位置和内容。

该图由两种类型的对象组成:blob(块)和 tree(树)

Blob(块)由 git add 命令存储。它们代表文件的内容。

Tree(树)会在执行提交时存储。Tree 表示工作副本中的目录。

以下是 Tree(树)对象,它记录了新提交的 data 目录的内容:

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt

第一行记录了再生 data/letter.txt 所需的所有内容。其中第一部分表示文件的权限。第二部分指出,该条目的内容由 blob (而不是 tree) 表示。第三部分是 blob 的哈希值。第四部分是文件的名称。

第二行为文件 data/number.txt 记录了相同的内容。

以下是 alpha 的树对象,它是项目的根目录:

040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data

该树中只有一行内容,指向 data 树。

Tree graph for the `a1` commit
提交 a1 生成的树形图

在上图中, root 树指向 data 树。 data 树指向文件 data/letter.txtdata/number.txt 的 blob (块)对象。

创建一个提交对象

git commit 命令会在创建树形图之后再创建一个 commit 对象。这个对象只是 .git/objects/ 目录下的一个文本文件:

tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500

a1

第一行指向树形图。哈希用于表示工作副本根的树对象。即:alpha 目录。最后一行是提交消息。

`a1` commit object pointing at its tree graph
a1 提交对象指向它的树形图

在新的提交中指向当前分支

最后,commit 命令将当前分支指向新的 commit 对象。

哪个是当前分支?Git 会到 .git/HEAD 中的 HEAD 文件中查找:

ref: refs/heads/master

这表示 HEAD 指向 mastermaster 是当前分支。

HEADmaster 都是 refs(引用)。ref 是 Git 或用户用于标识特定提交的标签。

其实表示 master ref(引用)的文件并不存在,因为这是对存储库的第一次提交。Git 在 .git/refs/heads/master 下创建文件,并将其内容设置为 commit 对象的哈希值:

74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd

(如果您在阅读时输入这些 Git 命令,则 a1 提交的哈希值将会与我的不同。内容对象(例如 blob 和 tree)则总是散列为相同的值。提交则不会,因为它们包括日期以及他们的创建者的名字。)

让我们将 HEADmaster 添加到 Git 图中:

`master` pointing at the `a1` commit

HEAD 指向 master 并且 master 指向 a1 的提交

HEAD 像提交之前一样指向 master,但是 master 现在存在了,并且指向新的提交对象。

执行不是第一次提交的提交

下面是执行 a1 commit 之后的 Git 图。包括工作副本和索引。

`a1` commit shown with the working copy and index

带有工作副本和索引的 a1 提交

注意,工作副本,索引和 a1 提交对于 data/letter.txtdata/number.txt 都具有相同的内容。索引和 HEAD 提交都使用哈希来引用 blob 对象,但是工作副本内容作为文本存储在其他位置。

~/alpha $ printf '2' > data/number.txt

现在用户将 data/number.txt 的内容设置为 2。这将更新工作副本,但索引和 HEAD 提交保持不变。

`data/number.txt` set to `2` in the working copy

工作副本中的>工作副本中的 data/num.txt 设置为 2

~/alpha $ git add data/number.txt

用户将文件添加到 Git。这会添加一个包含 2 的 blob 对象到 objects 目录。它将 data/number.txt 的索引条目指向新的 blob。

`data/number.txt` set to `2` in the working copy and index

在工作副本和索引中将 data/number.txt设置为 2

~/alpha $ git commit -m 'a2'
          [master f0af7e6] a2

用户执行提交。这里提交的步骤和前面的一样。

首先,创建一个新的树形图用来表示索引的内容。

data/number.txt 的索引项已更改。旧的 data 树不再反应 data 目录的索引状态。必须创建一个新的 data 树对象:

100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt

新的 data 树的哈希值与旧的 data 树的值不同。必须创建一个 root 树来记录这个哈希值:

040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data

其次,一个新的 commit 对象被创建。

tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556
parent 774b54a193d6cfdd081e581a007d2e11f784b9fe
author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500

a2

commit 对象的第一行指向新的 root 树对象。
第二行指向 a1: 提交的父级。为了找到父提交,Git 转到了 HEAD,接着是 master,找到 a1 提交的哈希值。

第三,将 master 分支文件的内容设置为新提交的哈希值。

`a2` commit

a2 commit

Git graph without the working copy and index

没有工作副本和索引的 Git 图

** Graph 属性**:内容存储为树对象。这意味着仅仅将差异存储在对象数据库中。看上图。 a2 提交重用了在 a1 提交之前创建的 a blob。 同样,如果整个目录在提交之间没有变化,则它的树以及它下面的所有 blob 和树都可以重用。 通常,从提交到提交的内容更改很少。这意味着 Git 可以在少量空间中存储大量提交历史记录。

** Graph 属性**:每个提交都有一个父级。 这意味着版本库可以存储项目的历史记录。

** Graph 属性**:refs(引用)是提交历史记录的一部分或另一部分的入口点。 这意味着可以为提交赋予有意义的名称。 用户使用诸如 fix-for-bug-376 之类的具体引用将其工作组织成对他们的项目有意义的谱系。 Git 使用诸如HEADMERGE_HEADFETCH_HEAD 的符号引用来支持操作提交历史记录的命令。

** Graph 属性**:objects/ 目录中的节点是不可变的。 这意味着内容是被编辑而不是被删除。 每一次添加的内容和每次提交的内容都在 objects 目录中的某个位置3

** Graph 属性**:refs(引用)是可变的。因此,ref 的含义可以改变。 master 指向的提交可能是当前项目的最佳版本,但很快,它将被更新、更好的提交所取代。

** Graph 属性**:refs(引用)所指向的工作副本和提交是随时可用的,而其他提交则不可用。 这意味着更容易回忆起最近的历史记录,但它的更改频率也更高。 或者:Git 的记忆力正在衰退,必须用越来越恶毒的刺激来刺激。

工作副本是历史记录中最容易回忆的点,因为它位于存储库的根目录中。 调用它甚至不需要 Git 命令。 这也是历史记录中最不固定的一点。 用户可以制作文件的多个版本,但是 Git 不会记录其中的任何一个版本,除非添加了它们。

HEAD 指向的提交很容易回忆。 检出的是分支的顶端。 要查看其内容,用户可以 stash 4 然后检查工作副本。 同时,HEAD 是更改频率最高的 ref(引用)。

具体引用指向的提交很容易回忆。 用户可以简单地签出该分支。 分支的提示更改的频率比 HEAD 的更改少,但通常足以使分支名称的含义发生变化。

很难回忆起没有任何引用指向的提交。 用户离 ref(引用)越远,他们就越难构建提交的含义。 但越往回走,人们改变历史的可能性就越小 5

签出一个提交

~/alpha $ git checkout 37888c2
          You are in 'detached HEAD' state...

用户使用 a2 提交的哈希值将其检出。(如果您正在运行这些 Git 命令,此命令将不起作用。你需要使用 git log 查找 a2 提交的哈希值。)

签出有四个步骤。

首先,Git 获取 a2 提交并获取其指向的树形图。

其次,它将树形图中的文件条目写入工作副本。 这不会导致任何变化。 因为 HEAD 已经通过 master 指向 a2 提交,所以工作副本中已经写入了树形图的内容。

第三,Git 将树形图中的文件条目写入索引。 这也不会导致任何变化。 索引已经具有 a2 提交的内容。

第四,HEAD 的内容设置为 a2 提交的哈希值:

f0af7e62679e144bb28c627ee3e8f7bdb235eee9

HEAD 的内容设置为哈希值会使版本库处于分离的 HEAD 状态。 请注意,在下图中,HEAD 直接指向 a2 提交,而不是指向 master

Detached `HEAD` on `a2` commit

a2 commit上分离的 HEAD

~/alpha $ printf '3' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a3'
          [detached HEAD 3645a0e] a3

用户将 data/number.txt 的内容设置为 3 并提交更改。 Git转到 HEAD 以获取 a3 提交的父级。 它没有查找并遵循分支引用,而是查找并返回 a2 提交的哈希值。

Git 更新 HEAD 以直接指向新的 a3 提交的哈希值。版本库仍处于分离的 HEAD 状态。 它不在分支上,因为没有提交指向 a3 或其后代之一。 这意味着很容易丢失。

从现在开始,tree (树)和 blob (块) 将大部分从图中省略。

`a3` commit that is not on a branch

a3 commit 没有在分支上

创建分支

~/alpha $ git branch deputy

用户创建一个名为 deputy 的新分支。 这只是在 .git/refs/heads/deputy 中创建了一个新文件,其中包含 HEAD 指向的哈希:a3 提交的哈希。

** Graph 属性**:分支仅是 refs(引用),而 refs(引用)仅是文件。 这意味着 Git 分支是轻量级的。

deputy 分支的创建将新的 a3 提交安全地放在分支上。 HEAD 仍然是分离的,因为它仍然直接指向提交。

`a3` commit now on the `deputy` branch

a3 commit 现在在 deputy 分支上

签出分支

~/alpha $ git checkout master
          Switched to branch 'master'

用户签出 master 分支。

首先,Git 获取 master 指向的 a2 提交,并获取提交指向的树形图。

其次,Git 将树形图中的文件条目写入工作副本的文件中。 这会将 data/number.txt 的内容设置为 2

第三,Git 将树形图中的文件条目写入索引。 这会将 data/number.txt 的条目更新为 blob 2 的哈希。

第四,Git 将 HEAD 指向 master,将其内容从散列更改为:

ref: refs/heads/master

`master` checked out and pointing at the `a2` commit

已切换到 master 分支并指向 a2 commit

签出与工作副本不兼容的分支

  ~/alpha $ printf '789' > data/number.txt
  ~/alpha $ git checkout deputy
            Your changes to these files would be overwritten
            by checkout:
            data/number.txt
            Commit your changes or stash them before you
            switch branches.

用户不小心将 data/number.txt 的内容设置为 789。 他们试图签出 deputy。Git 阻止了这次签出。

HEAD 指向 master,而 master 指向 a2,其中 data/number.txt 读取为 2deputy 指向 a3,其中 data/number.txt 读取为 3data/number.txt 的工作副本版本为 789。 所有这些版本都是不同的,必须解决差异。

Git 可以用签出的提交中的版本替换 data/number.txt 的工作副本版本。 但是,它选择不惜一切代价避免数据丢失。

Git 可以将工作副本版本与要签出的版本合并。 但这很复杂。

因此,Git 中止了签出。

~/alpha $ printf '2' > data/number.txt
~/alpha $ git checkout deputy
          Switched to branch 'deputy'

用户注意到他们不小心编辑了 data/number.txt,并将内容改回 2。 然后,他们成功签出了 deputy

`deputy` checked out

签出 deputy

合并祖先

~/alpha $ git merge master
          Already up-to-date.

用户将 master 合并到 deputy 中。 合并两个分支意味着合并两个提交。 第一个提交是 deputy 所指向的:接收者。 第二个提交是 master 指向的:给予者。 对于此次合并,Git 不执行任何操作。 它报告说 Already up-to-date.(已经是最新的了)。

** Graph 属性**:图形中的一系列提交被解释为对版本库内容所做的一系列更改。 这意味着在合并中,如果给予方提交是接收方提交的祖先,则 Git 不会执行任何操作。 这些更改已经被纳入其中。

合并后代

~/alpha $ git checkout master
          Switched to branch 'master'

用户签出 master 分支。

`master` checked out and pointing at the `a2` commit

master 已被签出,它指向 a2 commit

~/alpha $ git merge deputy
          Fast-forward

deputy 合并到 master。 Git 发现接收方提交 a2 是给予方提交 a3 的祖先。 它可以进行快速合并。

它获取给予方提交并获取其指向的树形图。 它将树形图中的文件条目写入工作副本和索引。 它 「fast-forwards」(快进)master 使其指向 a3

`a3` commit from `deputy` fast-forward merged into `master`

deputy 分支来的 a3 commit,快进合并到 master

** Graph属性**:图形中的一系列提交被解释为对版本库内容所做的一系列更改。 这意味着在合并中,如果给予方是接收方的后代,则历史记录不会更改。 已经有一个提交序列来描述要进行的更改:提交方与接收方之间的提交序列。 但是,尽管 Git 的历史记录没有变化,但 Git 的图形确实发生了变化。 HEAD 指向的具体引用已更新为指向给予者提交。

合并来自不同谱系的两个提交

~/alpha $ printf '4' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a4'
          [master 7b7bd9a] a4

用户将 number.txt 的内容设置为 4 并且提交这个变更到 master

~/alpha $ git checkout deputy
          Switched to branch 'deputy'
~/alpha $ printf 'b' > data/letter.txt
~/alpha $ git add data/letter.txt
~/alpha $ git commit -m 'b3'
          [deputy 982dffb] b3

用户签出 deputy 分支。把 data/letter.txt 的内容设置为 b 并且提交这个变更到 deputy

`a4` committed to `master`, `b3` committed to `deputy` and `deputy` checked out

a4 已提交到 master, b3 已提交到 deputy 并且 deputy 已被签出

** Graph属性**:提交可以共享父级。 这意味着可以在提交历史记录中创建新的谱系。

** Graph属性**:提交可以有多个父级。 这意味着单独的谱系可以通过具有两个父级的提交来连接:合并提交。

~/alpha $ git merge master -m 'b4'
          Merge made by the 'recursive' strategy.

用户将 master 合并到 deputy

Git 发现接收方 b3 和给予方 a4 处于不同谱系。 它进行合并提交。 此过程分为八个步骤。

首先,Git 将给予方提交的哈希值写入文件 alpha/.git/MERGE_HEAD 中。 这个文件的存在告诉 Git 它正在合并中。

其次,Git 查找基提交:接收方和给予方提交共同的最新祖先。

`a3`, the base commit of `a4` and `b3`

a3, a4 and b3 的基提交

** Graph 属性**:提交有父级。这意味着可以找到两个谱系分叉的点。 Git 从 b3 向后追溯以找到其所有祖先,从 a4 向后追溯以找到其所有祖先。它找到两个谱系 a3 共享的最新祖先。这是基提交。

第三,Git 从它们的树形图中生成基提交、接收方和给予方提交的索引。

第四,Git 生成一个 diff(差异),它将接收方提交和给予方提交对基提交所做的更改组合在一起。该 diff(差异)是指向更改的文件路径的列表:添加,删除,修改或冲突。

Git 获取出现在 base,接收方或给予方索引中的所有文件的列表。对于每个索引,它都会比较索引条目,以决定要对该文件进行的更改。它将相应的条目写入 diff(差异)。在这种情况下,diff(差异)具有两个条目。

第一个条目用于 data/letter.txt。这个文件的内容是a 在 base 中,b 在接收者中,a 在给予者中。base 和接收方的内容不同。但在 base 和给予者上是一样的。Git发现内容是由接收者修改的,而不是由给予者修改的。 data/letter.txt 的 diff 条目是修改,不是冲突。

diff 中的 第二个条目用于 data/number.txt。 在这种情况下,内容在 base 和接收者中是相同的,而与给予者内容是不同的。data/letter.txt 的 diff 条目也是一个修改。

** Graph属性**:可以找到合并的基提交。这意味着,如果仅在接收方或给予者中从 base 更改了文件,则 Git 可以自动解析该文件的合并。这减少了用户必须做的工作。

第五,diff(差异)中的条目所指示的更改将应用于工作副本。 data/letter.txt 的内容设置为 bdata/number.txt 的内容设置为 4

第六,将 diff(差异)中的条目指示的更改应用于索引。 data/letter.txt 的条目指向 b blob(块),data/number.txt 的条目指向 4 blob(块)。

第七,提交更新的索引:

tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d
parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b
parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7
author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500

b4

请注意,提交有两个父级。

第八,Git将当前分支,deputy,指向新提交。

`b4`, the merge commit resulting from the recursive merge of `a4` into `b3`

b4, 从 a4b3 的递归合并产生的合并提交

合并来自不同谱系的两个提交,这两个提交都修改了同一个文件

~/alpha $ git checkout master
          Switched to branch 'master'
~/alpha $ git merge deputy
          Fast-forward

用户签出 master。把 deputy 合并到 master。这会从 master 快进到 b4 提交。现在 masterdeputy 指向同一个提交。

`deputy` merged into `master` to bring `master` up to the latest commit, `b4`

deputy 合并到 master 这会使 master 更新到最新提交, b4

~/alpha $ git checkout deputy
          Switched to branch 'deputy'
~/alpha $ printf '5' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b5'
          [deputy bd797c2] b5

用户签出 deputy。 将 data/number.txt 的内容设置为 5,并将更改提交给 deputy

~/alpha $ git checkout master
          Switched to branch 'master'
~/alpha $ printf '6' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b6'
          [master 4c3ce18] b6

用户签出 master。把 data/number.txt 内容设置为 6 并且把变更提交到 master

`b5` commit on `deputy` and `b6` commit on `master`

b5 commit on deputy and b6 commit on master

~/alpha $ git merge deputy
          CONFLICT in data/number.txt
          Automatic merge failed; fix conflicts and
          commit the result.

用户将 deputy 合并到 master 中。 发生冲突,合并被暂停。 冲突合并的过程与未冲突合并的前六个步骤相同:设置 .git/MERGE_HEAD,找到 base 提交,生成 base 索引,接收方和给予方提交,创建 diff(差异),更新工作副本并更新索引。 由于有冲突,第七步的提交和第八步的 ref 更新不会发生。 让我们再次执行这些步骤,看看会发生什么。

首先,Git 将给予方提交的哈希值写入 .git/MERGE_HEAD 中的文件。

`MERGE_HEAD` written during merge of `b5` into `b6`

在合并 b5b6 的期间写入 MERGE_HEAD

其次,Git 找到 base 提交 b4

第三,Git为 base 提交,接收方提交和给予方提交生成索引。

第四,Git生成一个 diff(差异),它将接收方提交和给予方提交对 base 所做的更改组合在一起。 该 diff(差异)是指向更改的文件路径的列表:添加,删除,修改或冲突。

在这种情况下,该 diff(差异)仅包含一个条目:data/number.txt。 该条目被标记为冲突,因为 data/number.txt 的内容与接收方,给予方和 base 中都是不同的。

第五,diff(差异)中的条目所指示的更改将应用于工作副本。 对于有冲突的区域,Git 将这两个版本都写入工作副本中的文件中。 data/number.txt 的内容设置为:

<<<<<<< HEAD
6
=======
5
>>>>>>> deputy

第六,将 diff(差异)中的条目指示的变更应用于索引。 索引中的条目通过其文件路径和阶段的组合来唯一标识。 无冲突文件的条目的阶段为 0。 在合并之前,索引看起来像这样,其中 0 是阶段值:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb

将合并差异写入索引后,索引如下所示:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767
2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61

阶段 0data/letter.txt 的条目与合并之前的条目相同。 在阶段 0data/number.txt 条目消失了。 在它的位置有三个新条目。 阶段 1 的条目具有 base data / number.txt 内容的哈希。 阶段 2 的条目具有接收方 data/number.txt 内容的哈希值。 阶段 3 的条目具有给予方 data/number.txt 内容的哈希值。 这三个条目的存在告诉 Git data/number.txt 存在冲突。

合并暂停。

~/alpha $ printf '11' > data/number.txt
~/alpha $ git add data/number.txt

用户通过将 data/number.txt 的内容设置为 11 来整合两个冲突版本的内容。将文件添加到索引中。Git 会添加一个包含 11 的 blob。添加之前有冲突的文件会告诉 Git 冲突已解决。Git会将 123 阶段的 data/num.txt 条目从索引中移除。在阶段 0 添加data/num.txt 条目,其中包含新 blob 的哈希。索引现在显示为:

0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748
0 data/number.txt 9d607966b721abde8931ddd052181fae905db503
~/alpha $ git commit -m 'b11'
          [master 251a513] b11

第七,用户提交。 Git 在存储库中查看 .git/MERGE_HEAD,它告诉 Git 有一个合并进程正在进行。 它检查索引,发现没有冲突。 然后创建一个新的提交 b11,以记录已解析合并的内容。 它将删除位于 .git/MERGE_HEAD 的文件。 这样就完成了合并。

第八,Git 将当前分支 master 指向新提交。

`b11`, the merge commit resulting from the conflicted, recursive merge of `b5` into `b6`

b11, 由 b5b6的冲突,递归合并产生的提交

删除文件

此 Git 图表包括提交历史记录、最近提交的树和Blob以及工作副本和索引:

The working copy, index, `b11` commit and its tree graph

工作副本, 索引, b11 commit 以及它的树形图

~/alpha $ git rm data/letter.txt
          rm 'data/letter.txt'

用户告诉 Git 删除 data/letter.txt。 该文件将从工作副本中删除。 该条目将从索引中删除。

After `data/letter.txt` `rm`ed from working copy and index

执行 data/letter.txt rm 删除后的工作副本和索引

~/alpha $ git commit -m '11'
          [master d14c7d2] 11

用户提交。 作为提交的一部分,Git一如既往地构建一个树形图,用来表示索引的内容。 data/letter.txt 不包含在树形图中,因为它不在索引中。

`11` commit made after `data/letter.txt` `rm`ed

data/letter.txt rm 删除后的 11 commit

复制版本库

~/alpha $ cd ..
          ~ $ cp -R alpha bravo

用户将 alpha/ 库中的内容复制到 bravo/ 目录。这将产生以下目录结构:

~
├── alpha
|   └── data
|       └── number.txt
└── bravo
    └── data
        └── number.txt

现在在bravo目录中还有另一个 Git 图:

New graph created when `alpha` `cp`ed to `bravo`

alpha cp 复制到 bravo 后的新图

将一个存储库链接到另一个存储库

      ~ $ cd alpha
~/alpha $ git remote add bravo ../bravo

用户移回 alpha 版本库。 他们将 bravo 设置为 alpha 上的远程版本库。 这会在文件 alpha/.git/config 处添加一些行:

[remote "bravo"]
        url = ../bravo/

这些行显示在 ../bravo 目录中有一个名为 bravo 的远程版本库。

从远程获取分支

~/alpha $ cd ../bravo
~/bravo $ printf '12' > data/number.txt
~/bravo $ git add data/number.txt
~/bravo $ git commit -m '12'
          [master 94cd04d] 12

用户进入 bravo 版本库。 他们将 data/number.txt 的内容设置为 12,并将更改提交给 bravo 上的 master

`12` commit on `bravo` repository

bravo 仓库上的 12 commit

~/bravo $ cd ../alpha
~/alpha $ git fetch bravo master
          Unpacking objects: 100%
          From ../bravo
            * branch master -> FETCH_HEAD

用户进入 alpha 版本库。 并从 bravo 中获取 masteralpha 中。 此过程分为四个步骤。

首先,Git 在 bravo 上获取 master 指向的提交的哈希值。 这里是 12 提交的哈希值。

其次,Git 列出 12 提交所依赖的所有对象的列表:提交对象本身,其树形图中的对象,12 提交的祖先提交以及它们的树形图中的对象。 它从该列表中删除 alpha 对象数据库已经具有的所有对象。 它将其余的复制到 alpha/.git/objects/

第三,位于 alpha/.git/refs/remotes/bravo/master 的具体 ref (引用) 文件的内容被设置为 12 提交的哈希值。

第四,alpha/.git/FETCH_HEAD 的内容设置为:

94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo

这表明最新的获取命令从 bravo 获取了 master12 提交。

`alpha` after `bravo/master` fetched

bravo/master 获取的 alpha

** Graph 属性**:对象可复制。 这意味着可以在存储库之间共享历史记录。

** Graph 属性**:存储库可以存储远程分支 ref(引用),例如 alpha/.git/refs/remotes/bravo/master。 这意味着存储库可以在本地记录远程存储库上分支的状态。 它在获取时是正确的,但是如果远程分支发生更改,它将过时。

合并 FETCH_HEAD

~/alpha $ git merge FETCH_HEAD
          Updating d14c7d2..94cd04d
          Fast-forward

用户合并 FETCH_HEADFETCH_HEAD 只是另一个 ref (引用)。 它解析为 12 提交,即给予者。 HEAD 指向 11 提交,即接收者。 Git 进行快速合并,并将master 指向 12 提交。

`alpha` after `FETCH_HEAD` merged

合并 FETCH_HEAD 之后的 alpha

从远程拉取一个分支

~/alpha $ git pull bravo master
          Already up-to-date.

用户将 masterbravo 拉入 alpha。 Pull是「获取并合并 FETCH_HEAD」的简写。 Git 执行这两个命令并报告 master Already up-to-date (已经是最新的了)。

Clone 版本库

~/alpha $ cd ..
      ~ $ git clone alpha charlie
          Cloning into 'charlie'

用户移到上面的目录。 他们将 alpha 克隆到 charlie。 克隆到 charlie 的结果与用户生成 bravo 仓库的 cp 相似。 Git 创建一个名为charlie 的新目录。 它以 Git 版本库的形式创建charlie,添加名为 origin 的远程对象的 alpha,获取 origin 并且合并 FETCH_HEAD

将分支推送到远程上的已签出分支

      ~ $ cd alpha
~/alpha $ printf '13' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '13'
          [master 3238468] 13

用户回到 alpha 版本库。 他们将 data/number.txt 的内容设置为 13,并将更改提交给 alpha 上的 master

~/alpha $ git remote add charlie ../charlie

他们将 charlie 设置为 alpha 上的远程版本库。

~/alpha $ git push charlie master
          Writing objects: 100%
          remote error: refusing to update checked out
          branch: refs/heads/master because it will make
          the index and work tree inconsistent

推送 mastercharlie.

提交 13 所需的所有对象都被复制到 charlie 中。

此时,推送过程停止。 Git 一如既往地告诉用户出了什么问题。 它拒绝推送到在远程上签出的分支。 这是有道理的。 推送将更新远程索引和 HEAD。 如果有人在 remote(远程)上编辑工作副本,这将引起混乱。

此时,用户可以创建一个新分支,将 13 提交合并到其中,然后将该分支推送到 charlie。 但是,实际上,他们想要一个可以随时随地推送的存储库。 他们想要一个中央存储库,可以将其推送或拉出,但是没有人直接提交。 他们想要像 GitHub 远程之类的东西。 他们想要一个裸仓库。

Clone 一个裸版本库

~/alpha $ cd ..
      ~ $ git clone alpha delta --bare
          Cloning into bare repository 'delta'

用户移到上面的目录。 他们克隆了 delta 作为裸版本库。 这是一个普通的克隆,有两个不同点。 config 文件表明版本库是裸露的。 通常存储在 .git 目录中的文件存储在版本库的根目录中:

delta
├── HEAD
├── config
├── objects
└── refs

`alpha` and `delta` graphs after `alpha` cloned to `delta`

_alpha 克隆 delta 后的两者的树形图

推送一个分支到一个裸仓库

      ~ $ cd alpha
~/alpha $ git remote add delta ../delta

用户回到 alpha 仓库。 他们将 delta 设置为 alpha 上的远程仓库。

~/alpha $ printf '14' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '14'
          [master cb51da8] 14

data/number.txt 的内容设置为 14,并将更改提交给 alpha 上的 master

`14` commit on `alpha`

alpha上的 14 commit

~/alpha $ git push delta master
          Writing objects: 100%
          To ../delta
            3238468..cb51da8 master -> master

master 推送到 delta。 推送有三个步骤。

首先,将 master 分支上提交 14 所需的所有对象从 alpha/.git/objects/ 复制到 delta/objects/

其次,delta/refs/heads/master 被更新为指向 14 提交。

第三,将 alpha/.git/refs/remotes/delta/master 设置为指向 14 提交。 alpha 具有 delta 状态的最新记录。

`14` commit pushed from `alpha` to `delta`

_从 alpha 推送到 delta14 commit

总结

Git 建立在图形上。几乎每个 Git 命令都会操作该图。要深入了解 Git,请专注于此图的属性,而不是工作流或命令。

要了解有关 Git 的更多信息,请查看 .git 目录。这并不可怕。看看里面。更改文件的内容,然后看看会发生什么。手动创建一个提交。尝试看看您能一个 repo 搞到多么糟糕的程度。然后修复它。

  1. 在这种情况下,哈希值比原始内容长。但是,所有比散列中的字符数更长的内容将比原始内容表达得更简洁。

  2. 有机会将两个不同的内容散列为相同的值。但是这个机会很小

  3. git prune 删除从引用无法到达的所有对象。如果用户运行此命令,他们可能会丢失内容。

  4. git stash 将工作副本和HEAD提交之间的所有差异存储在安全的地方。然后可以恢复它们。

  5. rebase 命令可用于添加,编辑和删除历史记录中的提交。

`a1` 提交对象指向其树形图
a1 提交对象指向其树形图

git
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://codewords.recurse.com/issues/two...

译文地址:https://learnku.com/devtools/t/44847

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!