简体   繁体   English

你可以从另一个分支中存在的分支中删除Git提交,就像提交的“交集”一样吗?

[英]Can you remove Git commits from a branch that exist in another branch like an “intersection” of commits?

The simple situation I find myself in at times, where I am working on some changes and I create a branch. 我有时会发现自己处于简单的情况,我正在进行一些更改并创建一个分支。 As I move along in my changes I start finding some things that need cleanup or some other partially related thing to change that I want to start getting into. 当我在我的变化中移动时,我开始发现需要清理的一些事情或者我想要开始进入的一些其他部分相关的事情。 So, I want to keep the branches specific, so I quickly spin off another branch to start working on this other set of changes that may not have the same dependencies as the previous branch. 所以,我想保持特定的分支,所以我快速分离另一个分支,开始处理可能与前一个分支没有相同依赖关系的另一组更改。 In the end, I end up with 2 branches where I tried to isolate the changes, however, this 2nd branch originated from the first branch, which in term came from 'master'. 最后,我最终得到了两个分支,我试图隔离这些变化,然而,这个第二个分支起源于第一个分支,其中术语来自“主”。

I can (and have) updated each branch individually by merging 'master' into each of them, and want to position this 2nd branch ready to be merged into 'master' because it has less drastic dependencies than the 1st branch created. 我可以(并且已经)通过将'master'合并到每个分支中来单独更新每个分支,并且希望将第二个分支准备好合并到'master'中,因为它具有比创建的第一个分支更少的依赖性。 However, this branch also contains changes done in the 1st branch since it was spun off of it. 但是,此分支还包含自第一个分支分离后所做的更改。

So I'm wondering, is there a way to tell Git something like: "Remove all the commits that exist in this other branch" So that I'm left with my 2nd branch without all the changes done in the 1st branch, allowing me to merge this 2nd branch into 'master', and let me go back to work on the 1st branch I created. 所以我想知道,有没有办法告诉Git类似:“删除这个其他分支中存在的所有提交”这样我就离开了我的第二个分支而没有在第一个分支中完成所有更改,允许我将第二个分支合并为“master”,让我回到我创建的第一个分支上工作。

It's possible that I'm just not finding the right terminology in Git to see how it can already do that. 我可能只是在Git中找不到合适的术语,看看它是如何做到的。 But also, maybe it can't. 但是,也许它不能。 It would seem like it should be very doable though, seeing how Git is great at showing me only the appropriate diffs between branch 1 and 2 even after I individually update both branches from 'master'. 看起来它应该是非常可行的,看看Git如何很好地向我展示分支1和2之间的适当差异,即使在我从'master'单独更新两个分支之后也是如此。

And "removing" from the branch isn't necessary.. even if the idea is creating yet another branch but that is still somehow excluding the changes that were done in that 1st branch that also are in the 2nd branch would be sufficient. 并且从分支中“删除”是没有必要的..即使这个想法正在创建另一个分支但是仍然以某种方式排除在第一个分支中也在第二个分支中完成的更改就足够了。

Yes, you can do this. 是的,你可以这样做。 In some cases it's even ridiculously easy, as it's just done automatically by git rebase . 在某些情况下,它甚至可笑,因为它只是由git rebase自动完成。 In some cases it's ridiculously hard. 在某些情况下,这是非常艰难的。 Let's take a look at the cases. 我们来看看这些案例。

First, it's crucial, as it almost always is in Git, to draw the commit graph . 首先, 绘制提交图是至关重要的,因为它几乎总是在Git中。 To get there, let's start with reviewing Git basics. 为了实现这一目标,我们首先回顾一下Git的基础知识。 (This is a good idea since a lot of Git tutorials skip right over the basics, as the basics are boring and confusing. :-) ) First, let's look at what a commit is and does for you. (这是一个好主意,因为很多Git教程都跳过了基础知识,因为基础知识很无聊和令人困惑。:-))首先,让我们看一下提交什么,为你做什么。

What a commit is and does for you 什么是承诺对你有用

A commit , in Git, is a totally concrete thing. 在Git中, 提交是一个完全具体的事情。 We can look at any actual commit—most of them are pretty small—not with git show , which fancies them up a lot, but with git cat-file -p , which shows the immediate, raw contents (well, tree objects require minor tweaking, so sometimes "mostly raw") of an actual Git object: 我们可以看一下任何实际的提交 - 其中大多数都非常小 - 不是git show ,它们很喜欢它们,但是使用git cat-file -p ,它显示了直接的原始内容(好吧, tree对象需要较小的实际Git对象的调整,有时“大部分是原始的”):

