mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	Add merge menu with conflict resolver (#4889)
Implements https://github.com/jesseduffield/lazygit/issues/2026. I also tried to address issues mentioned in the https://github.com/jesseduffield/lazygit/pull/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](https://git-scm.com/docs/git-merge-file) for a 3-way merge with the `--ours`, `--theirs`, and `--union` flags. This approach avoids the issue mentioned in https://github.com/jesseduffield/lazygit/discussions/1608#discussioncomment-13002595, 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.
This commit is contained in:
		| @@ -619,7 +619,7 @@ keybinding: | ||||
|     viewResetOptions: D | ||||
|     fetch: f | ||||
|     toggleTreeView: '`' | ||||
|     openMergeTool: M | ||||
|     openMergeOptions: M | ||||
|     openStatusFilter: <c-b> | ||||
|     copyFileInfoToClipboard: "y" | ||||
|     collapseAll: '-' | ||||
|   | ||||
| @@ -153,7 +153,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 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 | | ||||
| @@ -210,7 +210,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_ | ||||
| | `` z `` | Undo | Undo last merge conflict resolution. | | ||||
| | `` e `` | Edit file | Open file in external editor. | | ||||
| | `` o `` | Open file | Open file in default application. | | ||||
| | `` M `` | Open external merge tool | Run `git mergetool`. | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | Return to files panel |  | | ||||
|  | ||||
| ## Main panel (normal) | ||||
|   | ||||
| @@ -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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 外部差分ツールを開く(git difftool) |  | | ||||
| | `` M `` | 外部マージツールを開く | `git mergetool`を実行します。 | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` f `` | フェッチ | リモートから変更をフェッチします。 | | ||||
| | `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます | | ||||
| | `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します | | ||||
| @@ -292,7 +292,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味 | ||||
| | `` z `` | 元に戻す | 最後のマージコンフリクト解決を元に戻します。 | | ||||
| | `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 | | ||||
| | `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 | | ||||
| | `` M `` | 外部マージツールを開く | `git mergetool`を実行します。 | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | ファイルパネルに戻る |  | | ||||
|  | ||||
| ## メインパネル(通常) | ||||
|   | ||||
| @@ -152,7 +152,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_ | ||||
| | `` z `` | 되돌리기 | Undo last merge conflict resolution. | | ||||
| | `` e `` | 파일 편집 | Open file in external editor. | | ||||
| | `` o `` | 파일 닫기 | Open file in default application. | | ||||
| | `` M `` | Git mergetool를 열기 | Run `git mergetool`. | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | 파일 목록으로 돌아가기 |  | | ||||
|  | ||||
| ## 메인 패널 (Normal) | ||||
| @@ -396,7 +396,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 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 | | ||||
|   | ||||
| @@ -78,7 +78,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 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 | | ||||
| @@ -218,7 +218,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_ | ||||
| | `` z `` | Ongedaan maken | Undo last merge conflict resolution. | | ||||
| | `` e `` | Verander bestand | Open file in external editor. | | ||||
| | `` o `` | Open bestand | Open file in default application. | | ||||
| | `` M `` | Open external merge tool | Run `git mergetool`. | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | Ga terug naar het bestanden paneel |  | | ||||
|  | ||||
| ## Normaal | ||||
|   | ||||
| @@ -193,7 +193,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_ | ||||
| | `` z `` | Cofnij | Cofnij ostatnie rozwiązanie konfliktu scalania. | | ||||
| | `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. | | ||||
| | `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. | | ||||
| | `` M `` | Otwórz zewnętrzne narzędzie scalania | Uruchom `git mergetool`. | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | Wróć do panelu plików |  | | ||||
|  | ||||
| ## Panel główny (zatwierdzanie) | ||||
| @@ -252,7 +252,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 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 | | ||||
|   | ||||
| @@ -78,7 +78,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 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 | | ||||
| @@ -278,7 +278,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_ | ||||
| | `` z `` | Desfazer | Desfazer resolução de conflitos de última mesclagem. | | ||||
| | `` e `` | Editar arquivo | Abrir arquivo no editor externo. | | ||||
| | `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. | | ||||
| | `` M `` | Abrir ferramenta de merge externa | Execute `git mergetool`. | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | Retornar ao painel de arquivos |  | | ||||
|  | ||||
| ## Painel principal (patch build) | ||||
|   | ||||
| @@ -122,7 +122,7 @@ _Связки клавиш_ | ||||
| | `` z `` | Отменить | Undo last merge conflict resolution. | | ||||
| | `` e `` | Редактировать файл | Open file in external editor. | | ||||
| | `` o `` | Открыть файл | Open file in default application. | | ||||
| | `` M `` | Открыть внешний инструмент слияния (git mergetool) | Run `git mergetool`. | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | Вернуться к панели файлов |  | | ||||
|  | ||||
| ## Главная панель (сборка патчей) | ||||
| @@ -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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 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 | | ||||
|   | ||||
| @@ -216,7 +216,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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 使用外部差异比较工具(git difftool) |  | | ||||
| | `` M `` | 打开外部合并工具(git mergetool) | 执行 `git mergetool`. | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` f `` | 抓取 | 从远程获取变更 | | ||||
| | `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 | | ||||
| | `` = `` | 展开全部文件 | 展开文件树中的全部目录 | | ||||
| @@ -305,7 +305,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_ | ||||
| | `` z `` | 撤销 | 撤消上次合并冲突解决 | | ||||
| | `` e `` | 编辑文件 | 使用外部编辑器打开文件 | | ||||
| | `` o `` | 打开文件 | 使用默认程序打开该文件 | | ||||
| | `` M `` | 打开外部合并工具(git mergetool) | 执行 `git mergetool`. | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | 返回文件面板 |  | | ||||
|  | ||||
| ## 正在暂存 | ||||
|   | ||||
| @@ -97,7 +97,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B | ||||
| | `` z `` | 復原 | Undo last merge conflict resolution. | | ||||
| | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | | ||||
| | `` o `` | 開啟檔案 | 使用預設軟體開啟 | | ||||
| | `` M `` | 開啟外部合併工具 | 執行 `git mergetool`。 | | ||||
| | `` M `` | View merge conflict options | View options for resolving merge conflicts. | | ||||
| | `` <esc> `` | 返回檔案面板 |  | | ||||
|  | ||||
| ## 主面板(預存) | ||||
| @@ -347,7 +347,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-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.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. | | ||||
| | `` <c-t> `` | 開啟外部差異工具 (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 | | ||||
|   | ||||
| @@ -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,46 @@ func (self *WorkingTreeCommands) ResetMixed(ref string) error { | ||||
|  | ||||
| 	return self.cmd.New(cmdArgs).Run() | ||||
| } | ||||
|  | ||||
| func (self *WorkingTreeCommands) ShowFileAtStage(path string, stage int) (string, error) { | ||||
| 	cmdArgs := NewGitCmd("show"). | ||||
| 		Arg(fmt.Sprintf(":%d:%s", stage, path)). | ||||
| 		ToArgv() | ||||
|  | ||||
| 	return self.cmd.New(cmdArgs).RunWithOutput() | ||||
| } | ||||
|  | ||||
| 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) MergeFileForFiles(strategy string, oursFilepath string, baseFilepath string, theirsFilepath string) (string, error) { | ||||
| 	cmdArgs := NewGitCmd("merge-file"). | ||||
| 		Arg(strategy). | ||||
| 		Arg("--stdout"). | ||||
| 		Arg(oursFilepath, baseFilepath, theirsFilepath). | ||||
| 		ToArgv() | ||||
|  | ||||
| 	return self.cmd.New(cmdArgs).RunWithOutput() | ||||
| } | ||||
|  | ||||
| // OIDs mode (Git 2.43+) | ||||
| func (self *WorkingTreeCommands) MergeFileForObjectIDs(strategy string, oursID string, baseID string, theirsID string) (string, error) { | ||||
| 	cmdArgs := NewGitCmd("merge-file"). | ||||
| 		Arg(strategy). | ||||
| 		Arg("--stdout"). | ||||
| 		Arg("--object-id"). | ||||
| 		Arg(oursID, baseID, theirsID). | ||||
| 		ToArgv() | ||||
|  | ||||
| 	return self.cmd.New(cmdArgs).RunWithOutput() | ||||
| } | ||||
|   | ||||
| @@ -272,6 +272,7 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([] | ||||
| 		{[]string{"gui", "skipUnstageLineWarning"}, "skipDiscardChangeWarning"}, | ||||
| 		{[]string{"keybinding", "universal", "executeCustomCommand"}, "executeShellCommand"}, | ||||
| 		{[]string{"gui", "windowSize"}, "screenMode"}, | ||||
| 		{[]string{"keybinding", "files", "openMergeTool"}, "openMergeOptions"}, | ||||
| 	} | ||||
|  | ||||
| 	for _, pathToReplace := range pathsToReplace { | ||||
|   | ||||
| @@ -897,7 +897,7 @@ keybinding: | ||||
|     toggleStagedAll: a | ||||
|     viewResetOptions: D | ||||
|     fetch: f | ||||
|     openMergeTool: M | ||||
|     openMergeOptions: M | ||||
|     openStatusFilter: <c-b> | ||||
|     copyFileInfoToClipboard: "y" | ||||
|     collapseAll: '-' | ||||
|   | ||||
| @@ -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:         "<c-b>", | ||||
| 				ConfirmDiscard:           "x", | ||||
| 				CopyFileInfoToClipboard:  "y", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -98,7 +98,7 @@ func (gui *Gui) resetHelpersAndControllers() { | ||||
| 		Bisect:          bisectHelper, | ||||
| 		Suggestions:     suggestionsHelper, | ||||
| 		Files:           helpers.NewFilesHelper(helperCommon), | ||||
| 		WorkingTree:     helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper), | ||||
| 		WorkingTree:     helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper, rebaseHelper), | ||||
| 		Tags:            helpers.NewTagsHelper(helperCommon, commitsHelper, gpgHelper), | ||||
| 		BranchesHelper:  helpers.NewBranchesHelper(helperCommon, worktreeHelper), | ||||
| 		GPG:             helpers.NewGpgHelper(helperCommon), | ||||
|   | ||||
| @@ -178,10 +178,13 @@ 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.openMergeConflictMenu), | ||||
| 			Description:       self.c.Tr.ViewMergeConflictOptions, | ||||
| 			Tooltip:           self.c.Tr.ViewMergeConflictOptionsTooltip, | ||||
| 			GetDisabledReason: self.require(self.itemsSelected(self.canOpenMergeConflictMenu)), | ||||
| 			OpensMenu:         true, | ||||
| 			DisplayOnScreen:   true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Key:         opts.GetKey(opts.Config.Files.Fetch), | ||||
| @@ -1024,6 +1027,34 @@ func (self *FilesController) createStashMenu() error { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (self *FilesController) openMergeConflictMenu(nodes []*filetree.FileNode) error { | ||||
| 	normalizedNodes := flattenSelectedNodesToFiles(nodes) | ||||
|  | ||||
| 	fileNodesWithConflicts := lo.Filter(normalizedNodes, func(node *filetree.FileNode, _ int) bool { | ||||
| 		return node.File != nil && node.File.HasInlineMergeConflicts | ||||
| 	}) | ||||
|  | ||||
| 	filepaths := lo.Map(fileNodesWithConflicts, func(node *filetree.FileNode, _ int) string { | ||||
| 		return node.GetPath() | ||||
| 	}) | ||||
|  | ||||
| 	return self.c.Helpers().WorkingTree.CreateMergeConflictMenu(filepaths) | ||||
| } | ||||
|  | ||||
| func (self *FilesController) canOpenMergeConflictMenu(nodes []*filetree.FileNode) *types.DisabledReason { | ||||
| 	normalizedNodes := flattenSelectedNodesToFiles(nodes) | ||||
|  | ||||
| 	hasFileNodesWithConflicts := lo.SomeBy(normalizedNodes, func(node *filetree.FileNode) bool { | ||||
| 		return node.File != nil && node.File.HasInlineMergeConflicts | ||||
| 	}) | ||||
|  | ||||
| 	if !hasFileNodesWithConflicts { | ||||
| 		return &types.DisabledReason{Text: self.c.Tr.NoFilesWithMergeConflicts} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (self *FilesController) openCopyMenu() error { | ||||
| 	node := self.context().GetSelected() | ||||
|  | ||||
| @@ -1237,6 +1268,38 @@ func isDescendentOfSelectedNodes(node *filetree.FileNode, selectedNodes []*filet | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // BFS algorithm for expanding directories into their children, | ||||
| // and for collecting the unique file nodes | ||||
| func flattenSelectedNodesToFiles(selectedNodes []*filetree.FileNode) []*filetree.FileNode { | ||||
| 	queue := append(make([]*filetree.FileNode, 0, len(selectedNodes)), selectedNodes...) | ||||
| 	visited := set.New[string]() | ||||
| 	var files []*filetree.FileNode | ||||
|  | ||||
| 	for len(queue) > 0 { | ||||
| 		// pop node from queue | ||||
| 		node := queue[0] | ||||
| 		queue = queue[1:] | ||||
|  | ||||
| 		nodeID := node.ID() | ||||
| 		if visited.Includes(nodeID) { | ||||
| 			continue | ||||
| 		} | ||||
| 		visited.Add(nodeID) | ||||
|  | ||||
| 		if node.File != nil { | ||||
| 			// unique file node -> collect it | ||||
| 			files = append(files, node) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// directory node -> enqueue children | ||||
| 		for _, ch := range node.Children { | ||||
| 			queue = append(queue, &filetree.FileNode{Node: ch}) | ||||
| 		} | ||||
| 	} | ||||
| 	return files | ||||
| } | ||||
|  | ||||
| func someNodesHaveUnstagedChanges(nodes []*filetree.FileNode) bool { | ||||
| 	return lo.SomeBy(nodes, (*filetree.FileNode).GetHasUnstagedChanges) | ||||
| } | ||||
|   | ||||
| @@ -3,21 +3,24 @@ package helpers | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
|  | ||||
| 	"github.com/jesseduffield/lazygit/pkg/commands/git_commands" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/commands/models" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/config" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/gui/context" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/gui/style" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/gui/types" | ||||
| 	"github.com/samber/lo" | ||||
| ) | ||||
|  | ||||
| type WorkingTreeHelper struct { | ||||
| 	c             *HelperCommon | ||||
| 	refHelper     *RefsHelper | ||||
| 	commitsHelper *CommitsHelper | ||||
| 	gpgHelper     *GpgHelper | ||||
| 	c                    *HelperCommon | ||||
| 	refHelper            *RefsHelper | ||||
| 	commitsHelper        *CommitsHelper | ||||
| 	gpgHelper            *GpgHelper | ||||
| 	mergeAndRebaseHelper *MergeAndRebaseHelper | ||||
| } | ||||
|  | ||||
| func NewWorkingTreeHelper( | ||||
| @@ -25,12 +28,14 @@ func NewWorkingTreeHelper( | ||||
| 	refHelper *RefsHelper, | ||||
| 	commitsHelper *CommitsHelper, | ||||
| 	gpgHelper *GpgHelper, | ||||
| 	mergeAndRebaseHelper *MergeAndRebaseHelper, | ||||
| ) *WorkingTreeHelper { | ||||
| 	return &WorkingTreeHelper{ | ||||
| 		c:             c, | ||||
| 		refHelper:     refHelper, | ||||
| 		commitsHelper: commitsHelper, | ||||
| 		gpgHelper:     gpgHelper, | ||||
| 		c:                    c, | ||||
| 		refHelper:            refHelper, | ||||
| 		commitsHelper:        commitsHelper, | ||||
| 		gpgHelper:            gpgHelper, | ||||
| 		mergeAndRebaseHelper: mergeAndRebaseHelper, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -247,3 +252,135 @@ func (self *WorkingTreeHelper) commitPrefixConfigsForRepo() []config.CommitPrefi | ||||
|  | ||||
| 	return self.c.UserConfig().Git.CommitPrefix | ||||
| } | ||||
|  | ||||
| func (self *WorkingTreeHelper) mergeFile(filepath string, strategy string) (string, error) { | ||||
| 	if self.c.Git().Version.IsOlderThan(2, 43, 0) { | ||||
| 		return self.mergeFileWithTempFiles(filepath, strategy) | ||||
| 	} | ||||
|  | ||||
| 	return self.mergeFileWithObjectIDs(filepath, strategy) | ||||
| } | ||||
|  | ||||
| func (self *WorkingTreeHelper) mergeFileWithTempFiles(filepath string, strategy string) (string, error) { | ||||
| 	showToTempFile := func(stage int, label string) (string, error) { | ||||
| 		output, err := self.c.Git().WorkingTree.ShowFileAtStage(filepath, stage) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		f, err := os.CreateTemp(self.c.GetConfig().GetTempDir(), "mergefile-"+label+"-*") | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		defer f.Close() | ||||
|  | ||||
| 		if _, err := f.Write([]byte(output)); err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		return f.Name(), nil | ||||
| 	} | ||||
|  | ||||
| 	baseFilepath, err := showToTempFile(1, "base") | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer os.Remove(baseFilepath) | ||||
|  | ||||
| 	oursFilepath, err := showToTempFile(2, "ours") | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer os.Remove(oursFilepath) | ||||
|  | ||||
| 	theirsFilepath, err := showToTempFile(3, "theirs") | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	defer os.Remove(theirsFilepath) | ||||
|  | ||||
| 	return self.c.Git().WorkingTree.MergeFileForFiles(strategy, oursFilepath, baseFilepath, theirsFilepath) | ||||
| } | ||||
|  | ||||
| func (self *WorkingTreeHelper) mergeFileWithObjectIDs(filepath, strategy string) (string, error) { | ||||
| 	baseID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filepath, 1) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	oursID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filepath, 2) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	theirsID, err := self.c.Git().WorkingTree.ObjectIDAtStage(filepath, 3) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return self.c.Git().WorkingTree.MergeFileForObjectIDs(strategy, oursID, baseID, theirsID) | ||||
| } | ||||
|  | ||||
| func (self *WorkingTreeHelper) CreateMergeConflictMenu(selectedFilepaths []string) error { | ||||
| 	onMergeStrategySelected := func(strategy string) error { | ||||
| 		for _, filepath := range selectedFilepaths { | ||||
| 			output, err := self.mergeFile(filepath, strategy) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if err = os.WriteFile(filepath, []byte(output), 0o644); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		err := self.c.Git().WorkingTree.StageFiles(selectedFilepaths, nil) | ||||
| 		self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}}) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	cmdColor := style.FgBlue | ||||
| 	return self.c.Menu(types.CreateMenuOptions{ | ||||
| 		Title: self.c.Tr.MergeConflictOptionsTitle, | ||||
| 		Items: []*types.MenuItem{ | ||||
| 			{ | ||||
| 				LabelColumns: []string{ | ||||
| 					self.c.Tr.UseCurrentChanges, | ||||
| 					cmdColor.Sprint("git merge-file --ours"), | ||||
| 				}, | ||||
| 				OnPress: func() error { | ||||
| 					return onMergeStrategySelected("--ours") | ||||
| 				}, | ||||
| 				Key: 'c', | ||||
| 			}, | ||||
| 			{ | ||||
| 				LabelColumns: []string{ | ||||
| 					self.c.Tr.UseIncomingChanges, | ||||
| 					cmdColor.Sprint("git merge-file --theirs"), | ||||
| 				}, | ||||
| 				OnPress: func() error { | ||||
| 					return onMergeStrategySelected("--theirs") | ||||
| 				}, | ||||
| 				Key: 'i', | ||||
| 			}, | ||||
| 			{ | ||||
| 				LabelColumns: []string{ | ||||
| 					self.c.Tr.UseBothChanges, | ||||
| 					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.OpenMergeTool, | ||||
| 				Key:     'm', | ||||
| 			}, | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -112,10 +112,11 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts) | ||||
| 			Tag:         "navigation", | ||||
| 		}, | ||||
| 		{ | ||||
| 			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.openMergeConflictMenu, | ||||
| 			Description:     self.c.Tr.ViewMergeConflictOptions, | ||||
| 			Tooltip:         self.c.Tr.ViewMergeConflictOptionsTooltip, | ||||
| 			OpensMenu:       true, | ||||
| 			DisplayOnScreen: true, | ||||
| 		}, | ||||
| 		{ | ||||
| @@ -320,6 +321,11 @@ func (self *MergeConflictsController) onLastConflictResolved() { | ||||
| 	self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) | ||||
| } | ||||
|  | ||||
| func (self *MergeConflictsController) openMergeConflictMenu() error { | ||||
| 	filepath := self.context().GetState().GetPath() | ||||
| 	return self.c.Helpers().WorkingTree.CreateMergeConflictMenu([]string{filepath}) | ||||
| } | ||||
|  | ||||
| func (self *MergeConflictsController) withRenderAndFocus(f func() error) func() error { | ||||
| 	return self.withLock(func() error { | ||||
| 		if err := f(); err != nil { | ||||
|   | ||||
| @@ -63,7 +63,6 @@ type TranslationSet struct { | ||||
| 	ToggleTreeViewTooltip                 string | ||||
| 	OpenDiffTool                          string | ||||
| 	OpenMergeTool                         string | ||||
| 	OpenMergeToolTooltip                  string | ||||
| 	Refresh                               string | ||||
| 	RefreshTooltip                        string | ||||
| 	Push                                  string | ||||
| @@ -898,6 +897,13 @@ type TranslationSet struct { | ||||
| 	BreakingChangesTitle                     string | ||||
| 	BreakingChangesMessage                   string | ||||
| 	BreakingChangesByVersion                 map[string]string | ||||
| 	ViewMergeConflictOptions                 string | ||||
| 	ViewMergeConflictOptionsTooltip          string | ||||
| 	NoFilesWithMergeConflicts                string | ||||
| 	MergeConflictOptionsTitle                string | ||||
| 	UseCurrentChanges                        string | ||||
| 	UseIncomingChanges                       string | ||||
| 	UseBothChanges                           string | ||||
| } | ||||
|  | ||||
| type Bisect struct { | ||||
| @@ -1136,7 +1142,6 @@ func EnglishTranslationSet() *TranslationSet { | ||||
| 		ToggleTreeViewTooltip:                "Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.\n\nThe default can be changed in the config file with the key 'gui.showFileTree'.", | ||||
| 		OpenDiffTool:                         "Open external diff tool (git difftool)", | ||||
| 		OpenMergeTool:                        "Open external merge tool", | ||||
| 		OpenMergeToolTooltip:                 "Run `git mergetool`.", | ||||
| 		Refresh:                              "Refresh", | ||||
| 		RefreshTooltip:                       "Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`.", | ||||
| 		Push:                                 "Push", | ||||
| @@ -1970,6 +1975,13 @@ 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.", | ||||
| 		NoFilesWithMergeConflicts:                "There are no files with merge conflicts.", | ||||
| 		MergeConflictOptionsTitle:                "Resolve merge conflicts", | ||||
| 		UseCurrentChanges:                        "Use current changes", | ||||
| 		UseIncomingChanges:                       "Use incoming changes", | ||||
| 		UseBothChanges:                           "Use both", | ||||
|  | ||||
| 		Actions: Actions{ | ||||
| 			// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) | ||||
|   | ||||
							
								
								
									
										77
									
								
								pkg/integration/tests/conflicts/merge_file_both.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								pkg/integration/tests/conflicts/merge_file_both.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| package conflicts | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/config" | ||||
| 	. "github.com/jesseduffield/lazygit/pkg/integration/components" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/integration/tests/shared" | ||||
| ) | ||||
|  | ||||
| func testDataBoth() (original, current, incoming, final string) { | ||||
| 	original = ` | ||||
| 1 | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5 | ||||
| 6 | ||||
| ` | ||||
| 	current = ` | ||||
| 1a | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5a | ||||
| 6 | ||||
| ` | ||||
| 	incoming = ` | ||||
| 1 | ||||
| 2 | ||||
| 3b | ||||
| 4 | ||||
| 5b | ||||
| 6 | ||||
| ` | ||||
| 	final = ` | ||||
| 1a | ||||
| 2 | ||||
| 3b | ||||
| 4 | ||||
| 5a | ||||
| 5b | ||||
| 6 | ||||
| ` | ||||
| 	return original, current, incoming, final | ||||
| } | ||||
|  | ||||
| var MergeFileBoth = NewIntegrationTest(NewIntegrationTestArgs{ | ||||
| 	Description:  "Conflicting file can be resolved to 'union' (both changes) version via merge-file", | ||||
| 	ExtraCmdArgs: []string{}, | ||||
| 	Skip:         false, | ||||
| 	SetupConfig:  func(config *config.AppConfig) {}, | ||||
| 	SetupRepo: func(shell *Shell) { | ||||
| 		original, current, incoming, _ := testDataBoth() | ||||
| 		shared.CreateMergeConflictFileForMergeFileTests(shell, original, current, incoming) | ||||
| 	}, | ||||
| 	Run: func(t *TestDriver, keys config.KeybindingConfig) { | ||||
| 		_, _, _, expected := testDataBoth() | ||||
|  | ||||
| 		t.Views().Files(). | ||||
| 			IsFocused(). | ||||
| 			Lines( | ||||
| 				Contains("file").IsSelected(), | ||||
| 			) | ||||
|  | ||||
| 		t.GlobalPress(keys.Files.OpenMergeOptions) | ||||
|  | ||||
| 		t.ExpectPopup().Menu(). | ||||
| 			Title(Equals("Resolve merge conflicts")). | ||||
| 			Select(Contains("Use both")). // merge-file --union | ||||
| 			Confirm() | ||||
|  | ||||
| 		t.Common().ContinueOnConflictsResolved("merge") | ||||
|  | ||||
| 		t.Views().Files().IsEmpty() | ||||
|  | ||||
| 		t.FileSystem().FileContent("file", Equals(expected)) | ||||
| 	}, | ||||
| }) | ||||
							
								
								
									
										76
									
								
								pkg/integration/tests/conflicts/merge_file_current.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								pkg/integration/tests/conflicts/merge_file_current.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| package conflicts | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/config" | ||||
| 	. "github.com/jesseduffield/lazygit/pkg/integration/components" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/integration/tests/shared" | ||||
| ) | ||||
|  | ||||
| func testDataCurrent() (original, current, incoming, final string) { | ||||
| 	original = ` | ||||
| 1 | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5 | ||||
| 6 | ||||
| ` | ||||
| 	current = ` | ||||
| 1 | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5a | ||||
| 6 | ||||
| ` | ||||
| 	incoming = ` | ||||
| 1b | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5b | ||||
| 6 | ||||
| ` | ||||
| 	final = ` | ||||
| 1b | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5a | ||||
| 6 | ||||
| ` | ||||
| 	return original, current, incoming, final | ||||
| } | ||||
|  | ||||
| var MergeFileCurrent = NewIntegrationTest(NewIntegrationTestArgs{ | ||||
| 	Description:  "Conflicting file can be resolved to 'ours' (current changes) version via merge-file", | ||||
| 	ExtraCmdArgs: []string{}, | ||||
| 	Skip:         false, | ||||
| 	SetupConfig:  func(config *config.AppConfig) {}, | ||||
| 	SetupRepo: func(shell *Shell) { | ||||
| 		original, current, incoming, _ := testDataCurrent() | ||||
| 		shared.CreateMergeConflictFileForMergeFileTests(shell, original, current, incoming) | ||||
| 	}, | ||||
| 	Run: func(t *TestDriver, keys config.KeybindingConfig) { | ||||
| 		_, _, _, expected := testDataCurrent() | ||||
|  | ||||
| 		t.Views().Files(). | ||||
| 			IsFocused(). | ||||
| 			Lines( | ||||
| 				Contains("file").IsSelected(), | ||||
| 			) | ||||
|  | ||||
| 		t.GlobalPress(keys.Files.OpenMergeOptions) | ||||
|  | ||||
| 		t.ExpectPopup().Menu(). | ||||
| 			Title(Equals("Resolve merge conflicts")). | ||||
| 			Select(Contains("Use current changes")). // merge-file --ours | ||||
| 			Confirm() | ||||
|  | ||||
| 		t.Common().ContinueOnConflictsResolved("merge") | ||||
|  | ||||
| 		t.Views().Files().IsEmpty() | ||||
|  | ||||
| 		t.FileSystem().FileContent("file", Equals(expected)) | ||||
| 	}, | ||||
| }) | ||||
							
								
								
									
										76
									
								
								pkg/integration/tests/conflicts/merge_file_incoming.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								pkg/integration/tests/conflicts/merge_file_incoming.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| package conflicts | ||||
|  | ||||
| import ( | ||||
| 	"github.com/jesseduffield/lazygit/pkg/config" | ||||
| 	. "github.com/jesseduffield/lazygit/pkg/integration/components" | ||||
| 	"github.com/jesseduffield/lazygit/pkg/integration/tests/shared" | ||||
| ) | ||||
|  | ||||
| func testDataIncoming() (original, current, incoming, final string) { | ||||
| 	original = ` | ||||
| 1 | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5 | ||||
| 6 | ||||
| ` | ||||
| 	current = ` | ||||
| 1a | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5a | ||||
| 6 | ||||
| ` | ||||
| 	incoming = ` | ||||
| 1 | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5b | ||||
| 6 | ||||
| ` | ||||
| 	final = ` | ||||
| 1a | ||||
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5b | ||||
| 6 | ||||
| ` | ||||
| 	return original, current, incoming, final | ||||
| } | ||||
|  | ||||
| var MergeFileIncoming = NewIntegrationTest(NewIntegrationTestArgs{ | ||||
| 	Description:  "Conflicting file can be resolved to 'theirs' (incoming changes) version via merge-file", | ||||
| 	ExtraCmdArgs: []string{}, | ||||
| 	Skip:         false, | ||||
| 	SetupConfig:  func(config *config.AppConfig) {}, | ||||
| 	SetupRepo: func(shell *Shell) { | ||||
| 		original, current, incoming, _ := testDataIncoming() | ||||
| 		shared.CreateMergeConflictFileForMergeFileTests(shell, original, current, incoming) | ||||
| 	}, | ||||
| 	Run: func(t *TestDriver, keys config.KeybindingConfig) { | ||||
| 		_, _, _, expected := testDataIncoming() | ||||
|  | ||||
| 		t.Views().Files(). | ||||
| 			IsFocused(). | ||||
| 			Lines( | ||||
| 				Contains("file").IsSelected(), | ||||
| 			) | ||||
|  | ||||
| 		t.GlobalPress(keys.Files.OpenMergeOptions) | ||||
|  | ||||
| 		t.ExpectPopup().Menu(). | ||||
| 			Title(Equals("Resolve merge conflicts")). | ||||
| 			Select(Contains("Use incoming changes")). // merge-file --theirs | ||||
| 			Confirm() | ||||
|  | ||||
| 		t.Common().ContinueOnConflictsResolved("merge") | ||||
|  | ||||
| 		t.Views().Files().IsEmpty() | ||||
|  | ||||
| 		t.FileSystem().FileContent("file", Equals(expected)) | ||||
| 	}, | ||||
| }) | ||||
| @@ -157,3 +157,20 @@ var CreateMergeConflictFileMultiple = func(shell *Shell) { | ||||
|  | ||||
| 	shell.RunCommandExpectError([]string{"git", "merge", "--no-edit", "second-change-branch"}) | ||||
| } | ||||
|  | ||||
| var CreateMergeConflictFileForMergeFileTests = func(shell *Shell, originalFileContent string, currentChangeFileContent string, incomingChangeFileContent string) { | ||||
| 	shell. | ||||
| 		NewBranch("original-branch"). | ||||
| 		EmptyCommit("one"). | ||||
| 		CreateFileAndAdd("file", originalFileContent). | ||||
| 		Commit("original"). | ||||
| 		NewBranch("current-change-branch"). | ||||
| 		UpdateFileAndAdd("file", currentChangeFileContent). | ||||
| 		Commit("first change"). | ||||
| 		Checkout("original-branch"). | ||||
| 		NewBranch("incoming-change-branch"). | ||||
| 		UpdateFileAndAdd("file", incomingChangeFileContent). | ||||
| 		Commit("second change"). | ||||
| 		Checkout("current-change-branch"). | ||||
| 		RunCommandExpectError([]string{"git", "merge", "--no-edit", "incoming-change-branch"}) | ||||
| } | ||||
|   | ||||
| @@ -150,6 +150,9 @@ var tests = []*components.IntegrationTest{ | ||||
| 	config.NegativeRefspec, | ||||
| 	config.RemoteNamedStar, | ||||
| 	conflicts.Filter, | ||||
| 	conflicts.MergeFileBoth, | ||||
| 	conflicts.MergeFileCurrent, | ||||
| 	conflicts.MergeFileIncoming, | ||||
| 	conflicts.ResolveExternally, | ||||
| 	conflicts.ResolveMultipleFiles, | ||||
| 	conflicts.ResolveNoAutoStage, | ||||
|   | ||||
| @@ -1115,7 +1115,7 @@ | ||||
|           "type": "string", | ||||
|           "default": "`" | ||||
|         }, | ||||
|         "openMergeTool": { | ||||
|         "openMergeOptions": { | ||||
|           "type": "string", | ||||
|           "default": "M" | ||||
|         }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user