简短版:
short of poring over
git
's source code, where can I find a full description of the heuristics thatgit
uses to associate chunks of content with specific tracked pathnames?
详细版本:
在下面的 (Unix) shell 演示交互中,a
和 b
这两个文件是“git-commit
'ted”,然后修改它们以便(有效地)将 a
的大部分内容传输到 b
,最后这两个文件再次提交。
要查找的关键是第二个 git commit
的输出以行结尾
rename a => b (99%)
即使从未发生文件重命名(通常意义上的)(!?!)。
在展示演示之前,这个简短的描述可能更容易理解。
文件a
和b
的内容是通过组合三个辅助文件../A
的内容生成的,../B
和 ../C
。象征性地,a
和 b
的状态可以表示为
../A + ../C -> a
../B -> b
就在第一次提交之前,以及
../A -> a
../B + ../C -> b
就在第二个之前。
好的,这是演示。
首先,我们显示辅助文件../A
、../B
和../C
的内容:
head ../A ../B ../C
# ==> ../A <==
# ...
#
# ==> ../B <==
# ###
#
# ==> ../C <==
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
(以#
开头的行对应到终端的输出;实际输出行没有前导#
。)
接下来,我们创建文件a
和b
,显示它们的内容,并提交它们
cat ../A ../C > a
cat ../B > b
head a b
# ==> a <==
# ...
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
#
# ==> b <==
# ###
git add a b
git commit --allow-empty-message -m ''
# [master (root-commit) 3576df7]
# 2 files changed, 8 insertions(+)
# create mode 100644 a
# create mode 100644 b
接下来,我们修改文件a
和b
,并显示它们的新内容:
cat ../A > a
cat ../B ../C > b
head a b
# ==> a <==
# ...
#
# ==> b <==
# ###
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
最后,我们提交修改后的a
和b
;注意 git commit
的输出:
git add a b
git commit --allow-empty-message -m ''
# [master 25b806f]
# 2 files changed, 2 insertions(+), 8 deletions(-)
# rewrite a (99%)
# rename a => b (99%)
我将这种行为合理化如下。
据我所知,git
将目录结构信息(例如它跟踪的文件的路径名)视为次要信息——或者元数据,如果你愿意的话——,与它跟踪的主要信息相关联,即各种内容 block 。
由于文件的内容和名称(包括路径名)都可能在提交之间发生变化,git
必须使用启发式方法将路径名与内容 block 相关联。但是启发式算法,就其本质而言,并不能保证 100% 的时间都有效。这种启发式方法的失败表现为历史记录不能忠实地代表实际发生的事情(例如,它报告文件重命名,即使没有文件被重命名,在通常意义上)。
这个解释的进一步确认(即,一些启发式在起作用)是,AFAICT,如果传输的 block 的大小不够大,git commit
的输出将不会包括 rewrite/rename
行。 (我在这篇文章的末尾包含了这个案例的演示,FWIW。)
My question is this: short of poring over
git
's source code, where can I find a full description of the heuristics thatgit
uses to associate chunks of content with specific tracked pathnames?
除了辅助文件../C
比之前少了一行之外,第二个demo在各方面都与第一个相同。
head ../A ../B ../C
# ==> ../A <==
# ...
#
# ==> ../B <==
# ###
#
# ==> ../C <==
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
cat ../A ../C > a
cat ../B > b
head a b
# ==> a <==
# ...
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
#
# ==> b <==
# ###
git add .
git commit -a --allow-empty-message -m ''
# [master (root-commit) a06a689]
# 2 files changed, 7 insertions(+)
# create mode 100644 a
# create mode 100644 b
cat ../A > a
cat ../B ../C > b
head a b
# ==> a <==
# ...
#
# ==> b <==
# ###
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
git add .
git commit -a --allow-empty-message -m ''
# [master 87415a1]
# 2 files changed, 5 insertions(+), 5 deletions(-)
最佳答案
正如您所注意到的,Git 使用启发式方法执行重命名检测,而不是被告知发生了重命名。事实上,git mv
命令只是在新文件路径上进行添加,并删除旧文件路径。因此,重命名检测是通过将添加的文件的内容与先前提交的已删除文件的内容进行比较来执行的。
首先,收集候选人。任何新文件都是可能的重命名目标,任何已删除的文件都是可能的重命名源。此外,重写更改被破坏,这样一个文件与其之前的版本有 50% 以上的不同,既是一个可能的重命名源,也是一个可能的重命名目标。
接下来,检测相同的重命名。如果您在不做任何更改的情况下重命名文件,则该文件将具有相同的哈希值。只需对索引中的哈希进行比较而不读取文件内容即可检测到这些,因此从候选列表中删除这些将减少您需要执行的比较次数。
最后进行相似度比较。每个候选文件中的每一行都被散列并收集在一个排序列表中。长行按 60 个字符拆分。假设空白行对相似性匹配没有太大贡献,则可以去除空白行。来自每个候选源的行散列与来自每个候选目标的行散列进行比较。如果两个列表相似度达到 60%,则它们被视为重命名。
关于git - 将内容修改分配给文件路径的 git 启发式是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21292562/