Home > front end >  Change loss in resolving git merge conflict
Change loss in resolving git merge conflict

Time:07-29

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.

enter image description here

  • Feature B has only one line changed. The point is, this line (line 70) is within the moved block in feature A enter image description here

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

    CodePudding user response:

    You have to realize that, in general, tasks such as "computing the diff" or "detecting conflicts" all rely on heuristics: in a commit, you just store the contents before and after your modifications, but not the sequence of editions that lead from one to another.


    In your precise example, it turns out part of the unexpected behavior comes from an indentation change :

    • if you look at the code for index.vue in commit 1e59f94c7, you see that there is an indentation between lines 60 and 61

    • but you don't see it in the diff displayed in the pull request, because line 61 of the original file is matched with an equivalent line with less indentation

    This means that the displayed diff is obtained with --ignore-space-change or one of its variant.

    In a terminal :

    • you will see the same diff as in the merge request if your run :

      git diff --ignore-space-change 1e59f94 9c0adf9
      
    • if you run git diff without the option, you will see a much different diff :

    diff --git a/index.vue b/index.vue
    index bf86463..a2241d7 100644
    --- a/index.vue
        b/index.vue
    @@ -31,68  31,44 @@
                   ></el-option>
                 </el-select>
               </el-form-item>
    -          <el-form-item label="related Feature:" prop="featureIdList">
    -            <el-select
    -              v-model="state.form.featureIdList"
    -              filterable
    -              remote
               <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,
                   }"
                   
                   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
    -              reserve-keyword
                   placeholder="selectnode"
                   size="mini"
    -              placeholder="keyword"
                   
                   popper-
    -              :remote-method="featureRemoteMethod"
    -              :loading="state.featureLoading"
                 >
                   <el-option
    -                v-for="item in state.featureIdList"
                     v-for="item in state.orgList"
                     :key="item.value"
                     :label="item.label"
                     :value="item.value"
                   ></el-option>
    -            </el-select>
                 </el-select> -->
               </el-form-item>
             </div>
             <transition name="filter">
               <div v-show="state.isShowAll" >
    -            <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,
    -                }"
    -                
    -                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"
    -                
    -                popper-
    -              >
    -                <el-option
    -                  v-for="item in state.orgList"
    -                  :key="item.value"
    -                  :label="item.label"
    -                  :value="item.value"
    -                ></el-option>
    -              </el-select> -->
    -            </el-form-item>
                 <el-form-item label="owner:" prop="admin">
                   <el-select
                     v-model="state.form.admin"
    

    This influences how conflicts are detected : if the merge is performed without some form of "ignore-space-change", the line checkStrictly: true -> checkStrictly: false falls within a modified block.

    From a terminal, you can test that :

    # from branch master:
    git merge -X ignore-space-change b
    

    does not trigger conflicts, and produces the code you expect in this specific case -- with some oddities in the indentation.


    As @matt suggested : external tools such as kdiff3 or meld do a better job at figuring this out in your example, but the only general advice regarding merging is : do audit them (with your brains on).

    CodePudding user response:

    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,
                }"
                
                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"
                
                popper-
              >
                <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,
                    }"
                    
                    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"
                    
                    popper-
                  >
                    <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,
                    }"
                    
                    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"
                    
                    popper-
                  >
                    <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.

    • Related