Skip to content

Conversation

lmnek
Copy link

@lmnek lmnek commented Sep 12, 2025

Implements #2026. I also tried to address issues mentioned in the #3477 PR.

Previously, pressing M opened an external merge tool. Now it opens a merge options menu that allows selecting all conflicts in chosen files as ours (HEAD), theirs (incoming), or union (both), while still providing access to the external merge tool.

This uses git-merge-file for a 3-way merge with the --ours, --theirs, and --union flags. This approach avoids the issue mentioned in #1608 (reply in thread), and correctly applies the chosen conflict resolutions while preserving changes from other branches. The command is executed with --object-id, which requires object IDs obtained via rev-parse, instead of relying on the standard version that works with full saved files.

Disclaimer: On my machine, the tests pass inconsistently. Sometimes they succeed, sometimes fail with errors such as POTENTIAL DEADLOCK: Recursive locking or Inconsistent locking. I haven’t yet identified the root cause of this issue.

PR Description

Please check if the PR fulfills these requirements

  • Cheatsheets are up-to-date (run go generate ./...)
  • Code has been formatted (see here)
  • Tests have been added/updated (see here for the integration test guide)
  • Text is internationalised (see here)
  • If a new UserConfig entry was added, make sure it can be hot-reloaded (see here)
  • Docs have been updated if necessary
  • You've read through your own file changes for silly mistakes etc

@stefanhaller
Copy link
Collaborator

O wow, this looks really nice at first glance. It might take me a while to review this, as I'm rather busy right now. Feel free to ping me if you haven't heard back after a week or two.

Disclaimer: On my machine, the tests pass inconsistently. Sometimes they succeed, sometimes fail with errors such as POTENTIAL DEADLOCK: Recursive locking or Inconsistent locking.

My guess is that you have go 1.25 installed locally. This is a known problem, I didn't have time to look into this yet. If you can use 1.24 to run the tests, it should be more reliable.

@bllendev
Copy link

yoo thanks for doing this i just ran into this prob, had a big rebase and i was really thinking this should exist lol, thanks again

lmnek and others added 2 commits September 21, 2025 17:44
Replace merge-tool with merge options menu that allows resolving all
conflicts for selected files as ours, theirs, or union, while still
providing access to the merge tool.
Copy link
Collaborator

@stefanhaller stefanhaller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really great work. Don't let the large number of review comments discourage you, many of them are only nitpicks.

fetch: f
toggleTreeView: '`'
openMergeTool: M
openMergeOptions: M
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change (users might have changed the keybinding in their config file). We have the technology to migrate users's config files to account for this, and we should do that by adding an entry to this array.

Description: self.c.Tr.OpenMergeTool,
Tooltip: self.c.Tr.OpenMergeToolTooltip,
Key: opts.GetKey(opts.Config.Files.OpenMergeOptions),
Handler: self.withItems(self.createMergeConflictMenu),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of implementing the menu here, we should implement it in WorkingTreeHelper.OpenMergeTool (and rename it accordingly). That way, it will also be used when you are in the merge conflict editor.

Edit: hm, when you are in the merge conflict editor you probably expect to work only on the current file you are in. So we'd have to pass a list of file paths to that function, and pass the selected ones from here, and the current one from merge_conflicts_controller.

fileNodes := lo.Filter(normalizedNodes, func(node *filetree.FileNode, _ int) bool {
return node.File != nil
})
filenames := lo.Map(fileNodes, func(node *filetree.FileNode, _ int) string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: would call the variable filepaths.

cmdArgs := NewGitCmd("merge-file").
Arg("--object-id").
Arg(strategy).
Arg("-p", oursID, baseID, theirsID).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicks: would use --stdout instead of -p for better readability, and put it on a line by itself; otherwise it looks like it is an option that takes the object ids as arguments.

Suggested change
Arg("-p", oursID, baseID, theirsID).
Arg("--stdout").
Arg(oursID, baseID, theirsID).


func (self *FilesController) createMergeConflictMenu(nodes []*filetree.FileNode) error {
onMergeStrategySelected := func(strategy string) error {
normalizedNodes := normalisedSelectedNodes(nodes)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relies on the assumption that all currently shown files have conflicts. This doesn't have to be true; while we automatically filter for only conflicted files when there are any, the user can open the filter menu (ctrl-b) and choose "no filter" to show all files.

Which means we should probably filter on the status again here.

Also, for files that have non-textual conflicts (see #4431), I guess the git rev-parse commands will probably error? So we should filter those out, too.

ViewMergeConflictOptions: "View merge conflict options",
ViewMergeConflictOptionsTooltip: "View options for resolving merge conflicts.",
MergeConflictOptionsTitle: "Resolve merge conflicts",
UseHead: "Use HEAD",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For non-textual conflicts we settled on the terms "current changes" (for head) and "incoming changes"; see #4431, so I'd suggest to use those here, too.

Also, we don't use Title Case in menu items, so it should be "Use current changes" and "Use both".

`

var MergeFileTheirs = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Conflicting file can be resolved to 'their' version via merge-file",
Copy link
Collaborator

@stefanhaller stefanhaller Sep 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't "ours" the more interesting case? I'd prefer a test that has both conflicts and non-conflicting changes, and tests that when you choose "ours" you get ours for the conflicts, but theirs for the non-conflicts. I pushed a commit that does this (aeeab45), also using test data that I find a bit easier to grok.

Actually, it might be worth testing all three cases; I find them all interesting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants