如何合并我移动的文件中 Git 中的更改?
How do I merge changes in Git in files that I moved?
我移动了一些目录。
当我合并时,有很多冲突的文件,因为其他开发人员已经提交了他们的更改。 egit Merge Tool 和 git mergetool
都说该文件已在本地或远程删除。见图像。
如何合并这些更改?
文件历史和重命名检测
你永远不需要担心 Git 中的 "preserving history"。 Git 根本没有 file 历史,它只有 commit 历史。也就是说,每个提交 "points to"(包含其父项的哈希 ID)——或者,对于合并,both 它的父项——而这个 是 历史:commit E
前面是commit D
,而commit D
前面是commit C
,依此类推。只要有提交,就有历史。
也就是说,Git 可以尝试来合成一个特定文件的历史,使用git log --follow
.您指定起始提交和路径名,然后 Git 检查,逐个提交,以查看在将当前提交的父项与当前提交进行比较时文件是否已重命名。这使用 Git 的 重命名检测 来识别提交 L(左)中的文件 a/b.txt
是 "the same file" 作为提交 R 中的文件 c/d.txt
(右)。
重命名检测有很多繁琐的旋钮,但在基本级别,基本上是这样的:
- Git 查看 commit L.
中的所有文件名
- Git 查看 commit R.
中的所有文件名
- 如果有一个文件名从L消失,出现在R,比如
a/b.txt
消失了,c/d.txt
是全新的,为什么,这是检测到重命名的候选。
- 既然有候选(未配对的 L 文件和未配对的 R 文件,Git 比较 这些未配对文件的内容。
未配对的文件进入配对队列(一个用于L,一个用于R ), Git 散列所有文件的内容。它已经有了内部 Git 散列,所以它首先直接比较所有这些。如果一个文件完全没有变化,它在L和中有相同的Git散列ID(但名称不同) ]R,可以立即配对并从配对队列中移除。
现在完全匹配已被删除,Git 尝试长而慢的 slog。它需要一个未配对的 L 文件,并为每个 R 文件计算一个 "similarity index"。如果一些 R 文件足够相似——或者有几个是——它采用 "most similar" R 文件并将其与 L 文件。如果没有文件足够相似,则 L 文件保持未配对状态(从队列中取出)并被视为 "deleted from L"。最终在未配对的 L 队列中没有文件,无论文件留在未配对的 R 队列中,这些文件都是 "added" ( R 中的新内容)。同时,所有配对文件都已重命名。
这意味着:当比较(git diff
)提交L到R时,如果两个文件足够相似,它们作为重命名配对。 默认相似度指数为 50%,因此文件需要 50% 匹配(无论那是什么意思——相似度指数计算有些不透明) , 但 精确 匹配对于 Git.
更容易和更快
请注意 git log --follow
启用重命名检测(仅在一个目标 R 文件上,因为我们正在 向后 通过日志,将父提交与我们在子中知道其名称的文件进行比较)。从 Git 版本 2.9 开始,git diff
和 git log -p
现在都自动打开重命名检测。在旧版本中,您必须使用 -M
选项来设置相似度阈值,或者将 diff.renames
配置为 true
,以获取 git diff
和 git log -p
来做重命名检测。
配对队列也有最大长度限制。这已经翻了两次,一次在 Git 1.5.6 中,一次在 Git 1.7.5 中。您可以自己控制它:它可配置为 diff.renameLimit
和 merge.renameLimit
。当前的限制是 400 和 1000。(如果将它们设置为零,Git 使用它自己的内部最大值,这会消耗大量的 CPU 时间——这就是为什么这两个限制存在于第一个地方。如果你设置 diff.renameLimit
而不是 merge.renameLimit
,git merge
使用你的 diff 设置。)
这导致适用于 git log --follow
的经验法则:如果可能,当您打算重命名某个文件或一组文件时,请自行提交重命名步骤,不要更改任何文件内容。 如果可能,请尽量减少重命名文件的数量:例如,不超过 400 个。您可以分多个步骤提交更多重命名,一次 400 个。但请记住,您正在权衡 git log --follow
能力和速度与无意义的提交使您的历史变得混乱:如果您需要重命名 50000 个文件,也许您应该这样做。
但这对合并有何影响?好吧,git merge
和 git log --follow
一样,总是打开重命名检测。但是哪个提交是 L 而哪个提交是 R?
合并和重命名检测
每当你 运行:
git merge <commit-specifier>
Git 必须在当前 (HEAD) 提交和指定的其他提交之间找到 merge base。 (通常这只是 git merge <branchname>
。通过将分支名称解析为它指向的提交来选择另一个分支的 tip 提交。根据 [=303 的定义=] 在 Git 中,那是那个分支的尖端提交,所以这个 "just works"。但是你可以指定 any 通过哈希 ID 提交,例如。)我们称此合并基础提交为 B(对于基础)。我们已经知道我们自己的提交是 HEAD
,尽管有些东西称之为 "local"。让我们将另一个提交称为 O(对于其他),尽管有些东西将此称为 "remote"(这很愚蠢:Git 中没有任何内容是远程的!)。
Git 然后实际上是 two git diff
s。比较 B 与 HEAD,因此对于这个特定的差异,L 是 B 和 R 是头。 Git 将根据我们在上面看到的规则检测或检测不到重命名。然后 Git 执行另一个 git diff
,它将 B 与 O 进行比较。 Git 将再次根据相同的规则检测或检测不到重命名。
如果某些文件在 B-vs-HEAD 中重命名,Git 会像往常一样区分其 内容 。如果某些文件在 B-vs-O 中重命名,Git 会像往常一样比较其内容。如果单个 B 文件 F 在 HEAD 和 中重命名为 两个不同的名称 O、Git 声明该文件存在 rename/rename 冲突,并在工作树中留下 both 名称供您清理。如果它在 只有一个 差异中被重命名——它在 HEAD 或 O 中仍然被称为 F——那么 Git 使用重命名的一方的新名称将文件存储在工作树中。在任何情况下,Git 都会尝试合并两组更改(来自 B-vs-HEAD 和 B-vs-O) 照常。1
当然,对于Git到检测重命名,文件的内容必须足够相似,一如既往。这对于 Java 文件(有时 Python 也是如此)尤其成问题,其中 文件名 嵌入到导入语句中。如果一个模块主要由 import 语句组成,只有几行自己的代码,则重命名引起的更改将淹没剩余的文件内容,文件甚至不会匹配 50%。
有解决办法,虽然有点难看。根据 git log --follow
的经验法则,我们可以先提交 只是 重命名,然后将内容更改 "fix all the imports" 作为单独的提交提交。然后,当我们去合并的时候,我们可以做 two 甚至 three merges:
git checkout ... # whatever branch we plan to merge into
git merge <hash> # merge with everything just before the Great Renaming
由于没有文件被重命名,这次合并将像往常一样进行得很好,或者很差。这是结果,以图表的形式。请注意,我们提供给 git merge
命令的散列是提交 A
的散列,就在执行所有重命名的 R
之前:
...--*--o--...--o--M <-- mainline
\ /
o--o--...-A--R--...--o <-- develop, with renames at R
然后:
git merge <hash of R>
因为每个文件的 内容 在名称方面完全相同,在其他 R
提交中——合并基础是提交 A
——效果这里只是拿起所有的重命名。我们保留来自 HEAD 提交 M
的文件内容,但保留来自 R
的名称。此合并应自动成功:
...--*--o--...--o--M--N <-- mainline
\ / /
o--o--...-A--R--...--o <-- develop, with renames at R
现在我们可以git merge develop
继续合并开发分支。
在许多情况下,我们不需要进行合并 M
,但无论如何这样做可能不是一个坏主意 如果 我们需要进行merge N
仅用于所有重命名。原因是提交 R
不起作用: 它有错误的导入名称。在对分期间必须跳过提交 R
。这意味着 merge N
同样不起作用,必须在二分法期间跳过。有 M
可能会更好,因为 M
实际上可以工作。
请注意,如果您这样做,您就是在扭曲/歪曲您的源代码,只是为了取悦您的版本控制系统。这不是一个好情况。它可能 没那么糟糕,但不要告诉自己它 好。
1我还需要看看发生rename/rename冲突时文件的两个副本会发生什么。由于 Git 将 names 都留在工作树中,因此两个名称是否包含相同的合并内容,以及是否需要任何冲突标记?也就是说,如果文件被命名为 base.txt
,现在被命名为 head.txt
和 other.txt
,head.txt
和 other.txt
的工作树版本是否始终匹配?
我移动了一些目录。
当我合并时,有很多冲突的文件,因为其他开发人员已经提交了他们的更改。 egit Merge Tool 和 git mergetool
都说该文件已在本地或远程删除。见图像。
如何合并这些更改?
文件历史和重命名检测
你永远不需要担心 Git 中的 "preserving history"。 Git 根本没有 file 历史,它只有 commit 历史。也就是说,每个提交 "points to"(包含其父项的哈希 ID)——或者,对于合并,both 它的父项——而这个 是 历史:commit E
前面是commit D
,而commit D
前面是commit C
,依此类推。只要有提交,就有历史。
也就是说,Git 可以尝试来合成一个特定文件的历史,使用git log --follow
.您指定起始提交和路径名,然后 Git 检查,逐个提交,以查看在将当前提交的父项与当前提交进行比较时文件是否已重命名。这使用 Git 的 重命名检测 来识别提交 L(左)中的文件 a/b.txt
是 "the same file" 作为提交 R 中的文件 c/d.txt
(右)。
重命名检测有很多繁琐的旋钮,但在基本级别,基本上是这样的:
- Git 查看 commit L. 中的所有文件名
- Git 查看 commit R. 中的所有文件名
- 如果有一个文件名从L消失,出现在R,比如
a/b.txt
消失了,c/d.txt
是全新的,为什么,这是检测到重命名的候选。 - 既然有候选(未配对的 L 文件和未配对的 R 文件,Git 比较 这些未配对文件的内容。
未配对的文件进入配对队列(一个用于L,一个用于R ), Git 散列所有文件的内容。它已经有了内部 Git 散列,所以它首先直接比较所有这些。如果一个文件完全没有变化,它在L和中有相同的Git散列ID(但名称不同) ]R,可以立即配对并从配对队列中移除。
现在完全匹配已被删除,Git 尝试长而慢的 slog。它需要一个未配对的 L 文件,并为每个 R 文件计算一个 "similarity index"。如果一些 R 文件足够相似——或者有几个是——它采用 "most similar" R 文件并将其与 L 文件。如果没有文件足够相似,则 L 文件保持未配对状态(从队列中取出)并被视为 "deleted from L"。最终在未配对的 L 队列中没有文件,无论文件留在未配对的 R 队列中,这些文件都是 "added" ( R 中的新内容)。同时,所有配对文件都已重命名。
这意味着:当比较(git diff
)提交L到R时,如果两个文件足够相似,它们作为重命名配对。 默认相似度指数为 50%,因此文件需要 50% 匹配(无论那是什么意思——相似度指数计算有些不透明) , 但 精确 匹配对于 Git.
请注意 git log --follow
启用重命名检测(仅在一个目标 R 文件上,因为我们正在 向后 通过日志,将父提交与我们在子中知道其名称的文件进行比较)。从 Git 版本 2.9 开始,git diff
和 git log -p
现在都自动打开重命名检测。在旧版本中,您必须使用 -M
选项来设置相似度阈值,或者将 diff.renames
配置为 true
,以获取 git diff
和 git log -p
来做重命名检测。
配对队列也有最大长度限制。这已经翻了两次,一次在 Git 1.5.6 中,一次在 Git 1.7.5 中。您可以自己控制它:它可配置为 diff.renameLimit
和 merge.renameLimit
。当前的限制是 400 和 1000。(如果将它们设置为零,Git 使用它自己的内部最大值,这会消耗大量的 CPU 时间——这就是为什么这两个限制存在于第一个地方。如果你设置 diff.renameLimit
而不是 merge.renameLimit
,git merge
使用你的 diff 设置。)
这导致适用于 git log --follow
的经验法则:如果可能,当您打算重命名某个文件或一组文件时,请自行提交重命名步骤,不要更改任何文件内容。 如果可能,请尽量减少重命名文件的数量:例如,不超过 400 个。您可以分多个步骤提交更多重命名,一次 400 个。但请记住,您正在权衡 git log --follow
能力和速度与无意义的提交使您的历史变得混乱:如果您需要重命名 50000 个文件,也许您应该这样做。
但这对合并有何影响?好吧,git merge
和 git log --follow
一样,总是打开重命名检测。但是哪个提交是 L 而哪个提交是 R?
合并和重命名检测
每当你 运行:
git merge <commit-specifier>
Git 必须在当前 (HEAD) 提交和指定的其他提交之间找到 merge base。 (通常这只是 git merge <branchname>
。通过将分支名称解析为它指向的提交来选择另一个分支的 tip 提交。根据 [=303 的定义=] 在 Git 中,那是那个分支的尖端提交,所以这个 "just works"。但是你可以指定 any 通过哈希 ID 提交,例如。)我们称此合并基础提交为 B(对于基础)。我们已经知道我们自己的提交是 HEAD
,尽管有些东西称之为 "local"。让我们将另一个提交称为 O(对于其他),尽管有些东西将此称为 "remote"(这很愚蠢:Git 中没有任何内容是远程的!)。
Git 然后实际上是 two git diff
s。比较 B 与 HEAD,因此对于这个特定的差异,L 是 B 和 R 是头。 Git 将根据我们在上面看到的规则检测或检测不到重命名。然后 Git 执行另一个 git diff
,它将 B 与 O 进行比较。 Git 将再次根据相同的规则检测或检测不到重命名。
如果某些文件在 B-vs-HEAD 中重命名,Git 会像往常一样区分其 内容 。如果某些文件在 B-vs-O 中重命名,Git 会像往常一样比较其内容。如果单个 B 文件 F 在 HEAD 和 中重命名为 两个不同的名称 O、Git 声明该文件存在 rename/rename 冲突,并在工作树中留下 both 名称供您清理。如果它在 只有一个 差异中被重命名——它在 HEAD 或 O 中仍然被称为 F——那么 Git 使用重命名的一方的新名称将文件存储在工作树中。在任何情况下,Git 都会尝试合并两组更改(来自 B-vs-HEAD 和 B-vs-O) 照常。1
当然,对于Git到检测重命名,文件的内容必须足够相似,一如既往。这对于 Java 文件(有时 Python 也是如此)尤其成问题,其中 文件名 嵌入到导入语句中。如果一个模块主要由 import 语句组成,只有几行自己的代码,则重命名引起的更改将淹没剩余的文件内容,文件甚至不会匹配 50%。
有解决办法,虽然有点难看。根据 git log --follow
的经验法则,我们可以先提交 只是 重命名,然后将内容更改 "fix all the imports" 作为单独的提交提交。然后,当我们去合并的时候,我们可以做 two 甚至 three merges:
git checkout ... # whatever branch we plan to merge into
git merge <hash> # merge with everything just before the Great Renaming
由于没有文件被重命名,这次合并将像往常一样进行得很好,或者很差。这是结果,以图表的形式。请注意,我们提供给 git merge
命令的散列是提交 A
的散列,就在执行所有重命名的 R
之前:
...--*--o--...--o--M <-- mainline
\ /
o--o--...-A--R--...--o <-- develop, with renames at R
然后:
git merge <hash of R>
因为每个文件的 内容 在名称方面完全相同,在其他 R
提交中——合并基础是提交 A
——效果这里只是拿起所有的重命名。我们保留来自 HEAD 提交 M
的文件内容,但保留来自 R
的名称。此合并应自动成功:
...--*--o--...--o--M--N <-- mainline
\ / /
o--o--...-A--R--...--o <-- develop, with renames at R
现在我们可以git merge develop
继续合并开发分支。
在许多情况下,我们不需要进行合并 M
,但无论如何这样做可能不是一个坏主意 如果 我们需要进行merge N
仅用于所有重命名。原因是提交 R
不起作用: 它有错误的导入名称。在对分期间必须跳过提交 R
。这意味着 merge N
同样不起作用,必须在二分法期间跳过。有 M
可能会更好,因为 M
实际上可以工作。
请注意,如果您这样做,您就是在扭曲/歪曲您的源代码,只是为了取悦您的版本控制系统。这不是一个好情况。它可能 没那么糟糕,但不要告诉自己它 好。
1我还需要看看发生rename/rename冲突时文件的两个副本会发生什么。由于 Git 将 names 都留在工作树中,因此两个名称是否包含相同的合并内容,以及是否需要任何冲突标记?也就是说,如果文件被命名为 base.txt
,现在被命名为 head.txt
和 other.txt
,head.txt
和 other.txt
的工作树版本是否始终匹配?