如何在重构期间移动文件而不引起冲突?

Em.*_*Em. 4 git

我正在使用以下目录结构的存储库:

my-project/
  a/
  b/
  c/
Run Code Online (Sandbox Code Playgroud)

我需要重构它并将a, b, 移动c到子文件夹中,如下所示:

my-project/
  subfolder/
    a/
    b/
    c/
Run Code Online (Sandbox Code Playgroud)

但是,有许多活动分支已被检查,人们正在这些分支上处理ab和 内的文件,c并且它们尚未位于子文件夹中。如何执行此重构而不导致我的所有队友的分支在尝试合并时发生冲突?

tor*_*rek 7

正如埃弗特评论的那样,理想情况下,您无需执行任何操作。

\n\n

在实践中,其效果多少有些变化。请参阅下面的详细讨论。请注意,任何配置设置(例如 )diff.renameLimit都是稍后执行差异或合并操作的人员的责任。如果是,您现在就可以设置您的设置,但如果是其他人,他们必须设置自己的设置(只要他们愿意)。

\n\n

细节

\n\n

重要的是要知道 Git 不存储对文件的更改。相反,Git 存储文件的快照每次提交都包含每个文件的完整快照。

\n\n

每个提交还包含一些元数据\xe2\x80\x94有关提交本身的信息\xe2\x80\x94,例如,包括进行提交的人的姓名和电子邮件地址,但还包括其先前提交的原始哈希ID,其中Git 调用提交的父级。该字符串一起提交,尽管是向后的。

\n\n

每个提交都有自己唯一的哈希 ID。该哈希 ID 实际上是提交内容的加密校验和,来自其主要数据\xe2\x80\x94、快照\xe2\x80\x94 及其元数据。这意味着一旦提交,它就永远不能更改,不能更改任何一位:更改任何单个位或任何一组位,只会使用新的唯一哈希 ID 进行新的提交,而现有的提交仍然是那里。

\n\n

Git 通常是向后运行的。分支名称保存最后一次提交的原始哈希 ID 。从那里,Git 找到倒数第二个提交,进而找到倒数第三个提交,依此类推:

\n\n
... <-F <-G <-H   <--branch\n
Run Code Online (Sandbox Code Playgroud)\n\n

该名称branch包含我们刚刚调用的某个提交的实际哈希 ID H。当 GitH从其大数据库中读取时,使用该哈希 IDH包含早期提交的哈希 ID G。Git 现在可以G从其 Git-objects-database 中读取数据,从而获取早期提交的哈希 ID F。这使得git log其他命令可以向后执行提交。

\n\n

同样,每次提交都保存其所有文件的快照。但 Git 会向您显示更改。其工作原理是,当您让 Git 向您显示一些提交时,Git 会查找该提交的父提交\xe2\x80\x94,其向后指向的链接指向其上一个提交\xe2\x80\x94,并提取到临时工作区域中(在记忆中)提交了。然后 Git 可以比较每次提交中的文件。

\n\n

如果 commitGREADME.mdcommitHREADME.md相同的,Git 甚至不会告诉你H有一个README.md. 但是,如果它们不同,Git 将比较两个不同README.md文件的内容并显示发生了什么变化。这就是你看到变化的方式。

\n\n

您可以比较任意两个提交,而不仅仅是父子提交,但比较父子提交非常常见:许多 Git 命令会自动执行此操作。有些(例如 )git diff让您选择两个提交,而一些 \xe2\x80\x94(例如git merge\xe2\x80\x94)可以自己选择一个提交,我们稍后会看到。

\n\n

重命名检测

\n\n

如果您重命名某些文件,实际发生的情况是较早的提交G有一个名为 的文件,README.txt而较晚的提交H有一个名为README.md. Git 注意到G 没有没有README.md,并猜测也许,只是也许,您在这两次提交中重命名了这两个文件。H README.txt

\n\n

如果你将整个文件集合重命名为 \xe2\x80\x94,在 Git 眼中,就没有目录了;文件只有长名称:a/b/c.ext是文件的名称,它不是a包含名为的文件夹的文件夹b,依此类推,它只是一个带有斜杠的长名称\xe2\x80\x94Git 会尝试尽可能匹配每个文件对。(最近,有人尝试改进名称匹配,以考虑典型的“文件夹化”。它出了几次错误,但我认为它现在又回来了。)

\n\n

此重命名检测在 Git 的内部差异引擎中是可选的。运行时git diff,它在现代 Git 中默认打开,但在非常旧的 Git 版本中默认关闭。您可以强制使用git diff --find-renamesgit diff -M简称)或在配置中设置diff.renames为。true

\n\n

合并和重命名检测

\n\n

当您运行时git checkout somebranch; git merge otherbranch,Git 依靠提交图来查找合并基础。我将在这里省略所有相关细节;有关更多信息,请参阅其他答案。

\n\n

考虑一个如下所示的提交图:

\n\n
          I--J   <-- somebranch (HEAD)\n         /\n...--G--H\n         \\\n          K--L   <-- otherbranch\n
Run Code Online (Sandbox Code Playgroud)\n\n

即,名称 somebranchotherbranch选择提交分别为JL。提交 throughH是在两个分支上,而提交I-J仅在 上somebranch,并且目前,提交K-L仅在 上otherbranch。您已运行git checkout somebranch,如附加HEAD到名称所示somebranch,并且git merge otherbranch现在正在启动。

