Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

git: what is the correct merging or rebasing workflow to modify a maintenance branch and apply those patches to another branch?

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.
like image 395
Brett Bonner Avatar asked Apr 12 '13 14:04

Brett Bonner


People also ask

Should I merge or rebase feature branch?

For individuals, rebasing makes a lot of sense. If you want to see the history completely same as it happened, you should use merge. Merge preserves history whereas rebase rewrites it . Rebasing is better to streamline a complex history, you are able to change the commit history by interactive rebase.

What is the difference between merging and rebasing in git?

Git merge is a command that allows you to merge branches from Git. Git rebase is a command that allows developers to integrate changes from one branch to another. In Git Merge logs will be showing the complete history of the merging of commits.

What is an alternative to merging in git rebasing?

While merging is definitely the easiest and most common way to integrate changes, it's not the only one: "Rebase" is an alternative means of integration.


2 Answers

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.

like image 99
eddiemoya Avatar answered Oct 25 '22 19:10

eddiemoya


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 : )

like image 41
Nick Tomlin Avatar answered Oct 25 '22 17:10

Nick Tomlin