Search code examples
gitmergecherry-pick

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.


Solution

  • 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.