Nitheesh A S
Nitheesh A S

Reputation: 387

Git rebase with merged commits

Not sure if this is a duplicate, but spent almost a day trying to figure out a neat solution and could not find an answer yet.

My git repo status is something like this now:

      C---D---E  feature-branch
     /
A---B---C---D---X---F  master
                |
         Reverts C and D

I had been working on feature-branch and the commits C & D got merged to master accidentally(complicated story). Others working on the repo reverted back those commits since it breaks the build.

Now, after the commit E, I want to rebase feature-branch with current master. But the problem is that since the commits C & D are already present in the master, those commits are not getting included while rebasing.

From git man page:

If the upstream branch already contains a change you have made (e.g., because you mailed a patch which was applied upstream), then that commit will be skipped. For example, running git rebase master on the following history (in which A' and A introduce the same set of changes, but have different committer information):

      A---B---C topic
     /
D---E---A'---F master

will result in:

               B'---C' topic
              /
D---E---A'---F master

How can I include the commits C & D (probably with a new SHA) now since the changes are reverted back by the commit X?

Upvotes: 3

Views: 726

Answers (3)

Mark Adelsberger
Mark Adelsberger

Reputation: 45659

tl;dr : You can do what you want with a command like

git rebase --onto master feature-branch~3 feature-branch

(where feautre-branch~3 works in the example, but in general would be any expression that resolves to commit B - such as the commit hash for B, etc.)

To see why that works, read on...


First, we should clarify your diagram a bit. You said

I had been working on the feature-branch and the commits C & D got merged to master accidentally(complicated story). Others working on the repo reverted back those commits since it breaks the build.

(emphasis added). Now merge could do one of two things[1], but duplicating commits C and D isn't one of them. The default behavior would come out looking like this:

A -- B -- C -- D -- !CD -- F <-(master)
                \
                 E <-(feature-branch)

Since master apparently was at B at the time of the accidental merge, the default behavior would be a "fast-forward" of master onto the existing D commit. It's then as though feature-branch hand't been created until after D.

(I also made some notation changes. !CD indicates a merge that reverted C and D (without the need for an annotation). And I find this way of drawing the lines to be more readable. But the important point is how C and D are represented...)

The other possibility, if you had used the --no-ff option (or if master had diverged from feature-branch before the merge) would look more like

       C - D -- E <--(feature-branch)
      /     \
A -- B ----- M -- !CD -- F <--(master)

Again merge would not duplicate C or D; instead it would create a "merge commit" M that incorporates changes from C and D into master's history (and makes C and D "reachable" from master).

In both cases, the "merge base" between feature-branch and master, after the merge, is D. So you have to solve two problems: the "duplicate patch id" problem described by the docs; and the merge-base being at a point that excludes your commits - because rebase won't even consider copying commits that are already reachable from the upstream.[2]

Often it's useful to solve just the "merge base" problem. At the end, we'll see why you don't have to for your purposes, but for completeness here's how you would:

First find an expression that resolves to commit B (such as the commit has for B, or feature-branch~3 in the above examples). Use it in a command like

git rebase -f feature-branch~3 feature-branch

This would copy commits C, D, and E so that you have

                 E
                /
A -- B -- C -- D -- !CD -- F <-(master)
      \          
       C' -- D' -- E' <-(feature-branch)

(where E technically still exists but is unreachable so might eventually get destroyed by gc). Of course this assumes the "fast-forward" case; if you had a merge commit, it would look different, but the upshot would be the same.

And that upshot is, you now could merge feature-branch into master without a problem. But if you want to rebase feature-branch to master (in preparation for a fast-forward), then you still have to solve the original problem you pointed out: C' and D' will be skipped as duplicates of C and D.

What you want is to thwart the patch ID duplicate check; and while there doesn't seem to be an actual option for that, you can do it by keeping git from knowing where to look for the duplicates. Instead of giving master as the upstream, give commit B as the upstream; and then rebase --onto master.

git rebase --onto master feature-branch~3 feature-branch

Not only does git have no reason to consider C and D when you give this command, but also because you're no longer using master as the upstream, the master-to-feature-branch merge base no longer matters. You're explicitly saying that B is your upstream, so this solves both problems at once - which is why, as I noted above, you end up not having to worry about the rebase -f command after all.


[1] Actually, there's at least a third thing it could do. If you had specified the --squash option, then it would not do a real merge, but instead would create a single new commit CD on master. This is essentially the same as the merge commit that is normally created, except it doesn't include D as a parent.

       C - D -- E <--(feature-branch)
      /     
A -- B ----- CD -- !CD -- F <--(master)

This is useful for some specific situations, but in general I don't recommend it as git "loses track" of the fact that C and D are already "accounted for" in master. In your situation, that would happen to work out nicely - which is why I know you didn't do it this way and didn't include this possibility in the main text of the answer. But in most cases it makes future interactions between the branches more difficult.


[2] These problems seem similar, but are distinct. In one case C is already itself reachable from the upstream to which you're rebasing; in the other case, it's not, but another commit that introduces the same changes is.

Upvotes: 2

Marlon Abeykoon
Marlon Abeykoon

Reputation: 12465

In the feature-branch execute,

git rebase master

which will rebase on master and result as you have expected by ignoring C D. ( A---B---C---D---X---F--E in feature-branch)

Then do git reset --soft <sha of F> to temporarily get rid of commit E.

Next git stash the reset commit changes (i.e E)

Then in the same feature-branch execute the following,

git cherry-pick <sha of C>
git cherry-pick <sha of D>

Finally git stash pop and do git commit so you will get back your E.

This will do what you expected.

I tested in my machine and below is the result obtained by git log --online in feature-branch

ae4b6bb E commited
eee0a6a D commited
3141961 C commited
a44aa27 F Commited
265da4c Commited X
9e2729d D commited
84a3a9b C commited
8a543ca B commited
4e4a8ca A commited

Upvotes: 1

Pavel Kukushkin
Pavel Kukushkin

Reputation: 11

You can use git cherry-pick for that. In your particular example, the command would then be:

in master# git cherry-pick topic~3..topic

Upvotes: 1

Related Questions