简体   繁体   English

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

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

I have a file in my local branch and I want to be able to rebase origin/main while making sure that after the rebase this file in my local branch will be the exact same as it is right now.我在我的本地分支中有一个文件,我希望能够对origin/main进行变基,同时确保在变基之后我本地分支中的这个文件将与现在完全相同。

Is there a way to do a rebase and guarantee that?有没有办法做一个变基并保证? Even better if during the rebase I don't have to answer any questions or resolve any conflicts for this file.如果在变基期间我不必回答任何问题或解决此文件的任何冲突,那就更好了。

TL;DR长话短说

Use a temporary tag to mark a commit that has the desired copy of the file.使用临时标记来标记具有所需文件副本的提交。 Then, use git rebase -i and insert x commands to run a short script after each pick .然后,使用git rebase -i和 insert x命令在每次pick后运行一个简短的脚本。 You have a choice of what, precisely, to put in this script, but this (untested) might be what you want:您可以准确地选择要放入此脚本中的内容,但这(未经测试)可能是您想要的:

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

Once this is all done, remove the temporary tag (and the script; it's not like it was difficult to write, and it has the tag and path hardcoded).一旦这一切都完成了,删除临时标签(和脚本;这并不难写,而且它有标签和路径硬编码)。

Long

To make sense of this answer, start by memorizing this fact: in Git, files aren't really in branches .要理解这个答案,首先要记住这个事实:在 Git 中,文件实际上并不在分支中。 Files are really in commits .文件确实在commits中。

Commits are contained in branches—or in other words, found by using branch names, then working from commit to commit, backwards, through the links that Git stores in each commit.提交包含在分支中——或者换句话说,通过使用分支名称找到,然后通过 Git 在每个提交中存储的链接从提交工作到提交,向后。 So you can go from branch name to commit and thence to file.因此,您可以 go 从分支名称提交,然后提交文件。 But that "to commit" step is critical, because each commit has a full snapshot of every file .但是“提交”这一步很关键,因为每次提交都有每个文件的完整快照

Next, let's look at what git rebase does and how it does it.接下来我们看看git rebase是干什么的,是怎么做的。 Remember that Git is all about commits , and each commit has a unique hash ID.请记住 Git 是关于提交的,每个提交都有一个唯一的 hash ID。 No part of any existing commit can ever be changed.任何现有提交的任何部分都不能更改。 So, since rebase literally can't change any of the existing commits, it necessarily has to work by copying the old (and lousy, or at least inadequate in some way) commits to new-and-improved commits.因此,由于 rebase 从字面上看不能更改任何现有提交,因此它必须通过将旧的(和糟糕的,或者至少在某种程度上不充分的)提交复制到新的和改进的提交来工作。 These new-and-improved commits are the same as the old commits in some way, and different in some way.这些新的和改进的提交在某些方面与旧提交相同,但在某些方面有所不同。

Each commit, as found by its unique hash ID, has two parts:根据其唯一的 hash ID 找到的每个提交都包含两个部分:

  • There's the main data of a commit: the source code snapshot that goes with this commit.有提交的主要数据:与此提交一起使用的源代码快照。 These aren't changes .这些不是变化 The snapshot has each file exactly as it should appear if that one particular commit is checked out later.如果稍后检出一个特定的提交,快照中的每个文件都与它应该出现的完全一样

  • Besides the data, each commit has some metadata , or information about the commit itself: who made it (name and email address), when (date and time stamp), and so on.除了数据之外,每个提交都有一些元数据,或者关于提交本身的信息:提交人(名称和 email 地址)、时间(日期和时间戳)等等。

    The metadata separate the "who made this commit" into two parts: the author is the name, email, and timestamp from whoever made the commit originally, and the committer is the name, email, and timestamp of the person who made this variant of the commit.元数据将“谁进行了此提交”分为两部分:作者是最初提交者的姓名 email 和时间戳,提交者是姓名 email 以及进行此变体的人的时间戳提交。 So when we copy an old commit like this, we retain the original author, but set up a new committer.所以当我们像这样复制一个旧的提交时,我们保留了原作者,但设置了一个新的提交者。 If you're copying your own commits, this means that the name-and-email doesn't really change—the old one had you as both, and the new one has you as both—but the committer time-stamps do change.如果您正在复制自己的提交,这意味着名称和电子邮件并没有真正改变——旧的有你两个,新的有你两个——但提交者时间戳确实改变了。

    Most importantly, though, each commit records the hash ID of its previous or parent commit.不过,最重要的是,每个提交都会记录其先前或提交的 hash ID。 The point of rebasing is typically to take a string of commits like this:变基的要点通常是像这样进行一串提交:

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

    and make new-and-improved versions of commits I , J , and K , so that the new commits descend from L rather than from H :并制作提交IJK的新版本和改进版本,以便新提交来自L而不是H

     I--J--K <-- feature /...--G--H--L <-- mainline \ I'-J'-K' <-- new-and-improved-feature

    where commit I' is a "copy" (sort of) of commit I , J' is a copy of J , and K' is a copy of K .其中提交I'是提交I的“副本”(某种程度上), J'J的副本,而K'K的副本。

Without worrying too much about the mechanics of the copying process—though I'll mention here that it uses git cherry-pick —let's make one last observation, which is that the way we (and Git) find commits is to use the branch name to find the last commit in the chain.无需过多担心复制过程的机制——尽管我会在这里提到它使用git cherry-pick让我们做最后一个观察,即我们(和 Git)查找提交的方式是使用分支名称找到链中的最后一次提交。 When commit H was the last commit of mainline , we found it because we had:当提交Hmainline最后一次提交时,我们发现它是因为我们有:

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

The name mainline held the hash ID of commit H .名称mainline持有提交H的 hash ID。 So git checkout mainline would extract commit H for us to use or work on/with.因此git checkout mainline将提取提交H供我们使用或处理/处理。 But then we, or someone, made a new commit that added on to mainline , which we are calling commit L , so that we have:但是后来我们,或者某人,做了一个新的提交,添加mainline ,我们称之为提交L ,所以我们有:

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

The name mainline now holds the hash ID of commit L .名称mainline现在拥有提交L的 hash ID。 A git checkout mainline command will extract commit L for us to use. git checkout mainline命令将提取提交L供我们使用。 To even find commit H , we have to have Git open up commit L and read its metadata.为了找到提交H ,我们必须让 Git 打开提交L并读取它的元数据。 This metadata contains the raw hash ID of earlier commit H .此元数据包含早期提交H的原始 hash ID。

What this means for us is that once we have accomplished this:这对我们来说意味着一旦我们完成了这个:

          I--J--K   <-- feature
         /
...--G--H--L   <-- mainline
            \
             I'-J'-K'   <-- new-and-improved-feature

we can take the name feature off commit K and paste it onto commit K' instead , like this:我们可以从提交K中删除名称feature并将其粘贴到提交K',如下所示:

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

Now, when we try to see what commits are on branch feature , we'll have Git start by using the name feature to locate commit K' .现在,当我们尝试查看分支feature上有哪些提交时,我们将从 Git 开始,使用名称feature来定位提交K' Commit K' points back to earlier commit J' , which points back to I', which points back to L .提交K'指向早期的提交J' ,后者指向I',后者指向L Our rebase will be complete once we move the branch name, and toss out any funky special name that we might have been using while building the I'-J'-K' sequence.一旦我们移动分支名称,我们的 rebase 将完成,并丢弃我们在构建I'-J'-K'序列时可能使用的任何时髦的特殊名称。

(Exercise: What happens to commits IJK ? Does it matter? How would we even know if they're still in the repository?) (练习:提交IJK会发生什么?这重要吗?我们怎么知道它们是否仍在存储库中?)

With the before-and-after in mind, let's look at how git rebase works考虑到之前和之后的情况,让我们看看git rebase是如何工作的

I mentioned above, rather briefly, that git rebase uses git cherry-pick to copy each commit.我在上面相当简短地提到, git rebase使用git cherry-pick来复制每个提交。 The cherry-pick command, in turn, works by... well, technically it's a full-blown three-way merge, but it's easier to see it, at first, by looking at what happens when we compare just two commits.反过来,cherry-pick 命令的工作方式是……好吧,从技术上讲,它是一个成熟的三向合并,但首先通过查看我们仅比较两个提交时发生的情况,可以更容易地看到它。

Let's start with this, our "before" picture:让我们从这张“之前”的照片开始:

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

We need to have Git check out commit L , which is where we want to have the new commits go. If we were doing this the normal way, we'd make a new branch name such as tmp , using:我们需要让 Git检出提交L ,这是我们想要新提交 go 的地方。如果我们以正常方式执行此操作,我们将创建一个新的分支名称,例如tmp ,使用:

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

(or the same with the git switch command in Git 2.23 or later). (或与 Git 2.23 或更高版本中的git switch命令相同)。 Git actually uses what it calls detached HEAD mode for this, with the special name HEAD pointing directly to a commit: Git 实际上为此使用了所谓的分离 HEAD模式,特殊名称HEAD直接指向提交:

git checkout <hash-of-L>

or:要么:

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

which produces this:产生这个:

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

Now Git runs git cherry-pick hash-of-I .现在 Git 运行git cherry-pick hash-of-I Git saved the hash IDs of commits I , J , and K during the whole setup process. Git 在整个设置过程中保存了提交IJK的 hash ID。 If you use git rebase --interactive here, you'll see pick commands that list these hash IDs.如果您在此处使用git rebase --interactive ,您将看到列出这些 hash ID 的pick命令。 1 The pick represents a cherry-pick command. 1 pick表示 cherry-pick 命令。

The cherry-pick itself winds up comparing the saved snapshot in commit H against the saved snapshot in commit I . cherry-pick 本身最终会将提交H中保存的快照与提交I中保存的快照进行比较。 The difference between these two snapshots is, in effect, a set of instructions that can be applied to a snapshot as well.这两个快照之间的区别实际上是一组也可以应用于快照的指令。 Applying that set of instructions to the snapshot in H produces the snapshot in I .将该指令集应用于H中的快照会生成I中的快照。 But what if we apply these instructions to the snapshot in L ?但是,如果我们将这些指令应用于L中的快照呢?

If we do just that—and assuming it works and has no merge conflicts 2 —and make a new commit from the result, we'll get commit I' .如果我们这样做 - 并假设它有效并且没有合并冲突2 - 并根据结果进行新的提交,我们将获得提交I' We will have Git save the original author information and the original commit message as-is, and generate a new set of committer information and use the snapshot we got by applying the diff.我们将 Git 原样保存原始作者信息和原始提交消息,并生成一组新的提交者信息并使用我们通过应用 diff 获得的快照 The result is:结果是:

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

Git now goes on to do a git cherry-pick hash-of-J , to copy commit J by comparing I -vs- J and applying this to I' : Git 现在继续执行git cherry-pick hash-of-J ,通过比较I -vs- J并将其应用于I'来复制提交J

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

Finally—since there are only three commits—we do our last cherry-pick of commit K , which compares J -vs- K (and J -vs- J' if you are interested in the merge aspect of cherry-pick) to build commit K' , which leaves us with this:最后——因为只有三个提交——我们最后一次选择提交K ,它比较J -vs- K (如果你对 cherry-pick 的合并方面感兴趣的话,还有J -vs- J' )来构建提交K' ,这给我们留下了这个:

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

and the only task left is to move the name feature to point to the current commit K' to get:剩下的唯一任务是将名称feature移动到指向当前提交K'以获取:

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

This completes the rebase process.这样就完成了变基过程。


1 The instruction sheet for git rebase , that you get to edit, has the hash IDs abbreviated. 1您要编辑的git rebase的说明表具有缩写的 hash ID。 I've never been quite sure why: Git has to expand them back out to use them internally.我一直不太清楚为什么:Git 必须将它们扩展回来才能在内部使用它们。 Maybe the Git folks just think they look less intimidating when there are 7 or 12 random-looking characters instead of 40. For git describe output, where this might go in someone's email or something, sure—but here, they're just instructions on a temporary page, and if you edit them you can use "move line" instructions in your editor.也许 Git 的人只是认为当有 7 或 12 个看起来随机的字符而不是 40 个时,他们看起来不那么令人生畏。对于git describe output,这可能是 go 在某人的 email 或其他东西中,当然只是 - 但在这里,他们的说明一个临时页面,如果你编辑它们,你可以在你的编辑器中使用“移动行”指令。

2 Merge conflicts, if any, arise from comparing the snapshot in H vs the snapshot in L as well. 2合并冲突(如果有的话)也来自比较H中的快照与L中的快照。 That's the case for the first cherry-pick, at least.至少,第一次挑选就是这种情况。 The two subsequent cherry-picks use commits I and J as the merge bases, with the --ours commits being the commit built in the previous step.随后的两个 cherry-picks 使用提交IJ作为合并基础, --ours提交是在一步中构建的提交。 This is where it all gets a little tricky.这就是一切变得有点棘手的地方。


What you want你想要什么

I believe what you want is that, after each cherry-pick, you'd like some particular file in the new (copied) exactly match some particular file in some particular earlier commit.我相信您想要的是,在每次挑选之后,您希望(复制)中的某些特定文件与某些特定的早期提交中的某些特定文件完全匹配。

Let's assume that existing commit K has the desired version of the file.假设现有提交K具有所需的文件版本。 What we'll do—to avoid depending on Git not moving the name feature , and to let you pick any commit—is to create a temporary lightweight tag identifying this commit:我们要做的是——为了避免依赖 Git 不移动名称feature ,并让你选择任何提交——是创建一个临时的轻量级标签来标识这个提交:

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

Note: if there is not a single fixed version of the file that should go into every copied commit, you'll want a different strategy for locating the source commit for the checkout , but the rest can continue to work.注意:如果没有一个固定版本的文件应该 go 到每个复制的提交中,您将需要一个不同的策略来定位checkout的源提交,但 rest 可以继续工作。

Next, we'll use git rebase -i .接下来,我们将使用git rebase -i This turns the set of cherry-picks into an editable instruction sheet.这会将精选集变成可编辑的说明书。 Using our editor, after each pick command, we add a line using the exec or x command:使用我们的编辑器,在每个pick命令之后,我们使用execx命令添加一行:

pick <hash>
x /tmp/script

(assuming our little script has been put in /tmp/script and made executable). (假设我们的小脚本已放入/tmp/script并可执行)。

Git will execute the cherry-pick command, all the way to its completion, which involves making the new commit ( I' , J' , or K' in our example). Git 将执行 cherry-pick 命令,一直执行到完成,这涉及进行新的提交(在我们的示例中为I'J'K' )。 Then it will run the script because of this x line.然后它会因为这个x行而运行脚本。 The script:剧本:

  1. Extracts a particular file from a particular commit: using temp-tag , we get the desired file from the desired commit, placing it into both Git's index and the working tree.从特定提交中提取特定文件:使用temp-tag ,我们从所需提交中获取所需文件,并将其放入 Git 的索引和工作树中。 (The index copy is the one that matters, but it's good to update the working tree too, for sanity's sake if nothing else.) (索引副本是最重要的,但最好也更新工作树,如果没有别的,为了理智起见。)

  2. Tests to see if the result merits replacing the tip commit ( git commit --amend ).测试结果是否值得替换提示提交 ( git commit --amend )。 This is our git diff-index --quiet HEAD .这是我们的git diff-index --quiet HEAD If the index still matches the current commit, there's nothing to change.如果索引仍然与当前提交匹配,则无需更改。 Otherwise, we'll run git commit --amend , which shoves the current commit out of the way and makes a new one.否则,我们将运行git commit --amend ,它将当前提交推开并创建一个新提交。 Using --no-edit , we tell git commit to simply re-use the existing commit message.使用--no-edit ,我们告诉git commit简单地重新使用现有的提交消息。

    Note: In this case, even if there are no changes, git commit --amend --no-edit is actually safe , but it's wasted effort.注意:在这种情况下,即使没有任何变化, git commit --amend --no-edit其实也是安全的,只是白费力气。 For this script and task, that's probably not really relevant, but it seems good not to perform a lot of unnecessary work.对于这个脚本和任务,这可能并不真正相关,但不执行大量不必要的工作似乎很好。

So, this will make sure that each replacement commit is itself replaced during the rebase, with a "corrected" replacement with the single file swapped out to the one we want.因此,这将确保在变基期间每个替换提交本身都被替换,并使用“更正”的替换将单个文件换出到我们想要的文件。 That way, by the time Git gets around to yanking the branch name off the old branch and putting it onto the end of the replacement commits, each of the replacement commits is the actual desired new-and-improved commit.这样,当 Git 开始将分支名称从旧分支中拉出来并将其放在替换提交的末尾时,每个替换提交都是实际需要的新的和改进的提交。

Aside from cleaning up (removing the lightweight temp-tag tag and removing the script), nothing else needs to be done.除了清理(删除轻量级temp-tag标签和删除脚本)之外,不需要做任何其他事情。

One workaround would be for me to copy-paste the file to a scratch pad, run the rebase with -Xours and then paste over the end result from my scratch pad.一种解决方法是将文件复制粘贴到暂存器,使用-Xours运行变基,然后从暂存器粘贴最终结果。

I don't really like that solution (and it doesn't generalise if we're talking about more than one file under conflict) but it seems like that's the quickest way forward.我真的不喜欢这种解决方案(如果我们讨论的是多个处于冲突状态的文件,它也不会一概而论)但它似乎是最快的前进方式。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM