[英]Disassociating Files in Dev Branch from Master Branch Counterparts So They Remain Separate After Merging
背景
我有两个(实际上是很多分支)已经分道扬镳,需要合并以生产流程。
dev 分支文件名与 master 分支相同。 dev 分支中的一些具有共同名称的文件“准备好”用于生产,而其他文件“未准备好”,因为它们需要重新工作和/或预计会产生令人讨厌的冲突。
对于名称相同的“未就绪”文件,我很想将它们与主对应文件分离,以便在分支合并后它们可以保持独立。 我曾尝试将开发分支文件名重命名为其他名称,例如git mv NewFile.txt DevFile.txt
,但是除了正常的文件内容合并行为之外,合并仅包含重命名。
核心问题
是否有某种方法可以解除 master 和 dev 分支中的文件的关联,以便文件在合并后保持独立? 理想情况下,同时还保留他们的历史?
更多详细信息
我试图在 github 的公共仓库中包含一个简化的工作示例。 所有 shell 命令添加/mv/merge 等都包含在 script.sh 中。 要克隆/查看,也许您可以使用git clone git@github.com:NedScandrett1/TestRepo.git
访问示例
是否有某种方法可以解除 master 和 dev 分支中的文件的关联,以便文件在合并后保持独立?
不完全是。 但是,您可能能够“足够接近”。 见下文。
理想情况下,同时还保留他们的历史?
文件没有历史记录,在 Git 中。 在 Git 中,提交是历史,如果你运行:
git log
你会看到所有的提交——所有的历史——从当前的提交开始并向后工作。
Git 总是像这样向后工作,因为提交本身在内部向后工作:
每个提交都有每个文件的完整快照,以及一些元数据:关于提交本身的信息。
快照存储 Git 在您或任何人制作快照时所知道的所有文件。 这些都不能更改:它们是提交的一部分,提交的编号——它的 hash ID——取决于这些文件内容。
元数据告诉 Git 提交的人、时间和原因(他们的日志消息)。 在此提交元数据中,Git 包含一个列表(通常只有一个提交),其中包含先前提交的 hash ID。 这也永远无法更改:此特定提交的 hash ID,即它在您的 Git 数据库中“我们拥有的所有提交”的编号,由这些内容(数据和元数据)决定。
元数据负责这个向后连接的字符串。 如果我们绘制一组简单的单父提交,将它们的实际 hash ID 替换为我们自己的理智的一个大写字母名称,我们会得到如下所示的内容:
... <-F <-G <-H
其中H
代表序列中最后一个提交的 hash ID(提交编号)。 (Side note: Git finds this hash ID in the branch name , which by definition holds the hash ID of the last commit in the chain. Updating the branch is done by sticking a new hash ID into the branch name. That hash ID, whatever它是,成为分支中的最后一次提交。)
我们——人类,也就是——喜欢将提交视为一组更改。 为此,Git 必须使用两个快照。 例如,要查看H
发生了什么变化,Git 将提取G
和H
的快照,并比较它们。 对于相同的东西,Git 什么也没说; 无论有什么不同, Git 告诉我们如何更改G
中的副本以匹配H
中的副本。 这向我们展示了发生了什么变化。 但事实上,我们所拥有的只是快照。
正如我在括号旁注中所说,分支名称仅包含某个链中最后一次提交的 hash ID。 在某个链条中排在最后并不意味着它是最后一个。 例如,如果我们有:
...--G--H--I <-- last
我们可以添加一个指向现有提交H
的新名称:
...--G--H <-- interesting
\
I <-- last
现在,名为interesting
的分支是在H
结束的提交链,而名为last
的分支是在I
结束的提交链。 提交H
在最后一个分支last
,它不是最后一个分支last
。
考虑到这一点,我们可以回顾一下第一个问题:
我曾尝试将开发分支文件名重命名为其他名称,例如
git mv NewFile.txt DevFile.txt
,但是除了正常的文件内容合并行为之外,合并只是简单地合并了重命名。 ...这样...文件在合并后保持独立...
当我们使用git merge
时,这就是我们真正在做的事情:
I--J <-- br1 (HEAD)
/
...--G--H
\
K--L <-- br2
我们已经运行git checkout br1
来选择提交J
作为当前提交,通过分支名称br1
作为当前分支。 在这些图中,这就是br1
所附的(HEAD)
告诉我们的。 然后我们运行git merge br2
。
Git 现在:
使用图(从J
向后和从L
向后的连接)来查找合并基础提交。 这是两个分支上最好的共享提交。 通常只有一个最佳共享提交,在这个特定的图中,很明显:它是提交H
。 提交G
也是共享的,但它不如H
,因为它“更靠后”。
运行一对git diff --find-renames
命令(或其内部等效命令,真的)。 Git 需要找出自共享起点(在本例中为提交H
)以来发生了什么变化,以分别在提交J
和L
中生成快照。 这需要两个单独git diff
命令。
如果您重命名了某些文件,那么--find-renames
部分可以解决这个问题。 所有 Git 都有快照。 提交H
有一些文件集,提交J
有一些文件集,提交L
有一些文件集。 如果提交H
有NewFile.txt
但没有DevFile.txt
,并且提交L
有DevFile.txt
但没有NewFile.txt
,那么,也许这些真的是“同一个文件”,不管这意味着什么。
找出这种“相同性”(如果有的话)是--find-renames
选项的工作。 使用git diff
时,您可以控制查找重命名设置。 当你使用git merge
, git merge
控制它们; 但请继续阅读。
为了继续合并,Git 现在尝试合并在步骤 2 中发现的两组更改。这里有很多细节,我们将直接跳过。 :-) 假设 Git能够组合这些,Git 然后将组合更改应用于H
中找到的文件。 这会保留我们的更改( br1
中的H
-vs- J
),同时添加它们的更改( br2 中的H
br2
L
)。
如果 Git 能够自行组合所有这些,它将 go on 进行新的合并提交,除非我们在git merge
行上告诉它--no-commit
或-n
。 如果 Git 遇到合并冲突,它会停止并给我们留下一大堆需要清理的东西。 这就是我们在步骤 3 中跳过的那些细节变得重要的时候。 如果它停止 - 每个--no-commit
或由于冲突 - 它仍然会记录正确的内容以在我们完成合并时进行新的合并提交。
合并完成后,我们最终得到:
I--J
/ \
...--G--H M <-- br1 (HEAD)
\ /
K--L <-- br2
提交M
是一个合并提交,它的特点是它向后链接到提交J
和提交L
。
当 Git 在历史中倒退时,一次提交一个,它有一点问题,因为它现在必须同时倒退到两个提交。 无需详细说明,Git 在这里所做的一件事是它没有显示发生了什么变化,因为只有当我们选择一个单一的前提交到 go 和我们的一个后提交时,才显示更改的内容才有效。 对于合并提交,有两个(或更多)“之前”提交。 (存在两个以上父级的合并,称为octopus merges ,但我们不会在这里处理它们。)
(您可以在此处使用一些技巧,例如-m
来“拆分”合并或--first-parent
以避免查看合并提交的第一个父级以外的任何内容。我们也不会在这里介绍它们;它们是对以后合并错误的取证工作更有用,而不是现在实现良好的合并。)
这种“检测重命名”操作对于以下方面很重要:
git diff
,如果你打开它;git log --follow
,我们稍后会描述; 和git merge
,你已经遇到过。 当git merge
运行它的两个git diff
操作时,合并是控制重命名检测的操作。 但是git merge
有命令行选项来告诉它如何控制它:
-X find-renames= number
告诉 Git 使用什么 *rename 阈值;-X no-renames
完全禁用重命名检测。 (在非常旧的 Git 版本中, -X find-renames=...
拼写为-X rename-threshold=...
)
启用重命名检测时,其默认值为50
。 这个值是一个百分比,尽管它的百分比到底是多少有点不确定。 这意味着可能的值范围是从零到 100。然而,零实际上永远不会发生,并且 100 保留用于“文件内容完全匹配”,因此有用的值通常是 1 到 99。默认值50
表示“50% 相似”。
每当 Git 比较两个文件时——我们称它们为 L 和 R,对于左侧和右侧文件——它可以计算“相似度指数”值。 这是基于观察 Git 在尝试确定 R 中保留 L 中的多少字节时所做的观察,以及 R 中的多少字节是全新的,或者需要从 L 中插入新的东西东西,什么的。 实际的计算是模糊的:它没有记录在任何地方。 但是,如果你自己运行git diff --find-renames
,并给它两次提交 hash ID,Git 会告诉你什么相似性索引was. 所以:
git diff --find-renames=01 --name-status L R
将比较提交 L 和提交 R,对于 R 中的每个新文件并从 L 中丢失,尝试猜测该文件是否被重命名。 使用此处的01
值,它将接受低至“相似度指数 1%”(1% 相似度)的匹配并将其作为重命名。 然后--name-status
选项使git diff
仅打印出文件名和状态-es,并且R
这意味着检测到重命名)将跟随实际相似性。
将--diff-filter=R
添加到我们的git diff
使其仅打印R
状态文件,以便我们查看每个检测到的重命名的实际相似性索引值是多少。
如果 Git没有检测到您希望它检测到的重命名,您可以尝试将git merge
与-X find-renames=25
或更小的值合并。 请注意, 2
表示 20%,而不是 2%; 你必须写02
或2%
来表示 2%。
如果 Git正在检测您不希望它检测到的重命名,您可以尝试将您的git merge
与-X find-renames=75
或更大的值,甚至-X no-renames
renames 合并。
git log --follow
注意当使用git log --follow
时,你必须给 Git 一个路径名:
git log --follow path/to/file.ext
Git 在这里所做的是从通常的反向、一次提交遍历一些提交图开始。 假设此时我们有这张图:
I--J
/ \
...--G--H M--N--O <-- somebranch (HEAD)
\ /
K--L
第一个比较是提交N
与提交O
。 Git 通过在内部运行git diff --find-renames NO
来检查名为path/to/file.ext
的文件是否被修改(打开了一些快捷方式以不打扰查看其他文件名)。 如果该文件的副本在N
和O
中都相同,则 Git 将移回N
而不显示提交O
。 如果文件在两个提交中都存在但不同,则 Git 显示提交O
并移回N
。 如果文件在N
中不存在,但从N
到O
的差异可以找到rename ,则 Git 显示提交O
并移回N
,但这一次,因为文件已从old/path/to/file.ext
, Git 现在开始寻找该名称。
所以,如果我们从O
移动到N
,我们现在正在寻找提交N
中的任何名称。 我们现在比较提交M
——一个合并提交:它仍然有一个快照,就像任何其他提交一样——提交N
,以查看文件是否更改和/或更改了名称。 如果它确实更改或更改名称(或两者), git log --follow
显示提交M
; 如果没有,它不会显示M
。
在这里,因为M
是一个合并提交,所以事情变得很棘手。 如果我们没有禁用Git 调用的历史简化功能,则 Git 现在会查看M
的所有父项,以找到文件(无论此时其名称是什么)匹配的任何父项。 如果它找到其中之一,Git 将 go 仅向下合并的那条腿。 这是在行动的历史简化。
您可以使用--full-history
来防止这种情况,但这与 `--find-renames 的交互作用很差,正如我们将看到的那样。
假设名称还没有改变,所以我们仍然有path/to/file.ext
。 这是我们在提交O
、 N
和M
中的文件名。 这也是提交L
中的名称,但在提交K
中,文件不同和/或具有不同的名称。
由于文件在L
中匹配,我们的git log --follow
将从M
走到L
,并将继续查看提交L
,然后K
,然后H
,然后G
,等等。
如果我们添加--full-history
,我们的git log --follow
也会从M
走到J
。 但是,如果它已经从L
到K
的分支,并且文件在L
到K
的转换中被重命名,Git 将在J
和I
提交中寻找错误的文件名!
这里要带走的是:
git log --follow
可以通过选择提交历史的有趣部分来尝试生成一个合成的、减少的“文件历史”。--follow
今天做的--follow 不太好。 它依赖于重命名检测——这并不糟糕,但一开始也不是很好——并在此基础上添加了一些非常草率的假设,这使得它变得更加糟糕。 改进它会很困难(大约十年前我看过这个,从那时起我和其他任何人都没有修复它)。您可能可以通过对重命名检测阈值大惊小怪获得有用的结果,但请注意,您可能希望对文件进行某种更改(即使只是添加一个大注释)以降低重命名相似性索引一点点。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.