$ git cat-file -p 3bc53220cb2dcf709f7a027a3f526befd021d858
tree 5654dad720d5b0a8177537390575cd6171c5fc50
parent 3e5c63943d35be1804d302c0393affc4916c3dc3
author Junio C Hamano <gitster@pobox.com> 1488233064 -0800
committer Junio C Hamano <gitster@pobox.com> 1488233064 -0800

First batch after 2.12

Signed-off-by: Junio C Hamano <gitster@pobox.com>

That's one whole commit right there. 那就是整个提交。 Its name—the one name that identifies that commit, from now until forever—is 3bc5322... (some big ugly hash ID that humans never want to deal with if they can avoid it). 它的名称 - 从现在到永远标识提交的一个名称 - 是3bc5322... (一些人类永远不想处理的丑陋哈希ID,如果他们可以避免它)。 It stores several more big ugly hash IDs. 它存储了几个更大的丑陋哈希ID。 One is for a tree , and some number—usually just one, again—are for parents . 一个用于 ,一些数字 - 通常只有一个 - 用于父母 It has an author (name, email address, and time-stamp) and committer, who are usually the same; 它有一个作者(姓名,电子邮件地址和时间戳)和提交者,他们通常是相同的; and it has a log message, which is whatever you want to write. 它有一条日志消息,无论你想写什么。

The tree attached to a commit is a source-tree snapshot. 附连到提交是源树快照。 It's the whole thing , not a set of changes. 这是整个事情 ,而不是一系列变化。 (Underneath, Git does get clever with compression, but the hash ID of the tree gets you hash IDs of files, and those files are the complete files, not some weird compressed thing.) Have Git extract that tree, and you get all the files. (在下面,Git 确实通过压缩变得聪明,但树的哈希ID获取文件的哈希ID,这些文件是完整的文件,而不是一些奇怪的压缩事物。)让Git提取那棵树,然后你得到所有的文件。

Because each commit stores a parent hash ID, we can start with a recent commit and work backwards. 因为每个提交都存储父哈希ID,所以我们可以从最近的提交开始并向后工作。 That's Git for you: backwards. 那是你的Git:倒退。 We start with the hash ID of the most recent commit, which we have Git save for us in a branch name. 我们从最近提交的哈希ID开始,我们在分支名称中为我们保存了Git。 We say that this branch name points to the commit: 我们说这个分支名称指向提交:

<--C   <--master

