Git粒度 - 解析一行内的差异

时间:2016-06-19 11:03:40

标签: git diff

git line-based或diff粒度是否可以增加到单词/字母分辨率?每行多个语句或使用git编写纯文本是值得的。

1 个答案:

答案 0 :(得分:9)

根据评论重新阅读这个问题,我想我看到你最初得到的是什么,所以我会给出一个真正的答案(vs Ismail Badawi's one line comment)。

Git as simple storage

Git有很多作品,其中一些比其他作品更好。在最低级别,Git充当基于内容的纯数据存储:具有键/值对的数据库,其中键本身简单(并且始终)是值的散列。也就是说,您无法选择密钥,在提供值后会为您提供密钥。

在这个非常低级别的键/值数据存储区之上是一个类似的低级接口,其中存储的值具有类型。四种类型的对象是" blob",它们保存您的实际文件; " trees",它允许Git将真实有用的文件名(如file.pyimage.jpg)映射到像b02f09aabdd957329476837f406588710142aebd这样的不可读的哈希值; "提交",它从不可读的哈希转换为关于提交的元数据,包括表示提交时文件的树;和注释标记对象,我们可以在这里忽略。

除此之外,Git提供引用。引用是包含分支和标签的一般形式,还有更多(远程跟踪分支,Git'注释"," stash"等等) )。这些将人类可读的名称如master转换为不可读的哈希值,然后Git使用它来获取提交,Git使用它来获取树的哈希值,Git使用它来获取文件的哈希值。

这是Git首次变得非常有用的级别。在这一点上,Git可以存储任何任意数据:它不需要由行,甚至单词组成;二进制表格,包括JPEG图像,在这里工作。唯一的限制是每个文件必须散列为自己的唯一值,除非它与该文件的先前版本相同。 (举一个例子,hash("a file\nwith two lines\n")匹配hash("a file\nwith two lines\n")即可,即使这些内容位于file1.txtfile2.txt,也可以。{什么不是好的是,如果hash("a file\nwith two lines!\n")具有相同的值,因为我们刚刚添加了一个字节:第二行上的感叹号。我们必须为此文件的内容,无论文件的名称如何。)

当你进行新的提交时,Git只关心内容。具体来说,在git commit时间,Git根据分阶段版本<打包所有跟踪文件的内容< / em>文件(属于 1 索引中的任何内容)。文件的名称(包括带有子目录的路径名,如果需要的话)都会进入一堆树对象,而文件&#39;内容本身会进入blob对象,或者当它们已经存在于数据库中时重新使用现有的blob对象。

(后者在大多数提交中都很常见,因为通常每次提交都有几十个,或几百个,甚至几万个文件,但每个新提交都会使大多数文件与之前的提交保持不变。 index / staging-area对于未更改的文件Shakespeare-Sonnet-107.txt具有与最后50次提交相同的blob-hash,因此我们只是在新提交中重新使用相同的blob。)

Git as compression scheme

如果你想用Git做的就是存储一些文件树的版本 - 更多的是备份系统而不是源代码控制系统,换句话说 - 然后是Git在压缩文件时效率的问题,之后一些变化,变得有趣。

对象存储的基本形式完美压缩相同的文件:如上所述,无论有多少提交包含名为Shakespeare-Sonnet-107.txt的文件,只要内容是逐字节相同的,我们只是重新使用其ID是该文件内容的哈希值的blob。 2

这种相同的基本存储方法 - 所谓的&#34;松散对象&#34; - 使用Zlib压缩数据。这适用于文本,其中大文本文件可能压缩到其原始大小的10%,但它对二进制文件的效果较差(压缩变化很大,我看到原始大小的1/3到1/2)对于任何已压缩的东西都很糟糕。但是,对于文本文件和编程语言源文件,我们通常可以做得更好。

大多数版本控制系统都提供所谓的delta compression,它最简单的形式只是&#34; diff old和new,并将指向另一个的指令转换为文件的存储形式&#34;也就是说,我们在一个线性提交链中对两个文件(通常是上一个和下一个版本)运行string-to-string correction algorithm - 而不是存储整个新文件,我们只是说&#34;删除第3行,添加新文件第47行&#34;或者其他什么。

对于版本控制系统,Git以不寻常的方式执行增量压缩。它永远不会简单地说&#34;哦,好吧,这是blah.py稍微不同的新版本,我会压缩最近的blah.py&#34;。相反,它会在游戏后期压缩对象,制作&#34;打包文件&#34;出于单个对象(以及重新打包,其他已经打包的对象,虽然规则在这里变得复杂 4 )并选择要通过各种启发式方法相互打包的对象。

此特定代码底部的算法是xdelta的修改版本。这适用于任意二进制数据,不依赖于换行符。

Git as version control

如果我们想使用Git进行真正的版本控制,那么 - 它设计的东西,毕竟 - 我们必须更进一步。我们将有许多特定于版本控制的任务,但是两个非常大的任务是查看已更改的内容合并来自不同开发路径的更改

要查看更改内容,Git会向我们提供git diffgit showgit log。所有这些都使用相同的基本差异引擎:给定两个提交,它匹配这些提交中的文件,然后匹配行 - aha,这里&#34; line&#34;进来! - 在匹配的文件中。

git diff的输出,以及git showgit log -p的输出非常注重线性。如果更改一行中的单词,则会将整行显示为已更改。您可以向git diff(以及git showgit log)提供标记,在Git找到这些面向行的更改后,将指示它们显示其中的哪些字词这些线实际上是不同的。从Git版本2.9开始,内部显示进一步得到了改进:有一个new diff-highlight script可以显示确切的变化。 (这些都在下面的面向行的diff上面工作:当你忽略空格变化时,这会显示出来,例如,你会看到空的差异,而不是没有差异。)

请注意,使用单词或字符显示对内部格式无影响,commit-blobs(存储完整)或包中的分层对象(xdelta不是面向行的)。这些是纯粹的显示选项。

三向合并

除了上述所有考虑因素之外,如果您打算使用Git的合并设施 - 您不必这样做;例如,Perforce的一些人使用p4merge而不是Git内置的合并 - 你需要知道这些是从运行两个常规的,因此面向行的git diff开始的。

特别是,当您运行git merge <other>时,Git会将<other>解析为提交ID,然后在当前(HEAD)提交之间找到合并基础和另一个提交。此合并基础提交 5 是一个起点。 Git产生两个差异:一个从基数到HEAD,一个从同一个基数到<other>。然后Git将两个差异组合在一起,将组合的更改应用于基本提交中的文件。

由于这些差异是面向行的,因此如果构造更改以使它们与行对应,则组合过程通常会更顺畅。因此,与git diff本身一样,您可能希望您的文件非常注重面向行。

如上所述,你不需要使用Git的内部合并,虽然编写合并脚本有点棘手,那些使用外部合并的人总是遇到various rough edges的绊脚石。

1 这意味着只有一个索引/暂存区域。事实上,有一个主要索引,一般情况下你只看到一个索引,但是有一些极端情况,例如git commit -a,其中有其他索引弹出暂时存在,并混淆这种模式。

2 从技术上讲,ID为hash("blob %d\0%s" % (len(bytes), bytes)),以Python-esque形式编写。也就是说,通过从单词&#34; blob&#34;,一个空格,内容长度的十进制表示,ASCII NUL字节开始,最后找到文件内容的哈希ID。实际内容。这可以保证您不能通过简单地编写一个内容与现有提交的内容相同的文件来破坏Git,例如 3 。这里的问题是可以测试每个对象的类型,而不是从上下文中假设类型,因此如果提交和常规文件具有相同的哈希,这将强制单个底层Git存储库对象具有两个类型commit < em>和类型blob,这是不允许的。

3 例如,您可以使用git rev-parse HEAD查看当前提交的内容,其ID为git cat-file -p HEAD打印的任何内容。将git cat-file的输出写入常规文件,并git add这个新文件,您将获得一个与git rev-parse HEAD显示所有内容不匹配的新ID,因为在内部,每个对象获得前缀为其类型名称,加上空格大小和NUL序列。

4 要将它们煮沸很多,一个包对象只能引用相同包中的其他对象,除了所谓的&#34; thin&# 34;包,用于通过网络传输。

5 假设有一个合并基础。如果有多个,则默认的递归策略通过合并基于合并的候选者来构造一个。通常只有一个合并基础,我们不必担心这一点。偶尔根本没有合并基地;在这种情况下,Git 2.9已将默认值更改为抱怨和失败,而不是从空树中合并。