[英]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.
如果在变基期间我不必回答任何问题或解决此文件的任何冲突,那就更好了。
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).一旦这一切都完成了,删除临时标签(和脚本;这并不难写,而且它有标签和路径硬编码)。
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
:并制作提交
I
、 J
和K
的新版本和改进版本,以便新提交来自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:当提交
H
是mainline
的最后一次提交时,我们发现它是因为我们有:
...--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
会发生什么?这重要吗?我们怎么知道它们是否仍在存储库中?)
git rebase
worksgit 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 在整个设置过程中保存了提交
I
、 J
和K
的 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 使用提交
I
和J
作为合并基础, --ours
提交是在上一步中构建的提交。 This is where it all gets a little tricky.这就是一切变得有点棘手的地方。
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
命令之后,我们使用exec
或x
命令添加一行:
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:剧本:
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.) (索引副本是最重要的,但最好也更新工作树,如果没有别的,为了理智起见。)
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.