The name master points to commit C . 名称master指向提交C (I'm using one letter names instead of big ugly hash IDs, which limits me to 26 commits but is a lot more convenient.) Commit C has another hash ID in it though, so C points to another commit. (我使用一个字母名称而不是大丑陋的哈希ID,这限制了我26次提交但更方便。)但是,提交C有另一个哈希ID,所以C指向另一个提交。 That's C 's parent, B . 那是C的父母, B B of course also points to another commit, but let's say our repository only has three commits total, so that B points back to A but A was the first commit. B当然也指向另一个提交,但是假设我们的存储库总共只有三个提交,所以B指回AA是第一个提交。

Since A was the first, it cannot have a parent. 由于A 第一个,它不能有父母。 So it doesn't: it does not point back any further. 所以它没有:它没有进一步指出。 We call A a root commit. 我们称A提交。 Every repository has at least one (and usually only one) root commit. 每个存储库至少有一个(通常只有一个)根提交。 1 That's where the action has to stop: we (or Git) can't go back any further. 1这就是行动必须停止的地方:我们(或Git)不能再回头了。

In any case, commits, once made, are permanent and unchanging. 无论如何,一旦提出,提交是永久性的,不变的。 2 This is because their hash ID is made by computing a cryptographic hash of all of the bits inside the commit (all the stuff you see with git cat-file -p ). 2这是因为它们的哈希ID是通过计算提交中所有的加密哈希来完成的(所有这些都是你用git cat-file -p看到的)。 If you change anything, you get a new and different hash ID. 如果您更改了任何内容,则会获得一个新的不同的哈希ID。 Each hash ID is always unique. 每个哈希ID始终是唯一的。 3 3

So, let's draw this out, but not bother with the internal arrows; 所以,让我们把它画出来,但不要打扰内部箭头; let's just keep the one for the branch name itself. 让我们保留一个用于分支名称本身。

A--B--C  <-- master

Each commit on its own, then, saves a snapshot for you. 然后,每个提交都会为您保存快照。 It's when you assemble them all together with their backwards arrows that you get the commit graph . 当你将它们与它们的向后箭头组合在一起时,就会得到提交图


1 Except, that is, a completely empty repository, which obviously has no commits. 1,除了,一个完全空的存储库,显然没有提交。 That's how you get root commits in the first place, by making a commit with no parent. 这就是你在第一时间获得root提交的方式,通过在没有父级的情况下进行提交。

2 Commits can, however, be garbage collected once you have no use for them. 2然而,一旦你对它们没有用处,它们就可以被垃圾收集 Git normally does this invisibly; Git通常无形地做到这一点; we'll see how that comes about soon. 我们会很快看到它是如何产生的。

3 Pay no attention to that web site behind the curtain! 3不要关注窗帘后面的网站! Seriously, though, the recent breakage of SHA-1 hashing is not an immediate problem for Git , but it does help push Git to switch to SHA-256. 但是,严重的是,最近SHA-1哈希的破坏对Git来说不是一个直接的问题 ,但它确实有助于推动Git切换到SHA-256。


Adding a new commit 添加新提交

Now that we see how the graph looks with three commits, let's add a new commit to master , to see how that works. 现在我们看到图表看起来如何通过三次提交,让我们为master添加一个新的提交,看看它是如何工作的。 First we will git checkout master as usual. 首先,我们将像往常一样git checkout master That fills in the index and the work-tree . 这填写了索引工作树 Then we'll work, git add stuff, and git commit . 然后我们会工作, git add东西,然后git commit

(Reminder: the work-tree is, well, where you do your work. When Git saves files, it lists them under unpronounceable hash-ID names, and stores them compressed, and thus keeps them in a form useful only to Git itself. To use those files, you need them in their normal form, and that's the work-tree. Meanwhile the index is where you and Git build the next commit. You work on files in the work-tree, then you run git add to copy them from the work-tree, into the index. You can git add at any time: that just updates the index from the work-tree again. The index starts out matching the current commit, and then you modify it until you are ready to make a new commit.) (提醒: 工作树就是你工作的地方。当Git保存文件时,它会将它们列在不可发音的哈希ID名称下,并将它们存储为压缩文件,从而将它们保存在仅对Git本身有用的形式中。要使用这些文件,你需要它们的正常形式,那就是工作树。同时索引是你和Git构建下一个提交的地方。你在工作树中处理文件,然后你运行git add来复制它们从工作树到索引。你可以随时git add :只是再次从工作树更新索引。索引开始匹配当前提交,然后你修改它直到你准备好做一个新的提交。)

When you run git commit , Git collects up your log message, then: 当你运行git commit ,Git会收集你的日志消息,然后:

  1. Writes out the index as a new tree : this is your saved snapshot, based on what you replaced in the index from the work-tree. 将索引写为新tree :这是您保存的快照,基于您在工作树索引中替换的内容。 The new tree gets its own hash ID. 新树获得自己的哈希ID。
  2. Writes a new commit object, with this new tree ID, the current commit's ID as its parent , you as the author and committer (and now as the time stamps), and your log message. 使用此新树ID, 当前提交的ID作为parent ,作为作者和提交者(现在作为时间戳)和日志消息写入新的commit对象。

Step 2 gets Git a new hash ID for our new commit; 第2步为Git提供了新提交的新哈希ID; let's call it D . 我们称之为D Since the new commit has C 's hash ID in it, D points back to C : 由于新提交中包含C的哈希ID,因此D指向C

A--B--C     <-- master (HEAD)
       \
        D

The last thing Git does, though, is to write D 's ID into the current branch name . 不过,Git做的最后一件事就是将D的ID写入当前的分支名称 If the current branch is master , this makes master point to D : 如果当前分支是master分支,则master指向D

A--B--C
       \
        D   <-- master (HEAD)

If we git checkout -b some new branch first , though—just before we make the new commit, that is—then look at our new starting setup: 如果我们首先 git checkout -b一些分支 - 尽管 - 在我们进行新提交之前,那就是 - 然后查看我们的新启动设置:

A--B--C     <-- branch (HEAD), master

Both names, branch and master , point to C , but HEAD says that we are on branch branch , not on master . 两个名称, branchmaster指向C ,但是HEAD说我们在分支branch ,而不是在master So when we make D and Git updates the current branch, we get this: 因此,当我们让D和Git更新当前分支时,我们得到:

A--B--C     <-- master
       \
        D   <-- branch (HEAD)

This is how branches grow. 这就是分支增长的方式。 A branch name just points to the tip commit of a branch; 分支名称只指向分支的提示 ; it's the commits themselves that form the graph. 它是构成图形的提交本身。

It's worth stopping at this point and thinking about commits ABC . 在这一点上值得停下来并考虑提交ABC They're on master , for sure. 他们当然是master ,当然。 But they are also on branch branch . 但他们也在 branch A commit, in Git, may be on many branches at the same time. 在Git中,提交可能同时在许多分支上。 What we need to do, quite often, is limit how far back we let Git go when we tell it: "Get me all the commits starting from this branch-tip and working backwards." 我们经常需要做的是限制我们告诉它时我们让Git走了多远:“从这个分支机构开始并向后工作,让我获得所有提交。”

Now for the exciting part! 现在为令人兴奋的部分!

Well, maybe exciting. 好吧,也许令人兴奋。 :-) You have made several branches with a bunch of new commits, so let's draw that: :-)你已经用一堆新的提交做了几个分支,所以让我们画出:

...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

Here, master ends at G , ie, commit G is the tip of master. 这里, masterG处结束,即提交G是主人的提示。 feature1 ends at L , and feature2 ends at Q . feature1L处结束,而feature2Q处结束。 Commits EFG are on all three branches. 提交EFG所有三个分支机构。 Commits PQ are only on feature2 . 提交PQ 仅适用feature2 Commits IJK are on both feature1 and feature2 . 提交IJK同时出现在feature1feature2 Commit L is only on feature1 . 提交L仅适用于feature1

Remember, again, these letters stand in for big ugly hash IDs, where the actual hash ID encodes everything in the commit: the saved tree and the parent IDs. 请记住,这些字母代表了大丑陋的哈希ID,其中实际的哈希ID对提交中的所有内容进行编码:保存的树 parent ID。 So L requires K 's hash ID, for instance. 例如, L 需要 K的哈希ID。 This kind of thing matters because we intend to copy some commits. 这种事情很重要,因为我们打算复制一些提交。

What you described wanting to do is to somehow transplant commits P and Q so that they sit atop master . 你所描述的想要做的是以某种方式移植提交PQ以便它们位于master之上。 What if there were a way to copy commits? 如果有办法复制提交怎么办? It turns out that there is: it's called git cherry-pick . 事实证明,它有:它被称为git cherry-pick

Cherry-picking 采摘樱桃

Remember that we noted earlier that a commit is a snapshot. 请记住,我们之前已经注意到提交一个快照。 It's not a set of changes. 这不是一组变化。 But right now we wish that a commit were a set of changes, because commit P is a lot like its parent commit K , but with some changes made. 但是现在我们希望提交一组更改,因为提交P 很像它的父提交K ,但是做了一些更改。 After all, you made P by having K checked out, then editing files and git add ing the new versions into the index and then git commit ting. 毕竟,你通过K签出来制作P ,然后编辑文件和git add新版本git add到索引中,然后git commit ting。

Fortunately, there's an easy 4 way to turn a snapshot into a changeset, by comparing it ( git diff ) against its parent commit. 幸运的是,把一个快照变成变更,通过比较它 (一个简单的4git diff对其父提交)。 The output from git diff is a minimal 5 set of instructions: "Remove this line from this file, add these other lines to that file, etc." git diff的输出是一组最小的5条指令:“从这个文件中删除这一行,将这些其他行添加到该文件中,等等” Applying those instructions to the tree in K will turn it into the tree in P . 将这些指令应用于K的树将其转换为P的树。

But what happens if we apply those instructions to some other tree? 但是,如果我们将这些指令应用于其他树,会发生什么? As it turns out, this often "just works". 事实证明,这通常“正常”。 We can git checkout commit G —the tip commit of branch master , but let's use a different branch name: 我们可以git checkout提交G branch master的提示,但是让我们使用不同的分支名称:

...--E--F--G                <-- master, temp (HEAD)
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

and then apply the diff to the work-tree. 然后将diff应用于工作树。 We'll assume that goes well, automatically git add the result to the index, and git commit while copying the log message from commit P . 我们假设这很顺利,自动git add结果git add到索引,并在从commit P 复制日志消息时进行git commit We'll call the new commit P' to say "like P , but with a different hash ID" (because it has a different tree, and a different parent): 我们将新的提交P'称为“像P一样,但使用不同的哈希ID”(因为它有不同的树和不同的父):

             P'             <-- temp (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

Now let's repeat this with Q . 现在让我们用Q重复一遍。 We run git diff PQ to turn Q into changes, apply those changes to P' , and commit the result as new Q' : 我们运行git diff PQQ转换为更改,将这些更改应用于P' ,并将结果提交为新的Q'

             P'-Q'          <-- temp (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

This is just the two git cherry-pick steps, plus of course creating the temporary branch. 这只是两个git cherry-pick步骤,当然还有创建临时分支。 But look what happens now if we erase the old name feature2 and change temp to feature2 : 但是看看现在发生了什么,如果我们删除旧名称feature2并将temp更改为feature2

             P'-Q'          <-- feature2 (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   [abandoned]

It now looks like we made feature2 by doing git checkout -b feature2 master , then writing P' and Q' from scratch! 现在看起来我们做出feature2git checkout -b feature2 master ,然后写P'Q'从头开始! That's just what you wanted. 这就是你想要的。


4 Easy, that is, after any number of master's and/or PhD theses on string-to-string edit problems . 4简单,即在字符串到字符串编辑问题上任意数量的硕士和/或博士论文之后。

5 "Minimal" in some sense, and somewhat tweak-able through different diff algorithms. 在某种意义上, 5 “最小”,并通过不同的差异算法进行一些调整。 Minimizing the edit distance is important for compression but not actually for correctness . 最小化编辑距离对于压缩很重要,但实际上不是正确性 However, when we go to apply the edit instructions to some other tree, the minimal-ness, and the exact instructions, really start to matter. 但是,当我们将编辑指令应用于其他树时,最小化和精确指令真正开始变得重要。


Git's rebase is automated cherry-pick plus the branch label moving Git的rebase是自动樱桃选择加上分支标签移动

We can do all of the above at once using: 我们可以使用以下方法一次完成以上所有操作:

git checkout feature2
git rebase --onto master feature1

What we are doing here is using feature1 as a way to tell Git what to stop copying. 我们在这里做的是使用feature1作为告诉Git 停止复制的方法。 Look back at the original graph, before the abandonment of the original commits. 在放弃原始提交之前,请回顾原始图表。 If we tell Git to start at feature1 and work backwards, that identifies commits L , K , J , I , G , F , and so on. 如果我们告诉Git从feature1开始feature1工作,那将标识提交LKJIGF等。 Those are the commits we explicitly say not to copy: commits that are on branch feature1 . 这些是我们明确表示复制的提交:分支feature1上的feature1

Meanwhile, the commits we do want to copy are those on feature2 : Q , P , K , J , and so on. 同时, 我们要复制的提交是那些feature2QPKJ ,等等。 But we stop as soon as we hit any of the forbidden ones, so we'll copy only the PQ commits. 但是一旦我们击中任何被禁止的,我们就会停止,所以我们复制PQ提交。

The place we tell git rebase to copy to is—or is "just after"—the tip of master , ie, copy commits so that they come after G . 我们告诉git rebase复制到的地方是 - 或者是“刚刚” - master的提示,即复制提交以便他们来到G之后。

Git rebase does it all for us, which is ridiculously easy. Git rebase为我们做了一切,这非常容易。 But there could be a hitch—or maybe several. 但可能有一个障碍 - 或者可能是几个。

Solving hitches 解决问题

Let's say that we start out with this as before: 让我们说我们像以前一样开始:

...--E--F--G                <-- master (HEAD)
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

We'd like to rebase feature2 onto master , skipping most of feature1 , but it turns out we need what we changed in commit J too. 我们重订feature2master ,跳过大部分的feature1 ,但事实证明,我们需要什么,我们改变了承诺J了。

We don't need I , or K , or L , just J (plus of course P and Q ). 我们不需要I ,或K ,或L ,只需J (加上当然PQ )。

We can't do this with just git rebase . 我们不能只使用 git rebase来做到这一点。 We may need an explicit git cherry-pick , to copy J . 我们可能需要一个明确的git cherry-pick来复制J But this is Git, so there are lots of ways to do this. 但这是Git,所以有很多方法可以做到这一点。

First, let's look at the explicit-cherry-pick method. 首先,让我们看一下显式樱桃挑选方法。 We'll go ahead and make a new branch and cherry-pick J : 我们将继续创建一个新的分支和挑选J

git checkout -b temp
git cherry-pick <hash-ID-of-J>

Now we have: 现在我们有:

             J'             <-- temp (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

Now we can transplant PQ as before. 现在我们可以像以前一样移植PQ We just change the --onto directive: 我们只需更改--onto指令:

git checkout feature2
git rebase --onto temp feature1

The result is: 结果是:

               P'-Q'        <-- feature2 (HEAD)
              /
             J'             <-- temp
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   [abandoned]

We don't need temp any more at all, so we can just git branch -d temp and straighten out our drawing: 我们根本不需要temp ,所以我们可以git branch -d temp并理顺我们的绘图:

             J'-P'-Q'       <-- feature2 (HEAD)
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   [abandoned]

Another way to get the same result 另一种获得相同结果的方法

Suppose that, instead of copying just PQ , we let git rebase copy IJKPQ . 假设,我们让git rebase复制IJKPQ ,而不是只复制PQ This might actually be easier: 这实际上可能更容易:

git checkout feature2
git rebase master

This time we don't need --onto : master tells Git both which commits to leave out and where to put the copies. 这一次,我们不需要--ontomaster告诉Git的这两个承诺离开了哪里放置副本。 We leave out commit G and earlier, and we copy after G . 我们遗漏了提交G和更早,并且我们在G之后复制。 The result is: 结果是:

             I'-J'-K'-P'-Q'  <-- feature2
            /
...--E--F--G                 <-- master
            \
             I--J--K--L      <-- feature1
                    \
                     P--Q    [abandoned]

Now we have too many commits copied, but now we run: 现在我们复制了太多提交,但现在我们运行:

git rebase -i master

which gives us a bunch of "pick" lines for each commit I' , J' , K' , P' , and Q' . 这给了我们每个提交I'J'K'P'Q'的一堆“选择”行。 We delete the ones for I' and K' . 我们删除I'K' Git now copies again , giving: Git现在再次复制,给出:

             J''-P''-Q''    <-- feature2
            /
...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   [abandoned]

which is what we want (the original copies are still in there, abandoned like the original-original PQ , but they were there for so little time, who cares anyway? :-) ). 这就是我们想要的东西(原始副本仍在那里,像最初的原始PQ一样被抛弃,但他们在那里的时间很短,无论如何都在乎?:-))。 And of course, we can make that first git rebase use -i and remove the pick lines, and just have the J'-P'-Q' copies, all in one step. 当然,我们可以让第一个git rebase使用-i并删除pick线,并且只需一步就可以获得J'-P'-Q'副本。

Eliminating redundant commits 消除冗余提交

That's fine as far as it goes, but now there is both a J and a J' . 就目前而言,这很好,但现在有JJ' Actually there's nothing wrong with this—you can leave this situation in place, and even merge with it like this, with no real harm done. 实际上这没有什么不妥 - 你可以把这种情况留在原地,甚至像这样合并,没有真正的伤害。 But you might want to make J' part of master first and then share it. 但是你可能想让 J'成为master J'一部分然后分享它。

Again, there is more than one way to do this. 同样,有不止一种方法可以做到这一点。 I want to illustrate one particular way, though, because git rebase has some magic in it. 我想说明一种特殊的方式,因为git rebase有一些神奇之处。

Let's say that we have done the feature2 rebasing so that we have this now. 假设我们已经完成了feature2所以现在我们已经完成了。 We'll drop the abandoned commits entirely, just like Git does when it eventually gets around to garbage-collecting them (note: you get at least 30 days by default before this happens, giving you about a month to change your mind): 我们将完全丢弃被遗弃的提交,就像Git最终会进行垃圾收集一样(请注意:在此之前,默认情况下至少会有30天的时间,给你一个月的时间来改变主意):

             J'-P'-Q'     <-- feature2
            /
...--E--F--G              <-- master
            \
             I--J--K--L   <-- feature1

You can now fast-forward master to include J' : 您现在可以快进 master以包含J'

git checkout master
git merge --ff-only <hash-id-of-J'>

This moves the labels , without changing the commit graph . 这会移动标签 ,而不会更改提交图 To make it easy to draw in ASCII text, though, I'll move J' down one row: 但是,为了便于以ASCII文本绘制,我将J'向下移动一行:

                P'-Q'     <-- feature2
               /
...--E--F--G--J'          <-- master
            \
             I--J--K--L   <-- feature1

(We could also get here by explicitly git cherry-pick ing J into master originally, then rebasing feature2 without any fancy footwork.) So now we'd like to copy feature1 's commits, adding them after J' , and removing J . (我们也可以通过明确地到达这里git cherry-pick荷兰国际集团J进入master原,然后重订feature2 ,没有任何花哨的脚法。)所以, 现在我们想复制feature1的承诺,加入后他们J'移除 J

We could do this with another git rebase -i , which lets us explicitly delete the original commit J . 我们可以使用另一个git rebase -i执行此操作,它允许我们显式删除原始提交J But we don't have to . 但我们没有必要 Well, we don't have to, most of the time . 嗯, 大部分时间我们都没有。 Instead, we just run: 相反,我们只运行:

git checkout feature1
git rebase master

This tells Git that it should consider IJKL as the candidates for the copy, and put the copies after J' (where master now points). 这告诉Git它应该将IJKL视为副本的候选者,并将副本放在J' (其中master现在指向)。 But—here's the magic— git rebase looks closely at all 6 of the commits that are on master that are not on feature1 (these are called the upstream commits, in at least a few bits of documentation). 但是 - 这里的魔法git rebase 密切关注 master上所有6个不在feature1 上的提交(这些提交称为上游提交,至少在几个文档中)。 In this case, that's J' itself. 在这种情况下,那就是J'本身。 For each such commit, Git diffs the commit against its parent (a la git cherry-pick ) and turns the result into a patch ID . 对于每个这样的提交,Git将提交与其父级(la git cherry-pick )区分开来,并将结果转换为补丁ID It does the same with each candidate commit . 它对每个候选提交都是一样的 If one of the candidates ( J ) has the same patch ID as one of the upstream commits, Git eliminates the candidate from the list! 如果候选者( J )中的一个具有与上游提交之一相同的补丁ID,则Git从列表中删除候选者!

Hence, as long as both J and J' have the same patch ID, Git automatically drops J , so that the final result is: 因此,只要JJ'都具有相同的补丁ID,Git就会自动丢弃J ,因此最终结果为:

                P'-Q'     <-- feature2
               /
...--E--F--G--J'          <-- master
               \
                I'-K'-L'  <-- feature1

which is just what we wanted. 这正是我们想要的。


6 All, that is, except merges. 6全部,即合并除外。 Rebase literally can't copy merges—a new merge has a different parent-set than the original, and cherry picking "undoes" a merge in the first place—so by default it skips them entirely. Rebase实际上无法复制合并 - 新合并具有与原始不同的父集合,并且樱桃选择首先“解除”合并 - 因此默认情况下它完全跳过它们。 Merges don't get a patch ID assigned, and don't get plucked out of the set because they were never in the set. 合并不会分配补丁ID,也不会从集合中删除,因为它们从未集合中。 It's usually a bad idea to rebase a graph-fragment that contains a merge. 修改包含合并的图形片段通常是个坏主意。

Git does have a mode that tries to do it. Git确实有一种尝试这样做的模式。 This mode re-performs the merges (as it has to: I leave working out the details as an exercise). 此模式重新执行合并(因为它必须:我将详细信息作为练习)。 But there are a bunch of dangers here, so it's usually best not to do this at all. 但这里有一堆危险,所以通常最好不要这样做。 I have said before that probably git rebase should default to "preserving" merges, but by erroring-out if there are merges, requiring either a "yes, go ahead and try to re-create merges" flag, or a "flatten away and remove merges" flag to proceed. 我曾经说过,也许git rebase应该默认为“保存”合并,但示数出是否合并,要求无论是“是的,继续前进,并尝试重新创建合并”标志,或“变平远和删除合并“标志继续。

It doesn't, though, so it's up to you to draw the graph and make sure your rebases make sense. 但事实并非如此,因此您需要绘制图表并确保您的rebase有意义。


When rebase goes wrong: merge conflicts 当rebase出错时:合并冲突

Any time you git rebase some commits, you run the risk of merge conflicts. 任何时候你git rebase一些提交,你冒着合并冲突的风险。 This is particularly true if you are plucking a segment of commits out of a long chain: 如果您从长链中提取一部分提交,则尤其如此:

        o--...--B--1--2--3--4--...--o   <-- topic
       /
...o--*--o--o--o--T                     <-- develop

If we want to "move" (copy, then remove) commits 1-4 into develop , there's a good chance some or all parts of some of those four commits depend, in some way, on the other top-row commits that come before them ( B and earlier). 如果我们想要“移动”(复制,然后删除)将1-4提交到develop ,那么这四个提交中的某些部分或部分部分很可能在某种程度上依赖于之前的其他顶级提交。他们( B和更早)。 When that happens, we tend to get merge conflicts, sometimes many. 当发生这种情况时,我们倾向于发生合并冲突,有时甚至是很多 Git winds up viewing the copy of commit 1 as a three-way merge operation, merging the changes from B to 1 with the changes from B to T . Git最终将提交1的副本视为三向合并操作,将从B1的更改与从BT的更改合并。 The changes "from" B to T may looks quite complex, and may not appear sensible out of context, because we have to "go backwards" through the commits before B down to * and then "forwards" up to T . “从” BT的变化可能看起来相当复杂,并且在上下文中可能看起来不合理,因为我们必须在B之前“向后”通过提交到*然后“转发”到T

It is up to you to figure out how, or even whether it is wise, to do this. 由你决定如何,甚至是否明智,这样做。

When rebase goes wrong: others are still using the originals 当rebase出错时:其他人仍在使用原件

Because rebase is fundamentally a copy operation, you must consider who might still have the original commits. 因为rebase基本上是一个复制操作,所以你必须考虑谁可能仍然拥有原始提交。 Since commits can be on many branches, it may be the case that you have the originals. 由于提交可以在许多分支上,因此可能是拥有原件。 This is what we saw when we had both J and J' , for instance. 例如,当我们同时拥有JJ' ,这就是我们所看到的。

Sometimes—even somewhat often—this may not be a big deal. 有时 - 甚至有点经常 - 这可能不是什么大问题。 Sometimes it is. 有时它是。 If all the extra copies are only in your own repository , you can resolve all this on your own. 如果所有额外副本在您自己的存储库中 ,您可以自行解决所有这些问题。 But what happens if you have published (pushed, or let others fetch from you) some of your commits? 但是如果您发布了某些提交(推送或让其他人从您那里获取)会发生什么? In particular, what if some other repository has those original commits, with their original hash IDs? 特别是,如果某个其他存储库具有原始哈希ID的原始提交,该怎么办? If you have published the original commits, you must tell everyone else who has them: "Hey, I'm abandoning the originals, I have shiny new copies elsewhere." 如果您已经发布了原始提交,您必须告诉其他拥有它们的人:“嘿,我放弃了原件,我在其他地方有新的副本。” You must get them to do the same thing, or else put up with the extra commit copies. 你必须让他们做同样的事情,否则忍受额外的提交副本。

Extra commits are sometimes harmless. 额外提交有时是无害的。 This is particularly true of merges, since git merge works hard to take only one copy of any given change (although Git cannot always get this quite right on its own, since each change—each git diff output—depends on context and other changes, and the minimal-edit-distance algorithms sometimes go wrong themselves, picking the wrong "minimum changes"). 对于合并来说尤其如此,因为git merge很难只获取任何给定更改的一个副本 (尽管Git不能总是自己得到这个,因为每个更改 - 每个git diff输出 - 取决于上下文和其他更改,并且最小编辑距离算法有时会出错,选择错误的“最小变化”。 Even if they do not break the tree , though, they do clutter up the commit history. 但是,即使它们没有打破 ,它们也会使提交历史变得混乱。 Whether and when this might be a problem is hard to predict. 是否以及何时可能成为问题很难预测。

Summary 摘要

For your goals, git rebase is a powerful tool. 为了您的目标, git rebase是一个强大的工具。 It needs a bit of care when using it, and the most important thing to remember is that it copies commits, then abandons—or tries to abandon—the originals. 在使用它时需要一点小心,最重要的是要记住它复制提交,然后放弃 - 或者试图放弃 - 原件。 This can go wrong in several ways, but the worst ones tend to occur when other people already have copies of your original commits, which generally means "when you have published (pushed) them". 这可能在几个方面出错,但最糟糕的情况往往发生在其他人已经拥有原始提交的副本时,这通常意味着“当你发布(推送)它们时”。

Drawing graphs can help. 绘图可以提供帮助。 Everyone should make a habit of drawing their graphs, and/or using git log --graph (get help from "a dog": git log --all --decorate --oneline --graph , A ll D ecorate O neline G raph) and/or graphical browsers like gitk (although I personally hate GUIs in general :-) ). 每个人都应养成绘制图形的习惯,和/或使用git log --graph (从“狗”中获取帮助: git log --all --decorate --oneline --graphA ll D ecorate O neline G raph)和/或像gitk这样的图形浏览器(虽然我个人讨厌GUI一般:-))。 Unfortunately, "real" graphs rapidly get very messy. 不幸的是,“真实”的图表很快变得非常混乱。 Git's built-in log --graph does a poor job separating rats-nest graphs. Git的内置log --graph - log --graph在分离大鼠巢图log --graph做得不好。 There are a lot of ad-hoc tools to deal with this, some built in to Git, but it definitely helps to have a lot of practice reading the graphs. 有很多特殊工具可以解决这个问题,有些工具是内置于Git的,但它确实有助于大量练习阅读图形。

If your history looks like: 如果您的历史记录如下:

...--E--F--G                <-- master
            \
             I--J--K--L     <-- feature1
                    \
                     P--Q   <-- feature2

In the simple case to remove feature1 then do: 在简单的情况下删除feature1然后执行:

git checkout feature2
git rebase --onto master feature1

This is an abbreviation of @torek 's answer which is phenominal, but hard to find the actual answer to the question. 这是@torek的答案的缩写,这是一个@torek的,但很难找到问题的实际答案。 Read @torek 's answer for more details and what to do in the non-simple cases. 阅读@torek的答案,了解更多细节以及在非简单情况下该怎么做。

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

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