Repository: microsoft/vscode (TypeScript)
Date: 2022-11-22 14:25:50+00:00
Task type: Bug Fix

Issue:
Auto resolve conflicts when changes are equal
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
- VS Code Version: 1.73.0
- OS Version: Windows_NT x64 10.0.19043

Steps to Reproduce:

1. Add a struct with some fields and functions in one branch
2. Add the same struct in another branch, with a minor difference in one of the fields.
3. Merge the branches and open the merge editor.
4. Choose Accept Combination.

On this example, `m_Count` is `int` on the left side and `const int` on the right side. I'd expect "Accept Combination" to have only a single copy for the matching lines, and duplicate only the different line (and then I'd choose which one to keep), but it duplicates the entire struct.

![image](https://user-images.githubusercontent.com/1246544/200266685-2027792f-8904-4100-b6ea-b2ef49784990.png)




Issue discussion:
I also see that deleted/deleted is considered a conflict. I see no reason why.

![image](https://user-images.githubusercontent.com/1246544/200269557-41863c14-7c8c-4c0b-880c-513c3f499bc7.png)

I also had duplicated code on two occasions when rebasing after the new VSCode update (and that's why I searched for this issue here). It seems that it kept the old code and added the new code and didn't complain or considered a change/conflict. I still haven't used `"Accept Combination"`, so this duplication happened without using this new feature.

In both situations, I had to manually remove the old code. If I find this situation again I will add here its screenshots.

Solution patch:
diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts
--- a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts
+++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts
@@ -140,6 +140,9 @@ export class MergeEditorModel extends EditorModel {
 				} else if (range.input2Diffs.length === 0) {
 					newState = ModifiedBaseRangeState.base.withInputValue(1, true);
 					handled = true;
+				} else if (range.isEqualChange) {
+					newState = ModifiedBaseRangeState.base.withInputValue(1, true);
+					handled = true;
 				} else {
 					newState = ModifiedBaseRangeState.base;
 					handled = false;
@@ -183,6 +186,8 @@ export class MergeEditorModel extends EditorModel {
 				appendLinesToResult(input2Lines, baseRange.input2Range);
 			} else if (baseRange.input2Diffs.length === 0) {
 				appendLinesToResult(input1Lines, baseRange.input1Range);
+			} else if (baseRange.isEqualChange) {
+				appendLinesToResult(input1Lines, baseRange.input1Range);
 			} else {
 				appendLinesToResult(baseLines, baseRange.baseRange);
 			}
diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts
--- a/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts
+++ b/src/vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange.ts
@@ -3,7 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { compareBy, numberComparator, tieBreakComparators } from 'vs/base/common/arrays';
+import { compareBy, equals, numberComparator, tieBreakComparators } from 'vs/base/common/arrays';
 import { BugIndicatingError } from 'vs/base/common/errors';
 import { splitLines } from 'vs/base/common/strings';
 import { Constants } from 'vs/base/common/uint';
@@ -47,6 +47,7 @@ export class ModifiedBaseRange {
 
 	public readonly input1CombinedDiff = DetailedLineRangeMapping.join(this.input1Diffs);
 	public readonly input2CombinedDiff = DetailedLineRangeMapping.join(this.input2Diffs);
+	public readonly isEqualChange = equals(this.input1Diffs, this.input2Diffs, (a, b) => a.getLineEdit().equals(b.getLineEdit()));
 
 	constructor(
 		public readonly baseRange: LineRange,


Test patch:
diff --git a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts
--- a/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts
+++ b/src/vs/workbench/contrib/mergeEditor/test/browser/model.test.ts
@@ -224,6 +224,24 @@ suite('merge editor model', () => {
 			}
 		);
 	});
+
+	test('auto-solve equal edits', async () => {
+		await testMergeModel(
+			{
+				"languageId": "javascript",
+				"base": "const { readFileSync } = require('fs');\n\nlet paths = process.argv.slice(2);\nmain(paths);\n\nfunction main(paths) {\n    // print the welcome message\n    printMessage();\n\n    let data = getLineCountInfo(paths);\n    console.log(\"Lines: \" + data.totalLineCount);\n}\n\n/**\n * Prints the welcome message\n*/\nfunction printMessage() {\n    console.log(\"Welcome To Line Counter\");\n}\n\n/**\n * @param {string[]} paths\n*/\nfunction getLineCountInfo(paths) {\n    let lineCounts = paths.map(path => ({ path, count: getLinesLength(readFileSync(path, 'utf8')) }));\n    return {\n        totalLineCount: lineCounts.reduce((acc, { count }) => acc + count, 0),\n        lineCounts,\n    };\n}\n\n/**\n * @param {string} str\n */\nfunction getLinesLength(str) {\n    return str.split('\\n').length;\n}\n",
+				"input1": "const { readFileSync } = require('fs');\n\nlet paths = process.argv.slice(2);\nmain(paths);\n\nfunction main(paths) {\n    // print the welcome message\n    printMessage();\n\n    const data = getLineCountInfo(paths);\n    console.log(\"Lines: \" + data.totalLineCount);\n}\n\nfunction printMessage() {\n    console.log(\"Welcome To Line Counter\");\n}\n\n/**\n * @param {string[]} paths\n*/\nfunction getLineCountInfo(paths) {\n    let lineCounts = paths.map(path => ({ path, count: getLinesLength(readFileSync(path, 'utf8')) }));\n    return {\n        totalLineCount: lineCounts.reduce((acc, { count }) => acc + count, 0),\n        lineCounts,\n    };\n}\n\n/**\n * @param {string} str\n */\nfunction getLinesLength(str) {\n    return str.split('\\n').length;\n}\n",
+				"input2": "const { readFileSync } = require('fs');\n\nlet paths = process.argv.slice(2);\nrun(paths);\n\nfunction run(paths) {\n    // print the welcome message\n    printMessage();\n\n    const data = getLineCountInfo(paths);\n    console.log(\"Lines: \" + data.totalLineCount);\n}\n\nfunction printMessage() {\n    console.log(\"Welcome To Line Counter\");\n}\n\n/**\n * @param {string[]} paths\n*/\nfunction getLineCountInfo(paths) {\n    let lineCounts = paths.map(path => ({ path, count: getLinesLength(readFileSync(path, 'utf8')) }));\n    return {\n        totalLineCount: lineCounts.reduce((acc, { count }) => acc + count, 0),\n        lineCounts,\n    };\n}\n\n/**\n * @param {string} str\n */\nfunction getLinesLength(str) {\n    return str.split('\\n').length;\n}\n",
+				"result": "<<<<<<< uiae\n>>>>>>> Stashed changes",
+				resetResult: true,
+			},
+			async model => {
+				await model.mergeModel.reset();
+
+				assert.deepStrictEqual(model.getResult(), `const { readFileSync } = require('fs');\n\nlet paths = process.argv.slice(2);\nrun(paths);\n\nfunction run(paths) {\n    // print the welcome message\n    printMessage();\n\n    const data = getLineCountInfo(paths);\n    console.log("Lines: " + data.totalLineCount);\n}\n\nfunction printMessage() {\n    console.log("Welcome To Line Counter");\n}\n\n/**\n * @param {string[]} paths\n*/\nfunction getLineCountInfo(paths) {\n    let lineCounts = paths.map(path => ({ path, count: getLinesLength(readFileSync(path, 'utf8')) }));\n    return {\n        totalLineCount: lineCounts.reduce((acc, { count }) => acc + count, 0),\n        lineCounts,\n    };\n}\n\n/**\n * @param {string} str\n */\nfunction getLinesLength(str) {\n    return str.split('\\n').length;\n}\n`);
+			}
+		);
+	});
 });
 
 async function testMergeModel(
@@ -245,6 +263,7 @@ interface MergeModelOptions {
 	input2: string;
 	base: string;
 	result: string;
+	resetResult?: boolean;
 }
 
 function toSmallNumbersDec(value: number): string {
@@ -301,7 +320,7 @@ class MergeModelInterface extends Disposable {
 			resultTextModel,
 			diffComputer,
 			{
-				resetResult: false
+				resetResult: options.resetResult || false
 			},
 			new MergeEditorTelemetry(NullTelemetryService),
 		));