From 224f8e7e50f2082ea9f3631f7dc98d2f25ea2773 Mon Sep 17 00:00:00 2001 From: tmwatchanan Date: Sat, 20 Jun 2026 18:43:09 +0700 Subject: [PATCH 1/5] Allow copying streamed git output from the command log Hook and push output is easy to read in lazygit but hard to reuse elsewhere. Parse git output blocks from the command log view so copies match what is shown, include the action and command above each block, and avoid logging clipboard operations back into the stream. --- pkg/commands/oscommands/os.go | 12 ++ pkg/gui/command_log_panel.go | 122 ++++++++++++++++++ pkg/gui/command_log_panel_test.go | 89 +++++++++++++ pkg/gui/extras_panel.go | 53 +++++++- pkg/gui/gui_common.go | 8 ++ pkg/gui/types/common.go | 3 + pkg/i18n/english.go | 8 ++ .../misc/copy_git_output_to_clipboard.go | 45 +++++++ pkg/integration/tests/test_list.go | 1 + 9 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 pkg/gui/command_log_panel_test.go create mode 100644 pkg/integration/tests/misc/copy_git_output_to_clipboard.go diff --git a/pkg/commands/oscommands/os.go b/pkg/commands/oscommands/os.go index f1ee86c4c9a..a4212b78d80 100644 --- a/pkg/commands/oscommands/os.go +++ b/pkg/commands/oscommands/os.go @@ -267,6 +267,15 @@ func (c *OSCommand) PipeCommands(cmdObjs ...*CmdObj) error { } func (c *OSCommand) CopyToClipboard(str string) error { + c.logCopyToClipboard(str) + return c.writeToClipboard(str) +} + +func (c *OSCommand) CopyToClipboardQuiet(str string) error { + return c.writeToClipboard(str) +} + +func (c *OSCommand) logCopyToClipboard(str string) { escaped := strings.ReplaceAll(str, "\n", "\\n") truncated := utils.TruncateWithEllipsis(escaped, 40) @@ -277,6 +286,9 @@ func (c *OSCommand) CopyToClipboard(str string) error { }, ) c.LogCommand(msg, false) +} + +func (c *OSCommand) writeToClipboard(str string) error { if c.UserConfig().OS.CopyToClipboardCmd != "" { cmdStr := utils.ResolvePlaceholderString(c.UserConfig().OS.CopyToClipboardCmd, map[string]string{ "text": c.Cmd.Quote(str), diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go index 8f2e06b9842..ee6b2ae5230 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -33,6 +33,128 @@ func (gui *Gui) LogAction(action string) { fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action)) } +func (gui *Gui) gitOutputBlocksFromView() []string { + if gui.Views.Extras == nil { + return nil + } + return gitOutputBlocksFromCommandLogLines(gui.Views.Extras.BufferLines(), gui.c.Tr.GitOutput) +} + +func (gui *Gui) lastGitOutput() string { + blocks := gui.gitOutputBlocksFromView() + if len(blocks) == 0 { + return "" + } + return blocks[len(blocks)-1] +} + +func (gui *Gui) allGitOutput() string { + return strings.Join(gui.gitOutputBlocksFromView(), "\n\n") +} + +func (gui *Gui) hasGitOutput() bool { + return gui.lastGitOutput() != "" +} + +func gitOutputBlocksFromCommandLogLines(lines []string, gitOutputHeader string) []string { + var blocks []string + + for i, line := range lines { + if line != gitOutputHeader { + continue + } + + block := commandLogEntryBeforeGitOutput(lines, i, gitOutputHeader) + if len(block) > 0 { + block = append(block, "") + } + block = append(block, gitOutputHeader) + block = append(block, gitOutputLinesAfterHeader(lines, i+1, gitOutputHeader)...) + + if trimmed := strings.TrimRight(strings.Join(block, "\n"), "\n"); trimmed != "" { + blocks = append(blocks, trimmed) + } + } + + return blocks +} + +func commandLogEntryBeforeGitOutput(lines []string, headerIdx int, gitOutputHeader string) []string { + i := headerIdx - 1 + for i >= 0 && lines[i] == "" { + i-- + } + + var commands []string + for i >= 0 && strings.HasPrefix(lines[i], " ") && !isCopyToClipboardLogLine(lines[i]) { + commands = append([]string{lines[i]}, commands...) + i-- + } + + if len(commands) == 0 { + return nil + } + + for i >= 0 && lines[i] == "" { + i-- + } + + var entry []string + if i >= 0 && !strings.HasPrefix(lines[i], " ") && lines[i] != gitOutputHeader { + entry = append(entry, lines[i]) + } + entry = append(entry, commands...) + + return entry +} + +func gitOutputLinesAfterHeader(lines []string, startIdx int, gitOutputHeader string) []string { + output := make([]string, 0, len(lines)-startIdx) + + for i := startIdx; i < len(lines); i++ { + line := lines[i] + if line == gitOutputHeader { + break + } + if isCopyToClipboardLogLine(line) { + continue + } + if strings.HasPrefix(line, " ") { + continue + } + if isStartOfNewCommandLogEntry(lines, i) { + break + } + output = append(output, line) + } + + return output +} + +func isCopyToClipboardLogLine(line string) bool { + trimmed := strings.TrimSpace(line) + return strings.HasPrefix(trimmed, "Copying '") && strings.HasSuffix(trimmed, "' to clipboard") +} + +func isStartOfNewCommandLogEntry(lines []string, i int) bool { + line := lines[i] + if line == "" || strings.HasPrefix(line, " ") { + return false + } + + for j := i + 1; j < len(lines); j++ { + if lines[j] == "" { + continue + } + if isCopyToClipboardLogLine(lines[j]) { + continue + } + return strings.HasPrefix(lines[j], " ") + } + + return false +} + func (gui *Gui) LogCommand(cmdStr string, commandLine bool) { if gui.Views.Extras == nil { return diff --git a/pkg/gui/command_log_panel_test.go b/pkg/gui/command_log_panel_test.go new file mode 100644 index 00000000000..6ab059930f6 --- /dev/null +++ b/pkg/gui/command_log_panel_test.go @@ -0,0 +1,89 @@ +package gui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const gitOutputHeader = "Git output:" + +func TestGitOutputBlocksFromCommandLogLines(t *testing.T) { + t.Parallel() + + lines := []string{ + "Push", + " git push", + "", + gitOutputHeader, + "line1", + "line2", + } + + assert.Equal(t, []string{"Push\n git push\n\nGit output:\nline1\nline2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) +} + +func TestGitOutputBlocksSkipCopyNotifications(t *testing.T) { + t.Parallel() + + lines := []string{ + "Push", + " git push", + gitOutputHeader, + "hook line", + " Copying 'hook line' to clipboard", + "hook line 2", + } + + assert.Equal(t, []string{"Push\n git push\n\nGit output:\nhook line\nhook line 2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) +} + +func TestGitOutputBlocksEndAtNextCommandLogEntry(t *testing.T) { + t.Parallel() + + lines := []string{ + "Push", + " git push", + gitOutputHeader, + "first command output", + "Stage file", + " git add foo", + "", + gitOutputHeader, + "second command output", + } + + assert.Equal(t, []string{ + "Push\n git push\n\nGit output:\nfirst command output", + "Stage file\n git add foo\n\nGit output:\nsecond command output", + }, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) +} + +func TestGitOutputBlocksMultipleBlocksJoined(t *testing.T) { + t.Parallel() + + lines := []string{ + "Push", + " git push", + gitOutputHeader, + "first command", + "Pull", + " git pull", + gitOutputHeader, + "second command", + } + + blocks := gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader) + assert.Equal(t, "Push\n git push\n\nGit output:\nfirst command\n\nPull\n git pull\n\nGit output:\nsecond command", joinGitOutputBlocks(blocks)) +} + +func joinGitOutputBlocks(blocks []string) string { + result := "" + for i, block := range blocks { + if i > 0 { + result += "\n\n" + } + result += block + } + return result +} diff --git a/pkg/gui/extras_panel.go b/pkg/gui/extras_panel.go index 980c31d4543..d4988b98103 100644 --- a/pkg/gui/extras_panel.go +++ b/pkg/gui/extras_panel.go @@ -1,6 +1,7 @@ package gui import ( + "errors" "io" "github.com/jesseduffield/lazygit/pkg/gocui" @@ -10,6 +11,13 @@ import ( ) func (gui *Gui) handleCreateExtrasMenuPanel() error { + noGitOutputDisabledReason := func() *types.DisabledReason { + if gui.hasGitOutput() { + return nil + } + return &types.DisabledReason{Text: gui.c.Tr.NoGitOutputToCopy} + } + return gui.c.Menu(types.CreateMenuOptions{ Title: gui.c.Tr.CommandLog, Items: []*types.MenuItem{ @@ -33,10 +41,50 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error { Keys: []gocui.Key{gocui.NewKeyRune('f')}, OnPress: gui.handleFocusCommandLog, }, + { + Label: gui.c.Tr.CopyGitOutputToClipboard, + Keys: []gocui.Key{gocui.NewKeyRune('c')}, + OnPress: gui.handleCopyLastGitOutputToClipboard, + DisabledReason: noGitOutputDisabledReason(), + }, + { + Label: gui.c.Tr.CopyAllGitOutputToClipboard, + Keys: []gocui.Key{gocui.NewKeyRune('a')}, + OnPress: gui.handleCopyAllGitOutputToClipboard, + DisabledReason: noGitOutputDisabledReason(), + }, }, }) } +func (gui *Gui) handleCopyLastGitOutputToClipboard() error { + output := gui.lastGitOutput() + if output == "" { + return errors.New(gui.c.Tr.NoGitOutputToCopy) + } + + if err := gui.os.CopyToClipboardQuiet(output); err != nil { + return err + } + + gui.c.Toast(gui.c.Tr.GitOutputCopiedToClipboard) + return nil +} + +func (gui *Gui) handleCopyAllGitOutputToClipboard() error { + output := gui.allGitOutput() + if output == "" { + return errors.New(gui.c.Tr.NoGitOutputToCopy) + } + + if err := gui.os.CopyToClipboardQuiet(output); err != nil { + return err + } + + gui.c.Toast(gui.c.Tr.GitOutputCopiedToClipboard) + return nil +} + func (gui *Gui) handleFocusCommandLog() error { gui.c.State().SetShowExtrasWindow(true) // TODO: is this necessary? Can't I just call 'return from context'? @@ -94,7 +142,10 @@ func (gui *Gui) goToExtrasPanelBottom() error { } func (gui *Gui) getCmdWriter() io.Writer { - return &prefixWriter{writer: gui.Views.Extras, prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.c.Tr.GitOutput)} + return &prefixWriter{ + writer: gui.Views.Extras, + prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.c.Tr.GitOutput), + } } // Ensures that the first write is preceded by writing a prefix. diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index c74a99a0568..29057f9d4f3 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -189,3 +189,11 @@ func (self *guiCommon) WithInlineStatus(item types.HasUrn, operation types.ItemO self.gui.helpers.InlineStatus.WithInlineStatus(helpers.InlineStatusOpts{Item: item, Operation: operation, ContextKey: contextKey}, f) return nil } + +func (self *guiCommon) LastGitOutput() string { + return self.gui.lastGitOutput() +} + +func (self *guiCommon) AllGitOutput() string { + return self.gui.allGitOutput() +} diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index b81fb15e135..d81da50936b 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -114,6 +114,9 @@ type IGuiCommon interface { ResetKeybindings() error + LastGitOutput() string + AllGitOutput() string + // hopefully we can remove this once we've moved all our keybinding stuff out of the gui god struct. GetInitialKeybindingsWithCustomCommands() ([]*Binding, []*gocui.ViewMouseBinding) diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 20d0d5ff64e..346b10c54e1 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -768,6 +768,10 @@ type TranslationSet struct { CommandLog string ToggleShowCommandLog string FocusCommandLog string + CopyGitOutputToClipboard string + CopyAllGitOutputToClipboard string + NoGitOutputToCopy string + GitOutputCopiedToClipboard string CommandLogHeader string RandomTip string ToggleWhitespaceInDiffView string @@ -1899,6 +1903,10 @@ func EnglishTranslationSet() *TranslationSet { ErrWorktreeMovedOrRemoved: "Cannot find worktree. It might have been moved or removed ¯\\_(ツ)_/¯", ToggleShowCommandLog: "Toggle show/hide command log", FocusCommandLog: "Focus command log", + CopyGitOutputToClipboard: "Copy last git output to clipboard", + CopyAllGitOutputToClipboard: "Copy all git outputs to clipboard", + NoGitOutputToCopy: "No git output to copy", + GitOutputCopiedToClipboard: "Git output copied to clipboard", CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n", RandomTip: "Random tip", ToggleWhitespaceInDiffView: "Toggle whitespace", diff --git a/pkg/integration/tests/misc/copy_git_output_to_clipboard.go b/pkg/integration/tests/misc/copy_git_output_to_clipboard.go new file mode 100644 index 00000000000..2dfc2208ce5 --- /dev/null +++ b/pkg/integration/tests/misc/copy_git_output_to_clipboard.go @@ -0,0 +1,45 @@ +package misc + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var CopyGitOutputToClipboard = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Copy streamed git output from the command log to the clipboard", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard" + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + + shell.CloneIntoRemote("origin") + + shell.SetBranchUpstream("master", "origin/master") + + shell.EmptyCommit("two") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.Push) + + t.Views().Status().Content(Equals("✓ repo → master")) + + t.GlobalPress(keys.Universal.ExtrasMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Command log")). + Select(Contains("Copy last git output to clipboard")). + Confirm() + + t.ExpectToast(Equals("Git output copied to clipboard")) + + t.FileSystem().FileContent("clipboard", + Contains("master -> master"). + Contains("git push"). + Contains("Push")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 1b264e50dc9..7c55017ce03 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -334,6 +334,7 @@ var tests = []*components.IntegrationTest{ interactive_rebase.ViewFilesOfTodoEntries, misc.ConfirmOnQuit, misc.CopyConfirmationMessageToClipboard, + misc.CopyGitOutputToClipboard, misc.CopyToClipboard, misc.DirenvApprovesEnvrc, misc.DirenvLoadedOnRepoSwitch, From a2b21978c63b5a7ed1e625ad8a25d255a3ea0e57 Mon Sep 17 00:00:00 2001 From: tmwatchanan Date: Sat, 20 Jun 2026 18:43:09 +0700 Subject: [PATCH 2/5] Keep indented stderr when copying git output from the command log Hook and linter output often uses leading spaces for stack traces and continuations. Those lines are visible in the log but were dropped on copy because every two-space-prefixed line was treated as lazygit log noise. --- pkg/gui/command_log_panel.go | 3 --- pkg/gui/command_log_panel_test.go | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go index ee6b2ae5230..be9fdc5b30d 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -119,9 +119,6 @@ func gitOutputLinesAfterHeader(lines []string, startIdx int, gitOutputHeader str if isCopyToClipboardLogLine(line) { continue } - if strings.HasPrefix(line, " ") { - continue - } if isStartOfNewCommandLogEntry(lines, i) { break } diff --git a/pkg/gui/command_log_panel_test.go b/pkg/gui/command_log_panel_test.go index 6ab059930f6..3d20740175f 100644 --- a/pkg/gui/command_log_panel_test.go +++ b/pkg/gui/command_log_panel_test.go @@ -23,6 +23,21 @@ func TestGitOutputBlocksFromCommandLogLines(t *testing.T) { assert.Equal(t, []string{"Push\n git push\n\nGit output:\nline1\nline2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) } +func TestGitOutputBlocksIncludeIndentedStderr(t *testing.T) { + t.Parallel() + + lines := []string{ + "Push", + " git push", + gitOutputHeader, + " at foo.go:10", + " at bar.go:20", + "hook failed", + } + + assert.Equal(t, []string{"Push\n git push\n\nGit output:\n at foo.go:10\n at bar.go:20\nhook failed"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) +} + func TestGitOutputBlocksSkipCopyNotifications(t *testing.T) { t.Parallel() From 799deabfefb76383694dbcc1a41c07418c538d93 Mon Sep 17 00:00:00 2001 From: tmwatchanan Date: Sat, 20 Jun 2026 18:43:09 +0700 Subject: [PATCH 3/5] Stop treating tool errors as command log boundaries when copying git output A non-indented error line followed by indented context matches the shape of an action plus command, so copied git output was truncated mid-block. Only treat the next line as a new log entry when it looks like a lazygit command rather than any indented text. --- pkg/gui/command_log_panel.go | 6 +++++- pkg/gui/command_log_panel_test.go | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go index be9fdc5b30d..09edd14ddc2 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -146,12 +146,16 @@ func isStartOfNewCommandLogEntry(lines []string, i int) bool { if isCopyToClipboardLogLine(lines[j]) { continue } - return strings.HasPrefix(lines[j], " ") + return isLazygitCommandLogLine(lines[j]) } return false } +func isLazygitCommandLogLine(line string) bool { + return isCopyToClipboardLogLine(line) || strings.HasPrefix(line, " git ") +} + func (gui *Gui) LogCommand(cmdStr string, commandLine bool) { if gui.Views.Extras == nil { return diff --git a/pkg/gui/command_log_panel_test.go b/pkg/gui/command_log_panel_test.go index 3d20740175f..485c731c663 100644 --- a/pkg/gui/command_log_panel_test.go +++ b/pkg/gui/command_log_panel_test.go @@ -38,6 +38,28 @@ func TestGitOutputBlocksIncludeIndentedStderr(t *testing.T) { assert.Equal(t, []string{"Push\n git push\n\nGit output:\n at foo.go:10\n at bar.go:20\nhook failed"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) } +func TestGitOutputBlocksIncludeToolErrorWithIndentedContext(t *testing.T) { + t.Parallel() + + lines := []string{ + "Push", + " git push", + gitOutputHeader, + "Error: validation failed", + " line 42: syntax error", + "more output", + "Stage file", + " git add foo", + gitOutputHeader, + "second command output", + } + + assert.Equal(t, []string{ + "Push\n git push\n\nGit output:\nError: validation failed\n line 42: syntax error\nmore output", + "Stage file\n git add foo\n\nGit output:\nsecond command output", + }, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) +} + func TestGitOutputBlocksSkipCopyNotifications(t *testing.T) { t.Parallel() From 01072bac90ff8b2cbb6052e034d767c84debf405 Mon Sep 17 00:00:00 2001 From: tmwatchanan Date: Sat, 20 Jun 2026 22:12:50 +0700 Subject: [PATCH 4/5] Open the full command log in the user's editor from the @ menu Avoids awkward multi-line terminal copy when reusing log content elsewhere. --- pkg/gui/command_log_panel.go | 7 +++ pkg/gui/extras_panel.go | 25 +++++++++++ pkg/i18n/english.go | 4 ++ .../tests/misc/open_command_log_in_editor.go | 43 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 5 files changed, 80 insertions(+) create mode 100644 pkg/integration/tests/misc/open_command_log_in_editor.go diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go index 09edd14ddc2..a30639baa14 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -56,6 +56,13 @@ func (gui *Gui) hasGitOutput() bool { return gui.lastGitOutput() != "" } +func (gui *Gui) commandLogContent() string { + if gui.Views.Extras == nil { + return "" + } + return strings.TrimRight(gui.Views.Extras.Buffer(), "\n") +} + func gitOutputBlocksFromCommandLogLines(lines []string, gitOutputHeader string) []string { var blocks []string diff --git a/pkg/gui/extras_panel.go b/pkg/gui/extras_panel.go index d4988b98103..951ba0f7525 100644 --- a/pkg/gui/extras_panel.go +++ b/pkg/gui/extras_panel.go @@ -3,6 +3,8 @@ package gui import ( "errors" "io" + "path/filepath" + "time" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" @@ -53,6 +55,11 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error { OnPress: gui.handleCopyAllGitOutputToClipboard, DisabledReason: noGitOutputDisabledReason(), }, + { + Label: gui.c.Tr.OpenCommandLogInEditor, + Keys: []gocui.Key{gocui.NewKeyRune('o')}, + OnPress: gui.handleOpenCommandLogInEditor, + }, }, }) } @@ -85,6 +92,24 @@ func (gui *Gui) handleCopyAllGitOutputToClipboard() error { return nil } +func (gui *Gui) handleOpenCommandLogInEditor() error { + content := gui.commandLogContent() + if content == "" { + return errors.New(gui.c.Tr.NoCommandLogToOpenInEditor) + } + + filepath := filepath.Join( + gui.os.GetTempDir(), + gui.c.Git().RepoPaths.RepoName(), + time.Now().Format("Jan _2 15.04.05.000000000")+"-command-log.txt", + ) + if err := gui.os.CreateFileWithContent(filepath, content); err != nil { + return err + } + + return gui.Helpers().Files.EditFiles([]string{filepath}) +} + func (gui *Gui) handleFocusCommandLog() error { gui.c.State().SetShowExtrasWindow(true) // TODO: is this necessary? Can't I just call 'return from context'? diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 346b10c54e1..aca3bc0cbe7 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -772,6 +772,8 @@ type TranslationSet struct { CopyAllGitOutputToClipboard string NoGitOutputToCopy string GitOutputCopiedToClipboard string + OpenCommandLogInEditor string + NoCommandLogToOpenInEditor string CommandLogHeader string RandomTip string ToggleWhitespaceInDiffView string @@ -1907,6 +1909,8 @@ func EnglishTranslationSet() *TranslationSet { CopyAllGitOutputToClipboard: "Copy all git outputs to clipboard", NoGitOutputToCopy: "No git output to copy", GitOutputCopiedToClipboard: "Git output copied to clipboard", + OpenCommandLogInEditor: "Open command log in editor", + NoCommandLogToOpenInEditor: "No command log to open in editor", CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n", RandomTip: "Random tip", ToggleWhitespaceInDiffView: "Toggle whitespace", diff --git a/pkg/integration/tests/misc/open_command_log_in_editor.go b/pkg/integration/tests/misc/open_command_log_in_editor.go new file mode 100644 index 00000000000..75c1c5aa0d8 --- /dev/null +++ b/pkg/integration/tests/misc/open_command_log_in_editor.go @@ -0,0 +1,43 @@ +package misc + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var OpenCommandLogInEditor = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Open the full command log in the user's editor", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().OS.Edit = "cp {{filename}} editor-output.txt" + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + + shell.CloneIntoRemote("origin") + + shell.SetBranchUpstream("master", "origin/master") + + shell.EmptyCommit("two") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.Push) + + t.Views().Status().Content(Equals("✓ repo → master")) + + t.GlobalPress(keys.Universal.ExtrasMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Command log")). + Select(Contains("Open command log in editor")). + Confirm() + + t.FileSystem().FileContent("editor-output.txt", + Contains("Push"). + Contains("git push"). + Contains("master -> master")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 7c55017ce03..ff0fabf8961 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -340,6 +340,7 @@ var tests = []*components.IntegrationTest{ misc.DirenvLoadedOnRepoSwitch, misc.DirenvUnloadsOnBlockedEnvrc, misc.InitialOpen, + misc.OpenCommandLogInEditor, misc.RecentReposOnLaunch, patch_building.Apply, patch_building.ApplyInReverse, From da09bb717152e4853a69981bf0280484fe547f3f Mon Sep 17 00:00:00 2001 From: tmwatchanan Date: Sat, 20 Jun 2026 22:24:14 +0700 Subject: [PATCH 5/5] Fix command log menu disabled state and editor export content Evaluate menu disabled reasons when rendering and invoking so copy items update while the menu is open. Export actions and git output without panel chrome, match copy-notification lines via i18n log templates, and disable open-in-editor until there is real log content. --- pkg/gui/command_log_panel.go | 86 +++++++++++++++---- pkg/gui/command_log_panel_test.go | 26 ++++-- pkg/gui/context/menu_context.go | 16 ++-- .../helpers/confirmation_helper.go | 4 +- pkg/gui/extras_panel.go | 37 +++++--- pkg/gui/types/common.go | 12 +++ pkg/i18n/english.go | 2 + .../misc/copy_all_git_output_to_clipboard.go | 45 ++++++++++ .../tests/misc/open_command_log_in_editor.go | 6 +- pkg/integration/tests/test_list.go | 1 + 10 files changed, 191 insertions(+), 44 deletions(-) create mode 100644 pkg/integration/tests/misc/copy_all_git_output_to_clipboard.go diff --git a/pkg/gui/command_log_panel.go b/pkg/gui/command_log_panel.go index a30639baa14..4028fa1de23 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -37,7 +37,11 @@ func (gui *Gui) gitOutputBlocksFromView() []string { if gui.Views.Extras == nil { return nil } - return gitOutputBlocksFromCommandLogLines(gui.Views.Extras.BufferLines(), gui.c.Tr.GitOutput) + return gitOutputBlocksFromCommandLogLines( + gui.Views.Extras.BufferLines(), + gui.c.Tr.GitOutput, + gui.isCopyToClipboardLogLine, + ) } func (gui *Gui) lastGitOutput() string { @@ -56,14 +60,69 @@ func (gui *Gui) hasGitOutput() bool { return gui.lastGitOutput() != "" } +func (gui *Gui) hasCommandLogEntries() bool { + return gui.commandLogContent() != "" +} + func (gui *Gui) commandLogContent() string { if gui.Views.Extras == nil { return "" } - return strings.TrimRight(gui.Views.Extras.Buffer(), "\n") + + introLine := gui.commandLogIntroLine() + var filtered []string + for _, line := range gui.Views.Extras.BufferLines() { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + if len(filtered) > 0 { + filtered = append(filtered, "") + } + continue + } + if trimmed == introLine { + continue + } + if strings.HasPrefix(trimmed, gui.c.Tr.RandomTip+":") { + continue + } + if gui.isCopyToClipboardLogLine(line) { + continue + } + if gui.isCreateFileLogLine(line) { + continue + } + filtered = append(filtered, line) + } + + return strings.TrimRight(strings.Join(filtered, "\n"), "\n") } -func gitOutputBlocksFromCommandLogLines(lines []string, gitOutputHeader string) []string { +func (gui *Gui) commandLogIntroLine() string { + return strings.TrimSpace(fmt.Sprintf( + gui.c.Tr.CommandLogHeader, + gui.c.UserConfig().Keybinding.Universal.ExtrasMenu, + )) +} + +func (gui *Gui) isCopyToClipboardLogLine(line string) bool { + return logLineMatchesTemplate(line, gui.c.Tr.Log.CopyToClipboard, "{{.str}}") +} + +func (gui *Gui) isCreateFileLogLine(line string) bool { + return logLineMatchesTemplate(line, gui.c.Tr.Log.CreateFileWithContent, "{{.path}}") +} + +func logLineMatchesTemplate(line string, template string, placeholder string) bool { + parts := strings.Split(template, placeholder) + if len(parts) != 2 { + return false + } + + trimmed := strings.TrimSpace(line) + return strings.HasPrefix(trimmed, parts[0]) && strings.HasSuffix(trimmed, parts[1]) +} + +func gitOutputBlocksFromCommandLogLines(lines []string, gitOutputHeader string, isCopyToClipboardLogLine func(string) bool) []string { var blocks []string for i, line := range lines { @@ -71,12 +130,12 @@ func gitOutputBlocksFromCommandLogLines(lines []string, gitOutputHeader string) continue } - block := commandLogEntryBeforeGitOutput(lines, i, gitOutputHeader) + block := commandLogEntryBeforeGitOutput(lines, i, gitOutputHeader, isCopyToClipboardLogLine) if len(block) > 0 { block = append(block, "") } block = append(block, gitOutputHeader) - block = append(block, gitOutputLinesAfterHeader(lines, i+1, gitOutputHeader)...) + block = append(block, gitOutputLinesAfterHeader(lines, i+1, gitOutputHeader, isCopyToClipboardLogLine)...) if trimmed := strings.TrimRight(strings.Join(block, "\n"), "\n"); trimmed != "" { blocks = append(blocks, trimmed) @@ -86,7 +145,7 @@ func gitOutputBlocksFromCommandLogLines(lines []string, gitOutputHeader string) return blocks } -func commandLogEntryBeforeGitOutput(lines []string, headerIdx int, gitOutputHeader string) []string { +func commandLogEntryBeforeGitOutput(lines []string, headerIdx int, gitOutputHeader string, isCopyToClipboardLogLine func(string) bool) []string { i := headerIdx - 1 for i >= 0 && lines[i] == "" { i-- @@ -115,7 +174,7 @@ func commandLogEntryBeforeGitOutput(lines []string, headerIdx int, gitOutputHead return entry } -func gitOutputLinesAfterHeader(lines []string, startIdx int, gitOutputHeader string) []string { +func gitOutputLinesAfterHeader(lines []string, startIdx int, gitOutputHeader string, isCopyToClipboardLogLine func(string) bool) []string { output := make([]string, 0, len(lines)-startIdx) for i := startIdx; i < len(lines); i++ { @@ -126,7 +185,7 @@ func gitOutputLinesAfterHeader(lines []string, startIdx int, gitOutputHeader str if isCopyToClipboardLogLine(line) { continue } - if isStartOfNewCommandLogEntry(lines, i) { + if isStartOfNewCommandLogEntry(lines, i, isCopyToClipboardLogLine) { break } output = append(output, line) @@ -135,12 +194,7 @@ func gitOutputLinesAfterHeader(lines []string, startIdx int, gitOutputHeader str return output } -func isCopyToClipboardLogLine(line string) bool { - trimmed := strings.TrimSpace(line) - return strings.HasPrefix(trimmed, "Copying '") && strings.HasSuffix(trimmed, "' to clipboard") -} - -func isStartOfNewCommandLogEntry(lines []string, i int) bool { +func isStartOfNewCommandLogEntry(lines []string, i int, isCopyToClipboardLogLine func(string) bool) bool { line := lines[i] if line == "" || strings.HasPrefix(line, " ") { return false @@ -153,13 +207,13 @@ func isStartOfNewCommandLogEntry(lines []string, i int) bool { if isCopyToClipboardLogLine(lines[j]) { continue } - return isLazygitCommandLogLine(lines[j]) + return isLazygitCommandLogLine(lines[j], isCopyToClipboardLogLine) } return false } -func isLazygitCommandLogLine(line string) bool { +func isLazygitCommandLogLine(line string, isCopyToClipboardLogLine func(string) bool) bool { return isCopyToClipboardLogLine(line) || strings.HasPrefix(line, " git ") } diff --git a/pkg/gui/command_log_panel_test.go b/pkg/gui/command_log_panel_test.go index 485c731c663..3556ef366bf 100644 --- a/pkg/gui/command_log_panel_test.go +++ b/pkg/gui/command_log_panel_test.go @@ -8,6 +8,12 @@ import ( const gitOutputHeader = "Git output:" +func englishCopyToClipboardLogLineMatcher() func(string) bool { + return func(line string) bool { + return logLineMatchesTemplate(line, "Copying '{{.str}}' to clipboard", "{{.str}}") + } +} + func TestGitOutputBlocksFromCommandLogLines(t *testing.T) { t.Parallel() @@ -20,7 +26,7 @@ func TestGitOutputBlocksFromCommandLogLines(t *testing.T) { "line2", } - assert.Equal(t, []string{"Push\n git push\n\nGit output:\nline1\nline2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) + assert.Equal(t, []string{"Push\n git push\n\nGit output:\nline1\nline2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher())) } func TestGitOutputBlocksIncludeIndentedStderr(t *testing.T) { @@ -35,7 +41,7 @@ func TestGitOutputBlocksIncludeIndentedStderr(t *testing.T) { "hook failed", } - assert.Equal(t, []string{"Push\n git push\n\nGit output:\n at foo.go:10\n at bar.go:20\nhook failed"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) + assert.Equal(t, []string{"Push\n git push\n\nGit output:\n at foo.go:10\n at bar.go:20\nhook failed"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher())) } func TestGitOutputBlocksIncludeToolErrorWithIndentedContext(t *testing.T) { @@ -57,7 +63,7 @@ func TestGitOutputBlocksIncludeToolErrorWithIndentedContext(t *testing.T) { assert.Equal(t, []string{ "Push\n git push\n\nGit output:\nError: validation failed\n line 42: syntax error\nmore output", "Stage file\n git add foo\n\nGit output:\nsecond command output", - }, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) + }, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher())) } func TestGitOutputBlocksSkipCopyNotifications(t *testing.T) { @@ -72,7 +78,7 @@ func TestGitOutputBlocksSkipCopyNotifications(t *testing.T) { "hook line 2", } - assert.Equal(t, []string{"Push\n git push\n\nGit output:\nhook line\nhook line 2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) + assert.Equal(t, []string{"Push\n git push\n\nGit output:\nhook line\nhook line 2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher())) } func TestGitOutputBlocksEndAtNextCommandLogEntry(t *testing.T) { @@ -93,7 +99,7 @@ func TestGitOutputBlocksEndAtNextCommandLogEntry(t *testing.T) { assert.Equal(t, []string{ "Push\n git push\n\nGit output:\nfirst command output", "Stage file\n git add foo\n\nGit output:\nsecond command output", - }, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader)) + }, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher())) } func TestGitOutputBlocksMultipleBlocksJoined(t *testing.T) { @@ -110,10 +116,18 @@ func TestGitOutputBlocksMultipleBlocksJoined(t *testing.T) { "second command", } - blocks := gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader) + blocks := gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher()) assert.Equal(t, "Push\n git push\n\nGit output:\nfirst command\n\nPull\n git pull\n\nGit output:\nsecond command", joinGitOutputBlocks(blocks)) } +func TestLogLineMatchesCopyToClipboardTemplate(t *testing.T) { + t.Parallel() + + matcher := englishCopyToClipboardLogLineMatcher() + assert.True(t, matcher(" Copying 'hook line' to clipboard")) + assert.False(t, matcher("Push")) +} + func joinGitOutputBlocks(blocks []string) string { result := "" for i, block := range blocks { diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 9feef1e4cba..283cfa36e34 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -138,7 +138,7 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { return lo.Map(menuItems, func(item *types.MenuItem, _ int) []string { displayStrings := item.LabelColumns - if item.DisabledReason != nil { + if item.DisabledReasonAtUse() != nil { displayStrings[0] = style.FgDefault.SetStrikethrough().Sprint(displayStrings[0]) } @@ -237,13 +237,15 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin } func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error { - if selectedItem != nil && selectedItem.DisabledReason != nil { - if selectedItem.DisabledReason.ShowErrorInPanel { - return errors.New(selectedItem.DisabledReason.Text) - } + if selectedItem != nil { + if disabledReason := selectedItem.DisabledReasonAtUse(); disabledReason != nil { + if disabledReason.ShowErrorInPanel { + return errors.New(disabledReason.Text) + } - self.c.ErrorToast(self.c.Tr.DisabledMenuItemPrefix + selectedItem.DisabledReason.Text) - return nil + self.c.ErrorToast(self.c.Tr.DisabledMenuItemPrefix + disabledReason.Text) + return nil + } } self.c.Context().Pop() diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 3663cd4eac9..5a8657002b5 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -443,11 +443,11 @@ func (self *ConfirmationHelper) IsPopupPanelFocused() bool { func (self *ConfirmationHelper) TooltipForMenuItem(menuItem *types.MenuItem) string { tooltip := menuItem.Tooltip - if menuItem.DisabledReason != nil && menuItem.DisabledReason.Text != "" { + if disabledReason := menuItem.DisabledReasonAtUse(); disabledReason != nil && disabledReason.Text != "" { if tooltip != "" { tooltip += "\n\n" } - tooltip += style.FgRed.Sprintf(self.c.Tr.DisabledMenuItemPrefix) + menuItem.DisabledReason.Text + tooltip += style.FgRed.Sprintf(self.c.Tr.DisabledMenuItemPrefix) + disabledReason.Text } return tooltip } diff --git a/pkg/gui/extras_panel.go b/pkg/gui/extras_panel.go index 951ba0f7525..54019aeb3d2 100644 --- a/pkg/gui/extras_panel.go +++ b/pkg/gui/extras_panel.go @@ -20,6 +20,13 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error { return &types.DisabledReason{Text: gui.c.Tr.NoGitOutputToCopy} } + noCommandLogDisabledReason := func() *types.DisabledReason { + if gui.hasCommandLogEntries() { + return nil + } + return &types.DisabledReason{Text: gui.c.Tr.NoCommandLogToOpenInEditor} + } + return gui.c.Menu(types.CreateMenuOptions{ Title: gui.c.Tr.CommandLog, Items: []*types.MenuItem{ @@ -44,21 +51,22 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error { OnPress: gui.handleFocusCommandLog, }, { - Label: gui.c.Tr.CopyGitOutputToClipboard, - Keys: []gocui.Key{gocui.NewKeyRune('c')}, - OnPress: gui.handleCopyLastGitOutputToClipboard, - DisabledReason: noGitOutputDisabledReason(), + Label: gui.c.Tr.CopyGitOutputToClipboard, + Keys: []gocui.Key{gocui.NewKeyRune('c')}, + OnPress: gui.handleCopyLastGitOutputToClipboard, + GetDisabledReason: noGitOutputDisabledReason, }, { - Label: gui.c.Tr.CopyAllGitOutputToClipboard, - Keys: []gocui.Key{gocui.NewKeyRune('a')}, - OnPress: gui.handleCopyAllGitOutputToClipboard, - DisabledReason: noGitOutputDisabledReason(), + Label: gui.c.Tr.CopyAllGitOutputToClipboard, + Keys: []gocui.Key{gocui.NewKeyRune('a')}, + OnPress: gui.handleCopyAllGitOutputToClipboard, + GetDisabledReason: noGitOutputDisabledReason, }, { - Label: gui.c.Tr.OpenCommandLogInEditor, - Keys: []gocui.Key{gocui.NewKeyRune('o')}, - OnPress: gui.handleOpenCommandLogInEditor, + Label: gui.c.Tr.OpenCommandLogInEditor, + Keys: []gocui.Key{gocui.NewKeyRune('o')}, + OnPress: gui.handleOpenCommandLogInEditor, + GetDisabledReason: noCommandLogDisabledReason, }, }, }) @@ -107,7 +115,12 @@ func (gui *Gui) handleOpenCommandLogInEditor() error { return err } - return gui.Helpers().Files.EditFiles([]string{filepath}) + if err := gui.Helpers().Files.EditFiles([]string{filepath}); err != nil { + return err + } + + gui.c.Toast(gui.c.Tr.CommandLogOpenedInEditor) + return nil } func (gui *Gui) handleFocusCommandLog() error { diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index d81da50936b..11ce2234fe6 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -284,6 +284,10 @@ type MenuItem struct { // and refuse to invoke the command DisabledReason *DisabledReason + // If non-nil, evaluated when rendering and invoking the menu item. Takes + // precedence over DisabledReason when set. + GetDisabledReason func() *DisabledReason + // Can be used to group menu items into sections with headers. MenuItems // with the same Section should be contiguous, and will automatically get a // section header. If nil, the item is not part of a section. @@ -299,6 +303,14 @@ func (self *MenuItem) ID() string { return self.Label } +func (self *MenuItem) DisabledReasonAtUse() *DisabledReason { + if self.GetDisabledReason != nil { + return self.GetDisabledReason() + } + + return self.DisabledReason +} + type Model struct { CommitFiles []*models.CommitFile Files []*models.File diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index aca3bc0cbe7..446604c5f24 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -774,6 +774,7 @@ type TranslationSet struct { GitOutputCopiedToClipboard string OpenCommandLogInEditor string NoCommandLogToOpenInEditor string + CommandLogOpenedInEditor string CommandLogHeader string RandomTip string ToggleWhitespaceInDiffView string @@ -1911,6 +1912,7 @@ func EnglishTranslationSet() *TranslationSet { GitOutputCopiedToClipboard: "Git output copied to clipboard", OpenCommandLogInEditor: "Open command log in editor", NoCommandLogToOpenInEditor: "No command log to open in editor", + CommandLogOpenedInEditor: "Command log opened in editor", CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n", RandomTip: "Random tip", ToggleWhitespaceInDiffView: "Toggle whitespace", diff --git a/pkg/integration/tests/misc/copy_all_git_output_to_clipboard.go b/pkg/integration/tests/misc/copy_all_git_output_to_clipboard.go new file mode 100644 index 00000000000..2688aaf1f6e --- /dev/null +++ b/pkg/integration/tests/misc/copy_all_git_output_to_clipboard.go @@ -0,0 +1,45 @@ +package misc + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var CopyAllGitOutputToClipboard = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Copy all streamed git outputs from the command log to the clipboard", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard" + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + + shell.CloneIntoRemote("origin") + + shell.SetBranchUpstream("master", "origin/master") + + shell.EmptyCommit("two") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.Push) + + t.Views().Status().Content(Equals("✓ repo → master")) + + t.GlobalPress(keys.Universal.ExtrasMenu) + + t.ExpectPopup().Menu(). + Title(Equals("Command log")). + Select(Contains("Copy all git outputs to clipboard")). + Confirm() + + t.ExpectToast(Equals("Git output copied to clipboard")) + + t.FileSystem().FileContent("clipboard", + Contains("master -> master"). + Contains("git push"). + Contains("Push")) + }, +}) diff --git a/pkg/integration/tests/misc/open_command_log_in_editor.go b/pkg/integration/tests/misc/open_command_log_in_editor.go index 75c1c5aa0d8..45f96c21d47 100644 --- a/pkg/integration/tests/misc/open_command_log_in_editor.go +++ b/pkg/integration/tests/misc/open_command_log_in_editor.go @@ -11,6 +11,7 @@ var OpenCommandLogInEditor = NewIntegrationTest(NewIntegrationTestArgs{ Skip: false, SetupConfig: func(config *config.AppConfig) { config.GetUserConfig().OS.Edit = "cp {{filename}} editor-output.txt" + config.GetUserConfig().Gui.ShowRandomTip = false }, SetupRepo: func(shell *Shell) { shell.EmptyCommit("one") @@ -35,9 +36,12 @@ var OpenCommandLogInEditor = NewIntegrationTest(NewIntegrationTestArgs{ Select(Contains("Open command log in editor")). Confirm() + t.ExpectToast(Equals("Command log opened in editor")) + t.FileSystem().FileContent("editor-output.txt", Contains("Push"). Contains("git push"). - Contains("master -> master")) + Contains("master -> master"). + DoesNotContain("You can hide/focus")) }, }) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index ff0fabf8961..767fe246402 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -333,6 +333,7 @@ var tests = []*components.IntegrationTest{ interactive_rebase.SwapWithConflict, interactive_rebase.ViewFilesOfTodoEntries, misc.ConfirmOnQuit, + misc.CopyAllGitOutputToClipboard, misc.CopyConfirmationMessageToClipboard, misc.CopyGitOutputToClipboard, misc.CopyToClipboard,