8

Objective: I need to make custom patches to a prior release of an upstream project, and I want to be able to apply those patches to the later version.

Problem: Rebasing from the maintenance branch to a later version creates conflicts with files that have not been modified in the maintenance branch.

Suspicion: I am applying merging or rebasing incorrectly for what I'm trying to accomplish.

Example: This is a logical example of what I want to accomplish, but understand that the commit history between tagged release v1.0 and v2.0 can be hundreds of commits in between.

I fork an upstream repo with multiple tagged releases (v1.0 and v2.0) with a master commit history A thru D. B is the last commit at tag v1.0. C is the last commit at tag v2.0. D is ongoing development.

$ git clone git@server:repo

A<--B(v1.0)<--C(v2.0)<--D Master

I branch off an earlier release tag (v1.0) to make a few custom modifications like so:

$ git checkout v1.0 -b v1.1

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F v2.1

What I now want to do is branch off a later release tag (v2.0) and apply the patch commits I made in the v1.1 branch (G' and H') to the v2.0 branch. I need to preserve the individual commits that I made in v1.0 in the v2.0 log.

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F<--G'<--H' v2.1

Question: What is the correct workflow to apply these changes and avoid conflicts? I have tried many combinations of merge and rebase, (including --onto) yet all fail. git rebase wants to default to a 3-way merge and conflict with unrelated files.

$ git rebase v2.1 v1.1
Falling back to patching base and 3-way merge
...
Failed to merge in the changes.

$ git checkout v2.1
$ git rebase v1.1
Falling back to patching base and 3-way merge
...
Failed to merge in the changes.
Brett Bonner
  • 425
  • 1
  • 4
  • 17

2 Answers2

7

I suggest you edit your question to include the exact command(s) with which you attempted to rebase --onto. That should be the way you accomplish what you are attempting but you may have run the command in such a way to trigger more rebasing than is actually necessary.

If your rebase commands rewrites everything between v1.0 and v.2.0, then this might result in a lot of unnecessary pain if that history includes conflicts resolved through non-fast-forward merges.

In interest of clarity, I've moved the explanation about merge conflicts and rebasing to the bottom of this answer. However that section is simply speculation, it would be helpful to see an example of the rebase --onto that you attempted. Not having that available now, I will provide what I think you should do. With that said, lets get started on your solution.


The Solution


Rebase --onto

I like to read the arguments for --onto backward to understand the better. Read backward, --onto <1> <2> <3> reads as - Take whatever commits are on <3>, that are not on <2>, and apply them to <1>. The commits are not "moved", the are "cloned", so your old commits are still where they were - the rebase --onto simply creates copies of them and applies them after <1>.

Its important to know that after performing a rebase --onto you may end up in a headless state. The new commits are applied as described above, but they do not immediately change the state of your branch. This adds an extra step in the process, but also give you the added security of knowing that the rebase can not break your branch - you will have a chance to review the changed history before applying tose changes to your branch.

Starting with this diagram.

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F v2.1

To get only G and H to follow F, without including E, which seems to be the case according to your description, then you should try the following command.

git rebase --onto F G^ v1.1

I wrote the above assuming as little as I could about the reality if your situation.

This will take whatever commits exist on v1.1 that do not exist on the commit immediately proceeding G, and applies them after F. Since the only commits actually being rewritten are G and H, then there is no reason why you should get any conflicts unrelated to what those two commits changed.


Headless State

As I described above, you may end up in a headless state. This means that you are not in your branch anymore. Your diagram at this point actually looks something like this...

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F v2.1
          \
           G'<--H' (currently checkout out headless state)

As you can see, branch v2.1 is still at F, but you've created a new history of A<--B<--C<--F<--G'<--H'. This is what you wanted, but its not in your v2.1 branch. So now review your history and verify that its what you wanted. Even test it if you want. Once verified you just need to checkout v2.1 and run the following command...

git checkout v2.1
git merge H'

This assumes you have no new commits on v2.1 that are not in H'. To ensure, you may want to use the --ff-only flag on the merge so that it will reject the merge instead of creating a merge commit.

Like I said above, this is an extra step to be aware of, but as a result of this you can reset assured that the git rebase --onto will not make a mess on your actual branch. If you find that the rebase did not work as intended - you can simply checkout v2.1 and see that no harm as been done.


The Result

After your fast-forward merge compeltes, you will have a history that looks like this...

     E<--G<--H v1.1
    /
A<--B<--C<--D Master
        \
         F<--G'<--H' v2.1


Cherry-picking vs Rebase --onto

