提交是不差异的快照

2021-04-09 02:59:18

Git令人困惑的声誉。用户绊倒术语和措辞误导他们的期望。这在命令中最明显的是“重写历史记录”,例如Git Cherry-Pick或Git Rebase。在我的经验中,这种混乱的根本原因是对可以随身携带的罪行的解释。但是,提交是快照,而不是差异!

如果我们剥离窗帘并查看Git如何存储您的存储库数据,我认为Git会变得可以理解。在我们调查此模型后,我们将探讨新的视角如何帮助我们了解Git Cherry-Pick和Git Rebase等命令。

如果你想真的很深,你应该阅读Pro Git Book的Git Internals章节。

我将使用在v2.29.2签出的git / git存储库作为一个例子。跟随我的命令行示例以获取额外的练习。

要了解Git对象的最重要部分是GIT由其对象ID(oid for short)引用,为对象提供唯一名称。我们将使用Git Rev-parse< Ref>命令发现这些OID。每个对象都基本上是一个纯文本文件,我们可以使用git cat-file -p -p< oid&gt来检查其内容。命令。

您也可以用于看到作为较短的十六进制字符串给出的OID。此字符串作为足够长的东西给出了存储库中只有一个对象的OID匹配该缩写。如果我们使用太短的缩写oid请求对象的类型,那么我们将看到匹配的oid列表

$ git cat-file -t e0c03error:short sha1 e0c03是模棱两可的:候选人是:提示:e0c03f27484提交2016-10-26 - Contrib / Buildsystems:忽略生成器中的无关文件/提示:e0c03653e72 treehhint:e0c03c3eecc blobfatal:不是有效的对象名称e0c03.

这些类型是什么:Blob,树和提交?让我们从底部开始工作。

在对象模型的底部,BLOB包含文件内容。要在当前修订时发现文件的OID,请运行Git Rev-Parse头:< path&gt ;.然后,使用git cat-file -p< oid>找到它的内容。

