我试图为文件的历史记录选择一个ID - 我希望它是或者引用其详细信息git log --follow <filename>
的“对象”。我想知道:
git如何知道一个文件在后续提交中是另一个文件的变体?当然,名称相同是一个强烈的提示,但它也跟踪提交时的重命名。它是否将计算结果保存在git log引用的位置(where?),或git log是否每次都重复这些计算? (这些是什么计算?)
理想情况下,我想使用nodegit访问或重新创建历史记录(提交/ blob shas列表)。
答案 0 :(得分:6)
我和其他人都在其他地方(而不是链接)详细描述了这一点,例如this answer到What's git's heuristic for assigning content modifications to file paths?或我对Git Diff of same files in two directories always result in "renamed"的回答。 git log --follow
的详细信息与git diff
的详细信息略有不同,因为git diff
通常会处理整个树,其中包含左侧和右侧文件,但git log --follow
仅适用于git diff
有一条特定的路径。 1
在任何情况下,在比较两个特定提交时都会发生重命名跟踪。对于一般git log
,他们任何两个提交 R (右侧)和 L (左侧 - 您选择两个) , 2 但对于git log --follow
,他们特别是父母和孩子。为方便起见,我们将这些 P 和 C 称为。使用diff_tree_sha1
,Git运行一个后差异步骤(从-B
调用;请参阅脚注),将所有内容修剪为一个文件。差异用 R = C 和 L = P 完成。但是,一般情况下更容易描述,因此我们将从此开始。
通常,在比较 R 与 L 时,Git:
您可以使用-Bn/m
(pair- b reaking)标志稍微修改一下,实际上需要两个可选整数(n
)。只有 -C
整数才能进行重命名检测。 3 您也可以使用n
标志对其进行修改;这只需要一个可选的 -M
,然后启用复制检测。在所有情况下,必须打开重命名检测。重命名检测通过n
启用,同样需要一个可选的整数 git log --follow
,或者在git status
和git diff --stat
等其他命令的情况下自动启用或合并后的n
。
在任何情况下,这里的整数 git diff <commit1> <commit2>
是所有这些不同选项的相似性(或不相似)度量值。这是我们了解重命名检测代码的地方。
假设我们首先进行了基本的git diff <tree1> <tree2>
或diff_tree_sha1
操作。这最终会调用builtin_diff_tree
in builtin/diff.c
,调用diffcore_break
(我们稍后会再次查看),然后调用log_tree_diff_flush
in log-tree.c
。这几乎会立即调用diffcore_std
in diff.c
,如果正确(diffcore_rename
,diffcore_merge_broken
或{-B
,则运行-M
,-C
和-B
函数选择{1}}和path/to/file
选项。
这三个功能在配对队列上运行。如何设置配对队列?我会把它留给另一个部分,因为它很复杂。现在,假设配对队列已经path/to/file
与path/to/file
匹配,当 L 和 R <中有path/to/L-only
时/ em>,如果文件路径只出现在 L 或仅出现在 R中,则会出现未配对的path/to/R-only
和diffcore_merge
diffcore_break
function is in diffcore-break.c
。它的工作是找到已经配对的文件,其 dis 相似性索引(当比较 L 和 R 版本时)高于某个阈值。如果是这样的话,就会破坏配对。 --find-copies-harder
函数位于同一文件的正下方;如果两个人都没有找到一个更好的伴侣,它会重新加入一对破碎的对子。相异度指数计算与相似度计算类似,但不相同。 4
更有趣diffcore_rename
function is in diffcore-rename.c
。我们现在可以忽略它a special case shortcut for --follow
。然后,它会查找exact renames,即blob哈希匹配的文件,即使它们的名称不相同。使用&#34;下一个文件&#34;有一些繁琐的位。如果多个 L 源与一些未配对的 R 目标具有相同的哈希值。
接下来,它checks how many unpaired entries there are,因为它将(实际上)执行num( L )倍num( R )比较要计算的文件他们的相似之处,这需要花费大量的时间和空间。它甚至会自动降级 * Idea here is very simple.
*
* Almost all data we are interested in are text, but sometimes we have
* to deal with binary data. So we cut them into chunks delimited by
* LF byte, or 64-byte sequence, whichever comes first, and hash them.
*
* For those chunks, if the source buffer has more instances of it
* than the destination buffer, that means the difference are the
* number of bytes not copied from source to destination. If the
* counts are the same, everything was copied from source to
* destination. If the destination has more, everything was copied,
* and destination added more.
*
* We are doing an approximation so we do not really have to waste
* memory by actually storing the sequence. We just hash them into
* somewhere around 2^16 hashbuckets and count the occurrences.
案例,这个案例太难了#34;然后,对于每个可能的 L 和 R 配对,它会计算similarity index and a name score。
相似性索引代码位于estimate_similarity
in diffcore-rename.c
。它依赖于函数diffcore_count_changes
in diffcore-delta.c
,它说明了这一点(我直接从文件中复制它,因为它是核心指标之一):
score = (int)(src_copied * MAX_SCORE / max_size);
final similarity index score是:
src_copied
其中max_size
是源中发生的散列块数(64字节或最新行),然后再次出现在目标中,'\r'
是大小,字节,无论哪个blob更大。 (此字节数不考虑已剥离的dir/oldbase
字符。这些字符仅从正在进行哈希处理的64或更新行的块中删除。)
&#34;名称得分&#34;实际上只是1(相同的基本名称)或0(不同的基本名称),即如果 L 文件是differentdir/oldbase
并且 R 文件是{1,则为1 {1}},但如果 L 文件为dir/oldbase
且 R 文件为anything/newbase
,则为0。当这两个文件同样相似时,这用于使Git优先于newdir/oldbase
{/ 1}}。
anything/newbase
代码调用(通过一系列函数)ll_diff_tree_paths
(两者都在diff_tree_sha1
中;我只链接到此处的最终函数)。这是一个复杂且极其优化的代码(Git在这里花了很多时间),所以我们只是快速概述并忽略复杂性(见脚注2)。此代码部分地查看每个树中每个blob的完整路径名(这些是顶部注释中的P1,...,Pn项),部分位于每个blob的blob哈希值这些名字。对于具有相同名称和相同内容的文件,它不执行任何操作(tree-diff.c
模式除外,在这种情况下,它将所有文件名排队)。对于具有相同名称和不同内容的文件,或者没有 L 或 R 名称的文件,它会调用(通过函数指针,存储在{{ 1}},--find-copies-harder
和opt->pathchange
)opt->change
中最终归结为diff_change
或diff_addremove
的内容。这些调用diff_queue
,将文件对(如果文件是新的或删除的话,其中一个是虚拟的)放入配对队列。
因此,短版本(如果我们不使用opt->add_remove
或diff.c
),配对队列只有在没有原始来源时才会有未配对的文件对应于 R 中文件的 L 中的文件,或 L 中与源文件对应的 R 中没有目标文件>。使用-C
时,它会列出每个源文件或每个已修改的源文件,以便可以扫描它们以获取副本(此处的选择取决于您是否使用--find-copies-harder
)。
-C
我们已在--find-copies-harder
代码中注明了一个快捷方式:它会跳过所有 R 文件名,这些文件名不是我们关心的文件名。 --follow
似乎有一些类似的黑客,虽然我不确定它们是否适用于此。代码也以不同的方式驱动,如脚注2所示。当我们将父 P 与子 C 进行差异,并在我们的配对队列中找到重命名时,我们然后在我们diffcore-rename.c
中将我们用作限制的文件的名称作为限制:我们将 C 中的新名称替换为 P 的。然后我们像往常一样继续进行差异,所以下次我们比较 P - 和 - C 对时,我们正在寻找ll_diff_tree_paths
而不是git log -- <path>
。如果我们检测到oldpath
已从newpath
重命名,我们会像以前一样将该名称再次转换为该位置。
请注意,所有oldpath
,reallyoldpath
和-B
机制在理论上适用 ,但快捷方式可能 - 它根本不适用我明白他们是否这样做 - 保持其中一些不起作用。
1 当使用-C
时,Git使用一般的diffcore代码来运行对断开和复制检测。一般代码是从调用想要进行简化的代码。查看调用try_to_follow_renames
in tree-diff.c
的函数diffcore_std
in diff.c
。这最终都会调用处理配对队列的diff_resolve_rename_copy
。然后-M
将结果修剪为一个有趣的文件;稍后通过diff_might_be_rename
调用diff_tree_sha1
进行测试。我认为这一切都来自log_tree_commit
,来自cmd_log_walk
或log_show_early
。这最后似乎是一个未经证实的黑客攻击,供一些GUI使用。
2 --follow
中的树匹配实际上在输出右侧接受一次提交,并在输入左侧接受提交的列表 - 手侧,用于组合差异目的。这就是Git如何设法显示合并提交。虽然try_to_follow_renames
如何使用合并提交,但有点不清楚。请参阅find_paths_generic
in combine-diff.c,其中也会调用git diff
。请注意,--follow
hack是因为调用diff_tree_sha1
而发生的,而这个组合差异合并处理代码会调用该函数一次每个父级。如果后面的名称将是但是,当它通过第二个父级时,已经更改了。也许这是一个错误。如果第二个父级决定新名称会导致另一个不同的重命名,会发生什么?从逻辑上讲,它应该按照拓扑顺序为每个父叉选择一个新名称,并考虑在叉子重新加入时以某种方式再次解析它们。
3 log --follow
中的第二个 diff_tree_sha1
值告诉Git什么时候不运行真正的差异,而只是描述变化在非 -renamed文件中&#34;删除所有原始行,将其替换为所有新行&#34;。这假设第一个m
值最终不打破配对,或者配对由于-Bn/m
值重新粘合在一起,或粘合到不同的来源为-B
副本。
4 有关详细信息,请参阅should_break
。这也使用-M
代码,但以不同的方式,使用&#34;添加&#34;计数。
答案 1 :(得分:1)
git log
每次都重复计算。
决定基于文件内容。当一个文件消失,另一个文件出现时,Git会比较内容并决定文件是否重命名,如果内容相同或非常相似(按某种方式)。