Git 变基同时在一个分支中维护文件的最新版本

Git rebase while maintaining the latest version of a file in one branch

我的本地分支中有一个文件,我希望能够变基 origin/main,同时确保变基后我本地分支中的该文件与现在的文件完全相同。

有没有办法进行变基并保证这一点?如果在变基期间我不必回答任何问题或解决此文件的任何冲突,那就更好了。

我的一个解决方法是将文件复制粘贴到便签本,运行 使用 -Xours 进行变基,然后从我的便签本粘贴最终结果。

我不是很喜欢这个解决方案(如果我们讨论的是多个处于冲突状态的文件,它也不会一概而论)但它似乎是最快的前进方式。

TL;DR

使用临时标记来标记具有所需文件副本的提交。然后,使用 git rebase -i 并在每个 pick 之后插入 x 命令到 运行 一个简短的脚本。您可以选择在此脚本中准确地放入什么,但这(未经测试)可能是您想要的:

#! /bin/sh
git checkout temp-tag -- path
git diff-index --quiet HEAD || git commit --amend --no-edit

一旦这一切完成,删除临时标签(和脚本;它并不难写,而且它有标签和路径硬编码)。

要理解这个答案,首先要记住这个事实:在 Git 中,文件实际上并不在 分支 中。文件确实在 commits.

提交包含在分支中——或者换句话说,通过使用分支名称找到,然后通过 Git 存储在每个提交中的链接从提交工作到提交,向后工作。因此,您可以从分支名称转到提交,然后再转到文件。但是“提交”步骤很关键,因为每个提交都有每个文件的完整快照

接下来,让我们看看git rebase是做什么的以及它是如何做的。请记住 Git 是关于 提交 ,并且每个提交都有一个唯一的哈希 ID。任何现有提交的任何部分都不能更改。因此,由于字面上的变基 不能 更改任何 现有 提交,它必须通过 copying 旧的(和糟糕的,或者至少在某种程度上不充分的)提交新的和改进的提交。这些新的和改进的提交在某些方面与旧提交相同,但在某些方面有所不同。

根据其唯一哈希 ID 找到的每个提交都包含两部分:

  • 这是提交的主要数据:与此提交一起使用的源代码快照。这些不是 更改 。快照中的每个文件 与它应该出现的完全一样 如果稍后检出该特定提交。

  • 除了数据之外,每个提交都有一些元数据,或者关于提交本身的信息:谁做的(姓名和电子邮件地址),什么时候(日期和时间戳),等等。

    元数据将“提交者”分为两部分:作者 是最初提交者的姓名、电子邮件和时间戳,committer 是做出此提交变体的人的姓名、电子邮件和时间戳。所以当我们像这样复制一个旧的提交时,我们保留了原作者,但设置了一个新的提交者。如果您正在复制自己的提交,这意味着名称和电子邮件并没有真正改变——旧的有你,新的有你——但是 committer time -邮票 做改变。

    不过,最重要的是,每个提交都会记录其先前或 父级 提交的哈希 ID。变基的要点通常是像这样进行一串提交:

              I--J--K   <-- feature
             /
    ...--G--H--L   <-- mainline
    

    并制作提交 IJK 的新改进版本,以便新提交来自 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)查找提交是使用 分支名称 来查找链中的 last 提交。当提交 Hmainlinelast 提交时,我们找到它是因为我们有:

...--G--H   <-- mainline

namemainline 持有提交 H 的哈希 ID。所以 git checkout mainline 会提取提交 H 供我们使用或工作 on/with。但是后来我们,或者某人,做了一个新的提交,上添加到mainline,我们称之为提交L,所以我们有:

...--G--H--L   <-- mainline

namemainline 现在保存提交 L 的哈希 ID。 git checkout mainline 命令将提取提交 L 供我们使用。为了 找到 提交 H,我们必须让 Git 打开提交 L 并读取它的元数据。此元数据包含早期提交的原始哈希 ID H

这对我们来说意味着一旦我们完成了这个:

          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 通过使用 name feature 找到提交 K'。提交 K' 指向更早的提交 J',后者指向 I',,后者指向 L。一旦我们移动分支名称,我们的 rebase 将完成,并扔掉我们在 building I'-J'-K' 序列时可能使用的任何时髦的特殊名称。

(练习:提交 I-J-K 会发生什么?这重要吗?我们怎么知道它们是否仍在存储库中?)

考虑到之前和之后的情况,让我们看看 git rebase 是如何工作的

我在上面相当简短地提到,git rebase 使用 git cherry-pick 来复制每个提交。反过来,cherry-pick 命令的工作方式是......好吧,从技术上讲,它是一个成熟的三向合并,但更容易 查看 它,首先,通过查看当我们只比较 两个 提交时会发生什么。

让我们从这张“之前”的照片开始:

          I--J--K   <-- feature
         /
...--G--H--L   <-- mainline

我们需要 Git 检出 提交 L,这是我们希望新提交去的地方。如果我们以正常方式执行此操作,我们将创建一个新的分支名称,例如 tmp,使用:

