1
0
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:
Stefan Haller
2025-10-09 08:51:04 +02:00
committed by GitHub
26 changed files with 556 additions and 44 deletions

View File

@@ -619,7 +619,7 @@ keybinding:
viewResetOptions: D
fetch: f
toggleTreeView: '`'
openMergeTool: M
openMergeOptions: M
openStatusFilter: <c-b>
copyFileInfoToClipboard: "y"
collapseAll: '-'

View File

@@ -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)

View File

@@ -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> `` | ファイルパネルに戻る | |
## メインパネル(通常)

View File

@@ -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 |

View File

@@ -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

View File

@@ -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 |

View File

@@ -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)

View File

@@ -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 |

View File

@@ -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> `` | 返回文件面板 | |
## 正在暂存

View File

@@ -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 |

View File

@@ -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()
}

View File

@@ -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 {

View File

@@ -897,7 +897,7 @@ keybinding:
toggleStagedAll: a
viewResetOptions: D
fetch: f
openMergeTool: M
openMergeOptions: M
openStatusFilter: <c-b>
copyFileInfoToClipboard: "y"
collapseAll: '-'

View File

@@ -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",

View File

@@ -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",

View File

@@ -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),

View File

@@ -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)
}

View File

@@ -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',
},
},
})
}

View File

@@ -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 {

View File

@@ -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)

View 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))
},
})

View 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))
},
})

View 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))
},
})

View File

@@ -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"})
}

View File

@@ -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,

View File

@@ -1115,7 +1115,7 @@
"type": "string",
"default": "`"
},
"openMergeTool": {
"openMergeOptions": {
"type": "string",
"default": "M"
},