Search code examples
gitgit-mergegit-diffgit-merge-conflict

Change loss in resolving git merge conflict


I just encountered this issue for the third time in my life as a coder. This time I decided to find the root cause, so I spend some time to produce a minimum case.

https://github.com/myst729/git-diff-merge-loss

The steps:

  1. Two features checkout their own branches (a and b) respectively, from the same base master.

  2. The change of feature A has been merged: https://github.com/myst729/git-diff-merge-loss/pull/3/files?w=1

    1. Delete a block of code (line 34-57)
    2. Move a block of code (line 61-95) to the position where the previous block was (from line 34)

    enter image description here

  3. Feature B has only one line changed. The point is, this line (line 70) is within the moved block in feature A https://github.com/myst729/git-diff-merge-loss/pull/4/files

    enter image description here

  4. Feature B cannot be merged unless conflict resolved. However, line 70 in feature B has been moved to line 43 in master now. It's not even inside the conflict block. See line 43 in https://github.com/myst729/git-diff-merge-loss/pull/4/conflicts , or run git merge master locally on branch b

    enter image description here

This is a minimum case, but in real development, the feature may be more complicated, thus the change may be accidentally excluded when resolving conflicts. And ideally, two features that have dependent relationship should not be developed in parallel. But sometimes we have to form a special task force and rush for a deadline.

So the questions are:

  1. Why does the diff shift happen?
  2. How to avoid the code loss, as it's outside the conflict block and completely unnoticed?

