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.
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.
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.
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.
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.
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.
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.
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
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 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.
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.
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.
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.
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 : )
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With