2

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?
10
  • 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. Commented Jul 28, 2022 at 10:00
  • @matt Use brains, agreed. I also doubt the two features to be developed in parallel. But as I stated, this happens when we are rushing for a deadline (I wasn't convinced either). Everyone is quite tired in the whole process, mistake happens. diff3 is something new for me, I will try it. Commented Jul 28, 2022 at 10:04
  • You'll find that it makes a huge difference. But parallel code can be problematic even without a merge conflict; I've seen Git automerge get things wrong. Commented Jul 28, 2022 at 10:34
  • 1
    @Leo : by the way, it's a very nice thing that you managed to extract and share a section of repository which displays this issue. congratulations for that. Commented Jul 28, 2022 at 17:36
  • 1
    That was actually covered in LeGEC's answer: "external tools such as kdiff3 or meld". A merge conflict GUI simply consults the :1, :2, and :3 versions of the file, as I said in my answer. Commented Jul 29, 2022 at 2:47

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

Comments

3

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, + }" + 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 - reserve-keyword + placeholder="selectnode" size="mini" - placeholder="keyword" class="item-width" popper-class="exp-list-filter-select" - :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" class="filter-form-advanced"> - <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> <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 with the second diff (that's what happens without some form of "ignore-space-changes), 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).

2 Comments

I don't know if there is an option to instruct github to try to merge with -X ignore-space-change
On GitHub, adding URL query w=1 turns ignore space on. You can remove the query, but that doesn't affect the online conflict resolve editor. I tried diff3 as Matt suggested, it does work.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.