Wont go into detail about cherry picking, but I want to make clear that the following..

git checkout v2.1
git cherry-pick G^..H

Is completely equivalent too...

git rebase --onto v2.1 G^ H
git checkout v2.1
git reset --hard <hash> <-- were hash is the commit the rebase kicks you into.

Cherry pick has fewer steps, the rebase can be done without checking out the "base", which in both cases here is v2.1. Also as explained bove rebase --onto doesn't directly effect your branch making it easier to recover from if something goes wrong. Both "clone" the commits they bringing onto the base, leaving the originals untouched.


The Problem


The above is a general explanation as to how to achieve what you are asking to do. Below is my suspicion as to why you had the problems you described.


Non-Fast-Forward Conflict Resolution & Rebasing

My guess is that between v1.0 and v2.0, you have some non-fast-forward merges that where used to resolve conflicts. When a conflict is resolved during a non-fast-forward merge, the resolution for that conflict is stored in the merge commit, rather than in the offending commits themselves. The merge commit occurs later in the history at the point of merge, rather than on the conflicting commits themselves.

When you rebase, git steps through each commit individually and recommits it - as a result you will relive all conflicts resulting from a non-fast-forward merge, but the resolution to that conflict is unavailable until later in the history when the merge occurred. Conflicts resolved with non-fast-forward merges are detrimental to your ability to rebase a branch in the future unless your willing to re-resolve all those conflicts one by one.


Your possible mistake

If my guess about your problem is correct, then you might have done the following...

git rebase --onto v1.1 F v1.1

This or some variation of this would result in taking all the commits in F that are not on v1.1 and appending them to the end of v1.1. As explained above this would result in each commit between B and F being re-committed one-by-one. If there are conflicts in there that were resolved with non-fast-forward merges - then you will relive each of those conflicts as the rebase steps though those commits.


Merging instead of Rebasing

Your question title suggests you may be open to simply merging these histories. If your not concerned with linear history, you may simply want to merge v1.1 into F. This shouldn't result in any strange conflicts but it will significantly muddy your history.

eddiemoya
  • 6,713
  • 1
  • 24
  • 34
  • tl;dr But I managed to read to the end of the first section ("Non-Fast-Forward Conflict Resolution & Rebasing"). I must say I have no idea, why you are writing about that? I understand that v1.1 is created by making just a few commits, without any merges and conflicts. If a rebase is executed correctly it should only replay the few commits between v1.0 and v1.1, so all this stuff about replaying merge commits with conflicts resolution is irrelevant. – Marcin Koziński Apr 17 '13 at 17:15
  • And as a side note you write it as if non-fast-forward merges had something to do with conflicts - but it's perfectly normal to have non-FF merge without conflicts. – Marcin Koziński Apr 17 '13 at 17:16
  • 1
    If your not going to read through then Im not sure why your commenting about not understanding. Its hard to me to know what your not understanding because I dont know what part you didnt read. – eddiemoya Apr 17 '13 at 17:46
  • As in explained in the answer - If a conflict is resolved during a non-fast-forward merge, the resolution is stored in the *merge commit*. When you rebase, git wont have that *merge commit* because it occurs at some future point after the actual commits were made. Conflicts resolved through a rebase are resolved within the original commit that has the conflict. Its simply a gotcha of merge-conflicts. Its likely the reason that the OP is running into irrelevant conflicts when rebasing. If there is some way i could make that clearer, by all means let me know.. – eddiemoya Apr 17 '13 at 17:51
  • Regarding your "side note". Yes, non-fast-forward merges dont always result in conflicts. What I am saying that if they do result in conflicts, and you chose to resolve those conflicts in a *merge commit*, then you will end up re-living those conflicts during a rebase, and you wont have the resolution handy because its in a future merge commit. This is all explain **in the answer**. – eddiemoya Apr 17 '13 at 17:54
2

While it may not have the fine-grained control of a rebase, or the ease of a merge, passing a range of commits to git cherry-pick seems better suited to taking individual changes on older branches and playing them onto the current branch.

So, if G and H are the last two commits in v1.1 you should be able to cherry pick them into v2.0 via:

git cherry-pick v1.1~1

(or manually providing the commit hashes)

If you've already tried this, and there are downsides, let me know. I'm still trying to perfect this sort of workflow myself : )

Nick Tomlin
  • 28,402
  • 11
  • 61
  • 90
  • 1
    Thank you. I have been using cherry-pick up until this point but I think one potential downside can be that cherry-picked branches can become further divergent from upstream. – Brett Bonner Apr 13 '13 at 02:34