Git工作原理

MarRVP 8年前
   <p>本文重点介绍了支持Git的图形结构,以及该图形的属性指示Git行为的方式。从基础开始,同时有实例讲解,根据实例建立一个更真实的模型,让你更好地理解 git 做了什么。</p>    <h3>创建项目</h3>    <pre>  <code class="language-groovy">~ $ mkdir alpha  ~ $ cd alpha  </code></pre>    <p>项目目录是 alpha</p>    <pre>  <code class="language-groovy">~/alpha $ mkdir data  ~/alpha $ printf 'a' > data/letter.txt  </code></pre>    <p>到目录 alpha 下创建了一个名为 data 的目录,在里面创建了一个名为 letter.txt 的文件,其中的内容是一个字符 a , alpha 目录结构如下:</p>    <pre>  <code class="language-groovy">alpha  └── data   └── letter.txt  </code></pre>    <h3>初始化仓库</h3>    <pre>  <code class="language-groovy">~/alpha $ git init   Initialized empty Git repository  </code></pre>    <p>git init 使当前目录变成了 Git 仓库,为此,它创建了一个 .git 目录并向其中写入了一些文件。这些文件定义了关于Git配置和项目历史的一切,它们只是普通文件。 用户可以使用文本编辑器或shell来读取和编辑它们。 这就是说,用户可以像他们的项目文件一样轻松地阅读和编辑他们项目的历史。</p>    <p>现在 alpha 目录的结构就像下面这样</p>    <pre>  <code class="language-groovy">alpha  ├── data  | └── letter.txt  └── .git   ├── objects   etc...  </code></pre>    <p>.git 目录及其内容归 Git 系统所有,所有其他的文件统称为工作副本,归用户所有。</p>    <h3>添加文件</h3>    <pre>  <code class="language-groovy">~/alpha $ git add data/letter.txt  </code></pre>    <p>运行上面的命令,有两个效果。</p>    <p>首先,它在 .git/objects/ 目录中创建了一个新的 blob 文件。</p>    <p>这个 blob 文件包含 data/letter.txt 的压缩内容。 它的名称通过文件内容的 Hash (应该是用的 sha1 )得到。取一段文本的 Hash 值意味着运行一个程序,将其内容变成一块较小的文本,这块文本是原始内容的唯一标识。例如, Git 将 aes 转换为 2e65efe2a145dda7ee51d1741299f848e5bf752e ,前两个字符用作对象数据库中的目录的名称: .git/objects/2e/ 。 散列的其余部分用作保存所添加文件的内容的 blob 文件的名称:</p>    <p>.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e 。</p>    <p>git add 将文件添加到 Git 并将其内容保存到 objects 目录中。 如果用户从工作副本中删除 data/letter.text ,它的内容在 Git 中仍然是安全的。</p>    <p>其次, git add 将文件添加到 索引 。 索引 是一个列表,其中包含 Git 已被告知要跟踪的每个文件。 它作为一个文件存储在 .git/index 。 文件的每一行将被跟踪的文件映射到其内容的 哈希 。 这是运行 git add 命令后的索引:</p>    <p>data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e</p>    <p>创建一个包含内容 1234 的文件 data/number.txt</p>    <pre>  <code class="language-groovy">~/alpha $ printf '1234' > data/number.txt  </code></pre>    <p>目录结构变成了下面这样:</p>    <pre>  <code class="language-groovy">alpha  └── data   └── letter.txt   └── number.txt  </code></pre>    <p>添加文件到 Git</p>    <pre>  <code class="language-groovy">~/alpha $ git add data  </code></pre>    <p>git add 命令创建一个包含 data/number.txt 内容的 blob 对象。 它为指向 blob 的 data/number.txt 添加一个索引条目。 这是 git add 命令第二次运行后的索引:</p>    <pre>  <code class="language-groovy">data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e  data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3  </code></pre>    <p>只有数据目录中的文件被列在索引中,虽然用户运行了 git add data 。 数据目录 data 不单独列出。</p>    <pre>  <code class="language-groovy">~/alpha $ printf '1' > data/number.txt  ~/alpha $ git add data  </code></pre>    <p>当最初创建 data/number.txt 时,想要输入内容 1 ,而不是 1234 .他们进行更正并将文件再次添加到索引。 此命令将使用新内容创建一个新的 blob 。 并且它更新 data/number.txt 的索引条目以指向新的 blob 。</p>    <h3>git commit</h3>    <pre>  <code class="language-groovy">~/alpha $ git commit -m 'a1'   [master (root-commit) 774b54a] a  </code></pre>    <p>进行 a1 提交, Git 打印了这次提交的相关信息。</p>    <p>commit 命令有三个步骤。 创建一个树形图来表示正在提交的项目版本的内容。 创建一个提交对象。 将当前分支指向新的提交对象。</p>    <h3>创建树形图</h3>    <p>Git 通过从索引创建树图来记录项目的当前状态。 此树图记录项目中每个文件的位置和内容。</p>    <p>该图由两种类型的对象组成: blob 和 树 。</p>    <p>Blob 是通过 git add 存储的。 它们表示文件的内容。</p>    <p>在 commit 时存储树。 树表示工作副本中的目录。</p>    <p>下面是记录新提交的 data 目录的内容的树对象:</p>    <pre>  <code class="language-groovy">100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt  100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt  </code></pre>    <p>第一行记录展示了 data/letter.txt 文件的信息。 第一部分是文件的权限。 第二部表示此条目的内容由 blob 而不是 树 表示。 第三部分描述了 blob 的 Hash 。 第四部分描述文件的名称。第二行当然就是文件 data/number.txt 文件的信息。</p>    <p>下面是 alpha 的树对象:</p>    <pre>  <code class="language-groovy">040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data  </code></pre>    <p>alpha 树对象只包含了一个指向 data 树指针。(译著:如果 alpha 目录下还有一个文件, alpha 树对象就还会多一行,就是指向多出文件的 blob 对象)</p>    <p><img src="https://simg.open-open.com/show/29f494582ca47462202a74abbf52edfb.png"></p>    <p>在上面的图中, root 树指向 data 树。 data 树指向 data/letter.txt 和 data/number.txt 的 blob 。</p>    <h3>创建一个提交对象</h3>    <p>git commit 在创建树图后创建一个 提交对象 。 提交对象 只是 .git/objects/ 中的另一个文本文件:</p>    <pre>  <code class="language-groovy">tree ffe298c3ce8bb07326f888907996eaa48d266db4  author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500  committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500    a1  </code></pre>    <p>第一行指向树图。 Hash 是表示工作副本的根的树对象。 也就是 alpha 目录。 最后一行是提交消息。</p>    <p><img src="https://simg.open-open.com/show/9c70f2adba179d62b45c8733607c2d47.png"></p>    <h3>将当前分支指向新的提交</h3>    <p>最后,commit命令将当前分支指向新的提交对象。哪个是当前分支? .git/HEAD 文件记录了当前分支:</p>    <pre>  <code class="language-groovy">ref: refs/heads/master  </code></pre>    <p>这说明 HEAD 指向 master , master 是主分支。</p>    <p>HEAD 和 master 都是 refs 。 ref 是 Git 用来标识特定提交的标签。</p>    <p>表示 master 引用的文件不存在,因为这是对仓库的第一次提交。 Git 在 .git/refs/heads/master 下创建文件,并将其内容设置为提交对象的哈希值:</p>    <pre>  <code class="language-groovy">74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd  </code></pre>    <p>(如果你在阅读时输入这些 Git 命令,你的 a1 提交的 哈希值 将不同于我的 哈希值 。 内容对象(如blob和树)总是散列为相同的值。 提交不会,因为它们包括创建者的日期和名称。)</p>    <p>添加 HEAD 和 master 到树图:</p>    <p><img src="https://simg.open-open.com/show/d90c0bc05f2b88de903193ef15bcd906.png"></p>    <p>HEAD 指向 master ,就像提交之前一样。 但 maste r现在存在并指向新的提交对象 a1 。</p>    <h3>再一次commit</h3>    <p>下面是 a1 提交后的 Git 结构图。 包含工作副本和索引。</p>    <p><img src="https://simg.open-open.com/show/9d8f8f2a228386539d54a14a8c861232.png"></p>    <p>工作副本,索引和 a1 提交都具有与 data/letter.txt 和 data/number.txt 相同的内容。 索引和 HEAD 提交都使用 Hash 来引用 blob 对象,但是工作副本内容作为文本存储在不同的地方。</p>    <pre>  <code class="language-groovy">~/alpha $ printf '2' > data/number.txt  </code></pre>    <p>将 data/number.txt 的内容设置为 2 .这会更新工作副本,但 索引 和 HEAD 不变。</p>    <p><img src="https://simg.open-open.com/show/189b1c3e3fb2a77bea20cdc4a6341341.png"></p>    <pre>  <code class="language-groovy">~/alpha $ git add data/number.txt  </code></pre>    <p>将文件添加到 Git 。 这会向 objects 目录添加一个包含 2 的 blob 。 它指向新 blob 的 data/number.txt 的索引条目。</p>    <p><img src="https://simg.open-open.com/show/9caab6d5fccb1639cac9440d34c85c31.png"></p>    <pre>  <code class="language-groovy">~/alpha $ git commit -m 'a2'   [master f0af7e6] a2  </code></pre>    <p>提交的步骤与之前相同。</p>    <p>首先,创建一个新的树形图来表示索引的内容。</p>    <p>data/number.txt 的索引条目已更改。 旧的数据树不再反映 data 目录的索引状态。 必须创建一个新的 data 树对象:</p>    <pre>  <code class="language-groovy">100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt  100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt  </code></pre>    <p>新数据树与旧数据树的哈希值不同。 必须创建一个新的根树以记录此 Hash 值:</p>    <pre>  <code class="language-groovy">040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data  </code></pre>    <p>其次,创建一个新的提交对象。</p>    <pre>  <code class="language-groovy">tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556  parent 774b54a193d6cfdd081e581a007d2e11f784b9fe  author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500  committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500    a2  </code></pre>    <p>提交对象的第一行指向新的根树对象。 第二行指向 a1 :新提交的父级。要找到父提交,要跟着 HEAD 和 master 来掌握并发现 a1 的提交哈希。</p>    <p>最后,master分支文件的内容被设置为新提交的 hash 值。</p>    <p><img src="https://simg.open-open.com/show/c316c3723933231d7f932ab90062b5df.png"></p>    <p><img src="https://simg.open-open.com/show/ff789f3e7db4a9b02ca380de83ea67d0.png"></p>    <p>内容存储为对象树。 这意味着只有diffs存储在对象数据库中。 看看上面的图表。 a2 commit 重用了在 a1 提交之前创建的 blob 。 类似地,如果提交中整个没有变,则其树以及其下的所有 blob 和树可以被重用。 一般来说,提交的内容更改很少。 这意味着 Git 可以在小的空间中存储大的提交历史。</p>    <p>每个提交都有一个父级。 这意味着存储库可以存储项目的历史记录。</p>    <p>refs 是提交历史的一部分或另一部分的入口点。 这意味着提交可以被赋予有意义的名称。 用户将他们的工作组织到对他们的项目有意义的谱系中,具体的参考如 fix-for-bug-376 。 Git 使用符号引用,如 HEAD , MERGE_HEAD 和 FETCH_HEAD 来支持操作提交历史记录的命令。</p>    <p>objects 目录中的节点是不可变的。 这意味着内容被编辑,而不是被删除。 每一次添加的内容和每次提交的对象都是在目录中.</p>    <p>refs 是可变的。 因此, ref 的含义可以改变。 master 指向的提交可能是当前项目的最佳版本,但是,很快,它将被更新的更好的提交取代。</p>    <h3>Check out a commit</h3>    <pre>  <code class="language-groovy">~/alpha $ git checkout 37888c2   You are in 'detached HEAD' state...  </code></pre>    <p>使用 Hash 值 checkout``a2 的提交(如果你在运行这些git命令,这里的 hash 值要换成你自己的,使用 git log 查看)</p>    <p>checkout 有四个步骤:</p>    <ul>     <li> <p>获取 a2 提交,并获取指向它的树图</p> </li>     <li> <p>它将树形图中的文件条目写入工作副本。 这将导致没有更改。 工作副本已经具有被写入其中的树图的内容,因为 HEAD 已经通过 master 指向 a2 提交。</p> </li>     <li> <p>将树图中的文件条目写入索引。 这也导致没有变化。 索引已经具有 a2 提交的内容。</p> </li>     <li> <p>HEAD 的内容设置为 a2 提交的哈希:</p> </li>    </ul>    <pre>  <code class="language-groovy">f0af7e62679e144bb28c627ee3e8f7bdb235eee9  </code></pre>    <p>将 HEAD 的内容设置为 Hash 值会使存储库处于分离的 HEAD 状态。 注意在下面的图表中, HEAD 直接指向 a2 提交,而不是指向 master 。</p>    <p><img src="https://simg.open-open.com/show/f0ad164f90d8800f02d08f45f4aecd60.png"></p>    <pre>  <code class="language-groovy">~/alpha $ printf '3' > data/number.txt  ~/alpha $ git add data/number.txt  ~/alpha $ git commit -m 'a3'   [detached HEAD 3645a0e] a3  </code></pre>    <p>将 data/number.txt 的内容设置为 3 ,并提交更改。 Git 去 HEAD 得到 a3 提交的父级。 而不是找到一个分支ref,它找到并返回 a2 提交的哈希。</p>    <p>Git 更新 HEAD 直接指向新的 a3 提交的哈希。 存储库仍处于分离的 HEAD 状态。 它不在一个分支上,因为没有提交指向 a3 或其一个后代。 这意味着它很容易丢失。</p>    <p><img src="https://simg.open-open.com/show/e50c8a620f7c415be44042af51c27cd3.png"></p>    <h3>创建分支</h3>    <pre>  <code class="language-groovy">~/alpha $ git branch deputy  </code></pre>    <p>创建一个新分支 deputy 。 这只是在 .git/refs/heads/deputy 创建一个新文件,其中包含 HEAD 指向的哈希, 也就是 a3 提交的哈希。</p>    <p>分支只是refs, refs只是文件。 这意味着Git分支是轻量级的。</p>    <p>deputy 分支的创建将新的 a3 提交安全地放置在分支上。 HEAD 仍然分离,因为它仍然直接指向一个提交。</p>    <p><img src="https://simg.open-open.com/show/9bd8e8483604655aef9fcb13bf9d3e4f.png"></p>    <h3>切换分支</h3>    <pre>  <code class="language-groovy">~/alpha $ git checkout master   Switched to branch 'master'  </code></pre>    <p>切换到了 master 分支</p>    <ul>     <li> <p>获取 a2 提交,并将 master 指向获取提交点的树图。</p> </li>     <li> <p>树形图中的文件条目替换工作副本的文件。 这将使 data/number.txt 的内容设置为2。</p> </li>     <li> <p>将树图中的文件条目写入索引。 这会将 data/number.txt 的条目更新为2个 blob 的散列。</p> </li>     <li> <p>改变 HEAD 的值</p> </li>    </ul>    <pre>  <code class="language-groovy">ref: refs/heads/master  </code></pre>    <p><img src="https://simg.open-open.com/show/d480f1b12e1b022e8beee86f001e4be8.png"></p>    <h3>切换到与工作副本不兼容(有改变)的分支</h3>    <pre>  <code class="language-groovy">~/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.  </code></pre>    <p>将 data/number.txt 的内容设置为 789 , 当 checkout 到 deputy 时, Git 报了一个错误。</p>    <p>HEAD 指向 master , master 指向 a2 ,其中 data/number.txt 的内容是 2 。 deputy 指向 a3 ,其中 data/number.txt 的内容是 3 。 data/number.txt 在工作副本的内容为 789 ,所有这些版本都不同,差异必须解决。</p>    <p>Git 可以使用要切换分支中提交的版本替换掉工作副本中的版本,这样可以避免数据丢失。</p>    <p>Git 可以合并工作副本的版本和要切换分支中的版本,但这很复杂。</p>    <p>所以 Git 报了一个错误,不能切换分支。</p>    <pre>  <code class="language-groovy">~/alpha $ printf '2' > data/number.txt  ~/alpha $ git checkout deputy   Switched to branch 'deputy'  </code></pre>    <p>把 data/number.txt 的内容变回 2 时,便切换成功了。</p>    <p><img src="https://simg.open-open.com/show/067dd095500a0297a37381911df03110.png"></p>    <h3>合并祖先</h3>    <pre>  <code class="language-groovy">~/alpha $ git merge master   Already up-to-date.  </code></pre>    <p>将主分支 master 和并到 deputy 分支。和并两个分支实际上是合并两个提交。第一个提交指向 deputy ,它是接收者。第二个提交指向 master ,它是提交者。可以理解为把 master 提交到 deputy 。对于这个合并, git 什么也没有做,因为两个分支的内容是一样的。</p>    <p>图中的一系列的提交可以看成是对存储库的一系列更改。这也就意味着,在合并中,如果提交者( master )是接收者( deputy )的祖先, git 将什么也不做,因为这些变化已经存在。</p>    <h3>合并后代</h3>    <pre>  <code class="language-groovy">~/alpha $ git checkout master   Switched to branch 'master'  </code></pre>    <p>切换到分支 master</p>    <p><img src="https://simg.open-open.com/show/d480f1b12e1b022e8beee86f001e4be8.png"></p>    <pre>  <code class="language-groovy">~/alpha $ git merge deputy   Fast-forward  </code></pre>    <p>合并 deputy 到 master 。 Git 发现接受者的 a2 提是提交者 a3 的祖先,它可以做快进合并。</p>    <p>获得提交者的提交 a3 并提供指向它的树图,将树图中的文件条目写入工作副本和索引。 快进 是指 master 指向 a3 。</p>    <p><img src="https://simg.open-open.com/show/492a33164dd143eb3acadd0750be650c.png"></p>    <p>在合并中,如果提交者( deputy 分支上的 a3 提价)是接收者( master 上的 a2 提价)的后代,则历史记录不改变。 已经有一系列提交描述了要做出的改变( 接收者和提交者之间的提交序列 )。 虽然Git历史没有改变,Git图确实改变。 HEAD指向的具体引用被更新为指向提交者( master 指向 a3 )。</p>    <h3>合并来自两个不同谱系的分支</h3>    <pre>  <code class="language-groovy">~/alpha $ printf '4' > data/number.txt  ~/alpha $ git add data/number.txt  ~/alpha $ git commit -m 'a4'   [master 7b7bd9a] a4  </code></pre>    <p>把 number.txt 的内容设置为 4 ,并提交到 master</p>    <pre>  <code class="language-groovy">~/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  </code></pre>    <p>切换到 deputy 分支,把 data/letter.txt 的内容设置为 b ,并提交到 deputy 。</p>    <p><img src="https://simg.open-open.com/show/d56f416b74403fd244cfa581884073ac.png"></p>    <p>提交可以共享父级,这意味着可以在提价的历史中创建新的谱系。</p>    <p>提交可以有多个父级。 这意味着单独的谱系可以通过具有两个父的提交来合并:合并提交。</p>    <pre>  <code class="language-groovy">~/alpha $ git merge master -m 'b4'   Merge made by the 'recursive' strategy.  </code></pre>    <p>合并 master 到 deputy</p>    <p>Git 发现接收者 b3 和提供者 a4 在不同的谱系中。 它做一个合并提交。 这个过程有八个步骤。</p>    <ol>     <li> <p>Git 将提交者的提交的哈希写入到 alpha/.git/MERGE_HEAD 文件。 这个文件的存在告诉 Git 在合并中。</p> </li>     <li> <p>Git 查找基本提交:接收者和提交者提交的最近的祖先的共同点。</p> </li>    </ol>    <p><img src="https://simg.open-open.com/show/d56f416b74403fd244cfa581884073ac.png"></p>    <p>提交有父级别。 这意味着可以找到两个谱系起始点。 Git 从 b3 向后跟踪,找到所有的祖先,从 a4 向后寻找所有的祖先。 它找到两个谱系共享的最近的祖先 a3 。 这是基本提交。</p>    <ol>     <li> <p>Git 从接收者和提交者提交的树图生成基本的索引。</p> </li>     <li> <p>Git 生成一个 diff ,它将接收者提交和提交者提交对基础提交所做的更改合并。 此 diff 是指向更改的文件路径列表:添加,删除,修改或冲突。</p> </li>    </ol>    <p>Git 获取出现在 base , receiver 或 giver 索引中的所有文件的列表。 比较较索引条目以决定对文件做出的更改。 它将一个相应的条目写入 diff 。 在这种情况下, diff 有两个条目。</p>    <p>第一个条目是是 data/letter.txt 。 文件内容在 base 和 receiver 中不同。 但是在 base 和 giver 中是一样的。 Git 看到内容被 reviceer 修改,但是没有被 giver 修改。 data/letter.txt 的 diff 条目是一个修改,而不是冲突。</p>    <p>diff 中的第二个条目是 data/number.txt 。 在这种情况下,文件内容在 base 和 receiver 中是相同的,并且在 giver 中是不同的。 data/letter.txt 的 diff 条目也是一个修改。</p>    <p>可以找到合并的 base 提交。 这意味着,如果一个文件只是从 receiver 或提 giver 的 base 改变, Git 可以自动解决该文件的合并。 这减少了用户必须做的工作。</p>    <ol>     <li> <p>由 diff 中的条目指示的更改将应用于工作副本。 data/letter.txt 的内容设置为 b , data/number.txt 的内容设置为 4 。</p> </li>     <li> <p>由 diff 中的条目指示的更改将应用于索引。 data/letter.txt 的条目指向 b blob , data/number.txt 的条目指向 4 blob 。</p> </li>     <li> <p>更新索引:</p> </li>    </ol>    <pre>  <code class="language-groovy">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  </code></pre>    <p>注意:这次提交有两个父级</p>    <ol>     <li>将当前分支 deputy 分支指向新的提交。</li>    </ol>    <p><img src="https://simg.open-open.com/show/e2a98cb7d7eaf46b8c5fc609c7a07b55.png"></p>    <h3>合并来自不同谱系的两个提交,这两个提交都修改同一个文件</h3>    <p>切换到 master 分支,并把 deputy 合并到 master , 快进 到 b4 ,现在 master 和 deputy 都指向同一个提交</p>    <p><img src="https://simg.open-open.com/show/9ea749dbdb555fbf514ec4d4e4a19d3a.png"></p>    <pre>  <code class="language-groovy">~/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  </code></pre>    <p>切换到 deputy 分支,把 data/number.txt 的内容设置为 5 ,并提交。</p>    <pre>  <code class="language-groovy">~/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  </code></pre>    <p>切换到 master 分支,把 data/number.txt 的内容设置为 6 ,并提交。</p>    <p><img src="https://simg.open-open.com/show/36525ae4085080c19f488fe856b3e07c.png"></p>    <pre>  <code class="language-groovy">~/alpha $ git merge deputy   CONFLICT in data/number.txt   Automatic merge failed; fix conflicts and   commit the result.  </code></pre>    <p>将 deputy 合并到 master 。存在冲突,并且合并已暂停。冲突合并的过程遵循与未冲突合并的过程相同的前六个步骤:设置 .git/MERGE_HEAD ,查找 base ,生成 base , receiver , giver 的索引,创建 diff ,更新工作副本和更新索引。由于冲突,不采取第七提交步骤和第八更新 ref 步骤。让我们再次看看这些步骤,发生了什么。</p>    <ol>     <li>Git 将 giver 提交的哈希写入 .git/MERGE_HEAD 文件。</li>    </ol>    <p><img src="https://simg.open-open.com/show/b87eb4ade42a7ccc2499a315b46b4f2c.png"></p>    <ol>     <li> <p>Git 找到 base 提交 b4</p> </li>     <li> <p>Git 从接收者和提交者提交的树图生成基本的索引。</p> </li>     <li> <p>Git 生成一个 diff ,它将接收者提交和提交者提交对基础提交所做的更改合并。 此 diff 是指向更改的文件路径列表:添加,删除,修改或冲突。</p> </li>    </ol>    <p>在这种情况下, diff 只包含一个条目: data/number.txt 。 该条目被标记为冲突,因为 data/number.txt 的内容在接收者,提供者和 base 中是不同的。</p>    <ol>     <li>由 diff 中的条目指示的更改将应用于工作副本。 对于冲突区域, Git 将两个版本写入工作副本中的文件。 data/number.txt 的内容设置为:</li>    </ol>    <pre>  <code class="language-groovy"><<<<<<< HEAD  6  =======  5  >>>>>>> deputy  </code></pre>    <ol>     <li>由 diff 中的条目指示的更改应用于索引。 索引中的条目通过其文件路径和阶段的组合成唯一标识。 未冲突文件的条目具有阶段 0 .在此合并之前,索引如下所示,其中 0 是阶段值:</li>    </ol>    <pre>  <code class="language-groovy">0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748  0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb  </code></pre>    <p>在合并 diff 被写入索引之后,索引如下所示:</p>    <pre>  <code class="language-groovy">0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748  1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767  2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb  3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61  </code></pre>    <p>在阶段 0 的 data/letter.txt 的条目与在合并之前相同。 在阶段 0 的 data/number.txt 的条目被去掉了。 它有三个新的条目。 阶段 1 的条目具有 base data/number.txt 内容的散列。 阶段 2 的条目具有 receiver data/number.txt 内容的散列。 阶段 3 的条目具有 giver data/number.txt 内容的散列。 这三个条目的存在告诉 Git data/number.txt 是冲突的。合并就暂停了。</p>    <pre>  <code class="language-groovy">~/alpha $ printf '11' > data/number.txt  ~/alpha $ git add data/number.txt  </code></pre>    <p>通过将 data/number.txt 的内容设置为 11 来合成两个冲突版本的内容。他们将文件添加到索引。 Git 添加一个包含 11 的 Blob 。添加一个冲突的文件告诉 Git 冲突已解决。 Git 从索引中删除阶段 1 , 2 和 3 的 data/number.txt 条目。 在阶段 0 的 data/number.txt 的条目中添加新 blob 的散列。 该索引现在为:</p>    <pre>  <code class="language-groovy">0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748  0 data/number.txt 9d607966b721abde8931ddd052181fae905db503  </code></pre>    <pre>  <code class="language-groovy">~/alpha $ git commit -m 'b11'   [master 251a513] b11  </code></pre>    <ol>     <li> <p>提交。 Git 在存储库中看到 .git/MERGE_HEAD ,告诉它合并正在进行。 然后检查索引并发现没有冲突。 就创建一个新的提交 b11 ,以记录解析的合并的内容。 z最后会删除 .git/MERGE_HEAD 文件。 这将完成合并。</p> </li>     <li> <p>将当前分支 master 指向新的提交。</p> </li>    </ol>    <p><img src="https://simg.open-open.com/show/ec33a688d96fd85cc6f6a1e8392b8019.png"></p>    <h3>移除一个文件</h3>    <p>下面的图包括提交历史、最近提交的树和 blob 以及工作副本和索引:</p>    <p><img src="https://simg.open-open.com/show/f824230cca044aafc1e26bfc9f834cad.png"></p>    <pre>  <code class="language-groovy">~/alpha $ git rm data/letter.txt   rm 'data/letter.txt'  </code></pre>    <p>告诉 Git 删除 data/letter.txt 。 该文件从工作副本中删除。 该条目从索引中删除。</p>    <p><img src="https://simg.open-open.com/show/a7668ec36f4f76e39d61a64379f72b7d.png"></p>    <pre>  <code class="language-groovy">~/alpha $ git commit -m '11'   [master d14c7d2] 11  </code></pre>    <p>提交。 作为提交的一部分,一如既往,Git构建一个表示索引内容的树形图。 data/letter.txt 不包括在树图中,因为它不在索引中。</p>    <p><img src="https://simg.open-open.com/show/a2dfb85ccb9d96c7b01abf607c4b663a.png"></p>    <h3>复制存储库</h3>    <pre>  <code class="language-groovy">~/alpha $ cd ..   ~ $ cp -R alpha bravo  </code></pre>    <p>将 alpha/ 存储库的内容复制到 bravo/ 目录。 这将产生以下目录结构:</p>    <pre>  <code class="language-groovy">~  ├── alpha  | └── data  | └── number.txt  └── bravo   └── data   └── number.txt  </code></pre>    <p>现在 bravo 目录中有另一个 Git 图:</p>    <p><img src="https://simg.open-open.com/show/bc33291d107035897e354b5cf5e35dd2.png"></p>    <h3>将存储库链接到另一个存储库</h3>    <pre>  <code class="language-groovy"> ~ $ cd alpha  ~/alpha $ git remote add bravo ../bravo  </code></pre>    <p>移回到 alpha 存储库。 他们将 bravo 设置为 alpha 上的远程存储库。 这会在 alpha/.git/config 文件中添加:</p>    <pre>  <code class="language-groovy">[remote "bravo"]   url = ../bravo/  </code></pre>    <h3>从远程获取分支</h3>    <pre>  <code class="language-groovy">~/alpha $ cd ../bravo  ~/bravo $ printf '12' > data/number.txt  ~/bravo $ git add data/number.txt  ~/bravo $ git commit -m '12'   [master 94cd04d] 12  </code></pre>    <p>进入 bravo 存储库。 将 data/number.txt 的内容设置为 12 ,并将更改提交到 bravo 上的 master 。</p>    <p><img src="https://simg.open-open.com/show/154134f86f62365b8d43d9385db2e85d.png"></p>    <pre>  <code class="language-groovy">~/bravo $ cd ../alpha  ~/alpha $ git fetch bravo master   Unpacking objects: 100%   From ../bravo   * branch master -> FETCH_HEAD  </code></pre>    <p>进入 alpha 存储库。 从 bravo 获取 master 到 alpha 。 这个过程有四个步骤。</p>    <ol>     <li> <p>Git 获取 master 在 bravo 上指向的提交的哈希。 这是 12 提交的哈希。</p> </li>     <li> <p>Git 提供了 12 提交所依赖的所有对象的列表:提交对象本身,其树图中的对象, 12 提交的祖先提交和它们的树图中的对象。 它从此列表中删除 alpha 对象数据库已有的对象。 它将其余部分复制到 alpha/.git/objects/ 。</p> </li>     <li> <p>将 alpha/.git/refs/remotes/bravo/master 下的具体 ref 文件的内容设置为12提交的哈希值。</p> </li>     <li> <p>将 alpha/.git/FETCH_HEAD 的内容设置为:</p> </li>    </ol>    <pre>  <code class="language-groovy">94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo  </code></pre>    <p>下图表示了 fetch 命令从 bravo 获取了 master 的 12 提交</p>    <p><img src="https://simg.open-open.com/show/80ba3da28a984efe4d3ce2ea1558656d.png"></p>    <p>对象是可以复制的,这意味着可以在存储库之间共享历史记录。</p>    <p>存储库可以存储远程分支引用,如 alpha/.git/refs/remotes/bravo/master , 这意味着存储库可以在本地记录在远程存储库上分支的状态。 在获取时是正确的,但如果远程分支改变,它将过期。</p>    <h3>合并FETCH_HEAD</h3>    <pre>  <code class="language-groovy">~/alpha $ git merge FETCH_HEAD   Updating d14c7d2..94cd04d   Fast-forward  </code></pre>    <p>合并 FETCH_HEAD , FETCH_HEAD 只是另一个 ref 。 解析了 12 提交, giver 。 master 开始指向 11 提交。 Git 做一个快进合并,并将 master 指向在 12 提交。</p>    <p><img src="https://simg.open-open.com/show/cd6841a3a5cb14cf551f37ab08e17fd1.png"></p>    <h3>从远程分支Pull</h3>    <pre>  <code class="language-groovy">~/alpha $ git pull bravo master   Already up-to-date.  </code></pre>    <p>将 bravo 的 master 拉到 alpha 。 Pull 是 fetch and merge FETCH_HEAD 的缩写。</p>    <h3>Clone一个存储库</h3>    <pre>  <code class="language-groovy">~/alpha $ cd ..   ~ $ git clone alpha charlie   Cloning into 'charlie'  </code></pre>    <p>移动到上面的目录。 clone alpha 到 charlie 。 clone 到 charlie 具有与生成 bravo 存储库的 cp 类似的结果。 Git 创建一个名为 charlie 的新目录。 它将 charlie 作为一个Git仓库,将 alpha 添加为远程仓库被称为 origin ,获取源并合并 FETCH_HEAD 。</p>    <h3>Push分支到远程分支</h3>    <pre>  <code class="language-groovy"> ~ $ cd alpha  ~/alpha $ printf '13' > data/number.txt  ~/alpha $ git add data/number.txt  ~/alpha $ git commit -m '13'   [master 3238468] 13  </code></pre>    <p>返回到 alpha 仓库,把 data/number.txt 的内容设置为 13 ,并提交。</p>    <pre>  <code class="language-groovy">~/alpha $ git remote add charlie ../charlie  </code></pre>    <p>设置 alpha 的远程仓库为 charlie</p>    <pre>  <code class="language-groovy">~/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  </code></pre>    <p>push master 到 charlie .</p>    <p>13 提交所需的所有对象都复制到 charlie 。</p>    <p>此时,推送过程停止。 Git 告诉我们出了什么问题。 它拒绝推送到远程分支。 这是有道理的, 因为推送将更新远程索引和 HEAD 。 这将导致混乱,如果有人正在编辑远程的工作副本。(这也有其他的解决办法,可以google一下)</p>    <p>此时,可以创建一个新的分支,将 13 提交合并到其中,并将该分支推送到 charlie 。但是我们想要一个类似 GitHub 那样的中央仓库,无论什么时候都可以 push pull 。(中央仓库为什么可以?因为在初始化仓库的时候使用的是 git init --bare , 初始化成一个裸存储库,远程仓库应该都要这么初始化。)</p>    <h3>Clone 一个裸仓库</h3>    <pre>  <code class="language-groovy">~/alpha $ cd ..   ~ $ git clone alpha delta --bare   Cloning into bare repository 'delta'  </code></pre>    <p>移动到上面的目录。 将 delta clone 为裸存储库。 这是一个有两个区别的普通 clone 。 配置文件指示存储库是裸的。 通常存储在 .git 目录中的文件存储在存储库的根目录中如下:</p>    <pre>  <code class="language-groovy">delta  ├── HEAD  ├── config  ├── objects  └── refs  </code></pre>    <p><img src="https://simg.open-open.com/show/85c94812ddded695819d041ecff6823d.png"></p>    <h3>Push分支到裸存储库</h3>    <pre>  <code class="language-groovy"> ~ $ cd alpha  ~/alpha $ git remote add delta ../delta  </code></pre>    <p>返回到 alpha 存储库。 将 delta 设置为 alpha 上的远程存储库。</p>    <pre>  <code class="language-groovy">~/alpha $ printf '14' > data/number.txt  ~/alpha $ git add data/number.txt  ~/alpha $ git commit -m '14'   [master cb51da8] 14  </code></pre>    <p>将 data/number.txt 的内容设置为 14 ,并将更改提交到 alpha 上的 master</p>    <p><img src="https://simg.open-open.com/show/31245f796812605453635b95a2f021f9.png"></p>    <pre>  <code class="language-groovy">~/alpha $ git push delta master   Writing objects: 100%   To ../delta   3238468..cb51da8 master -> master  </code></pre>    <p>push master 到 delta ,有3个步骤</p>    <ol>     <li> <p>master 分支上的 14 提交所需的所有对象都从 alpha/.git/objects/ 复制到 delta/objects / 。</p> </li>     <li> <p>delta/refs/heads/master 被更新为指向 14 提交。</p> </li>     <li> <p>alpha/.git/refs/remotes/delta/master 设置为指向 14 提交。 alpha 具有 delta 的状态的最新记录.</p> </li>    </ol>    <p><img src="https://simg.open-open.com/show/83c17b855b0007d7a65629f8dd1b421f.png"></p>    <p> </p>    <p>来自:http://deweixu.me/2016/11/05/how-git-works/</p>    <p> </p>