At high level, it looks like that this approach should have worked.
My guess is that you think git merge rollback
replays the two commits unique to rollback
onto the current branch (main
). If git did that, the first commit would do nothing since main
already has that change. The second commit would restore the properties file to its old state. This is what you expected. But this is not how git merge
works.
git merge
does not replay commits
What git merge
does by default is called a three-way merge. As explained in an answer to another question:
You might be best off looking for a description of a 3-way merge algorithm. A high-level description would go something like this:
- Find a suitable merge base
B
- a version of the file that is an ancestor of both of the new versions (X
and Y
), and usually the most recent such base (although there are cases where it will have to go back further, which is one of the features of git
s default recursive
merge)
- Perform diffs of
X
with B
and Y
with B
.
- Walk through the change blocks identified in the two diffs. If both sides introduce the same change in the same spot, accept either one; if one introduces a change and the other leaves that region alone, introduce the change in the final; if both introduce changes in a spot, but they don't match, mark a conflict to be resolved manually.
The git merge docs has a less detailed explanation, but explains exactly the behavior that is confusing you:
With the strategies that use 3-way merge (including the default, ort), if a change is made on both branches, but later reverted on one of the branches, that change will be present in the merged result; some people find this behavior confusing. It occurs because only the heads and the merge base are considered when performing a merge, not the individual commits. The merge algorithm therefore considers the reverted change as no change at all, and substitutes the changed version instead.
I believe the reason does this is for a few reasons:
- 99% of the time, merging is about parallel lines of development, working on different things, and you want to merge both those things. Thus you don't want to, by default, give priority to changes made on one branch over the other. If
rollback
were a feature branch, you wouldn't want it to just override the changes made by impl
.
- 99.9% when a developer one branch makes a change and subsequently undoes it, the intent is "Pretend I never did that". So the behavior that confuses some people most of the time does what people expect.
- You can achieve the other behavior by specifying a different merge strategy, e.g.
git merge -s ort -X theirs rollaback
, or by using other commands like cherry-pick
or rebase
, depending on what you exactly want.
solutions #1: git revert
Nix the rollback
branch and just use git revert
:
git checkout main
git revert main..IMPL
This is simplest.
solutions #2: branch rollback
from impl
If for some reason you must do it via a PR, or for whatever reason must have a rollback
branch, then branch rollback
off of impl
, not master
:
git checkout impl
git checkout -b rollback
git revert main..IMPL
It will look like this:
1 main
\
2 impl
\
3 rollback
merging impl
to deploy:
1---4 main
\ /
2 impl
\
3 rollback
merging rollback
to rollback:
1---4-------5 main
\ / /
2 impl /
\ /
3---- rollback
Not only will app.properties
be reverted as you expected, the git history is simpler AND a better representation of what happened. Compare it to the result of your current approach, which has two identical, redundant commits (2 and 3), and misleadingly splits into three branches at the beginning.
2 rollout
/ \
1---5---6 main
\ /
3---4 rollback
It looks even better if you allow fast-forward
merges.
1 main
\
2 impl
\
3 rollback
Deploy ends up like this:
1---2 main, impl
\
3 rollback
and rollback like this:
impl
1---2---3 main, rollback