Mikhail Mikhail - 3 days ago 4
Git Question

How to override base during merge?

The problem



See the image. I had a
master
branch. At commit
A
, I branched
dev
branch from it. At point
B
I synced
dev
with
master
, creating
M1
. At point
C
our team branched
release
branch from it. In future, I will need to merge
dev
back to
release
.

Unfortunately, I accidentally merged commit
D
from
master
to my
dev
branch, creating
M2
. Now I cannot merge
dev
to
release
since it contains commits
C..D
, which belong to
master
and should not go to
release
.

My development is not over, and I'm not going to merge
dev
to
release
right now. However, I want to stay synced and merge
release
to my
dev
branch. I expect such merges to happen several more times in future, before I finish development and merge
dev
to
release
.

Thus, at some point I would need to revert
M2
from
dev
. I want to do it as early as possible, since commits from
release
may conflict with changes in
M2
. Remember that some of these changes do not exist in
release
.

Since I want to revert
M2
as early as possible, I would like to do it before I merge from
release
to
dev
. That's where the problem actually begins. Thank you for reading to this point :)

Problem

I can revert
M2
in
dev
, this is not a problem. However, after that, when I merge
release
to
dev
, git computes merge base as
C
. While I want merge base to be
B
, to pretend like merge
M2
just didn't happen at all. Because of this incorrect base, changes
B..C
are actually automatically discarded by the merge. Git thinks, I manually removed them, since this is what he sees in revert commit of
M2
!

Let me clarify this. Imagine someone created file
foo.txt
in
C
. This file will be added to
dev
with
M2
. And it will be removed from
dev
once I revert
M2
. When I merge
release
to
dev
, git sees
foo.txt
in
release
, it sees
foo.txt
in base commit
C
, but it does not see
foo.txt
in
dev
. Thus git thinks I removed
foo.txt
. But I didn't do it.

There would be no problem if I specified
B
as the merge base. Is there a way to do it in git?

My solution



Since I found no way to override merge base, I did a little hack. I post it here because I had another related question.

See second image. I started new local branch
tmp
from
E
, commit before
M2
. I cherry-picked to this branch all changes from
dev
except for
M2
. I merged
release
to
tmp
and commited result
M3
with only one parent from
tmp
(note dashed line on the image).

I reverted
M2
in
dev
, commit
G
. I created "fake" merge from
release
to
dev
. This commit contained no changes, but had two parents: from both
dev
and
release
. I then cherry-picked
M3
to
dev
and squashed it with my empty fake merge. I thus created
M4
, with correct changes and with correct parents.

enter image description here

The question is: did I actually need this "fake" merge? Maybe there is a way to cherry-pick
M3
to
dev
and make it to have two parents?

Questions



I'll summarize questions here:


  • Is there a way to manually set base during merge?

  • Is there a way to manually specify parents for a commit?



Thank you if you were able to read through this!

Update



As I realized after the discussion, my solution (or any other solution to the stated problem) has a crucial defect. As I've said, at some point I will merge
dev
to
release
. As I have not said, at some point after that we will merge
release
to
master
. And at this point we will face exactly the same problem. The base for this merge will be resolved to
D
, not to
C
. And this will lead to the similar problem, but at a much greater scale.

Thus, the best solution to this problem is to continue development in
tmp
, and make it the new
dev
, effectively excluding bad merge
M2
from the history.

Answer

I'm pretty sure the short answer for both your bottom line questions is no:

  • You definitely can't manually set a base for a commit, and in any case, that doesn't make any sense. How would GIT know to relate existing commits in your 'us' branch to those in the other base? (suppose for example it's on another branch out after C, just to make it hard).
  • This is kind of possible using git reset, though it's a workaround. You git reset <wanted base>. All the changes between your current commit and the new base are uncommitted in the work area, so you can make a new commit that you want (though you lose some tree information you may have wanted).

In any case you can stich your dev branch around M1 to get rid of it:

git checkout dev
git rebase -i <commit prior to M1 or even to A>

The -i will allow to remove the merge commit.

EDIT

Your workaround's final commit in dev may seem like what you want, but you have to remember in git, if there's a line below connecting to you then those commits are a part of you as well. What you did was elaborately rebased to delete M2, so commits C..D would not be taken into account in any parent.

If you could tell git to just merge dev torelease based at B, then the following I think is a good estimate of what would happen:

  1. GIT sees B..C are the shared, and just skips them.
  2. Additions between M1 and M2 are merged to dev
  3. Still have M2 here! so merge C..D to dev. Doesn't matter at all that you started from B, you will get here!
  4. and continue the rest.

There is no getting around C..D if M2 exists in the hierarchy. But, you can skip it by reverting or rebasing, or doing that crazy branch bypass you did.

Comments