git checkout -b tmp <hash-of-L>

(或与 Git 2.23 或更高版本中的 git switch 命令相同)。 Git 实际上使用它所谓的 detached HEAD 模式,特殊名称 HEAD 直接指向提交:

git checkout <hash-of-L>

或:

git switch --detach <hash-of-L>

产生这个:

             I--J--K   <-- feature
            /
   ...--G--H--L   <-- HEAD, mainline

现在 Git 运行s git cherry-pick <em>hash-of-I</em> . Git 在整个设置过程中保存了提交 IJK 的哈希 ID。如果您在此处使用 git rebase --interactive,您将看到列出这些哈希 ID 的 pick 命令。1 pick 代表一个 cherry-pick 命令。

cherry-pick 本身结束时将提交 H 中保存的快照与提交 I 中保存的快照进行比较。这两个快照之间的 差异 实际上是一组指令,这些指令也可以应用于 快照。将该指令集应用于 H 中的快照会生成 I 中的快照。但是如果我们将这些指令应用到 L 中的快照呢?

如果我们这样做——假设它有效并且没有合并冲突2——并根据结果进行新的提交,我们将得到提交I'.我们将 Git 原样保存原始作者信息和原始提交消息,并生成一组新的提交者信息并使用我们通过 diff 得到的 snapshot .结果是:

             I--J--K   <-- feature
            /
   ...--G--H--L   <-- mainline
               \
                I'  <-- HEAD

Git 现在继续做一个 git cherry-pick <em>hash-of-J</em>,通过比较 I-vs-J 并将其应用于 I':

来复制提交 J
             I--J--K   <-- feature
            /
   ...--G--H--L   <-- mainline
               \
                I'-J'  <-- HEAD

最后——因为只有三个提交——我们最后一次挑选提交 K,比较 J-vs-K(和 J-vs-J' 如果你对 cherry-pick 的合并方面感兴趣)构建提交 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 的指令 sheet 的哈希 ID 已缩写。我一直不太清楚为什么:Git 必须将它们扩展回来才能在内部使用它们。也许 Git 的人只是认为当有 7 或 12 个看起来随机的字符而不是 40 个时,他们看起来不那么吓人。对于 git describe 输出,这可能会出现在某人的电子邮件或其他东西中,当然 - 但在这里,它们只是临时页面上的说明,如果您编辑它们,您可以在编辑器中使用“移动行”说明。

2合并冲突(如果有的话)也来自比较 H 中的快照与 L 中的快照。至少,第一次挑选就是这种情况。随后的两个 cherry-picks 使用提交 IJ 作为合并基础,--ours 提交是在 previous 步骤中构建的提交.这就是一切变得有点棘手的地方。


你想要什么

我相信您想要的是,在每次挑选之后,您希望 new(已复制)中的某些特定文件与某些特定文件中的某些特定文件完全匹配较早提交。

让我们假设现有提交 K 具有所需的文件版本。我们要做的是——为了避免依赖 Git 不移动名称 feature,并让你选择任何提交——是创建一个临时的轻量级标签来标识这个提交:

git tag temp-tag <hash-of-K-or-whatever>

注意:如果没有一个固定版本的文件应该进入每个复制的提交,您将需要一个不同的策略来定位源提交 checkout,但其余的可以继续工作。

接下来,我们将使用 git rebase -i。这会将精选集变成可编辑的指令 sheet。使用我们的编辑器,在 each pick 命令之后,我们使用 execx 命令添加一行:

pick <hash>
x /tmp/script

(假设我们的小脚本已放入 /tmp/script 并可执行)。

Git 将执行 cherry-pick 命令,一直执行到完成,这涉及进行新的提交(I'J'K'在我们的例子中)。然后它将 运行 由于此 x 行的脚本。剧本:

  1. 从特定提交中提取特定文件:使用 temp-tag,我们从所需提交中获取所需文件,将其放入 Git 的索引和工作树。 (索引副本很重要,但最好也更新工作树,为了理智起见。)

  2. 测试结果是否值得替换提示提交 (git commit --amend)。这是我们的git diff-index --quiet HEAD。如果索引仍然与当前提交匹配,则无需更改。否则,我们将 运行 git commit --amend,这会将当前提交推开并创建一个新提交。使用 --no-edit,我们告诉 git commit 简单地重新使用现有的提交消息。

    注意:在这种情况下,即使没有任何变化,git commit --amend --no-edit实际上是安全的,但这是白费力气。对于这个脚本和任务,这可能不是真正相关的,但不执行很多不必要的工作似乎很好。

因此,这将确保在变基期间每个替换提交本身都被替换,并使用“更正”的替换将单个文件换出到我们想要的文件。这样,当 Git 开始将分支名称从旧分支中拉出来并将其放在替换提交的末尾时,每个替换提交都是实际需要的新的和改进的提交。

除了清理(删除轻量级 temp-tag 标签和脚本)之外,不需要做任何其他事情。