\n\n

HGit 现在将自行查找合并基础提交。找到合并基础后,Git 现在需要将HJ、 和中的快照分别转换L您更改的内容somebranch它们更改的内容otherbranch

\n\n

由于git diff可以找到重命名,因此合并只会git diff在打开 find-renames 选项的情况下运行:

\n\n
git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed\ngit diff --find-renames <hash-of-H> <hash-of-L>   # what they changed\n
Run Code Online (Sandbox Code Playgroud)\n\n

在没有重命名的合并中,重命名检测器找不到任何内容,Git 只是进行合并,例如,README.md基于通过比较\'s和\'sH发现的更改,然后再次基于\'s进行合并。但是,当进行命名时,Git 必须将每个文件对配对。例如,如果:HREADME.mdJREADME.mdLREADME.md

\n\n
    \n
  • README.txtHREADME.mdJ, 并且
  • \n
  • README.txtHREADME.txtL
  • \n
\n\n

然后重命名了该文件,但他们没有。这些操作的组合就是重命名文件

\n\n

因此,在您的情况下,您将进行一次提交,其中文件的名称现在是subfolder/a/file.ext等等,而它们过去只是a/file.ext等等。如果运气好的话,Git 将在该 diff 中正确地将合并库a/file.ext与其他人的进行匹配,与您的. 差异将显示一侧重命名了文件,而另一侧没有重命名,这两个更改的组合包括“重命名文件”。a/file.exta/file.extsubfolder/a/file.ext

\n\n

这什么时候会出问题?

\n\n

Git 的重命名检测器由其 diff 引擎处理,取决于三件事:

\n\n
    \n
  • 旧名称下的文件必须位于左侧且右侧不存在,而新名称下的文件必须位于右侧且不存在-左边。

    \n\n

    也就是说,假设我们曾经有path/to/file.ext并且现在有new/path/to/file.ext。我们将旧的提交放在左侧,新的提交放在右侧。但是如果我们也在path/to/file.ext右边创建了一个新的不同的呢?

    \n\n

    Git 甚至不会尝试比较path/to/file.ext左侧和new/path/to/file.ext右侧,因为它将左侧path/to/file.ext与右侧新的但不相关的匹配path/to/file.ext。Git 只会new/path/to/file.ext在右侧调用一个新文件

    \n\n

    因此,最初的左右比较必须在左侧显示一些“已删除”文件,在右侧显示一些“新”文件。重命名检测器将尝试将左侧删除的文件与右侧添加的文件进行匹配,并将此类文件对转换为(检测到的)重命名。

  • \n
  • 即使你有这样的一对,Git 也不会调用重命名的文件,除非内容相似。也就是说,假设您不仅重命名了该文件,而且还更改了某些内容。Git 将对相似性进行快速测试,以百分比表示。如果左侧和右侧文件至少“50% 相似”,Git 会认为这是重命名的候选者。

    \n\n

    Git 必须对每个左侧和右侧未配对的文件执行此操作。也就是说,对于每个文件对,Git 必须计算相似性索引。类似于?left/file1.a​ 与right/file2.b有多相似?对两侧的每个文件重复此操作。left/file1.aright/file3.c

    \n\n

    为了让这个过程更快,Git 可以轻松匹配 100% 相同的文件。因此,您可以先提交重命名操作,然后将更改提交到重命名的文件,并在逐次提交时使事情变得更好git log

    \n\n

    这在合并期间不太有用,因为git merge永远不会逐个提交。(我认为它应该有一个选项可以这样做,只是为了查找重命名,但它没有。)

    \n\n

    默认相似度阈值 50% 只是默认值。运行时,您可以使用带有数字的或选项git diff来提高或降低所需的最小相似度,例如将其降低到 30% 或提高到 75%。1 使用 时,可以通过 来选择限制。(在旧版本的 Git 中,您需要:检查您的特定 Git 版本的文档,例如。)-M--find-renames-M30-M75git merge-X find-renames=<number>-X rename-threshold=<number>git help merge

  • \n
  • 最后,Git 施加重命名限制,因为很难找到相似的文件。

    \n\n

    现代Git(Git 2.26)中的默认限制是400,即可以检测到400个重命名。如果您重命名了 402 个文件,则将检测到其中 400 个文件,而有两个则不会。您可以使用以下方法提高或降低此限制diff.renameLimit。将其设置为零告诉 Git 不要人为限制重命名检测。

    \n\n

    git merge命令有自己单独的配置旋钮,但除了默认启用重命名检测(即使在旧版本的 Git 中)之外,diff.renameLimit如果您不设置单独的配置旋钮,它也会服从您的命令merge.renameLimit.

  • \n
\n\n

我将其设置diff.renameLimit为零,以取消重命名上限。这使得一些git diff命令git merge运行得非常好慢,但我喜欢不必担心它(并且我知道如果需要的话可以将其重新打开)。

\n\n
\n\n

1注意,--find-renames=4是指 40%,而不是 4%。您可以添加%字符 , --find-renames=4%,或者直接写为--find-renames=04。不过,将重命名阈值降低得太低可能并不明智,因为 Git 会开始发现没有意义的重命名。

\n

  • [有趣的是你应该这么说](http://web.torek.net/torek/tmp/book.pdf)(但这本书已经停滞多年了......)。我修正了上面的一些格式错误。 (3认同)