Solution

  • Just to supplement my earlier comments, I'm going to turn them into an answer. What I said was:

    You just have to use your brains when you resolve the conflict. However, you'd use them a lot better if you had a better view of what's happened! Configure your merge conflict style to diff3 so that you are shown the original state of affairs.

    To see what I mean, consider first what your merge-conflicted file shows. As you display in your screen shot, on the one hand we have HEAD, which is branch b:

            <el-form-item label="node:" prop="orgIdList">
              <org-tree-cascader
                :onlyAuthorized="true"
                :defaultProp="{
                  expandTrigger: ExpandTrigger.HOVER,
                  multiple: true,
                  value: 'id',
                  label: 'name',
                  emitPath: false,
                  checkStrictly: false,
                }"
                class="item-width"
                placeholder="selectnode"
                @updateOrgListValue="(handleUpdateOrgIdList as any)"
                ref="orgTreeCascaderRef"
              ></org-tree-cascader>
              <!-- <el-select
                v-model="state.form.orgList"
                :loading="state.orgLoading"
                multiple
                collapse-tags
                clearable
                placeholder="selectnode"
                size="mini"
                class="item-width"
                popper-class="exp-list-filter-select"
              >
                <el-option
                  v-for="item in state.orgList"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                ></el-option>
              </el-select> -->
            </el-form-item>
    

    On the other hand, we have master, which is empty. This greatly reduces the likelihood that a human being is going to notice what has happened.

    What has happened?

    The LCA

    In the LCA (the commit from which b "split off" from master), there is only one occurrence of the entry for

    <el-form-item label="node:" prop="orgIdList">
    

    It is at line 61, and its checkStrictly is true.

    b

    In b, into which we are merging, there is also only one occurrence of the entry for

    <el-form-item label="node:" prop="orgIdList">
    

    It is also at line 61, but it differs from the LCA in the value of checkStrictly, which is now false.

    master

    In master, which we are merging, there is still only one occurrence of

    <el-form-item label="node:" prop="orgIdList">
    

    but it is at line 34, and its checkStrictly is unchanged from the LCA.

    So what happened?

    What happened may thus be easily summarized: b changed one line of the group, but master moved the whole group.

    How do I know?

    Ah. It's because I know how to "ask questions" when we are paused during a merge conflict. In particular, I can say:

    % git show :1:index.vue # the LCA version
    % git show :2:index.vue # the `b` version
    % git show :3:index.vue # the `master` version
    

    How to discover what's happened?

    So far, so good; but the practical problem, as you rightly say, is that the display of the merge-conflicted file itself, which I displayed at the start of this answer, is not very enlightening. In the merge-conflicted file, the line

    <el-form-item label="node:" prop="orgIdList">
    

    occurs twice — once at line 34, where master has it, and again in the display of HEAD, showing where b has it. But, as you rightly complain, the chances of a human being noticing this and working out what has happened, or even realizing that anything interesting has happened, seem very slim.

    This is because, in part, your eye is drawn to the conflict area — and line 34, which is sort of the giveaway here, is not in that area.

    Display more information!

    But now let's say you have configured merge.conflictStyle as diff3. Then the LCA, which was not present in your version of the conflicted file, is present! Here is the entire display of the conflicted region in diff3 style:

    <<<<<<< HEAD
                <el-form-item label="node:" prop="orgIdList">
                  <org-tree-cascader
                    :onlyAuthorized="true"
                    :defaultProp="{
                      expandTrigger: ExpandTrigger.HOVER,
                      multiple: true,
                      value: 'id',
                      label: 'name',
                      emitPath: false,
                      checkStrictly: false,
                    }"
                    class="item-width"
                    placeholder="selectnode"
                    @updateOrgListValue="(handleUpdateOrgIdList as any)"
                    ref="orgTreeCascaderRef"
                  ></org-tree-cascader>
                  <!-- <el-select
                    v-model="state.form.orgList"
                    :loading="state.orgLoading"
                    multiple
                    collapse-tags
                    clearable
                    placeholder="selectnode"
                    size="mini"
                    class="item-width"
                    popper-class="exp-list-filter-select"
                  >
                    <el-option
                      v-for="item in state.orgList"
                      :key="item.value"
                      :label="item.label"
                      :value="item.value"
                    ></el-option>
                  </el-select> -->
                </el-form-item>
    ||||||| 1e59f94
                <el-form-item label="node:" prop="orgIdList">
                  <org-tree-cascader
                    :onlyAuthorized="true"
                    :defaultProp="{
                      expandTrigger: ExpandTrigger.HOVER,
                      multiple: true,
                      value: 'id',
                      label: 'name',
                      emitPath: false,
                      checkStrictly: true,
                    }"
                    class="item-width"
                    placeholder="selectnode"
                    @updateOrgListValue="(handleUpdateOrgIdList as any)"
                    ref="orgTreeCascaderRef"
                  ></org-tree-cascader>
                  <!-- <el-select
                    v-model="state.form.orgList"
                    :loading="state.orgLoading"
                    multiple
                    collapse-tags
                    clearable
                    placeholder="selectnode"
                    size="mini"
                    class="item-width"
                    popper-class="exp-list-filter-select"
                  >
                    <el-option
                      v-for="item in state.orgList"
                      :key="item.value"
                      :label="item.label"
                      :value="item.value"
                    ></el-option>
                  </el-select> -->
                </el-form-item>
    =======
    >>>>>>> master
    

    Between the display of b at the top and master (still empty) at the bottom, we now have the LCA. And thus we can now see clearly what happened between the LCA and b: the value of checkStrictly has changed.

    We still do not necessarily realize what happened with master; it looks like it simply deleted this stretch, whereas in fact it moved it, and it now appears at line 34, which is not in the conflicted area. But we do understand why there's a conflict! One side changed this text, the other side "deleted" it. That was not at all obvious before, because we didn't know what the initial state of things was.

    But we are now a bit more likely to discover this, because the line

    <el-form-item label="node:" prop="orgIdList">
    

    now appears three times: once in the b version, once in the LCA version, and once at line 34! That fact should be enough to get us thinking.

    Braaaaaains

    And that brings us back to my original comment. The best tool for working out what to do now is to use your brains. You have two things to decide: where should the <el-form-item label="node:" prop="orgIdList"> entry go, and what should the value of its checkStrictly be? That is the problem that Git has set us; it's a true conflict, a decision that Git cannot perform automatically on its own.

    The point is merely to gather enough information so that you know that that is the decision you have to make. What I'm suggesting is that with your display of the merge conflict, you are unlikely to work it out. With diff3, you are much more likely, and you could then use git show, as I demonstrated earlier, to put your finger on the history of what has happened and decide how to resolve it.

    One more thing

    Maybe I'm burying the lede here... Keep in mind that moving code is not a "thing" in Git's idea of a diff. If you move a line from one place to another distant place in the same file, that's two separate hunks: one where a new line appeared, and one where a line was deleted.

    So in this case, nothing is going to get Git to see the code at line 34 as part of the merge conflict. It just isn't! It's a different area of the file from the conflict area — a place where some new lines appeared, that's all. And reading the conflicted file is never going to call this to your attention, as it is just not where the conflict is.

    On the other hand, you, a human, can see quite well what happened if you say git diff b...master. That diff will show you how code appeared at line 34 and disappeared at line 61, and you, the human, will know what this means: the code was moved.