diff --git a/docs/Config.md b/docs/Config.md index 4bd589fe095..f9dadd8eaed 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -619,7 +619,7 @@ keybinding: viewResetOptions: D fetch: f toggleTreeView: '`' - openMergeTool: M + openMergeOptions: M openStatusFilter: copyFileInfoToClipboard: "y" collapseAll: '-' diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index 01a935102f7..fdfdb245130 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -153,7 +153,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). | | `` ` `` | Toggle file tree view | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | Open external diff tool (git difftool) | | -| `` M `` | Open external merge tool | Run `git mergetool`. | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | Fetch | Fetch changes from remote. | | `` - `` | Collapse all files | Collapse all directories in the files tree | | `` = `` | Expand all files | Expand all directories in the file tree | diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md index 9215f63dd7a..fa68d4ab1a5 100644 --- a/docs/keybindings/Keybindings_ja.md +++ b/docs/keybindings/Keybindings_ja.md @@ -235,7 +235,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味 | `` D `` | リセット | 作業ツリーのリセットオプション(例:作業ツリーの完全破棄)を表示します。 | | `` ` `` | ファイルツリービューを切り替え | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | 外部差分ツールを開く(git difftool) | | -| `` M `` | 外部マージツールを開く | `git mergetool`を実行します。 | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | フェッチ | リモートから変更をフェッチします。 | | `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます | | `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します | diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md index 96ef98ad077..5c1fbf9326d 100644 --- a/docs/keybindings/Keybindings_ko.md +++ b/docs/keybindings/Keybindings_ko.md @@ -396,7 +396,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` D `` | 초기화 | View reset options for working tree (e.g. nuking the working tree). | | `` ` `` | 파일 트리뷰로 전환 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | Open external diff tool (git difftool) | | -| `` M `` | Git mergetool를 열기 | Run `git mergetool`. | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | Fetch | Fetch changes from remote. | | `` - `` | Collapse all files | Collapse all directories in the files tree | | `` = `` | Expand all files | Expand all directories in the file tree | diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 71c35fcdfff..a2acfa85b4a 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -78,7 +78,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). | | `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | Open external diff tool (git difftool) | | -| `` M `` | Open external merge tool | Run `git mergetool`. | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | Fetch | Fetch changes from remote. | | `` - `` | Collapse all files | Collapse all directories in the files tree | | `` = `` | Expand all files | Expand all directories in the file tree | diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 5f776570922..c3b10acd396 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -252,7 +252,7 @@ _Legenda: `` oznacza ctrl+b, `` oznacza alt+b, `B` oznacza shift+b_ | `` D `` | Reset | Wyświetl opcje resetu dla drzewa roboczego (np. zniszczenie drzewa roboczego). | | `` ` `` | Przełącz widok drzewa plików | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | | -| `` M `` | Otwórz zewnętrzne narzędzie scalania | Uruchom `git mergetool`. | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | Pobierz | Pobierz zmiany ze zdalnego serwera. | | `` - `` | Collapse all files | Collapse all directories in the files tree | | `` = `` | Expand all files | Expand all directories in the file tree | diff --git a/docs/keybindings/Keybindings_pt.md b/docs/keybindings/Keybindings_pt.md index c6349bb8303..62bc1e52d00 100644 --- a/docs/keybindings/Keybindings_pt.md +++ b/docs/keybindings/Keybindings_pt.md @@ -78,7 +78,7 @@ _Legend: `` means ctrl+b, `` means alt+b, `B` means shift+b_ | `` D `` | Restaurar | Opções de redefinição de exibição para árvore de trabalho (por exemplo, nukando a árvore de trabalho). | | `` ` `` | Alternar exibição de árvore de arquivo | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | Abrir ferramenta de diff externa (git difftool) | | -| `` M `` | Abrir ferramenta de merge externa | Execute `git mergetool`. | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | Buscar | Buscar alterações do controle remoto. | | `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos | | `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo | diff --git a/docs/keybindings/Keybindings_ru.md b/docs/keybindings/Keybindings_ru.md index 2586a78e4f2..58b71521131 100644 --- a/docs/keybindings/Keybindings_ru.md +++ b/docs/keybindings/Keybindings_ru.md @@ -390,7 +390,7 @@ _Связки клавиш_ | `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). | | `` ` `` | Переключить вид дерева файлов | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | Open external diff tool (git difftool) | | -| `` M `` | Открыть внешний инструмент слияния (git mergetool) | Run `git mergetool`. | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | Получить изменения | Fetch changes from remote. | | `` - `` | Collapse all files | Collapse all directories in the files tree | | `` = `` | Expand all files | Expand all directories in the file tree | diff --git a/docs/keybindings/Keybindings_zh-CN.md b/docs/keybindings/Keybindings_zh-CN.md index decb0e6035c..c62bcd0b43c 100644 --- a/docs/keybindings/Keybindings_zh-CN.md +++ b/docs/keybindings/Keybindings_zh-CN.md @@ -216,7 +216,7 @@ _图例:`` 意味着ctrl+b, `意味着Alt+b, `B` 意味着shift+b_ | `` D `` | 重置 | 查看工作树的重置选项(例如:清除工作树)。 | | `` ` `` | 切换文件树视图 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | 使用外部差异比较工具(git difftool) | | -| `` M `` | 打开外部合并工具(git mergetool) | 执行 `git mergetool`. | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | 抓取 | 从远程获取变更 | | `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 | | `` = `` | 展开全部文件 | 展开文件树中的全部目录 | diff --git a/docs/keybindings/Keybindings_zh-TW.md b/docs/keybindings/Keybindings_zh-TW.md index 66fae12b701..2f3368e0054 100644 --- a/docs/keybindings/Keybindings_zh-TW.md +++ b/docs/keybindings/Keybindings_zh-TW.md @@ -347,7 +347,7 @@ _說明:`` 表示 Ctrl+B、`` 表示 Alt+B,`B`表示 Shift+B | `` D `` | 重設 | View reset options for working tree (e.g. nuking the working tree). | | `` ` `` | 顯示檔案樹狀視圖 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.

The default can be changed in the config file with the key 'gui.showFileTree'. | | `` `` | 開啟外部差異工具 (git difftool) | | -| `` M `` | 開啟外部合併工具 | 執行 `git mergetool`。 | +| `` M `` | View merge conflict options | View options for resolving merge conflicts. | | `` f `` | 擷取 | 同步遠端異動 | | `` - `` | Collapse all files | Collapse all directories in the files tree | | `` = `` | Expand all files | Expand all directories in the file tree | diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go index e6b066d346f..839d379f84a 100644 --- a/pkg/commands/git_commands/working_tree.go +++ b/pkg/commands/git_commands/working_tree.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/models" @@ -407,3 +408,26 @@ func (self *WorkingTreeCommands) ResetMixed(ref string) error { return self.cmd.New(cmdArgs).Run() } + +func (self *WorkingTreeCommands) ObjectIDAtStage(path string, stage int) (string, error) { + cmdArgs := NewGitCmd("rev-parse"). + Arg(fmt.Sprintf(":%d:%s", stage, path)). + ToArgv() + + output, err := self.cmd.New(cmdArgs).RunWithOutput() + if err != nil { + return "", err + } + + return strings.TrimSpace(output), nil +} + +func (self *WorkingTreeCommands) MergeFile(strategy string, oursID string, baseID string, theirsID string) (string, error) { + cmdArgs := NewGitCmd("merge-file"). + Arg("--object-id"). + Arg(strategy). + Arg("-p", oursID, baseID, theirsID). + ToArgv() + + return self.cmd.New(cmdArgs).RunWithOutput() +} diff --git a/pkg/config/app_config_test.go b/pkg/config/app_config_test.go index faba2e4e848..cc7ffea790d 100644 --- a/pkg/config/app_config_test.go +++ b/pkg/config/app_config_test.go @@ -897,7 +897,7 @@ keybinding: toggleStagedAll: a viewResetOptions: D fetch: f - openMergeTool: M + openMergeOptions: M openStatusFilter: copyFileInfoToClipboard: "y" collapseAll: '-' diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index c30d030aeaf..0323e6de1fb 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -487,7 +487,7 @@ type KeybindingFilesConfig struct { ViewResetOptions string `yaml:"viewResetOptions"` Fetch string `yaml:"fetch"` ToggleTreeView string `yaml:"toggleTreeView"` - OpenMergeTool string `yaml:"openMergeTool"` + OpenMergeOptions string `yaml:"openMergeOptions"` OpenStatusFilter string `yaml:"openStatusFilter"` CopyFileInfoToClipboard string `yaml:"copyFileInfoToClipboard"` CollapseAll string `yaml:"collapseAll"` @@ -950,7 +950,7 @@ func GetDefaultConfig() *UserConfig { ViewResetOptions: "D", Fetch: "f", ToggleTreeView: "`", - OpenMergeTool: "M", + OpenMergeOptions: "M", OpenStatusFilter: "", ConfirmDiscard: "x", CopyFileInfoToClipboard: "y", diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go index c0319f5c0c6..d4b847c946d 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -124,8 +124,8 @@ func (gui *Gui) getRandomTip() string { formattedKey(config.Universal.Remove), ), fmt.Sprintf( - "If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open 'git mergetool'", - formattedKey(config.Files.OpenMergeTool), + "If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open merge options", + formattedKey(config.Files.OpenMergeOptions), ), fmt.Sprintf( "To revert a commit, press '%s' on that commit", diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 69b4865efa1..f5dafeb37fa 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -3,6 +3,7 @@ package controllers import ( "errors" "fmt" + "os" "path/filepath" "strings" @@ -12,6 +13,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" @@ -178,10 +180,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types Description: self.c.Tr.OpenDiffTool, }, { - Key: opts.GetKey(opts.Config.Files.OpenMergeTool), - Handler: self.c.Helpers().WorkingTree.OpenMergeTool, - Description: self.c.Tr.OpenMergeTool, - Tooltip: self.c.Tr.OpenMergeToolTooltip, + Key: opts.GetKey(opts.Config.Files.OpenMergeOptions), + Handler: self.withItems(self.createMergeConflictMenu), + Description: self.c.Tr.ViewMergeConflictOptions, + Tooltip: self.c.Tr.ViewMergeConflictOptionsTooltip, + OpensMenu: true, + DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Files.Fetch), @@ -1024,6 +1028,92 @@ func (self *FilesController) createStashMenu() error { }) } +func (self *FilesController) createMergeConflictMenu(nodes []*filetree.FileNode) error { + onMergeStrategySelected := func(strategy string) error { + normalizedNodes := normalisedSelectedNodes(nodes) + fileNodes := lo.Filter(normalizedNodes, func(node *filetree.FileNode, _ int) bool { + return node.File != nil + }) + filenames := lo.Map(fileNodes, func(node *filetree.FileNode, _ int) string { + return node.GetPath() + }) + + for _, filename := range filenames { + baseID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filename, 1) + if err != nil { + return err + } + + oursID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filename, 2) + if err != nil { + return err + } + + theirsID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filename, 3) + if err != nil { + return err + } + + output, err := self.c.Git().WorkingTree.MergeFile(strategy, oursID, baseID, theirsID) + if err != nil { + return err + } + + if err = os.WriteFile(filename, []byte(output), 0o644); err != nil { + return err + } + } + + err := self.c.Git().WorkingTree.StageFiles(filenames, nil) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + } + + cmdColor := style.FgBlue + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.MergeConflictOptionsTitle, + Items: []*types.MenuItem{ + { + LabelColumns: []string{ + self.c.Tr.UseHead, + cmdColor.Sprint("git merge-file --ours"), + }, + OnPress: func() error { + return onMergeStrategySelected("--ours") + }, + Key: 'h', + }, + { + LabelColumns: []string{ + self.c.Tr.UseIncoming, + cmdColor.Sprint("git merge-file --theirs"), + }, + OnPress: func() error { + return onMergeStrategySelected("--theirs") + }, + Key: 'i', + }, + { + LabelColumns: []string{ + self.c.Tr.UseBoth, + cmdColor.Sprint("git merge-file --union"), + }, + OnPress: func() error { + return onMergeStrategySelected("--union") + }, + Key: 'b', + }, + { + LabelColumns: []string{ + self.c.Tr.OpenMergeTool, + cmdColor.Sprint("git mergetool"), + }, + OnPress: self.c.Helpers().WorkingTree.OpenMergeTool, + Key: 'm', + }, + }, + }) +} + func (self *FilesController) openCopyMenu() error { node := self.context().GetSelected() diff --git a/pkg/gui/controllers/merge_conflicts_controller.go b/pkg/gui/controllers/merge_conflicts_controller.go index 23c53f08857..d586a0c697d 100644 --- a/pkg/gui/controllers/merge_conflicts_controller.go +++ b/pkg/gui/controllers/merge_conflicts_controller.go @@ -112,7 +112,7 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts) Tag: "navigation", }, { - Key: opts.GetKey(opts.Config.Files.OpenMergeTool), + Key: opts.GetKey(opts.Config.Files.OpenMergeOptions), Handler: self.c.Helpers().WorkingTree.OpenMergeTool, Description: self.c.Tr.OpenMergeTool, Tooltip: self.c.Tr.OpenMergeToolTooltip, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index d162d20dc20..4226f5b6b75 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -898,8 +898,13 @@ type TranslationSet struct { BreakingChangesTitle string BreakingChangesMessage string BreakingChangesByVersion map[string]string + ViewMergeConflictOptions string + ViewMergeConflictOptionsTooltip string + MergeConflictOptionsTitle string + UseHead string + UseIncoming string + UseBoth string } - type Bisect struct { MarkStart string ResetTitle string @@ -1970,6 +1975,12 @@ func EnglishTranslationSet() *TranslationSet { CustomCommands: "Custom commands", NoApplicableCommandsInThisContext: "(No applicable commands in this context)", SelectCommitsOfCurrentBranch: "Select commits of current branch", + ViewMergeConflictOptions: "View merge conflict options", + ViewMergeConflictOptionsTooltip: "View options for resolving merge conflicts.", + MergeConflictOptionsTitle: "Resolve merge conflicts", + UseHead: "Use HEAD", + UseIncoming: "Use Incoming", + UseBoth: "Use Both", Actions: Actions{ // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) diff --git a/pkg/integration/tests/conflicts/merge_file_theirs.go b/pkg/integration/tests/conflicts/merge_file_theirs.go new file mode 100644 index 00000000000..3128bcc0d7f --- /dev/null +++ b/pkg/integration/tests/conflicts/merge_file_theirs.go @@ -0,0 +1,85 @@ +package conflicts + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var OriginalFileContent = ` +1 +2 +3 +4 +5 +6 +` + +var FirstChangeFileContent = ` +1 +2 +3 +4 +5a +6 +` + +var SecondChangeFileContent = ` +1b +2 +3 +4 +5b +6 +` + +var MergeSecondFileFinalContent = ` +1b +2 +3 +4 +5a +6 +` + +var MergeFileTheirs = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Conflicting file can be resolved to 'our' version via merge-file", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + NewBranch("original-branch"). + EmptyCommit("one"). + CreateFileAndAdd("file", OriginalFileContent). + Commit("original"). + NewBranch("first-change-branch"). + UpdateFileAndAdd("file", FirstChangeFileContent). + Commit("first change"). + Checkout("original-branch"). + NewBranch("second-change-branch"). + UpdateFileAndAdd("file", SecondChangeFileContent). + Commit("second change"). + Checkout("first-change-branch"). + RunCommandExpectError([]string{"git", "merge", "--no-edit", "second-change-branch"}) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("file").IsSelected(), + ) + + t.GlobalPress(keys.Files.OpenMergeOptions) + + t.ExpectPopup().Menu(). + Title(Equals("Resolve merge conflicts")). + Select(Contains("Use HEAD")). // merge-file --theirs + Confirm() + + t.Common().ContinueOnConflictsResolved("merge") + + t.Views().Files().IsEmpty() + + t.FileSystem().FileContent("file", Equals(MergeSecondFileFinalContent)) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a292227b388..8b2b033479d 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -150,6 +150,7 @@ var tests = []*components.IntegrationTest{ config.NegativeRefspec, config.RemoteNamedStar, conflicts.Filter, + conflicts.MergeFileTheirs, conflicts.ResolveExternally, conflicts.ResolveMultipleFiles, conflicts.ResolveNoAutoStage, diff --git a/schema/config.json b/schema/config.json index aa4e41012c6..c9cd926827c 100644 --- a/schema/config.json +++ b/schema/config.json @@ -1115,7 +1115,7 @@ "type": "string", "default": "`" }, - "openMergeTool": { + "openMergeOptions": { "type": "string", "default": "M" },