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..4028fa1de23 100644 --- a/pkg/gui/command_log_panel.go +++ b/pkg/gui/command_log_panel.go @@ -33,6 +33,190 @@ 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, + gui.isCopyToClipboardLogLine, + ) +} + +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 (gui *Gui) hasCommandLogEntries() bool { + return gui.commandLogContent() != "" +} + +func (gui *Gui) commandLogContent() string { + if gui.Views.Extras == nil { + return "" + } + + 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 (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 { + if line != gitOutputHeader { + continue + } + + 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, isCopyToClipboardLogLine)...) + + if trimmed := strings.TrimRight(strings.Join(block, "\n"), "\n"); trimmed != "" { + blocks = append(blocks, trimmed) + } + } + + return blocks +} + +func commandLogEntryBeforeGitOutput(lines []string, headerIdx int, gitOutputHeader string, isCopyToClipboardLogLine func(string) bool) []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, isCopyToClipboardLogLine func(string) bool) []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 isStartOfNewCommandLogEntry(lines, i, isCopyToClipboardLogLine) { + break + } + output = append(output, line) + } + + return output +} + +func isStartOfNewCommandLogEntry(lines []string, i int, isCopyToClipboardLogLine func(string) bool) 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 isLazygitCommandLogLine(lines[j], isCopyToClipboardLogLine) + } + + return false +} + +func isLazygitCommandLogLine(line string, isCopyToClipboardLogLine func(string) bool) 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 new file mode 100644 index 00000000000..3556ef366bf --- /dev/null +++ b/pkg/gui/command_log_panel_test.go @@ -0,0 +1,140 @@ +package gui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +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() + + lines := []string{ + "Push", + " git push", + "", + gitOutputHeader, + "line1", + "line2", + } + + assert.Equal(t, []string{"Push\n git push\n\nGit output:\nline1\nline2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher())) +} + +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, englishCopyToClipboardLogLineMatcher())) +} + +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, englishCopyToClipboardLogLineMatcher())) +} + +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, englishCopyToClipboardLogLineMatcher())) +} + +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, englishCopyToClipboardLogLineMatcher())) +} + +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, 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 { + if i > 0 { + result += "\n\n" + } + result += block + } + return result +} 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 980c31d4543..54019aeb3d2 100644 --- a/pkg/gui/extras_panel.go +++ b/pkg/gui/extras_panel.go @@ -1,7 +1,10 @@ package gui import ( + "errors" "io" + "path/filepath" + "time" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" @@ -10,6 +13,20 @@ import ( ) func (gui *Gui) handleCreateExtrasMenuPanel() error { + noGitOutputDisabledReason := func() *types.DisabledReason { + if gui.hasGitOutput() { + return nil + } + 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{ @@ -33,10 +50,79 @@ 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, + GetDisabledReason: 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, + GetDisabledReason: noCommandLogDisabledReason, + }, }, }) } +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) 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 + } + + 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 { gui.c.State().SetShowExtrasWindow(true) // TODO: is this necessary? Can't I just call 'return from context'? @@ -94,7 +180,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..11ce2234fe6 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) @@ -281,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. @@ -296,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 20d0d5ff64e..446604c5f24 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -768,6 +768,13 @@ type TranslationSet struct { CommandLog string ToggleShowCommandLog string FocusCommandLog string + CopyGitOutputToClipboard string + CopyAllGitOutputToClipboard string + NoGitOutputToCopy string + GitOutputCopiedToClipboard string + OpenCommandLogInEditor string + NoCommandLogToOpenInEditor string + CommandLogOpenedInEditor string CommandLogHeader string RandomTip string ToggleWhitespaceInDiffView string @@ -1899,6 +1906,13 @@ 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", + 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/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/misc/open_command_log_in_editor.go b/pkg/integration/tests/misc/open_command_log_in_editor.go new file mode 100644 index 00000000000..45f96c21d47 --- /dev/null +++ b/pkg/integration/tests/misc/open_command_log_in_editor.go @@ -0,0 +1,47 @@ +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" + config.GetUserConfig().Gui.ShowRandomTip = false + }, + 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.ExpectToast(Equals("Command log opened in editor")) + + t.FileSystem().FileContent("editor-output.txt", + Contains("Push"). + Contains("git push"). + 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 1b264e50dc9..767fe246402 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -333,12 +333,15 @@ var tests = []*components.IntegrationTest{ interactive_rebase.SwapWithConflict, interactive_rebase.ViewFilesOfTodoEntries, misc.ConfirmOnQuit, + misc.CopyAllGitOutputToClipboard, misc.CopyConfirmationMessageToClipboard, + misc.CopyGitOutputToClipboard, misc.CopyToClipboard, misc.DirenvApprovesEnvrc, misc.DirenvLoadedOnRepoSwitch, misc.DirenvUnloadsOnBlockedEnvrc, misc.InitialOpen, + misc.OpenCommandLogInEditor, misc.RecentReposOnLaunch, patch_building.Apply, patch_building.ApplyInReverse,