[英]Git rebase while maintaining the latest version of a file in one branch
我在我的本地分支中有一个文件,我希望能够对origin/main
进行变基,同时确保在变基之后我本地分支中的这个文件将与现在完全相同。
有没有办法做一个变基并保证? 如果在变基期间我不必回答任何问题或解决此文件的任何冲突,那就更好了。
使用临时标记来标记具有所需文件副本的提交。 然后,使用git rebase -i
和 insert x
命令在每次pick
后运行一个简短的脚本。 您可以准确地选择要放入此脚本中的内容,但这(未经测试)可能是您想要的:
#! /bin/sh
git checkout temp-tag -- path
git diff-index --quiet HEAD || git commit --amend --no-edit
一旦这一切都完成了,删除临时标签(和脚本;这并不难写,而且它有标签和路径硬编码)。
要理解这个答案,首先要记住这个事实:在 Git 中,文件实际上并不在分支中。 文件确实在commits中。
提交包含在分支中——或者换句话说,通过使用分支名称找到,然后通过 Git 在每个提交中存储的链接从提交工作到提交,向后。 因此,您可以 go 从分支名称提交,然后提交文件。 但是“提交”这一步很关键,因为每次提交都有每个文件的完整快照。
接下来我们看看git rebase
是干什么的,是怎么做的。 请记住 Git 是关于提交的,每个提交都有一个唯一的 hash ID。 任何现有提交的任何部分都不能更改。 因此,由于 rebase 从字面上看不能更改任何现有提交,因此它必须通过将旧的(和糟糕的,或者至少在某种程度上不充分的)提交复制到新的和改进的提交来工作。 这些新的和改进的提交在某些方面与旧提交相同,但在某些方面有所不同。
根据其唯一的 hash ID 找到的每个提交都包含两个部分:
有提交的主要数据:与此提交一起使用的源代码快照。 这些不是变化。 如果稍后检出一个特定的提交,快照中的每个文件都与它应该出现的完全一样。
除了数据之外,每个提交都有一些元数据,或者关于提交本身的信息:提交人(名称和 email 地址)、时间(日期和时间戳)等等。
元数据将“谁进行了此提交”分为两部分:作者是最初提交者的姓名 email 和时间戳,提交者是姓名 email 以及进行此变体的人的时间戳提交。 所以当我们像这样复制一个旧的提交时,我们保留了原作者,但设置了一个新的提交者。 如果您正在复制自己的提交,这意味着名称和电子邮件并没有真正改变——旧的有你两个,新的有你两个——但提交者时间戳确实改变了。
不过,最重要的是,每个提交都会记录其先前或父提交的 hash ID。 变基的要点通常是像这样进行一串提交:
I--J--K <-- feature /...--G--H--L <-- mainline
并制作提交I
、 J
和K
的新版本和改进版本,以便新提交来自L
而不是H
:
I--J--K <-- feature /...--G--H--L <-- mainline \ I'-J'-K' <-- new-and-improved-feature
其中提交I'
是提交I
的“副本”(某种程度上), J'
是J
的副本,而K'
是K
的副本。
无需过多担心复制过程的机制——尽管我会在这里提到它使用git cherry-pick
让我们做最后一个观察,即我们(和 Git)查找提交的方式是使用分支名称找到链中的最后一次提交。 当提交H
是mainline
的最后一次提交时,我们发现它是因为我们有:
...--G--H <-- mainline
名称mainline
持有提交H
的 hash ID。 因此git checkout mainline
将提取提交H
供我们使用或处理/处理。 但是后来我们,或者某人,做了一个新的提交,添加到mainline
,我们称之为提交L
,所以我们有:
...--G--H--L <-- mainline
名称mainline
现在拥有提交L
的 hash ID。 git checkout mainline
命令将提取提交L
供我们使用。 为了找到提交H
,我们必须让 Git 打开提交L
并读取它的元数据。 此元数据包含早期提交H
的原始 hash ID。
这对我们来说意味着一旦我们完成了这个:
I--J--K <-- feature
/
...--G--H--L <-- mainline
\
I'-J'-K' <-- new-and-improved-feature
我们可以从提交K
中删除名称feature
并将其粘贴到提交K'
上,如下所示:
I--J--K ???
/
...--G--H--L <-- mainline
\
I'-J'-K' <-- feature
现在,当我们尝试查看分支feature
上有哪些提交时,我们将从 Git 开始,使用名称feature
来定位提交K'
。 提交K'
指向早期的提交J'
,后者指向I',
后者指向L
。 一旦我们移动分支名称,我们的 rebase 将完成,并丢弃我们在构建I'-J'-K'
序列时可能使用的任何时髦的特殊名称。
(练习:提交IJK
会发生什么?这重要吗?我们怎么知道它们是否仍在存储库中?)
git rebase
是如何工作的我在上面相当简短地提到, git rebase
使用git cherry-pick
来复制每个提交。 反过来,cherry-pick 命令的工作方式是……好吧,从技术上讲,它是一个成熟的三向合并,但首先通过查看我们仅比较两个提交时发生的情况,可以更容易地看到它。
让我们从这张“之前”的照片开始:
I--J--K <-- feature
/
...--G--H--L <-- mainline
我们需要让 Git检出提交L
,这是我们想要新提交 go 的地方。如果我们以正常方式执行此操作,我们将创建一个新的分支名称,例如tmp
,使用:
git checkout -b tmp <hash-of-L>
(或与 Git 2.23 或更高版本中的git switch
命令相同)。 Git 实际上为此使用了所谓的分离 HEAD模式,特殊名称HEAD
直接指向提交:
git checkout <hash-of-L>
要么:
git switch --detach <hash-of-L>
产生这个:
I--J--K <-- feature
/
...--G--H--L <-- HEAD, mainline
现在 Git 运行git cherry-pick hash-of-I
。 Git 在整个设置过程中保存了提交I
、 J
和K
的 hash ID。 如果您在此处使用git rebase --interactive
,您将看到列出这些 hash ID 的pick
命令。 1 pick
表示 cherry-pick 命令。
cherry-pick 本身最终会将提交H
中保存的快照与提交I
中保存的快照进行比较。 这两个快照之间的区别实际上是一组也可以应用于快照的指令。 将该指令集应用于H
中的快照会生成I
中的快照。 但是,如果我们将这些指令应用于L
中的快照呢?
如果我们这样做 - 并假设它有效并且没有合并冲突2 - 并根据结果进行新的提交,我们将获得提交I'
。 我们将 Git 原样保存原始作者信息和原始提交消息,并生成一组新的提交者信息并使用我们通过应用 diff 获得的快照。 结果是:
I--J--K <-- feature
/
...--G--H--L <-- mainline
\
I' <-- HEAD
Git 现在继续执行git cherry-pick hash-of-J
,通过比较I
-vs- J
并将其应用于I'
来复制提交J
:
I--J--K <-- feature
/
...--G--H--L <-- mainline
\
I'-J' <-- HEAD
最后——因为只有三个提交——我们最后一次选择提交K
,它比较J
-vs- K
(如果你对 cherry-pick 的合并方面感兴趣的话,还有J
-vs- J'
)来构建提交K'
,这给我们留下了这个:
I--J--K <-- feature
/
...--G--H--L <-- mainline
\
I'-J'-K' <-- HEAD
剩下的唯一任务是将名称feature
移动到指向当前提交K'
以获取:
I--J--K ???
/
...--G--H--L <-- mainline
\
I'-J'-K' <-- feature (HEAD)
这样就完成了变基过程。
1您要编辑的git rebase
的说明表具有缩写的 hash ID。 我一直不太清楚为什么:Git 必须将它们扩展回来才能在内部使用它们。 也许 Git 的人只是认为当有 7 或 12 个看起来随机的字符而不是 40 个时,他们看起来不那么令人生畏。对于git describe
output,这可能是 go 在某人的 email 或其他东西中,当然只是 - 但在这里,他们的说明一个临时页面,如果你编辑它们,你可以在你的编辑器中使用“移动行”指令。
2合并冲突(如果有的话)也来自比较H
中的快照与L
中的快照。 至少,第一次挑选就是这种情况。 随后的两个 cherry-picks 使用提交I
和J
作为合并基础, --ours
提交是在上一步中构建的提交。 这就是一切变得有点棘手的地方。
我相信您想要的是,在每次挑选之后,您希望新(复制)中的某些特定文件与某些特定的早期提交中的某些特定文件完全匹配。
假设现有提交K
具有所需的文件版本。 我们要做的是——为了避免依赖 Git 不移动名称feature
,并让你选择任何提交——是创建一个临时的轻量级标签来标识这个提交:
git tag temp-tag <hash-of-K-or-whatever>
注意:如果没有一个固定版本的文件应该 go 到每个复制的提交中,您将需要一个不同的策略来定位checkout
的源提交,但 rest 可以继续工作。
接下来,我们将使用git rebase -i
。 这会将精选集变成可编辑的说明书。 使用我们的编辑器,在每个pick
命令之后,我们使用exec
或x
命令添加一行:
pick <hash>
x /tmp/script
(假设我们的小脚本已放入/tmp/script
并可执行)。
Git 将执行 cherry-pick 命令,一直执行到完成,这涉及进行新的提交(在我们的示例中为I'
、 J'
或K'
)。 然后它会因为这个x
行而运行脚本。 剧本:
从特定提交中提取特定文件:使用temp-tag
,我们从所需提交中获取所需文件,并将其放入 Git 的索引和工作树中。 (索引副本是最重要的,但最好也更新工作树,如果没有别的,为了理智起见。)
测试结果是否值得替换提示提交 ( git commit --amend
)。 这是我们的git diff-index --quiet HEAD
。 如果索引仍然与当前提交匹配,则无需更改。 否则,我们将运行git commit --amend
,它将当前提交推开并创建一个新提交。 使用--no-edit
,我们告诉git commit
简单地重新使用现有的提交消息。
注意:在这种情况下,即使没有任何变化, git commit --amend --no-edit
其实也是安全的,只是白费力气。 对于这个脚本和任务,这可能并不真正相关,但不执行大量不必要的工作似乎很好。
因此,这将确保在变基期间每个替换提交本身都被替换,并使用“更正”的替换将单个文件换出到我们想要的文件。 这样,当 Git 开始将分支名称从旧分支中拉出来并将其放在替换提交的末尾时,每个替换提交都是实际需要的新的和改进的提交。
除了清理(删除轻量级temp-tag
标签和删除脚本)之外,不需要做任何其他事情。
一种解决方法是将文件复制粘贴到暂存器,使用-Xours
运行变基,然后从暂存器粘贴最终结果。
我真的不喜欢这种解决方案(如果我们讨论的是多个处于冲突状态的文件,它也不会一概而论)但它似乎是最快的前进方式。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.