How do I swap the order of two parents of a Git commit?
Actualy, there's a really cool command I learned recently that will do exactly what you want:
git commit-tree -p HEAD^2 -p HEAD^1 -m "Commit message" "HEAD^{tree}"
This will create a new commit based on what is currently HEAD, but pretend that it's parents were HEAD^2,HEAD^1 (note this is the reversed order).
git-commit-tree prints the new revision as output, so you might combine it with a git-reset-hard:
git reset --hard $(git commit-tree -p HEAD^2 -p HEAD^1 -m "New commit message" "HEAD^{tree}")
One way would be to simulate a merge. So how can we do that?
Lets assume you have the something like following commit graph:
* (master) Merge branch 'feature'
|\
| * (feature) feature commit
* | master commit
. .
. .
. .
Keep the changes
We want to merge master
into feature
but we want to keep the changes, so at first we switch to master
, from which we "manually" update our HEAD reference to point at feature
while not changing the working tree.
git checkout master
git symbolic-ref HEAD refs/heads/feature
The symbolic-ref
command is similar to git checkout feature
but doesn't touch the working tree. So all changes from master
remain.
Undo the old merge
Now we have all changes from the merge in the working tree. So we continue with "undoing" the merge by resetting master
. If you don't feel comfortable loosing the reference onto the merge commit you can create a temporary tag or branch.
(If you want to keep the commit message, now is a good time to copy it somewhere save.)
# Optional
git tag tmp master
git branch -f master master^
Now your commit tree should look just like before the merge.
Fake the merge
And here comes the hacky part. We want to trick git into believing that we are currently merging. We can achieve this by manually creating a MERGE_HEAD
file in the .git
folder containing the hash of the commit we want to merge.
So we do this:
git rev-parse master > .git/MERGE_HEAD
If you are using a git bash, git will now tell you that it is currently in the process of merging.
To finish our merge we just have to commit
.
git commit
# Enter your commit message
And it's done. We recreated our merge commit but with swapped parents. So you commit history should now look like this:
* (feature) Merge branch 'master'
|\
| * (master) master commit
* | feature commit
. .
. .
. .
If you need any further information don't hesitate to ask.
Inspired by this answer, I came up with this:
git replace -g HEAD HEAD^2 HEAD^1 &&
git commit --amend &&
git replace -d HEAD@{1}
The first commands switches the two parents in something called a replacement ref, but only stores it locally, and people have called it a hack.
The second command creates a new commit.
The third command deletes the older replacement ref, so it doesn't mess up the other commits depending on that commit.
Update - It's never that easy, is it?
I've recognized a flaw in the original instructions I suggested: when doing a checkout
with path arguments, you might expect a file to be removed from your working path because it isn't in the commit you're checking out; but then you might be surprised...
As with any history rewrite, it's worth noting that you probably shouldn't do this (using any of the methods form any of these answers, including this one) to any merge you've already pushed. That said...
The previous answers are fine, but if you'd like to avoid using (or needing to know) plumbing commands or other git inner workings - and if that's more important to you than having a one-liner like Jared's solution - then here's an alternative:
The overall structure of this solution is similar to zeeker's, but where he uses plumbing commands to manipulate HEAD or "tricks" git into completing a merge, we'll just use porcelain.
So as before we have
* (master) Merge Commit
| \
| * (feature) fixed something
* | older commit on master
Let's begin:
1) Tag the old merge
We'll actually be using this tag. (If you want to write down the abbreviated SHA1 instead, that'll work; but the point here is making the fix user friendly, so...)
git tag badmerge master
Now we have
* (master) [badmerge] Merge Commit
| \
| * (feature) fixed something
* | older commit on master
2) Take the merge out of master's history
git branch -f master master^
Simple enough:
* [badmerge] Merge Commit
| \
| * (feature) fixed something
* | (master) older commit on master
3) Start a new merge
git checkout feature
If we just run git merge master
now, we know the merge will fail; but we don't want to redo the manual conflict resolution. If we had a way to overlay the data from badmerge onto our new merge commit, we'd be all set... and we do!
To start the merge (a) without creating conflict state to be cleaned up, but (b) leaving the merge commit open so we can "fix" it:
git merge --no-commit --strategy=ours master
4) Overlay the already-resolved/merged data from badmerge
Making sure we're in the root directory of the repo we might then do git checkout badmerge -- .
(note that we've provided a path (.) to git checkout
, so it only updates the working tree and the index); but this actually can be a problem.
If the merge resulted in files being deleted (but they're in our working tree right now), then the above command won't get rid of them. So if we're not sure, we have a couple options to be safe from that possibility:
We could first clear our working tree... but we do need to be careful about not wiping out the .git folder, and may need special handling for anything in our ignore file.
In the simple case - .git is the only .-file, nothing being ignored - we could do
rm -rf *
git checkout badmerge -- .
Or, if that seems too risky, another approach is to skip the rm
, do the git checkout badmerge -- .
, and then diff against badmerge to see if anything needs cleaning up.
5) Complete the merge
git commit
git tag -d badmerge