我试图为文件的历史记录选择一个id - 我希望它是或者引用其详细信息的"对象" git log --follow <filename>.我在想:
git如何知道一个文件是后续提交中另一个文件的变体?当然,名称相同是一个强烈的提示,但它也跟踪提交时的重命名.它是否将计算结果保存在git log引用的位置(where?),或git log是否每次都重复这些计算?(这些计算是什么?)
理想情况下,我想使用nodegit访问或重新创建历史记录(提交/ blob shas列表).
其他人和我在其他地方用不同的(并没有链接)细节描述了这一点,例如,这个答案是什么是git的启发式方法,用于为文件路径分配内容修改?或者我对两个目录中相同文件的Git Diff的回答总是导致"重命名".细节git log --follow与它们的细节略有不同git diff,因为git diff通常处理整个树 - 充满左侧和右侧文件,但git log --follow仅适用于一个特定路径.1
无论如何,在比较两个特定提交时会发生重命名跟踪.对于将军来说,git diff他们是任意两个提交R(右侧)和L(左侧 - 您选择两个),2但是因为git log它们特别是父母和孩子.为方便起见,我们称之为P和C. 有了git log --follow,Git运行一个后差异步骤(调用来自diff_tree_sha1;参见脚注),将所有内容修剪为一个文件.差异用R = C和L = P完成.但是,一般情况下更容易描述,因此我们将从那开始.
通常,在比较R vs L时,Git:
您可以使用-B(pair- b reaking)标志稍微修改一下,该标志实际上需要两个可选的整数().只有整数才能重命名检测.3 你也可以用旗子修改它; 这只需要一个可选项,然后打开复制检测.在所有情况下,必须打开重命名检测.重命名检测通过,同样采用可选的整数,或在其他命令或合并后的情况下自动启用.-Bn/mn-Cn-Mngit log --followgit statusgit diff --stat
在任何情况下,n这里的整数是所有这些不同选项的相似性(或不相似性)度量值.这是我们了解重命名检测代码的地方.
假设我们首先有一个基本git diff <commit1> <commit2>或git diff <tree1> <tree2>操作.这卷起调用builtin_diff_tree中builtin/diff.c,它调用diff_tree_sha1(我们将再次看到更高版本),然后log_tree_diff_flush在log-tree.c.这几乎立即调用diffcore_std的diff.c,它运行的diffcore_break,diffcore_rename以及diffcore_merge_broken如果有合适的(功能-B,-M或者-C,和-B再次)被选中的选项.
这三个功能在配对队列上运行.如何设置配对队列?我会把它留给另一个部分,因为它很复杂.现在,只假定配对队列已经已经path/to/file与匹配path/to/file有当path/to/file两个大号和[R ,否则具有未配对path/to/L-only,并path/to/R-only为那里是只发生在一个文件路径的情况下大号或只在[R .
该diffcore_break功能在diffcore-break.c.它的任务是找到已配对的文件,文件的DIS相似性指数(比较时,大号和[R版本)高于某个阈值.如果是这样的话,就会破坏配对.该diffcore_merge函数位于同一文件的正下方; 如果两个人都找不到"更好的伴侣",它就会重新加入一对破碎的对子.相异度指数计算与相似度计算类似但不相同.4
更有趣的diffcore_rename功能是diffcore-rename.c.它有一个特殊的案例快捷方式--follow,我们现在可以忽略它.然后它会查找精确重命名,即blob哈希匹配的文件,即使它们的名称不匹配.如果多个L源与一些未配对的R目的地具有相同的散列,则使用"下一个文件"有一些可疑的位.
接下来,它检查有多少未配对的条目,因为它将(实际上)进行num(L)次num(R)次文件比较以计算它们的相似性,这将花费大量的时间和空间.它甚至会自动降级--find-copies-harder"太难" 的案件.然后,对于每个可能的L和R配对,它计算相似性指数和名称得分.
相似性索引代码estimate_similarity在diffcore-rename.c.它依赖于函数diffcore_count_changesindiffcore-delta.c,它说明了这一点(我直接从文件中复制它,因为它是核心指标之一):
* 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.
Run Code Online (Sandbox Code Playgroud)
这里有一个秘密位:如果文件被认为是"非二进制",则相似性索引会忽略\r字符,\r紧接着就是\n.
在最后的相似性指数得分是:
score = (int)(src_copied * MAX_SCORE / max_size);
Run Code Online (Sandbox Code Playgroud)
其中src_copied是源中发生的散列块(64字节或最新换行)的数量,然后再次出现在目标中,并且max_size是以较大的blob为单位的大小(以字节为单位).(此字节数不考虑已剥离的'\r'字符.这些仅仅是从正在进行哈希处理的64或更新行的块中删除的.)
"名称得分"实际上只是1(相同的基本名称)或0(不同的基本名称),即,如果L文件是dir/oldbase和R文件是1 differentdir/oldbase,则为1 ,但如果L文件是dir/oldbase并且R文件是,则为0 anything/newbase.这是用来做的Git支持newdir/oldbase超过anything/newbase当这两个文件同样类似.
该diff_tree_sha1代码调用(通过一系列的功能)ll_diff_tree_paths(两者都在tree-diff.c,我只是链接到最终的功能在这里).这是一个复杂且极其优化的代码(Git在这里花了很多时间),所以我们只是快速概述并忽略复杂性(见脚注2).此代码部分地查看每个树中每个blob 的完整路径名(这些是顶部注释中的P1,...,Pn项),部分位于每个名称的blob哈希值.对于具有相同名称和相同内容的文件,它不执行任何操作(--find-copies-harder模式除外,在这种情况下,它会对所有文件名进行排队).对于具有相同的名称和文件不同的内容,或者没有大号或[R的名字,它会调用(通过函数指针,在存储opt->pathchange,opt->change和opt->add_remove)什么最终归结为diff_change或者diff_addremove,无论是在diff.c.这些调用diff_queue将文件对(如果文件是新的或删除的,其中一个是虚拟的)放入配对队列中.
因此,短版(如果我们不使用-C或--find-copies-harder),配对队列具有未配对的文件,只有当在没有原始源文件大号对应于文件[R ,或无目的地文件[R对应于源文件大号.有了-C它,它还列出了每个源文件或每个修改过的源文件,以便可以扫描它们的副本(这里的选择取决于您是否使用过--find-copies-harder).
--follow我们已经注意到diffcore-rename.c代码中的一个快捷方式:它跳过所有不是我们关心的文件名的R文件名.似乎有一些类似的黑客入侵ll_diff_tree_paths,虽然我不确定它们是否适用于此.代码也以不同的方式驱动,如脚注2所示.当我们将父P与子C进行差异,并在我们的配对队列中找到一个重命名时,我们转出我们正在使用的文件名作为在我们的限制git log -- <path>:我们在更换新名称ç与重命名源路径P.然后我们像往常一样继续进行差异,所以下次我们比较一对P和C对时,我们正在寻找oldpath而不是newpath.如果我们检测到oldpath重命名reallyoldpath,我们会像以前一样将该名称再次切换到位.
需要注意的是所有的-B,-C以及-M机械适用于理论,但快捷方式可能-这是不是在所有我清楚他们是否做,保持它的一些工作.
1使用时--follow,Git使用一般的diffcore代码来运行成对和复制检测.从想要进行简化的代码中调用通用代码.请参阅功能try_to_follow_renames中tree-diff.c,它调用diffcore_std的diff.c.这最终调用diff_resolve_rename_copy处理配对队列.然后try_to_follow_renames将结果修剪为一个有趣的文件; 这是后来通过diff_might_be_rename调用来测试的diff_tree_sha1.我认为这一切都来自于log_tree_commit,cmd_log_walk或来自任何一个或log_show_early.这最后似乎是一个未经证实的黑客攻击,供一些GUI使用.
2树匹配git diff实际上接受输出右侧的单个提交,以及输入左侧的提交列表,用于组合差异.这就是Git如何设法显示合并提交.但是有点不清楚如何--follow使用merge进行提交.请参阅find_paths_genericcombine-diff.c,它diff_tree_sha1也会调用.请注意,log --followhack是作为调用的结果而发生的diff_tree_sha1,并且这个组合差异合并处理代码每个父级调用一次该函数. 但是,如果后面的名称将被更改,则它在通过第二个父项时已被更改.也许这是一个错误.如果第二个父级决定新名称会导致另一个不同的重命名,会发生什么?从逻辑上讲,它应该按照拓扑顺序选择每个父分叉最多一个新名称,并考虑在叉子重新加入时以某种方式再次解析它们.
3第二个,m值告诉Git什么时候不运行真正的差异,而只是将非扩展文件中的变化描述为"删除所有原始行,用所有新行替换它们".这假设第一个值最终没有打破配对,或者配对由于值而重新粘合在一起,或者作为副本粘贴到不同的源.-Bn/m-B-M-C
4详见should_break.这也使用diffcore-delta.c代码,但以不同的方式,使用"添加"计数.