Reputation: 1847
I'm in this situation
\ /
C
| |
B F
| |
[dev]A D
\ /
\ /
M [Master]
Master was at commit D
and this mistake happened: the branch at commit A
has been mistakenly merged into master and pushed. Moreover the commit M does not contain all the changes in A,B,C,...
because has been modified by hand before committing. All these commits have already been pushed.
I need to create a new branch with the same changes as in [dev]
and merge it later into master, at the moment I can't because those commits have already been merged.
Also all the changes in [dev]
comes from various merges from different branches.
Important limitation: my git server does not allow me to force push into master branch, so I cannot reset master branch.
Is there a way to put changes in [dev]
into [master]
after a bad merge [M]
?
EDIT: I am allowed to force push in branches which are not master.
Upvotes: 0
Views: 295
Reputation: 45819
So it sounds like the issues are
1) The merge should not yet have happened
2) The merge was edited in a way that means M
doesn't apply all dev
changes over D
3) Eventually you will want to merge dev
(including A
through C
) into master
4) Force pushes are not allowed on master
We're left with options that aren't great.
To address (1) and (2), you just have to revert M
. Because of (4) you can't rewrite the history of master
, so all methods of returning master
to a state that matches D
are equivalent to git revert
. That resolves (1) and makes (2) moot.
But now (3) is a problem. Most solutions involve a force-push to dev
; while you can't force-push master
, you haven't indicated either way whether rewriting dev
history is allowed. (If not, bear with me anyway, because I'll be building a solution for that case on top of this...)
If rewriting dev
history is ok, then the question is simply how detailed a history you want in the replacement branch (remembering that the original history will still appear, whether you want it or not, before the commit where you revert M
).
The simplest option I can think of (but the one with the least historical fidelity on the new branch) would look like
git checkout D
git branch -f dev
git merge --squash C
git commit -m "Replacement dev branch"
git push -f
Note that if you did want to manually edit the result of the "squash merge", you could do it after the merge
command but before the commit
command; but again, my understanding is that the edits done previously were part of the problem...
So this creates a single new commit rooted at D
, introducing the same changes relative to D
as a merge of C
, but without the "2nd parent" relationship of a real merge - which means git will "forget" that these changes are the same as the changes you already merged into master
.
\ /
C
| |
B F
| |
A D
\ / \
\ / \
M CBA [Dev]
|
W [Master]
In this diagram, the TREE
(content) at W
matches D
, and CBA
matches what M
would've been if it hadn't been incorrectly edited. You could have rooted CBA
at the original branch point of dev
instead of at D
(that's controlled by the checkout
command); only difference is whether existing conflicts should be resolved now or later, given that the history is going to be "off" either way.
If you have more specific requirements for what the "new history" of dev
should look like, let us know and I can suggest a procedure that might come closer to meeting them.
In any event, because this rewrites history on dev
, it is best done when nobody has unpushed changes on dev
(otherwise they'll have to be rebased to the new dev
branch). And even then, everybody will have to force the update of their local dev
branch. See "recovering from upstream rebase" in the git rebase
docs, because that's essentially what has to happen.
Which brings me to the other possibility: if you can't (or don't want to) rewrite dev
history either, then extra steps are needed to ensure that dev
is always moving in a "normal" way (as far as the remote sees).
git checkout D
git branch -f dev
git merge --squash C
git commit -m "Replacement dev branch"
git rebase master
git push
Note that although we move the dev
branch, we later rebase
it back to a descendant of origin/dev
; so no force-push is needed. If this seems unnerving, you can create a temp branch for the squash merge and then do a simple ff merge to advance dev
after the rebase
, but the result is the same.
\ /
C
| |
B F
| |
A D
\ / \
\ / \
M CBA
|
W [Master]
/
/
CBA' [Dev]
Note that CBA
is unreachable and will eventually be garbage-collected; I show it here for no particular reason. The new dev
is at CBA'
- a rewrite of CBA
.
Again, if you have specific requirements for how the history should look between W
and CBA'
let us know and we can try to work out a procedure - but again, remember that the original history is still visible before M
anyway.
Upvotes: 1
Reputation: 1732
You could revert merge, than revert merge's revert commit, than reset last commit, and choose manually which patches to apply.
git revert -m 1 <commit id>
git revert HEAD
git reset HEAD~1
Than you would have all unstaged changes from merge commit available to edit with GUI or with:
git add -i
With this you could interactively apply or reset patches. Or just edit by hand in files.
Upvotes: 0
Reputation: 616
After merge when you checkout into your branch and give the below mentioned comment into you putty, where you last correct commit id put into the #commit_id place. Warning - once you will back into the previous code you cannot retrieve last merge code.
git revert #commit id
Upvotes: 0
Reputation: 930
A mangled merge-commit is always difficult to fix, since you can't (fully) revert a merge to start over. (You can revert the changes it brought in, but re-merging will not re-introduce these changes). Usually, the easiest solution is to rewrite history and start over, but I understand that that isn't possible.
The next best thing is to create a fixup commit that brings the state to the way you want it. If I understood your question correctly, the problem is that the merge was not "fully" done (because of edits in the merge-phase), and you actually want the full merge to be in master.
In that case, I would:
# Undo the merge, locally
git checkout master
git reset --hard D # the commitID from your graph, just before the merge
# Re-do the merge correctly, locally
git merge dev
N=$(git rev-parse HEAD); echo $N # print as reference, call this commit N
# restore to upstream state
git reset --hard origin/master
Now you need to create a new commit on top of master M'
, that brings M
to the desired state N
.
I have a solution for that part of the problem, but I'm not particularly proud of it. There are probably better ways to do this, but here it goes:
# Figure out the tree-ID of the wanted state:
TREE_ID=$( git cat-file -p $N | grep '^tree' | sed 's/tree //' ); echo $TREE_ID
# Generate a commit with that tree ID. I.e. all files will be exactly as
# they are in commit N: everything you did in M to mangle the merge will be undone
COMMIT_ID=$( echo "Fixup merge" | git commit-tree $TREE_ID -p HEAD ); echo $COMMIT_ID
# ^^^^ this will be the commit message; change if needed
# pull the generated commit in to the master branch
git reset --hard $COMMIT_ID
Upvotes: 0