$ Git Rev-Parse头:Readme.mdeb8115e6b04814f0c37146bbe3dbc35f3e8992e0 $ git cat-file -p eb8115e6b04814f0c37146bbe3dbc35f3e8992e0 | head-n 8 [![构建状态](https://github.com/git/git/workflows/ci/pr/badge.png)](https://github.com/git/git/actions?query =分支%3amaster +事件%3apush)Git - 快速,可扩展,分布式修订控制系统=============================== ========================== git是一个快速,可扩展,分布式的修订控制系统,具有anunusipy富有的命令集,提供了高级运算和满访问内部。

如果我在磁盘上编辑README.MD文件,则GIT状态通知该文件具有最近修改的时间和哈希内容。如果内容与Head的当前OID不匹配:Readme.md,则Git Status将文件报告为“修改在磁盘上”。通过这种方式,我们可以看到当前工作目录中的文件内容是否与头部的预期内容匹配。

请注意,Blobs包含文件内容,但不是文件名!名称来自Git的目录表示:树木。树是路径条目的有序列表,与对象类型,文件模式和该路径处对象的OID配对。子目录也代表为树,所以树木可以指向其他树木!

我们将使用图表来可视化这些对象的相关方式。我们使用盒子和树木的三角形。

$ git rev-parse头^ {tree} 75130889f941ceb57c6ceb95c6f28dfc83b609c $ git cat-fire-p_ 75130889f941eCeb57c6ceb95c6f28dfc83b609c |头-n 15100644 BLOB c2f5fe385af1bbc161f6c010bdcf0048ab6671ed .cirrus.yml100644 BLOB c592dda681fecfaa6bf64fb3f539eafaf4123ed8 .clang-format100644 BLOB f9d819623d832113014dd5d5366e8ee44ac9666a .editorconfig100644斑点b08a1416d86012134f823fe51443f498f4911909 .gitattributes040000树fbe854556a4ae3d5897e7b92a3eb8636bb08f031 .github100644 BLOB 6232d339247fae5fdaeffed77ae0bbe4176ab2de .gitignore100644斑点cbeebdab7a5e2c6afec338c3534930f569c90f63 .gitmodules100644斑点bde7aba756ea74c3af562874ab5c81a829e43c83 .mailmap100644斑点05f3e3f8d79117c1d32bf5e433d0fd49de93125c .travis.yml100644斑点5ba86d68459e61f87dae1332c7f2402860b4280c .tsan -suppressions100644 BLOB fc4645d5c08bd005238fc72cfa709495d8722e6a CODE_OF_CONDUCT.md100644斑点536e55524db72bd2acf175208aef4f3dfc148d42 COPYING040000树a58410edddbdd133cca6b3322bebe4fb37be93fa Documentation100755 BLOB ca6ccb49866c595c80718d167e40cfad1ee7f376 GIT-VERSION-GEN100644 BLOB 9ba33e6a141a3906eb707dd11d1af4b0f8191a55 INSTALL

树为每个子项提供名称。树还包括每个条目的UNIX文件权限,对象类型(BLOB或TREE)和OID等信息。我们将输出剪切到前15个条目,但我们可以使用Grep发现这棵树有一个readme.md条目,指向我们的早期Blob OID。

树木可以使用这些路径条目指向Blob和其他树木。请记住,这些关系与路径名配对,但我们并不总是在我们的图表中显示这些名称。

树本身不知道存储库中存在的位置,这是指向树的对象的角色。由< ref> ^ {树}引用的树是一个特殊的树:根树。此名称基于您提交的特殊链接。

提交是一个快照。每个提交都包含指向其根树的指针,表示当时的工作目录的状态。提交有与上一个快照对应的父级提交列表。没有父母的提交是一个root提交,并且具有多个父母的提交是合并提交。提交还包含描述快照(包括作者和提名(包括名称,电子邮件地址和日期)和提交消息的快照。提交消息是提交作者描述该提交关于父母的目的的机会。

例如,GIT存储库中的v2.29.2的提交描述了该发布,并由Git维护者撰写并提交。

$ GIT中REV-解析HEAD898f80736c75878acc02dc55672317fcc0e0a5a6 / C / _git / GIT中((v2.29.2))$ GIT中猫文件-p 898f80736c75878acc02dc55672317fcc0e0a5a6tree 75130889f941eceb57c6ceb95c6f28dfc83b609cparent a94bce62b99be35f2ee2b4c98f97c222e7dd9d82author JUNIOÇ滨野< [email protected]> 1604006649 -0700Committer Junio C Hamano< [email protected]& gt; 1604006649 -0700GIT 2.29.2SIGNID-off-BY:Junio C Hamano< [email protected]& gt;

在GIT日志中看起来有点远,我们可以看到一个更具描述性的提交消息,谈论该提交与其父级之间的变化。

$ git cat-file -p 16b0bb99a5dabdff2cfc390e9d92eftree d0e42501b1cf65395255caaa0286普通56706dba33f5d4457395c651cf1cd0333f5d4457395c651cf1cd033c6c03c7aaauthor jeff国王& gt; gt; gt; 1603436979 -0400Committer Junio C Hamano& lt; [email protected] & gt; 1603466719 -0700AM:修复破碎的电子邮件 - Committer-Date-Is-Author-DateCommit E8CBE2118A(AM:停止导出Git_Committer_date,2020-08-17)重写用于设置提交日期以使用FMT_IDE()而不是设置的代码环境变量并让Commit_tree()处理它。但它介绍了两个错误: - 我们使用作者电子邮件字符串而不是提交人电子邮件 - 在解析提交信件时,我们使用错误的变量来蜂化电子邮件的长度,从而始终是Azero-LengthThis提交修复,这导致我们对此选项的测试通过sitebase"适用"后端到现在成功。jeff-off-by:jeff king& lt; [email protected]& amp; gt;签名:Junio C hamano& lt; [email protected] & gt;

在我们的图表中,我们将使用圆圈表示提交。注意头韵?我们来复习:

在Git中,我们在历史悠久中移动,并在大部分时间提及OID的情况下进行更改。这是因为分支为我们关心的提交提供指针。具有名称主要的分支实际上是名为refs / heads / main的git的引用。这些文件实际上包含引用提交的OID的十六进制字符串。在您工作时,这些参考文献将其内容更改为指向其他提交。

这意味着分支与我们以前的Git对象有很大差异。提交,树和斑点是不可变的,这意味着你无法改变他们的内容。如果更改内容,则会获得不同的哈希,从而引用新对象的新oid!分支机构由用户命名,以提供含义,例如中继或我的专题项目。我们使用分支来跟踪和分享工作。

特殊的参考头指向当前分支。当我们添加提交到头时,它会自动更新该分支到新提交。

$ git switch -c my-branchswected to新的分支和#39;我的分支' $ cat .git / refs / head / my-branch1ec19b7757a1acb11332f06e8e812b505490afc6 $ cat .git / headref:refs / heads / my-brang

请注意创建My-Branch如何创建包含当前提交OID的文件(.git / refs / heads / my-branch)和.git / head文件已更新为此分支。现在,如果我们通过创建新的提交来更新头,分支我的分支将更新以指向该新提交!

让我们将所有这些新术语放入一个巨型图片中。分支指向提交,提交指向其他提交和他们的根树,树指向Blob和其他树木,而且Blob不指向任何东西。这是一个包含所有对象的图表一次:

在此图中,时间从左到右移动。提交和父母之间的箭头从右到左。每个提交都有一个根树。头部点到这里的主要分支机构,主要点到最近的提交。此提交的根树在下面完全扩展,而树木的其余部分具有指向这些对象的箭头。其原因是,来自多根根树的相同对象!由于这些树通过其OID(它们的内容)引用这些对象,因此这些快照不需要多个相同数据的副本。通过这种方式,Git的对象模型形成Merkle树。

当我们以这种方式查看对象模型时,我们可以看到提交为何是快照:它们直接链接到该提交的预期工作目录的完整视图!

即使提交是快照,我们常常在历史视图或GitHub上查看历史视图中的提交。实际上,提交消息经常是指这个差异。通过比较提交和其父的根图,从快照数据动态生成差异。 Git可以在时间上比较任何两个快照,而不仅仅是邻近的提交。

要比较两个提交,首先看他们的根树,几乎总是不同。然后,当当前树的路径具有不同的OID时,通过跟随对在子树上进行深度优先搜索。在下面的示例中,根树对文档具有不同的值,因此我们将其重复到这两种树中。这些树具有不同的M.md值,因此两种斑点被逐行比较,并且显示了该差异。仍然在文档中,n.md是相同的,所以跳过,我们弹回根树。然后,根树看到事项目录具有相同的OID以及README.MD条目。

在上图中,我们注意到从未访问过的东西树,因此没有任何可达物体被访问过。这样,计算差异的成本是相对于具有不同内容的路径的数量。

现在我们有致意是快照的理解,我们可以动态计算任何两个提交之间的差异。那为什么这个共同的知识不是吗?为什么新用户偶然偶然达到这个想法,提交是差异?

我最喜欢的类比之一是认为犯罪是具有波浪/部分二元性,其中有时它们被视为快照和其他时间,它们被视为差异。此事的症状真的进入了不同类型的数据,实际上不是Git对象:补丁。

修补程序是一个文本文档,用于描述如何更改现有代码库。修补程序是极其分布式的组可以在不使用Git提交的情况下共享代码。您可以看到这些在Git邮件列表中随身携带。

补丁包含更改的描述以及为什么它是有价值的,然后是差异。这个想法是有人可以使用该推理作为应用该副本的理由作为其代码的副本。

git可以使用git format-patch将提交转换为修补程序。然后可以使用Git应用将补丁应用于Git存储库。这是在开源早期分享代码的主导方式,但大多数项目通过拉动请求直接转移到分享Git提交。

共享修补程序的最大问题是修补程序丢失父信息,新提交的父级等于现有头部。此外,即使您在提交时间使用相同的父级,您也会得到不同的提交,但提交人也会更改!这是Git在提交对象中具有“作者”和“提交人”详细信息的基本原因。

使用修补程序的最大问题是,当您的工作目录与发件人的先前提交不匹配时,很难应用补丁。失去提交历史使得难以解决冲突。

这种“围绕移动补丁”的想法已转移到几个Git命令中,作为“搬家的搬家”。相反,实际发生的是重播的提交差异,创建新的提交。

git cherry-pick< oid>命令创建一个新的差异与< oid&gt的新提交。谁的父母是当前的提交。 Git基本上遵循以下步骤:

创建一个新的提交,其根树与新工作目录匹配,父父母是head的提交。

GIT创建新提交后,Git Log -1 -P头的输出应匹配Git Log -1 -P< OID&gt的输出。

重要的是要认识到,我们没有“移动”致力于在我们目前的头顶上,我们创建了一个新的提交,其差异与旧的提交相匹配。

git rebase命令将自己呈现为移动提交以具有新历史记录的方式。在最基本的形式中,它真的只是一系列Git Cherry-Pick命令,在不同的提交之上重播差异。

最重要的是git rebase< target>将发现从头到达的提交列表,但无法从<目标&gt ;.您可以使用git log -oneline< target> .. head。

然后,rebase命令只需导航到< target>从最旧的提交开始,位置并开始在此提交范围内执行Git Cherry-Pick命令。最后,我们有一组新的提交,不同的oid,但与原始提交范围相似的差异。

例如,考虑当前头中的三个提交序列,因为分支目标分支。在运行Git Rebase目标时,计算公共基础p以确定提交列表A,B和C.然后将这些是樱桃摘在目标顶部,以构建新的犯下A',B',和C'

提交A',B&#39 ;,和C'是全新的致力于与A,B和C共享大量信息,但是不同的新对象。事实上,旧的提交仍然存在于您的存储库中,直到垃圾收集运行。

我们甚至可以检查这两个提交范围如何使用Git Range-Diff命令不同!我将在Git存储库中使用一些示例提交来绑定到v2.29.2标签上,然后稍微修改提示提交。

$ git checkout -f 8e86cf65816 $ git rebase v2.29.2 $ echo额外线>> Readme.md $ git commit -a -amend -m"替换的提交消息" $ git范围 - diff v2。 29.2 8E86CF65816 HEAD1:17E7DBCBC = 1:2AA8919906边带:避免报告不完整的边带消息2:8E86CF6581! 2:E08FFF1D8B SideBand:报告未处理的不完整的边带消息作为错误@@元数据作者:Johannes Schindelin< [email protected]> ## comment消息## - sideBand:报告未处理的不完整的边带消息作为错误+替换提交消息 - 验证不完整的边带消息是否由`recv_sideband()`/`demultiplex_sideband()` - 代码正确处理了不完整的边带消息是非常棘手的:它们必须在循环终点的末尾刷新 - “recv_sideBand()”,但实际刷新由--`multiplex_sideBand()`(因此必须以某种方式知道 - 循环完成返回后)。 - - 要捕获不完整的边带消息可能不会出现的未来错误,以错误地显示,让' s捕获该条件并报告错误。 - - 签名:Johannes Schindelin< [email protected]> - 签名 - 逐个:junio c hamano< [email protected]& gt; + ## Readme.md ## + @@ Readme.md:和名称(取决于您的心情):+ [文档/ giteveryday.txt]:文档/ giteveryday.txt + [文档/ gitcvs-migration.txt] :文档/ gitcvs-migration.txt + [文档/提交斑点]:文档/提交份数++额外行## pkt-line.c ## @@ pkt-line.c:int recv_sideband(const char * me,int in_stream, int)

请注意,提交17E7DBBCBC和2AA8919906的生成范围 - DEMAC索赔是“相等的”,这意味着它们会产生相同的补丁。第二对提交是不同的,显示提交消息已更改,并且readme.md的编辑不在原始提交中。

如果您在一起,您还可以看到这两个提交集的提交历史如何仍然存在。新的提交有v2.29.2标签作为历史记录中的第三个提交,而旧的提交则具有(早)v2.29.0-rc0标签作为第三个提交。

$ git的日志--oneline -3 HEADe08fff1d8b2(HEAD)代替提交message2aa89199065带:避免报告不完整的带messages898f80736c7(标签:v2.29.2)Git的2.29.2 $ git的日志--oneline -3 8e86cf658168e86cf65816带:报告未处理的不完整边带信息为Bugs17E7DBBCBCE SideBand:避免报告不完整的边带消息47ae905ffb9(标签:v2.28.0)git 2.28

如果您在对象模型中仔细查找,则可能注意到Git从不追踪所存储的对象数据中的提交之间的更改。你可能想知道“Git如何知道重命名发生了?”

git不跟踪重命名。 Git内没有数据结构,可以存储一个重命名在提交和其父之间发生重命名的记录。相反,Git试图在动态差异计算期间检测重命名。此重命名检测有两个阶段:精确的重命名和编辑重命名。

在首次计算差异之后,Git检查差异的内部模型,以发现添加或删除了哪些路径。当然,从一个位置移动到另一个位置的文件将显示为删除第一位置和第二个位置的删除。 Git尝试匹配这些添加和删除以创建一组推断的重命名。

此匹配算法的第一阶段查找添加和删除的路径的OID,并查看是否有完全匹配。这种确切的比赛在一起配对。

第二阶段是昂贵的部分:我们如何检测重命名和编辑的文件? git通过每个添加的文件迭代,并将该文件与每个已删除的文件进行比较,以将相似性分数计算为常见的行的百分比。默认情况下,任何大于50%的常见计数中的50%作为潜在的编辑重命名。该算法继续比较这些对,直到找到最大匹配。

你注意到了一个问题吗?此算法运行A * D Diff,其中A是添加和D的数量是删除的数量。这是二次!为避免长期重命名计算,如果A + D,Git将跳过该部分检测编辑重命名的部分

......