为什么在将多个存储库合并到一个单一存储库后,文件上的 运行 a "git log -m --follow" 会出现不相关的历史记录?

Why does unrelated history appear when running a "git log -m --follow" on a file after merging in multiple repos into one monolithic repository?

我有几个不同的 git 存储库,我想将它们合并成一个整体存储库,同时保留它们的历史记录。我找到了一种方法来执行此操作,但我对 git 日志向我显示的单个文件历史记录感到有些困惑。

这是我的输出:

git log --oneline

组合回购的输出

------- (HEAD -> master) Merge repoC into mono repo
------- Merge repoB into mono repo
------- Merge repoA into mono repo
------- initial commit
------- Add README to repoC
------- Add README to repoB
------- Add README to repoA

git log --oneline repoA/README.md

组合回购的输出

------- Merge repoA into mono repo

git log --oneline -m --follow repoA/README.md

组合回购的输出

 ------- (from -------) (HEAD -> master) Merge repoC into mono repo
 ------- (from -------) Merge repoB into mono repo
 ------- (from -------) Merge repoA into mono repo
 ------- (from -------) Merge repoA into mono repo
 ------- initial commit
 ------- Add README to repoC
 ------- Add README to repoB
 ------- Add README to repoA

从所有单独的存储库作为捆绑包开始,我执行以下操作来创建我的整体存储库:

回购 A/B/C

git init
echo "repo" > README.md
git add .
git commit -m 'Add README to repo'
git bundle create ../repo{A,B,C}.bundle --all

创建组合存储库 git 初始化 回声 "initial" > README.md git 添加 . git commit -m 'initial commit'

对于每个 repo

mkdir repo{A,B,C}
git fetch ../repo{A,B,C}.bundle master
git merge --allow-unrelated-histories -s ours --no-commit FETCH_HEAD
git read-tree --prefix=repoA -u FETCH_HEAD
git commit -m "Merge repo{A,B,C} into mono repo"

为什么在 运行 '-m --follow' 时我会得到不相关的 git 特定文件的提交历史记录?我希望只看到与该文件有关的提交。

已更新(尝试记录具有不同名称和内容的文件):

  git log -m --follow --oneline repoB/sue.md`
  -------(from  -------) (HEAD -> master) Merge repo C into mono repo`
  -------(from  -------) Merge repo B into mono repo`
  -------(from -------) Merge repo B into mono repo`

要扩展 ,您应该了解在 Git 中,文件的 身份 是以一种相当奇怪的方式定义的。

文件身份 在版本控制系统 (VCSes) 中是一个核心概念。 VCS 如何知道文件 include/lib.h 是或不是 "the same" 文件作为文件 lib/lib.h

一些 VCS 采取的方法是,当文件 首次引入 到 VCS 中时,您告诉 VCS 一些特殊的东西,例如 hg add <em>路径</em>。从那时起,任何时候文件重命名,你告诉VCS一些特殊的东西,比如hg mv [- -after] <em>old-name</em> <em>new-name</em>。 VCS 可以使用它来跟踪文件在一系列提交中的身份:修订版 X 中的 lib/lib.h 是或不是 "the same" 文件作为修订版 R 中的 include/lib.h,具体取决于你是否告诉 VCS 在 R 和 X 之间有一个重命名操作。

另一方面,

Git 做了一些完全不同的事情:它试图通过 content 来识别 file-pairs,给出任何两个修订。也就是说,给定修订版 R 和 X 作为一对,Git 查看 R 中的每个文件和 X 中的每个文件。如果 RX 都有名为 include/lib.h 的文件,那么几乎可以肯定 相同 [=123] =] 文件,因此 lib/lib.h(在 R 或 X 中)绝对不是 include/lib.h(在其他版本中)相同的 文件,但它可能与 lib/lib.h 相同的文件(在另一个版本中)。但是,如果恰好两个修订版中的一个具有 include/lib.h 而另一个具有 lib/lib.h,则该文件 可能已在这两个修订版之间重命名

一般来说,由于 CPU-time-related 原因,给定任何一对修订版,如果某些路径 P 存在于两个修订版中,Git 假定文件是未改名。使用 git diff——但不是 git merge 而不是 git log——你可以添加一个标志来说明 不要假设文件没有被重命名只是因为它们存在于两个修订版中。这是 -B(中断配对)参数。

然后,只要启用重命名检测([=20=中的-M选项,[=22=中的--follow选项,和各种其他条件):对于所有 un 配对的文件,由于 -B 或因为给定路径仅存在于两个修订之一中,Git 查找具有 相似内容 的文件,为它们计算 "similarity index",and/or 相似名称 。 (例如,如果两个文件都以 /lib.h 结尾,则匹配组件名称会有 +1 奖励。作为关键优化,因为它在内部很容易完成并且效果很好,Git 将快速将文件与100% 相同的内容,只有在失败后,才计算相似度指数。)然后它会将任何具有满足或超过您给它的百分比要求的相似度指数的文件配对:-M50 是默认值,但您可以例如,要求与 -M75 有“75% 相似度”。

这些paired-up个文件是两次修订中的"the same"个文件。 git diff 也是如此,然后在 paired-up 文件之间产生差异,对于典型的 git merge,运行s two git diffs,一个从合并基础到两个 tip 提交之一,然后第二个从同一个合并基础到两个 tip 提交中的另一个。最重要的是,对于 --followgit log 也是如此:paired-up 文件名将 --follow 操作指向 更改它正在查找的文件名for 如果早期版本中的文件具有 不同的 名称。

(你的 merge -s ours 不是 典型的合并:ours 策略在计算源代码时忽略除 HEAD 提交之外的所有提交新提交,所以它根本不会打扰任何 diff-ing。)

这会如何影响 git log --follow

For git log --follow <em>path</em>跟随路径名为path的文件 跨重命名,Git 必须执行这些 pair-at-a-time 差异,以便它可以检测到文件实际上已重命名。使用的对是 parent of CC itself,其中 C 是提交由于图形遍历而发现,即 git log 即将显示或不显示的提交,具体取决于它是否触及路径名为 path 的文件。

合并提交在这里出现问题。合并提交的定义是它至少有 两个 parents。这就是 -m(拆分合并)选项的用武之地:拆分合并意味着在这个 git log 操作期间假装合并提交 N parents,实际上是N 分开不同的提交。这些 N 提交中的第一个有一个 parent:合并的第一个 parent。第二个提交有一个parent:合并的第二个parent。第 N 个提交将第 N 个 parent 作为单个 parent,依此类推。所以如果合并有三个 parents,它被分成三个虚拟提交,每个有一个 parent.

这解决了配对问题:每个虚拟提交现在只有 一个 parent,并且 Git 可以 运行 差异通常的方法,检测任何重命名。如果 Git 找到 一个重命名,那只是意味着当它去显示 parent 提交时——在完成之后对于这些 N 虚拟提交中的每一个——它应该停止寻找路径名 path,而是开始寻找名称为 diff.

中的旧 名称

由于您正在寻找 repoA/README.md,Git 开始寻找该特定路径。 Git 每次查找时都会在拆分虚拟提交中找到 那个名字 repoA/README.md。每个拆分虚拟提交的 parent 都有名为 README.md 的文件,因此在 Git 之后每个 parent 打印一次拆分虚拟提交 - 每个 parent/child 对里面有 repoA/README.md 因为每个这样的 child 提交(合并本身)里面有 repoA/README.md——它移动到 parents,一次一个,现在看对于名为 README.md 的文件。它发现每个 parent 提交都有这样一个文件,所以它打印每个 parent 提交。