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

Git granularity -- resolving diffs within a line

是否可以将 git 基于行或差异的粒度提高到 word/letter 分辨率?每行多条语句或使用 git 编写纯文本是值得的。

根据评论重新阅读问题,我想我明白了你最初的意思,所以我会给出一个真实的答案(对比 )。

Git作为简单存储

Git 有相当多的作品集,其中一些作品在一起玩得比其他作品更好。在最低级别,Git 充当纯基于内容的数据存储:一个具有 key/value 对的数据库,其中键本身只是(并且始终)是值的散列。即不能选择key,key是你提供value后给你提供的。

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

在这些之上,Git 提供了 参考资料 。引用是包含分支和标签的一般形式,再加上更多(远程跟踪分支、Git 的 "notes"、"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")具有相同的值,因为我们刚刚在第二行添加了一个字节:感叹号。我们必须为该文件的 content 获取一个新的、不同的哈希值,而不管文件的 [=114] =]姓名.)

当你进行新提交时,Git只关心内容。具体来说,在git commit时,Git打包内容基于文件的 暂存版本 的所有跟踪文件(即 1 索引中的任何内容)。文件的名称(如果需要,包括带有子目录的路径名)都进入一堆树对象,文件的内容本身进入 blob 对象,或者当它们已经在数据库中时重新使用现有的 blob 对象。

(后者对于大多数提交来说很常见,因为通常每次提交中都有数十个、数百个甚至数万个文件,但每次新提交都会保留大部分文件与上一次提交相比没有变化. index/staging-area 对于未更改的文件 Shakespeare-Sonnet-107.txt 具有与最近 50 次提交相同的 blob 哈希,因此我们只是在新提交中再次使用相同的 blob。)

Git 作为压缩方案

如果你想用 Git 做的只是存储一些文件树的一堆版本——换句话说,更多的是备份系统而不是源代码控制系统——那么效率如何的问题Git 正在压缩文件,经过一些改动,变得有趣。

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

同样的基本存储方法——即所谓的 "loose object"——使用 Zlib 压缩数据。这对文本非常有效,其中一个大文本文件可能会压缩到其原始大小的 10%,但它对二进制文件效果不佳(压缩范围很大,我见过原始大小的 1/3 到 1/2)和对于任何已经压缩的东西来说都非常糟糕。不过,对于文本文件和编程语言源文件,我们通常可以做得更好。

大多数版本控制系统在两个文件上提供所谓的 delta compression, which in its simplest form is just "diff old and new, and turn the instructions that change one to the other into the stored form of the file." That is, we run a string-to-string correction algorithm——通常是前一个版本和下一个版本,在线性提交链中——而不是存储整个新文件,我们只是说 "remove line 3, add new line 47" 或其他什么。

Git 以一种不寻常的方式执行增量压缩,至少对于版本控制系统而言。它从不简单地说 "oh, well, here's a new slightly different version of blah.py, I'll compress against the most recent blah.py"。相反,它在游戏后期压缩对象,使 "pack files" 脱离单个对象(并且,为了重新打包,其他已经打包的对象,尽管这里的规则变得复杂 4)并通过各种启发式方法选择要相互打包的对象。

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

Git 作为版本控制

但是,如果我们想使用 Git 进行真正的版本控制——毕竟它的设计目的——我们必须看得更远。我们将有许多特定于版本控制的任务,但两个真正重要的任务是 查看更改的内容合并来自不同开发路径的更改

要查看发生了什么变化,Git 为我们提供了 git diffgit showgit log。所有这些都使用相同的基本差异引擎:给定两个提交,它匹配这些提交中的文件,然后匹配行——啊哈,这就是 "lines" 进来的地方! —在匹配的文件中。

git diff 以及 git showgit log -p 的输出非常面向行。如果您更改一行中的一个词,它会显示整行已更改。您可以向 git diff(以及 git showgit log)提供一些标志,在 Git 发现这些面向行的更改后,将指示它们显示哪个词行内的 (s) 实际上是不同的。从 Git 2.9 版开始,行内显示得到了进一步改进:有一个 new diff-highlight script 可以准确显示更改内容。 (这些都在下面的面向行的差异之上工作:当您忽略白色 space 更改时,这会显示出来,例如,您会看到空的差异块,而不是根本没有差异。)

请注意,使用文字或字符显示对内部格式没有影响,无论是提交 blob(完整存储)还是包中的 deltified 对象(xdelta 不是行-导向)。这些纯粹是显示选项。

三路合并

除了上述所有注意事项外,如果您打算使用 Git 的合并功能——您不必这样做;例如,一些使用 Perforce 的人使用 p4merge,而不是 Git 的内置合并——你需要知道这些开始于 运行 两个常规的,因此是面向行的,git diffs.

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

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

如上所述,您不需要使用 Git 的内部合并,尽管编写合并脚本有点棘手,而那些使用外部合并的人总是在 various rough edges 上磕磕绊绊。 =54=]


1这意味着只有一个索引/暂存区。事实上,有一个 primary 索引,通常你只会看到一个索引,但是有一堆极端情况,例如 git commit -a,其中会弹出其他索引临时存在,搞乱这个模型。

2从技术上讲,ID 是 hash("blob %d[=42=]%s" % (len(bytes), bytes)),以 Python 形式书写。也就是说,文件内容的哈希 ID 是通过以下方式找到的:单词 "blob"、一个 space、内容长度的十进制表示、一个 ASCII NUL 字节,最后是实际内容.这保证您不能通过简单地编写一个内容与某些现有提交相同的文件来破坏 Git,例如 3。这里的问题是可以测试每个对象的类型,而不是从上下文中假设类型,因此如果提交和常规文件具有相同的哈希值,这将强制单个底层 Git 存储库对象具有类型提交 类型 blob,这是不允许的。

3您可以查看当前提交的内容,其 ID 是 git rev-parse HEAD 打印的任何内容,例如 git cat-file -p HEAD。将 git cat-file 的输出写入一个常规文件,然后 git add 这个新文件,您将得到一个与 git rev-parse HEAD 显示的内容不匹配的新 ID——这都是因为,在内部,每个对象以其类型名称为前缀,加上 space-size-and-NUL 序列。

4把它们归结起来,一个包对象只能引用 same 包内的其他对象,除了 so -称为 "thin" 包,用于跨网络传输。

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