diff --git a/docs/Config.md b/docs/Config.md index 92cc9ebd7..837230367 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -171,7 +171,6 @@ keybinding: diffingMenu-alt: '' # deprecated copyToClipboard: '' submitEditorText: '' - appendNewline: '' extrasMenu: '@' toggleWhitespaceInDiffView: '' increaseContextInDiffView: '}' diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index d68cfbc80..ee2538c1f 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -87,33 +87,34 @@ func writeString(file *os.File, str string) { func localisedTitle(tr *i18n.TranslationSet, str string) string { contextTitleMap := map[string]string{ - "global": tr.GlobalTitle, - "navigation": tr.NavigationTitle, - "branches": tr.BranchesTitle, - "localBranches": tr.LocalBranchesTitle, - "files": tr.FilesTitle, - "status": tr.StatusTitle, - "submodules": tr.SubmodulesTitle, - "subCommits": tr.SubCommitsTitle, - "remoteBranches": tr.RemoteBranchesTitle, - "remotes": tr.RemotesTitle, - "reflogCommits": tr.ReflogCommitsTitle, - "tags": tr.TagsTitle, - "commitFiles": tr.CommitFilesTitle, - "commitMessage": tr.CommitMessageTitle, - "commits": tr.CommitsTitle, - "confirmation": tr.ConfirmationTitle, - "information": tr.InformationTitle, - "main": tr.NormalTitle, - "patchBuilding": tr.PatchBuildingTitle, - "mergeConflicts": tr.MergingTitle, - "staging": tr.StagingTitle, - "menu": tr.MenuTitle, - "search": tr.SearchTitle, - "secondary": tr.SecondaryTitle, - "stash": tr.StashTitle, - "suggestions": tr.SuggestionsCheatsheetTitle, - "extras": tr.ExtrasTitle, + "global": tr.GlobalTitle, + "navigation": tr.NavigationTitle, + "branches": tr.BranchesTitle, + "localBranches": tr.LocalBranchesTitle, + "files": tr.FilesTitle, + "status": tr.StatusTitle, + "submodules": tr.SubmodulesTitle, + "subCommits": tr.SubCommitsTitle, + "remoteBranches": tr.RemoteBranchesTitle, + "remotes": tr.RemotesTitle, + "reflogCommits": tr.ReflogCommitsTitle, + "tags": tr.TagsTitle, + "commitFiles": tr.CommitFilesTitle, + "commitMessage": tr.CommitMessageTitle, + "commitDescription": tr.CommitDescriptionTitle, + "commits": tr.CommitsTitle, + "confirmation": tr.ConfirmationTitle, + "information": tr.InformationTitle, + "main": tr.NormalTitle, + "patchBuilding": tr.PatchBuildingTitle, + "mergeConflicts": tr.MergingTitle, + "staging": tr.StagingTitle, + "menu": tr.MenuTitle, + "search": tr.SearchTitle, + "secondary": tr.SecondaryTitle, + "stash": tr.StashTitle, + "suggestions": tr.SuggestionsCheatsheetTitle, + "extras": tr.ExtrasTitle, } title, ok := contextTitleMap[str] diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 38459e351..0c5008e38 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -8,6 +8,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/oscommands" ) +var ErrInvalidCommitIndex = errors.New("invalid commit index") + type CommitCommands struct { *GitCommon } @@ -18,11 +20,6 @@ func NewCommitCommands(gitCommon *GitCommon) *CommitCommands { } } -// RewordLastCommit rewords the topmost commit with the given message -func (self *CommitCommands) RewordLastCommit(message string) error { - return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(message)).Run() -} - // ResetAuthor resets the author of the topmost commit func (self *CommitCommands) ResetAuthor() error { return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run() @@ -45,11 +42,7 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [ } func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj { - splitMessage := strings.Split(message, "\n") - lineArgs := "" - for _, line := range splitMessage { - lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line)) - } + messageArgs := self.commitMessageArgs(message) skipHookPrefix := self.UserConfig.Git.SkipHookPrefix noVerifyFlag := "" @@ -57,7 +50,23 @@ func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj { noVerifyFlag = " --no-verify" } - return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), lineArgs)) + return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), messageArgs)) +} + +// RewordLastCommit rewords the topmost commit with the given message +func (self *CommitCommands) RewordLastCommit(message string) error { + messageArgs := self.commitMessageArgs(message) + return self.cmd.New(fmt.Sprintf("git commit --allow-empty --amend --only%s", messageArgs)).Run() +} + +func (self *CommitCommands) commitMessageArgs(message string) string { + msg, description, _ := strings.Cut(message, "\n") + descriptionArgs := "" + if description != "" { + descriptionArgs = fmt.Sprintf(" -m %s", self.cmd.Quote(description)) + } + + return fmt.Sprintf(" -m %s%s", self.cmd.Quote(msg), descriptionArgs) } // runs git commit without the -m argument meaning it will invoke the user's editor @@ -178,3 +187,13 @@ func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error { func (self *CommitCommands) CreateFixupCommit(sha string) error { return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run() } + +// a value of 0 means the head commit, 1 is the parent commit, etc +func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) { + hash, _ := self.cmd.New(fmt.Sprintf("git log -1 --skip=%d --pretty=%%H", value)).DontLog().RunWithOutput() + formattedHash := strings.TrimSpace(hash) + if len(formattedHash) == 0 { + return "", ErrInvalidCommitIndex + } + return self.GetCommitMessage(formattedHash) +} diff --git a/pkg/commands/git_commands/commit_test.go b/pkg/commands/git_commands/commit_test.go index 268e44e46..4cc8a8de2 100644 --- a/pkg/commands/git_commands/commit_test.go +++ b/pkg/commands/git_commands/commit_test.go @@ -9,12 +9,32 @@ import ( ) func TestCommitRewordCommit(t *testing.T) { - runner := oscommands.NewFakeRunner(t). - ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil) - instance := buildCommitCommands(commonDeps{runner: runner}) + type scenario struct { + testName string + runner *oscommands.FakeCmdObjRunner + input string + } + scenarios := []scenario{ + { + "Single line reword", + oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil), + "test", + }, + { + "Multi line reword", + oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil), + "test\nline 2\nline 3", + }, + } + for _, s := range scenarios { + s := s + t.Run(s.testName, func(t *testing.T) { + instance := buildCommitCommands(commonDeps{runner: s.runner}) - assert.NoError(t, instance.RewordLastCommit("test")) - runner.CheckForMissingCalls() + assert.NoError(t, instance.RewordLastCommit(s.input)) + s.runner.CheckForMissingCalls() + }) + } } func TestCommitResetToCommit(t *testing.T) { @@ -274,3 +294,40 @@ Merge pull request #1750 from mark2185/fix-issue-template }) } } + +func TestGetCommitMessageFromHistory(t *testing.T) { + type scenario struct { + testName string + runner *oscommands.FakeCmdObjRunner + test func(string, error) + } + scenarios := []scenario{ + { + "Empty message", + oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "", nil).Expect("git rev-list --format=%B --max-count=1 ", "", nil), + func(output string, err error) { + assert.Error(t, err) + }, + }, + { + "Default case to retrieve a commit in history", + oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "sha3 \n", nil).Expect("git rev-list --format=%B --max-count=1 sha3", `commit sha3 + use generics to DRY up context code`, nil), + func(output string, err error) { + assert.NoError(t, err) + assert.Equal(t, "use generics to DRY up context code", output) + }, + }, + } + + for _, s := range scenarios { + s := s + t.Run(s.testName, func(t *testing.T) { + instance := buildCommitCommands(commonDeps{runner: s.runner}) + + output, err := instance.GetCommitMessageFromHistory(2) + + s.test(output, err) + }) + } +} diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index fb0638e43..fb36bb7ea 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -165,6 +165,7 @@ type KeybindingUniversalConfig struct { Select string `yaml:"select"` GoInto string `yaml:"goInto"` Confirm string `yaml:"confirm"` + ConfirmInEditor string `yaml:"confirmInEditor"` Remove string `yaml:"remove"` New string `yaml:"new"` Edit string `yaml:"edit"` @@ -193,7 +194,6 @@ type KeybindingUniversalConfig struct { CopyToClipboard string `yaml:"copyToClipboard"` OpenRecentRepos string `yaml:"openRecentRepos"` SubmitEditorText string `yaml:"submitEditorText"` - AppendNewline string `yaml:"appendNewline"` ExtrasMenu string `yaml:"extrasMenu"` ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"` IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"` @@ -492,6 +492,7 @@ func GetDefaultConfig() *UserConfig { Select: "", GoInto: "", Confirm: "", + ConfirmInEditor: "", Remove: "d", New: "n", Edit: "e", @@ -520,7 +521,6 @@ func GetDefaultConfig() *UserConfig { DiffingMenuAlt: "", CopyToClipboard: "", SubmitEditorText: "", - AppendNewline: "", ExtrasMenu: "@", ToggleWhitespaceInDiffView: "", IncreaseContextInDiffView: "}", diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go index 4c8ddae2b..051c76573 100644 --- a/pkg/gui/commit_message_panel.go +++ b/pkg/gui/commit_message_panel.go @@ -15,7 +15,6 @@ func (gui *Gui) handleCommitMessageFocused() error { map[string]string{ "keyBindClose": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Return), "keyBindConfirm": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Confirm), - "keyBindNewLine": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.AppendNewline), }, ) diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index c3e50c2a1..bcdec70d1 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -78,17 +78,11 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int { return lineCount } -func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) { - panelWidth := gui.getConfirmationPanelWidth() - panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) - return gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight) +func (gui *Gui) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) { + return gui.getPopupPanelDimensionsAux(panelWidth, contentHeight) } -func (gui *Gui) getConfirmationPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) { - return gui.getConfirmationPanelDimensionsAux(panelWidth, contentHeight) -} - -func (gui *Gui) getConfirmationPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) { +func (gui *Gui) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) { width, height := gui.g.Size() if panelHeight > height*3/4 { panelHeight = height * 3 / 4 @@ -186,7 +180,7 @@ func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPane } confirmationView := gui.Views.Confirmation confirmationView.Editable = opts.Editable - confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor) + confirmationView.Editor = gocui.EditorFunc(gui.promptEditor) if opts.Editable { textArea := confirmationView.TextArea diff --git a/pkg/gui/context.go b/pkg/gui/context.go index f097df807..d69b245ff 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -9,6 +9,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) // This file is for the management of contexts. There is a context stack such that @@ -146,6 +147,36 @@ func (gui *Gui) popContext() error { return gui.activateContext(newContext, types.OnFocusOpts{}) } +func (gui *Gui) removeContexts(contextsToRemove []types.Context) error { + gui.State.ContextManager.Lock() + + if len(gui.State.ContextManager.ContextStack) == 1 { + gui.State.ContextManager.Unlock() + return nil + } + + rest := lo.Filter(gui.State.ContextManager.ContextStack, func(context types.Context, _ int) bool { + for _, contextToRemove := range contextsToRemove { + if context.GetKey() == contextToRemove.GetKey() { + return false + } + } + return true + }) + gui.State.ContextManager.ContextStack = rest + contextToActivate := rest[len(rest)-1] + gui.State.ContextManager.Unlock() + + for _, context := range contextsToRemove { + if err := gui.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil { + return err + } + } + + // activate the item at the top of the stack + return gui.activateContext(contextToActivate, types.OnFocusOpts{}) +} + func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error { view, _ := gui.g.View(c.GetViewName()) diff --git a/pkg/gui/context/commit_message_context.go b/pkg/gui/context/commit_message_context.go new file mode 100644 index 000000000..6be4763e5 --- /dev/null +++ b/pkg/gui/context/commit_message_context.go @@ -0,0 +1,95 @@ +package context + +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type CommitMessageContext struct { + types.Context + viewModel *CommitMessageViewModel +} + +// when selectedIndex (see below) is set to this value, it means that we're not +// currently viewing a commit message of an existing commit: instead we're making our own +// new commit message +const NoCommitIndex = -1 + +type CommitMessageViewModel struct { + // index of the commit message, where -1 is 'no commit', 0 is the HEAD commit, 1 + // is the prior commit, and so on + selectedindex int + // if true, then upon escaping from the commit message panel, we will preserve + // the message so that it's still shown next time we open the panel + preserveMessage bool + // the full preserved message (combined summary and description) + preservedMessage string + // invoked when pressing enter in the commit message panel + onConfirm func(string) error + + // The message typed in before cycling through history + // We store this separately to 'preservedMessage' because 'preservedMessage' + // is specifically for committing staged files and we don't want this affected + // by cycling through history in the context of rewording an old commit. + historyMessage string +} + +func NewCommitMessageContext( + view *gocui.View, + opts ContextCallbackOpts, +) *CommitMessageContext { + viewModel := &CommitMessageViewModel{} + return &CommitMessageContext{ + viewModel: viewModel, + Context: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.PERSISTENT_POPUP, + View: view, + WindowName: "commitMessage", + Key: COMMIT_MESSAGE_CONTEXT_KEY, + Focusable: true, + HasUncontrolledBounds: true, + }), + opts, + ), + } +} + +func (self *CommitMessageContext) SetSelectedIndex(value int) { + self.viewModel.selectedindex = value +} + +func (self *CommitMessageContext) GetSelectedIndex() int { + return self.viewModel.selectedindex +} + +func (self *CommitMessageContext) GetPreserveMessage() bool { + return self.viewModel.preserveMessage +} + +func (self *CommitMessageContext) GetPreservedMessage() string { + return self.viewModel.preservedMessage +} + +func (self *CommitMessageContext) SetPreservedMessage(message string) { + self.viewModel.preservedMessage = message +} + +func (self *CommitMessageContext) GetHistoryMessage() string { + return self.viewModel.historyMessage +} + +func (self *CommitMessageContext) SetHistoryMessage(message string) { + self.viewModel.historyMessage = message +} + +func (self *CommitMessageContext) OnConfirm(message string) error { + return self.viewModel.onConfirm(message) +} + +func (self *CommitMessageContext) SetPanelState(index int, title string, preserveMessage bool, onConfirm func(string) error) { + self.viewModel.selectedindex = index + self.viewModel.preserveMessage = preserveMessage + self.viewModel.onConfirm = onConfirm + self.GetView().Title = title +} diff --git a/pkg/gui/context/context.go b/pkg/gui/context/context.go index 5a88b4a26..7fc0d52f6 100644 --- a/pkg/gui/context/context.go +++ b/pkg/gui/context/context.go @@ -33,13 +33,14 @@ const ( INFORMATION_CONTEXT_KEY types.ContextKey = "information" LIMIT_CONTEXT_KEY types.ContextKey = "limit" - MENU_CONTEXT_KEY types.ContextKey = "menu" - CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation" - SEARCH_CONTEXT_KEY types.ContextKey = "search" - COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage" - SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules" - SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions" - COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog" + MENU_CONTEXT_KEY types.ContextKey = "menu" + CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation" + SEARCH_CONTEXT_KEY types.ContextKey = "search" + COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage" + COMMIT_DESCRIPTION_CONTEXT_KEY types.ContextKey = "commitDescription" + SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules" + SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions" + COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog" ) var AllContextKeys = []types.ContextKey{ @@ -97,7 +98,8 @@ type ContextTree struct { CustomPatchBuilderSecondary types.Context MergeConflicts *MergeConflictsContext Confirmation types.Context - CommitMessage types.Context + CommitMessage *CommitMessageContext + CommitDescription types.Context CommandLog types.Context // display contexts @@ -129,6 +131,7 @@ func (self *ContextTree) Flatten() []types.Context { self.Menu, self.Confirmation, self.CommitMessage, + self.CommitDescription, self.MergeConflicts, self.StagingSecondary, diff --git a/pkg/gui/context_config.go b/pkg/gui/context_config.go index b19824237..aa056f5c5 100644 --- a/pkg/gui/context_config.go +++ b/pkg/gui/context_config.go @@ -217,17 +217,29 @@ func (gui *Gui) contextTree() *context.ContextTree { }, }, ), - CommitMessage: context.NewSimpleContext( + CommitMessage: context.NewCommitMessageContext( + gui.Views.CommitMessage, + context.ContextCallbackOpts{ + OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused), + }, + ), + CommitDescription: context.NewSimpleContext( context.NewBaseContext(context.NewBaseContextOpts{ Kind: types.PERSISTENT_POPUP, - View: gui.Views.CommitMessage, - WindowName: "commitMessage", - Key: context.COMMIT_MESSAGE_CONTEXT_KEY, + View: gui.Views.CommitDescription, + WindowName: "commitDescription", + Key: context.COMMIT_DESCRIPTION_CONTEXT_KEY, Focusable: true, HasUncontrolledBounds: true, }), context.ContextCallbackOpts{ - OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused), + OnFocus: func(opts types.OnFocusOpts) error { + _, err := gui.g.SetViewBeneath("commitDescription", "commitMessage", 10) + if err != nil { + return err + } + return nil + }, }, ), Search: context.NewSimpleContext( diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index b8fba8c85..4b13eaa20 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -9,7 +9,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" "github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands" - "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/snake" ) @@ -26,10 +25,24 @@ func (gui *Gui) resetControllers() { rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper) suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions) - setCommitMessage := gui.getSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage }) - getSavedCommitMessage := func() string { - return gui.State.savedCommitMessage + setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage }) + setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription }) + getCommitSummary := func() string { + return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent()) } + + getCommitDescription := func() string { + return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent()) + } + commitsHelper := helpers.NewCommitsHelper(helperCommon, + gui.State.Model, + gui.State.Contexts, + getCommitSummary, + setCommitSummary, + getCommitDescription, + setCommitDescription, + gui.RenderCommitLength, + ) gpgHelper := helpers.NewGpgHelper(helperCommon, gui.os, gui.git) gui.helpers = &helpers.Helpers{ Refs: refsHelper, @@ -38,7 +51,7 @@ func (gui *Gui) resetControllers() { Bisect: helpers.NewBisectHelper(helperCommon, gui.git), Suggestions: suggestionsHelper, Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand), - WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitMessage, getSavedCommitMessage), + WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitSummary, commitsHelper, gpgHelper), Tags: helpers.NewTagsHelper(helperCommon, gui.git), GPG: gpgHelper, MergeAndRebase: rebaseHelper, @@ -52,6 +65,7 @@ func (gui *Gui) resetControllers() { ), Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc), AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper), + Commits: commitsHelper, } gui.CustomCommandsClient = custom_commands.NewClient( @@ -84,27 +98,12 @@ func (gui *Gui) resetControllers() { bisectController := controllers.NewBisectController(common) - getCommitMessage := func() string { - return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent()) - } - - onCommitAttempt := func(message string) { - gui.State.savedCommitMessage = message - gui.Views.CommitMessage.ClearTextArea() - } - - onCommitSuccess := func() { - gui.State.savedCommitMessage = "" - _ = gui.c.Refresh(types.RefreshOptions{ - Scope: []types.RefreshableView{types.STAGING}, - }) - } - commitMessageController := controllers.NewCommitMessageController( common, - getCommitMessage, - onCommitAttempt, - onCommitSuccess, + ) + + commitDescriptionController := controllers.NewCommitDescriptionController( + common, ) remoteBranchesController := controllers.NewRemoteBranchesController(common) @@ -115,8 +114,7 @@ func (gui *Gui) resetControllers() { filesController := controllers.NewFilesController( common, gui.enterSubmodule, - setCommitMessage, - getSavedCommitMessage, + setCommitSummary, ) mergeConflictsController := controllers.NewMergeConflictsController(common) remotesController := controllers.NewRemotesController( @@ -250,6 +248,10 @@ func (gui *Gui) resetControllers() { commitMessageController, ) + controllers.AttachControllers(gui.State.Contexts.CommitDescription, + commitDescriptionController, + ) + controllers.AttachControllers(gui.State.Contexts.RemoteBranches, remoteBranchesController, ) diff --git a/pkg/gui/controllers/commit_description_controller.go b/pkg/gui/controllers/commit_description_controller.go new file mode 100644 index 000000000..5624fa448 --- /dev/null +++ b/pkg/gui/controllers/commit_description_controller.go @@ -0,0 +1,60 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type CommitDescriptionController struct { + baseController + *controllerCommon +} + +var _ types.IController = &CommitMessageController{} + +func NewCommitDescriptionController( + common *controllerCommon, +) *CommitDescriptionController { + return &CommitDescriptionController{ + baseController: baseController{}, + controllerCommon: common, + } +} + +func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Universal.TogglePanel), + Handler: self.switchToCommitMessage, + }, + { + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: self.close, + }, + { + Key: opts.GetKey(opts.Config.Universal.ConfirmInEditor), + Handler: self.confirm, + }, + } + + return bindings +} + +func (self *CommitDescriptionController) Context() types.Context { + return self.context() +} + +func (self *CommitDescriptionController) context() types.Context { + return self.contexts.CommitMessage +} + +func (self *CommitDescriptionController) switchToCommitMessage() error { + return self.c.PushContext(self.contexts.CommitMessage) +} + +func (self *CommitDescriptionController) close() error { + return self.helpers.Commits.CloseCommitMessagePanel() +} + +func (self *CommitDescriptionController) confirm() error { + return self.helpers.Commits.HandleCommitConfirm() +} diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index e5cdb866d..0772cc16b 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -1,33 +1,24 @@ package controllers import ( + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type CommitMessageController struct { baseController *controllerCommon - - getCommitMessage func() string - onCommitAttempt func(message string) - onCommitSuccess func() } var _ types.IController = &CommitMessageController{} func NewCommitMessageController( common *controllerCommon, - getCommitMessage func() string, - onCommitAttempt func(message string), - onCommitSuccess func(), ) *CommitMessageController { return &CommitMessageController{ baseController: baseController{}, controllerCommon: common, - - getCommitMessage: getCommitMessage, - onCommitAttempt: onCommitAttempt, - onCommitSuccess: onCommitSuccess, } } @@ -41,6 +32,18 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) Key: opts.GetKey(opts.Config.Universal.Return), Handler: self.close, }, + { + Key: opts.GetKey(opts.Config.Universal.PrevItem), + Handler: self.handlePreviousCommit, + }, + { + Key: opts.GetKey(opts.Config.Universal.NextItem), + Handler: self.handleNextCommit, + }, + { + Key: opts.GetKey(opts.Config.Universal.TogglePanel), + Handler: self.switchToCommitDescription, + }, } return bindings @@ -50,30 +53,63 @@ func (self *CommitMessageController) Context() types.Context { return self.context() } -// this method is pointless in this context but I'm keeping it consistent -// with other contexts so that when generics arrive it's easier to refactor -func (self *CommitMessageController) context() types.Context { +func (self *CommitMessageController) context() *context.CommitMessageContext { return self.contexts.CommitMessage } -func (self *CommitMessageController) confirm() error { - message := self.getCommitMessage() - self.onCommitAttempt(message) +func (self *CommitMessageController) handlePreviousCommit() error { + return self.handleCommitIndexChange(1) +} - if message == "" { - return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr) +func (self *CommitMessageController) handleNextCommit() error { + if self.context().GetSelectedIndex() == context.NoCommitIndex { + return nil + } + return self.handleCommitIndexChange(-1) +} + +func (self *CommitMessageController) switchToCommitDescription() error { + if err := self.c.PushContext(self.contexts.CommitDescription); err != nil { + return err + } + return nil +} + +func (self *CommitMessageController) handleCommitIndexChange(value int) error { + currentIndex := self.context().GetSelectedIndex() + newIndex := currentIndex + value + if newIndex == context.NoCommitIndex { + self.context().SetSelectedIndex(newIndex) + self.helpers.Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage()) + return nil + } else if currentIndex == context.NoCommitIndex { + self.context().SetHistoryMessage(self.helpers.Commits.JoinCommitMessageAndDescription()) } - cmdObj := self.git.Commit.CommitCmdObj(message) - self.c.LogAction(self.c.Tr.Actions.Commit) + validCommit, err := self.setCommitMessageAtIndex(newIndex) + if validCommit { + self.context().SetSelectedIndex(newIndex) + } + return err +} - _ = self.c.PopContext() - return self.helpers.GPG.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error { - self.onCommitSuccess() - return nil - }) +// returns true if the given index is for a valid commit +func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, error) { + commitMessage, err := self.git.Commit.GetCommitMessageFromHistory(index) + if err != nil { + if err == git_commands.ErrInvalidCommitIndex { + return false, nil + } + return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr) + } + self.helpers.Commits.UpdateCommitPanelView(commitMessage) + return true, nil +} + +func (self *CommitMessageController) confirm() error { + return self.helpers.Commits.HandleCommitConfirm() } func (self *CommitMessageController) close() error { - return self.c.PopContext() + return self.helpers.Commits.CloseCommitMessagePanel() } diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index b028dae6e..dcbf05e76 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -15,9 +15,8 @@ type FilesController struct { baseController // nolint: unused *controllerCommon - enterSubmodule func(submodule *models.SubmoduleConfig) error - setCommitMessage func(message string) - getSavedCommitMessage func() string + enterSubmodule func(submodule *models.SubmoduleConfig) error + setCommitMessage func(message string) } var _ types.IController = &FilesController{} @@ -26,13 +25,11 @@ func NewFilesController( common *controllerCommon, enterSubmodule func(submodule *models.SubmoduleConfig) error, setCommitMessage func(message string), - getSavedCommitMessage func() string, ) *FilesController { return &FilesController{ - controllerCommon: common, - enterSubmodule: enterSubmodule, - setCommitMessage: setCommitMessage, - getSavedCommitMessage: getSavedCommitMessage, + controllerCommon: common, + enterSubmodule: enterSubmodule, + setCommitMessage: setCommitMessage, } } diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go new file mode 100644 index 000000000..4126d9ee5 --- /dev/null +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -0,0 +1,166 @@ +package helpers + +import ( + "strings" + + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ICommitsHelper interface { + UpdateCommitPanelView(message string) +} + +type CommitsHelper struct { + c *types.HelperCommon + + model *types.Model + contexts *context.ContextTree + getCommitSummary func() string + setCommitSummary func(string) + getCommitDescription func() string + setCommitDescription func(string) + renderCommitLength func() +} + +var _ ICommitsHelper = &CommitsHelper{} + +func NewCommitsHelper( + c *types.HelperCommon, + model *types.Model, + contexts *context.ContextTree, + getCommitSummary func() string, + setCommitSummary func(string), + getCommitDescription func() string, + setCommitDescription func(string), + renderCommitLength func(), +) *CommitsHelper { + return &CommitsHelper{ + c: c, + model: model, + contexts: contexts, + getCommitSummary: getCommitSummary, + setCommitSummary: setCommitSummary, + getCommitDescription: getCommitDescription, + setCommitDescription: setCommitDescription, + renderCommitLength: renderCommitLength, + } +} + +func (self *CommitsHelper) SplitCommitMessageAndDescription(message string) (string, string) { + for _, separator := range []string{"\n\n", "\n\r\n\r", "\n", "\n\r"} { + msg, description, found := strings.Cut(message, separator) + if found { + return msg, description + } + } + return message, "" +} + +func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) { + summary, description := self.SplitCommitMessageAndDescription(message) + + self.setCommitSummary(summary) + self.setCommitDescription(description) + self.renderCommitLength() +} + +func (self *CommitsHelper) JoinCommitMessageAndDescription() string { + if len(self.getCommitDescription()) == 0 { + return self.getCommitSummary() + } + return self.getCommitSummary() + "\n" + self.getCommitDescription() +} + +func (self *CommitsHelper) UpdateCommitPanelView(message string) { + // first try the passed in message, if not fallback to context -> view in that order + if message != "" { + self.SetMessageAndDescriptionInView(message) + return + } + message = self.contexts.CommitMessage.GetPreservedMessage() + if message != "" { + self.SetMessageAndDescriptionInView(message) + } else { + self.SetMessageAndDescriptionInView(self.getCommitSummary()) + } +} + +type OpenCommitMessagePanelOpts struct { + CommitIndex int + Title string + PreserveMessage bool + OnConfirm func(string) error + InitialMessage string +} + +func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error { + self.contexts.CommitMessage.SetPanelState( + opts.CommitIndex, + opts.Title, + opts.PreserveMessage, + opts.OnConfirm, + ) + + self.UpdateCommitPanelView(opts.InitialMessage) + + return self.pushCommitMessageContexts() +} + +func (self *CommitsHelper) OnCommitSuccess() { + // if we have a preserved message we want to clear it on success + if self.contexts.CommitMessage.GetPreserveMessage() { + self.contexts.CommitMessage.SetPreservedMessage("") + } + self.SetMessageAndDescriptionInView("") +} + +func (self *CommitsHelper) HandleCommitConfirm() error { + fullMessage := self.JoinCommitMessageAndDescription() + + if fullMessage == "" { + return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr) + } + + err := self.contexts.CommitMessage.OnConfirm(fullMessage) + if err != nil { + return err + } + + return nil +} + +func (self *CommitsHelper) CloseCommitMessagePanel() error { + if self.contexts.CommitMessage.GetPreserveMessage() { + message := self.JoinCommitMessageAndDescription() + + self.contexts.CommitMessage.SetPreservedMessage(message) + } else { + self.SetMessageAndDescriptionInView("") + } + + self.contexts.CommitMessage.SetHistoryMessage("") + + return self.PopCommitMessageContexts() +} + +func (self *CommitsHelper) PopCommitMessageContexts() error { + return self.c.RemoveContexts(self.commitMessageContexts()) +} + +func (self *CommitsHelper) pushCommitMessageContexts() error { + for _, context := range self.commitMessageContexts() { + if err := self.c.PushContext(context); err != nil { + return err + } + } + + return nil +} + +func (self *CommitsHelper) commitMessageContexts() []types.Context { + return []types.Context{ + self.contexts.CommitDescription, + self.contexts.CommitMessage, + } +} diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index a66d013bd..471e8a03a 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -15,6 +15,7 @@ type Helpers struct { GPG *GpgHelper Upstream *UpstreamHelper AmendHelper *AmendHelper + Commits *CommitsHelper } func NewStubHelpers() *Helpers { @@ -33,5 +34,6 @@ func NewStubHelpers() *Helpers { GPG: &GpgHelper{}, Upstream: &UpstreamHelper{}, AmendHelper: &AmendHelper{}, + Commits: &CommitsHelper{}, } } diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index 17850b994..6fa85bc35 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -20,13 +20,14 @@ type IWorkingTreeHelper interface { } type WorkingTreeHelper struct { - c *types.HelperCommon - git *commands.GitCommand - contexts *context.ContextTree - refHelper *RefsHelper - model *types.Model - setCommitMessage func(message string) - getSavedCommitMessage func() string + c *types.HelperCommon + git *commands.GitCommand + contexts *context.ContextTree + refHelper *RefsHelper + model *types.Model + setCommitMessage func(message string) + commitsHelper *CommitsHelper + gpgHelper *GpgHelper } func NewWorkingTreeHelper( @@ -36,16 +37,18 @@ func NewWorkingTreeHelper( refHelper *RefsHelper, model *types.Model, setCommitMessage func(message string), - getSavedCommitMessage func() string, + commitsHelper *CommitsHelper, + gpgHelper *GpgHelper, ) *WorkingTreeHelper { return &WorkingTreeHelper{ - c: c, - git: git, - contexts: contexts, - refHelper: refHelper, - model: model, - setCommitMessage: setCommitMessage, - getSavedCommitMessage: getSavedCommitMessage, + c: c, + git: git, + contexts: contexts, + refHelper: refHelper, + model: model, + setCommitMessage: setCommitMessage, + commitsHelper: commitsHelper, + gpgHelper: gpgHelper, } } @@ -94,7 +97,7 @@ func (self *WorkingTreeHelper) OpenMergeTool() error { }) } -func (self *WorkingTreeHelper) HandleCommitPress() error { +func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage string) error { if err := self.prepareFilesForCommit(); err != nil { return self.c.Error(err) } @@ -107,28 +110,25 @@ func (self *WorkingTreeHelper) HandleCommitPress() error { return self.PromptToStageAllAndRetry(self.HandleCommitPress) } - savedCommitMessage := self.getSavedCommitMessage() - if len(savedCommitMessage) > 0 { - self.setCommitMessage(savedCommitMessage) - } else { - commitPrefixConfig := self.commitPrefixConfigForRepo() - if commitPrefixConfig != nil { - prefixPattern := commitPrefixConfig.Pattern - prefixReplace := commitPrefixConfig.Replace - rgx, err := regexp.Compile(prefixPattern) - if err != nil { - return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error())) - } - prefix := rgx.ReplaceAllString(self.refHelper.GetCheckedOutRef().Name, prefixReplace) - self.setCommitMessage(prefix) - } - } + return self.commitsHelper.OpenCommitMessagePanel( + &OpenCommitMessagePanelOpts{ + CommitIndex: context.NoCommitIndex, + InitialMessage: initialMessage, + Title: self.c.Tr.CommitSummary, + PreserveMessage: true, + OnConfirm: self.handleCommit, + }, + ) +} - if err := self.c.PushContext(self.contexts.CommitMessage); err != nil { - return err - } - - return nil +func (self *WorkingTreeHelper) handleCommit(message string) error { + cmdObj := self.git.Commit.CommitCmdObj(message) + self.c.LogAction(self.c.Tr.Actions.Commit) + _ = self.commitsHelper.PopCommitMessageContexts() + return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error { + self.commitsHelper.OnCommitSuccess() + return nil + }) } // HandleCommitEditorPress - handle when the user wants to commit changes via @@ -154,9 +154,27 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error { return self.c.ErrorMsg(self.c.Tr.SkipHookPrefixNotConfigured) } - self.setCommitMessage(skipHookPrefix) + return self.HandleCommitPressWithMessage(skipHookPrefix) +} - return self.HandleCommitPress() +func (self *WorkingTreeHelper) HandleCommitPress() error { + message := self.contexts.CommitMessage.GetPreservedMessage() + + if message != "" { + commitPrefixConfig := self.commitPrefixConfigForRepo() + if commitPrefixConfig != nil { + prefixPattern := commitPrefixConfig.Pattern + prefixReplace := commitPrefixConfig.Replace + rgx, err := regexp.Compile(prefixPattern) + if err != nil { + return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error())) + } + prefix := rgx.ReplaceAllString(self.refHelper.GetCheckedOutRef().Name, prefixReplace) + message = prefix + } + } + + return self.HandleCommitPressWithMessage(message) } func (self *WorkingTreeHelper) PromptToStageAllAndRetry(retry func() error) error { diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 35e314357..3d0e6bc5c 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" @@ -19,7 +20,6 @@ type ( type LocalCommitsController struct { baseController *controllerCommon - pullFiles PullFilesFn } @@ -209,24 +209,30 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error { return nil } - message, err := self.git.Commit.GetCommitMessage(commit.Sha) + commitMessage, err := self.git.Commit.GetCommitMessage(commit.Sha) if err != nil { return self.c.Error(err) } - // TODO: use the commit message panel here - return self.c.Prompt(types.PromptOpts{ - Title: self.c.Tr.LcRewordCommit, - InitialContent: message, - HandleConfirm: func(response string) error { - self.c.LogAction(self.c.Tr.Actions.RewordCommit) - if err := self.git.Rebase.RewordCommit(self.model.Commits, self.context().GetSelectedLineIdx(), response); err != nil { - return self.c.Error(err) - } - - return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + return self.helpers.Commits.OpenCommitMessagePanel( + &helpers.OpenCommitMessagePanelOpts{ + CommitIndex: self.context().GetSelectedLineIdx(), + InitialMessage: commitMessage, + Title: self.c.Tr.Actions.RewordCommit, + PreserveMessage: false, + OnConfirm: self.handleReword, }, - }) + ) +} + +func (self *LocalCommitsController) handleReword(message string) error { + err := self.git.Rebase.RewordCommit(self.model.Commits, self.contexts.LocalCommits.GetSelectedLineIdx(), message) + if err != nil { + return self.c.Error(err) + } + self.helpers.Commits.OnCommitSuccess() + _ = self.helpers.Commits.PopCommitMessageContexts() + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) } func (self *LocalCommitsController) doRewordEditor() error { diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go index 9e00f8351..94f01db74 100644 --- a/pkg/gui/editors.go +++ b/pkg/gui/editors.go @@ -4,15 +4,9 @@ import ( "unicode" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" ) func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool { - newlineKey, ok := keybindings.GetKey(gui.c.UserConfig.Keybinding.Universal.AppendNewline).(gocui.Key) - if !ok { - newlineKey = gocui.KeyAltEnter - } - switch { case key == gocui.KeyBackspace || key == gocui.KeyBackspace2: textArea.BackSpaceChar() @@ -30,7 +24,7 @@ func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch textArea.MoveRightWord() case key == gocui.KeyArrowRight || key == gocui.KeyCtrlF: textArea.MoveCursorRight() - case key == newlineKey: + case key == gocui.KeyEnter: if allowMultiline { textArea.TypeRune('\n') } else { @@ -66,22 +60,20 @@ func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch // we've just copy+pasted the editor from gocui to here so that we can also re- // render the commit message length on each keypress func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { - matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, true) - - // This function is called again on refresh as part of the general resize popup call, - // but we need to call it here so that when we go to render the text area it's not - // considered out of bounds to add a newline, meaning we can avoid unnecessary scrolling. - err := gui.resizePopupPanel(v, v.TextArea.GetContent()) - if err != nil { - gui.c.Log.Error(err) - } + matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false) v.RenderTextArea() gui.RenderCommitLength() - return matched } -func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { +func (gui *Gui) commitDescriptionEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { + matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, true) + v.RenderTextArea() + gui.RenderCommitLength() + return matched +} + +func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false) v.RenderTextArea() diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 479b8aa50..61ec83125 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -83,13 +83,13 @@ func (gui *Gui) filesRenderToMain() error { return gui.c.RenderToMainViews(refreshOpts) } -func (gui *Gui) getSetTextareaTextFn(getView func() *gocui.View) func(string) { +func (gui *Gui) getCommitMessageSetTextareaTextFn(getView func() *gocui.View) func(string) { return func(text string) { // using a getView function so that we don't need to worry about when the view is created view := getView() view.ClearTextArea() view.TextArea.TypeString(text) - _ = gui.resizePopupPanel(view, view.TextArea.GetContent()) + gui.resizeCommitMessagePanels() view.RenderTextArea() } } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index aed1d5430..2d1e3e334 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -195,10 +195,6 @@ type GuiRepoState struct { // back in sync with the repo state ViewsSetup bool - // we store a commit message in this field if we've escaped the commit message - // panel without committing or if our commit failed - savedCommitMessage string - ScreenMode WindowMaximisation CurrentPopupOpts *types.CreatePopupPanelOpts diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index 835aa4f54..7c306a0d5 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -58,6 +58,10 @@ func (self *guiCommon) PopContext() error { return self.gui.popContext() } +func (self *guiCommon) RemoveContexts(contexts []types.Context) error { + return self.gui.removeContexts(contexts) +} + func (self *guiCommon) CurrentContext() types.Context { return self.gui.currentContext() } diff --git a/pkg/gui/refresh.go b/pkg/gui/refresh.go index 2926152ed..5fd68dedc 100644 --- a/pkg/gui/refresh.go +++ b/pkg/gui/refresh.go @@ -85,6 +85,7 @@ func (gui *Gui) Refresh(options types.RefreshOptions) error { types.REMOTES, types.STATUS, types.BISECT_INFO, + types.STAGING, }) } else { scopeSet = set.NewFromSlice(options.Scope) @@ -563,6 +564,13 @@ func (gui *Gui) refreshStatus() { func (gui *Gui) refreshStagingPanel(focusOpts types.OnFocusOpts) error { secondaryFocused := gui.secondaryStagingFocused() + mainFocused := gui.mainStagingFocused() + + // this method could be called when the staging panel is not being used, + // in which case we don't want to do anything. + if !mainFocused && !secondaryFocused { + return nil + } mainSelectedLineIdx := -1 secondarySelectedLineIdx := -1 @@ -647,6 +655,10 @@ func (gui *Gui) secondaryStagingFocused() bool { return gui.currentStaticContext().GetKey() == gui.State.Contexts.StagingSecondary.GetKey() } +func (gui *Gui) mainStagingFocused() bool { + return gui.currentStaticContext().GetKey() == gui.State.Contexts.Staging.GetKey() +} + func (gui *Gui) refreshPatchBuildingPanel(opts types.OnFocusOpts) error { selectedLineIdx := -1 if opts.ClickedWindowName == "main" { diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index aeb1da4c0..fc6b26989 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -42,6 +42,10 @@ type IGuiCommon interface { PushContext(context Context, opts ...OnFocusOpts) error PopContext() error + // Removes all given contexts from the stack. If a given context is not in the stack, it is ignored. + // This is for when you have a group of contexts that are bundled together e.g. with the commit message panel. + // If you want to remove a single context, you should probably use PopContext instead. + RemoveContexts([]Context) error CurrentContext() Context CurrentStaticContext() Context IsCurrentContext(Context) bool diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 8f2055245..9c6d5539f 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -78,28 +78,24 @@ func (gui *Gui) resizeCurrentPopupPanel() error { return nil } - if v == gui.Views.Menu { + c := gui.c.CurrentContext() + + if c == gui.State.Contexts.Menu { gui.resizeMenu() - } else if v == gui.Views.Confirmation || v == gui.Views.Suggestions { + } else if c == gui.State.Contexts.Confirmation || c == gui.State.Contexts.Suggestions { gui.resizeConfirmationPanel() - } else if gui.isPopupPanel(v.Name()) { - return gui.resizePopupPanel(v, v.Buffer()) + } else if c == gui.State.Contexts.CommitMessage || c == gui.State.Contexts.CommitDescription { + gui.resizeCommitMessagePanels() } return nil } -func (gui *Gui) resizePopupPanel(v *gocui.View, content string) error { - x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content) - _, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0) - return err -} - func (gui *Gui) resizeMenu() { itemCount := gui.State.Contexts.Menu.GetList().Len() offset := 3 panelWidth := gui.getConfirmationPanelWidth() - x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsForContentHeight(panelWidth, itemCount+offset) + x0, y0, x1, y1 := gui.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset) menuBottom := y1 - offset _, _ = gui.g.SetView(gui.Views.Menu.Name(), x0, y0, x1, menuBottom, 0) @@ -121,7 +117,7 @@ func (gui *Gui) resizeConfirmationPanel() { wrap = false } panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight - x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight) + x0, y0, x1, y1 := gui.getPopupPanelDimensionsAux(panelWidth, panelHeight) confirmationViewBottom := y1 - suggestionsViewHeight _, _ = gui.g.SetView(gui.Views.Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0) @@ -129,6 +125,21 @@ func (gui *Gui) resizeConfirmationPanel() { _, _ = gui.g.SetView(gui.Views.Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0) } +func (gui *Gui) resizeCommitMessagePanels() { + panelWidth := gui.getConfirmationPanelWidth() + content := gui.Views.CommitDescription.TextArea.GetContent() + summaryViewHeight := 3 + panelHeight := gui.getMessageHeight(false, content, panelWidth) + minHeight := 7 + if panelHeight < minHeight { + panelHeight = minHeight + } + x0, y0, x1, y1 := gui.getPopupPanelDimensionsAux(panelWidth, panelHeight) + + _, _ = gui.g.SetView(gui.Views.CommitMessage.Name(), x0, y0, x1, y0+summaryViewHeight-1, 0) + _, _ = gui.g.SetView(gui.Views.CommitDescription.Name(), x0, y0+summaryViewHeight, x1, y1+summaryViewHeight, 0) +} + func (gui *Gui) globalOptionsMap() map[string]string { keybindingConfig := gui.c.UserConfig.Keybinding diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 468cb9830..579e4da48 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -26,20 +26,21 @@ type Views struct { PatchBuildingSecondary *gocui.View MergeConflicts *gocui.View - Options *gocui.View - Confirmation *gocui.View - Menu *gocui.View - CommitMessage *gocui.View - CommitFiles *gocui.View - SubCommits *gocui.View - Information *gocui.View - AppStatus *gocui.View - Search *gocui.View - SearchPrefix *gocui.View - Limit *gocui.View - Suggestions *gocui.View - Tooltip *gocui.View - Extras *gocui.View + Options *gocui.View + Confirmation *gocui.View + Menu *gocui.View + CommitMessage *gocui.View + CommitDescription *gocui.View + CommitFiles *gocui.View + SubCommits *gocui.View + Information *gocui.View + AppStatus *gocui.View + Search *gocui.View + SearchPrefix *gocui.View + Limit *gocui.View + Suggestions *gocui.View + Tooltip *gocui.View + Extras *gocui.View // for playing the easter egg snake game Snake *gocui.View @@ -94,6 +95,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping { // popups. {viewPtr: &gui.Views.CommitMessage, name: "commitMessage"}, + {viewPtr: &gui.Views.CommitDescription, name: "commitDescription"}, {viewPtr: &gui.Views.Menu, name: "menu"}, {viewPtr: &gui.Views.Suggestions, name: "suggestions"}, {viewPtr: &gui.Views.Confirmation, name: "confirmation"}, @@ -199,10 +201,17 @@ func (gui *Gui) createAllViews() error { gui.Views.AppStatus.Frame = false gui.Views.CommitMessage.Visible = false - gui.Views.CommitMessage.Title = gui.c.Tr.CommitMessage + gui.Views.CommitMessage.Title = gui.c.Tr.CommitSummary gui.Views.CommitMessage.Editable = true gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor) + gui.Views.CommitDescription.Visible = false + gui.Views.CommitDescription.Title = gui.c.Tr.CommitDescriptionTitle + gui.Views.CommitDescription.Subtitle = gui.Tr.CommitDescriptionSubTitle + gui.Views.CommitDescription.FgColor = theme.GocuiDefaultTextColor + gui.Views.CommitDescription.Editable = true + gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor) + gui.Views.Confirmation.Visible = false gui.Views.Suggestions.Visible = false diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go index 2d300caf7..95e02d099 100644 --- a/pkg/i18n/chinese.go +++ b/pkg/i18n/chinese.go @@ -47,7 +47,7 @@ func chineseTranslationSet() TranslationSet { StagingTitle: "正在暂存", MergingTitle: "正在合并", NormalTitle: "正常", - CommitMessage: "提交信息", + CommitSummary: "提交信息", CredentialsUsername: "用户名", CredentialsPassword: "密码", CredentialsPassphrase: "输入 SSH 密钥的密码", @@ -94,7 +94,7 @@ func chineseTranslationSet() TranslationSet { LcNewBranch: "新分支", LcDeleteBranch: "删除分支", NoBranchesThisRepo: "此仓库中没有分支", - CommitMessageConfirm: "{{.keyBindClose}}:关闭,{{.keyBindNewLine}}:新行,{{.keyBindConfirm}}:确认", + CommitMessageConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认", CommitWithoutMessageErr: "您必须编写提交消息才能进行提交", CloseConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认", LcClose: "关闭", diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index f52908cea..a3bdc1778 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -13,7 +13,7 @@ func dutchTranslationSet() TranslationSet { MainTitle: "Hoofd", StagingTitle: "Staging", NormalTitle: "Normaal", - CommitMessage: "Commitbericht", + CommitSummary: "Commitbericht", CredentialsUsername: "Gebruikersnaam", CredentialsPassword: "Wachtwoord", CredentialsPassphrase: "Voer een wachtwoordzin in voor de SSH-sleutel", @@ -60,7 +60,7 @@ func dutchTranslationSet() TranslationSet { LcNewBranch: "nieuwe branch", LcDeleteBranch: "verwijder branch", NoBranchesThisRepo: "Geen branches voor deze repo", - CommitMessageConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindNewLine}}: Nieuwe lijn, {{.keyBindConfirm}}: Bevestig", + CommitMessageConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig", CommitWithoutMessageErr: "Je kan geen commit maken zonder commit bericht", CloseConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig", LcClose: "sluiten", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 4d384da71..d8d5e4727 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -27,7 +27,7 @@ type TranslationSet struct { MergeConfirmTitle string NormalTitle string LogTitle string - CommitMessage string + CommitSummary string CredentialsUsername string CredentialsPassword string CredentialsPassphrase string @@ -197,6 +197,8 @@ type TranslationSet struct { MergeOptionsTitle string RebaseOptionsTitle string CommitMessageTitle string + CommitDescriptionTitle string + CommitDescriptionSubTitle string LocalBranchesTitle string SearchTitle string TagsTitle string @@ -698,7 +700,7 @@ func EnglishTranslationSet() TranslationSet { MergingTitle: "Main Panel (Merging)", NormalTitle: "Main Panel (Normal)", LogTitle: "Log", - CommitMessage: "Commit message", + CommitSummary: "Commit summary", CredentialsUsername: "Username", CredentialsPassword: "Password", CredentialsPassphrase: "Enter passphrase for SSH key", @@ -750,7 +752,7 @@ func EnglishTranslationSet() TranslationSet { LcNewBranch: "new branch", LcDeleteBranch: "delete branch", NoBranchesThisRepo: "No branches for this repo", - CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindNewLine}}: new line, {{.keyBindConfirm}}: confirm", + CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm", CommitWithoutMessageErr: "You cannot commit without a commit message", CloseConfirm: "{{.keyBindClose}}: close/cancel, {{.keyBindConfirm}}: confirm", LcClose: "close", @@ -866,7 +868,9 @@ func EnglishTranslationSet() TranslationSet { RecentRepos: "recent repositories", MergeOptionsTitle: "Merge Options", RebaseOptionsTitle: "Rebase Options", - CommitMessageTitle: "Commit Message", + CommitMessageTitle: "Commit Summary", + CommitDescriptionTitle: "Commit description", + CommitDescriptionSubTitle: "Press tab to toggle focus", LocalBranchesTitle: "Local Branches", SearchTitle: "Search", TagsTitle: "Tags", diff --git a/pkg/i18n/japanese.go b/pkg/i18n/japanese.go index 891d737b0..d68056812 100644 --- a/pkg/i18n/japanese.go +++ b/pkg/i18n/japanese.go @@ -34,7 +34,7 @@ func japaneseTranslationSet() TranslationSet { MergingTitle: "メインパネル (Merging)", NormalTitle: "メインパネル (Normal)", LogTitle: "ログ", - CommitMessage: "コミットメッセージ", + CommitSummary: "コミットメッセージ", CredentialsUsername: "ユーザ名", CredentialsPassword: "パスワード", CredentialsPassphrase: "SSH鍵のパスフレーズを入力", @@ -85,7 +85,7 @@ func japaneseTranslationSet() TranslationSet { LcNewBranch: "新しいブランチを作成", LcDeleteBranch: "ブランチを削除", NoBranchesThisRepo: "リポジトリにブランチが存在しません", - CommitMessageConfirm: "{{.keyBindClose}}: 閉じる, {{.keyBindNewLine}}: 改行, {{.keyBindConfirm}}: 確定", + CommitMessageConfirm: "{{.keyBindClose}}: 閉じる, {{.keyBindConfirm}}: 確定", CommitWithoutMessageErr: "コミットメッセージを入力してください", CloseConfirm: "{{.keyBindClose}}: 閉じる/キャンセル, {{.keyBindConfirm}}: 確認", LcClose: "閉じる", diff --git a/pkg/i18n/korean.go b/pkg/i18n/korean.go index 88e7db8f6..7a150dcb6 100644 --- a/pkg/i18n/korean.go +++ b/pkg/i18n/korean.go @@ -33,7 +33,7 @@ func koreanTranslationSet() TranslationSet { MergingTitle: "메인 패널 (Merging)", NormalTitle: "메인 패널 (Normal)", LogTitle: "로그", - CommitMessage: "커밋 메시지", + CommitSummary: "커밋 메시지", CredentialsUsername: "사용자 이름", CredentialsPassword: "패스워드", CredentialsPassphrase: "SSH키의 passphrase 입력", @@ -84,7 +84,7 @@ func koreanTranslationSet() TranslationSet { LcNewBranch: "새 브랜치 생성", LcDeleteBranch: "브랜치 삭제", NoBranchesThisRepo: "저장소에 브랜치가 존재하지 않습니다.", - CommitMessageConfirm: "{{.keyBindClose}}: 닫기, {{.keyBindNewLine}}: 개행, {{.keyBindConfirm}}: 확인", + CommitMessageConfirm: "{{.keyBindClose}}: 닫기, {{.keyBindConfirm}}: 확인", CommitWithoutMessageErr: "커밋 메시지를 입력하세요.", CloseConfirm: "{{.keyBindClose}}: 닫기/취소, {{.keyBindConfirm}}: 확인", LcClose: "닫기", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 00ed9f38f..a658cff65 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -10,7 +10,7 @@ func polishTranslationSet() TranslationSet { StashTitle: "Schowek", UnstagedChanges: "Zmiany poza poczekalnią", StagedChanges: "Zmiany w poczekalni", - CommitMessage: "Komunikat commita", + CommitSummary: "Komunikat commita", CredentialsUsername: "Użytkownik", CredentialsPassword: "Hasło", CredentialsPassphrase: "Fraza", @@ -55,7 +55,7 @@ func polishTranslationSet() TranslationSet { LcNewBranch: "nowa gałąź", LcDeleteBranch: "usuń gałąź", NoBranchesThisRepo: "Brak gałęzi dla tego repozytorium", - CommitMessageConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindNewLine}}: nowa linia, {{.keyBindConfirm}}: potwierdź", + CommitMessageConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź", CommitWithoutMessageErr: "Nie możesz commitować bez komunikatu", CloseConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź", LcClose: "zamknij", diff --git a/pkg/integration/components/commit_description_panel_driver.go b/pkg/integration/components/commit_description_panel_driver.go new file mode 100644 index 000000000..46d36652d --- /dev/null +++ b/pkg/integration/components/commit_description_panel_driver.go @@ -0,0 +1,25 @@ +package components + +type CommitDescriptionPanelDriver struct { + t *TestDriver +} + +func (self *CommitDescriptionPanelDriver) getViewDriver() *ViewDriver { + return self.t.Views().CommitDescription() +} + +func (self *CommitDescriptionPanelDriver) Type(value string) *CommitDescriptionPanelDriver { + self.t.typeContent(value) + + return self +} + +func (self *CommitDescriptionPanelDriver) SwitchToSummary() *CommitMessagePanelDriver { + self.getViewDriver().PressTab() + return &CommitMessagePanelDriver{t: self.t} +} + +func (self *CommitDescriptionPanelDriver) AddNewline() *CommitDescriptionPanelDriver { + self.t.press(self.t.keys.Universal.Confirm) + return self +} diff --git a/pkg/integration/components/commit_message_panel_driver.go b/pkg/integration/components/commit_message_panel_driver.go index e420334bf..d077761fc 100644 --- a/pkg/integration/components/commit_message_panel_driver.go +++ b/pkg/integration/components/commit_message_panel_driver.go @@ -10,19 +10,36 @@ func (self *CommitMessagePanelDriver) getViewDriver() *ViewDriver { // asserts on the text initially present in the prompt func (self *CommitMessagePanelDriver) InitialText(expected *Matcher) *CommitMessagePanelDriver { + return self.Content(expected) +} + +// asserts on the current context in the prompt +func (self *CommitMessagePanelDriver) Content(expected *Matcher) *CommitMessagePanelDriver { self.getViewDriver().Content(expected) return self } +// asserts that the confirmation view has the expected title +func (self *CommitMessagePanelDriver) Title(expected *Matcher) *CommitMessagePanelDriver { + self.getViewDriver().Title(expected) + + return self +} + func (self *CommitMessagePanelDriver) Type(value string) *CommitMessagePanelDriver { self.t.typeContent(value) return self } +func (self *CommitMessagePanelDriver) SwitchToDescription() *CommitDescriptionPanelDriver { + self.getViewDriver().PressTab() + return &CommitDescriptionPanelDriver{t: self.t} +} + func (self *CommitMessagePanelDriver) AddNewline() *CommitMessagePanelDriver { - self.t.press(self.t.keys.Universal.AppendNewline) + self.t.press(self.t.keys.Universal.Confirm) return self } @@ -49,6 +66,20 @@ func (self *CommitMessagePanelDriver) Confirm() { self.getViewDriver().PressEnter() } +func (self *CommitMessagePanelDriver) Close() { + self.getViewDriver().PressEscape() +} + func (self *CommitMessagePanelDriver) Cancel() { self.getViewDriver().PressEscape() } + +func (self *CommitMessagePanelDriver) SelectPreviousMessage() *CommitMessagePanelDriver { + self.getViewDriver().SelectPreviousItem() + return self +} + +func (self *CommitMessagePanelDriver) SelectNextMessage() *CommitMessagePanelDriver { + self.getViewDriver().SelectNextItem() + return self +} diff --git a/pkg/integration/components/popup.go b/pkg/integration/components/popup.go index b342fa03c..46df83e23 100644 --- a/pkg/integration/components/popup.go +++ b/pkg/integration/components/popup.go @@ -62,9 +62,22 @@ func (self *Popup) CommitMessagePanel() *CommitMessagePanelDriver { return &CommitMessagePanelDriver{t: self.t} } +func (self *Popup) CommitDescriptionPanel() *CommitMessagePanelDriver { + self.inCommitDescriptionPanel() + + return &CommitMessagePanelDriver{t: self.t} +} + func (self *Popup) inCommitMessagePanel() { self.t.assertWithRetries(func() (bool, string) { currentView := self.t.gui.CurrentContext().GetView() return currentView.Name() == "commitMessage", "Expected commit message panel to be focused" }) } + +func (self *Popup) inCommitDescriptionPanel() { + self.t.assertWithRetries(func() (bool, string) { + currentView := self.t.gui.CurrentContext().GetView() + return currentView.Name() == "commitDescription", "Expected commit description panel to be focused" + }) +} diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go index 280843cf3..eb9c0d7f7 100644 --- a/pkg/integration/components/view_driver.go +++ b/pkg/integration/components/view_driver.go @@ -361,6 +361,11 @@ func (self *ViewDriver) PressEnter() *ViewDriver { return self.Press(self.t.keys.Universal.Confirm) } +// i.e. pressing tab +func (self *ViewDriver) PressTab() *ViewDriver { + return self.Press(self.t.keys.Universal.TogglePanel) +} + // i.e. pressing escape func (self *ViewDriver) PressEscape() *ViewDriver { return self.Press(self.t.keys.Universal.Return) diff --git a/pkg/integration/components/views.go b/pkg/integration/components/views.go index 667fe9471..1a6e54b7e 100644 --- a/pkg/integration/components/views.go +++ b/pkg/integration/components/views.go @@ -207,6 +207,10 @@ func (self *Views) CommitMessage() *ViewDriver { return self.regularView("commitMessage") } +func (self *Views) CommitDescription() *ViewDriver { + return self.regularView("commitDescription") +} + func (self *Views) Suggestions() *ViewDriver { return self.regularView("suggestions") } diff --git a/pkg/integration/tests/commit/commit_multiline.go b/pkg/integration/tests/commit/commit_multiline.go index 4967ffb77..d36a5fdb4 100644 --- a/pkg/integration/tests/commit/commit_multiline.go +++ b/pkg/integration/tests/commit/commit_multiline.go @@ -22,14 +22,20 @@ var CommitMultiline = NewIntegrationTest(NewIntegrationTestArgs{ PressPrimaryAction(). Press(keys.Files.CommitChanges) - t.ExpectPopup().CommitMessagePanel().Type("first line").AddNewline().AddNewline().Type("third line").Confirm() - + t.ExpectPopup().CommitMessagePanel(). + Type("first line"). + SwitchToDescription(). + AddNewline(). + AddNewline(). + Type("fourth line"). + SwitchToSummary(). + Confirm() t.Views().Commits(). Lines( Contains("first line"), ) t.Views().Commits().Focus() - t.Views().Main().Content(MatchesRegexp("first line\n\\s*\n\\s*third line")) + t.Views().Main().Content(MatchesRegexp("first line\n\\s*\n\\s*fourth line")) }, }) diff --git a/pkg/integration/tests/commit/history.go b/pkg/integration/tests/commit/history.go new file mode 100644 index 000000000..9938ae35a --- /dev/null +++ b/pkg/integration/tests/commit/history.go @@ -0,0 +1,53 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var History = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Cycling through commit message history in the commit message panel", + ExtraCmdArgs: "", + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("initial commit") + shell.EmptyCommit("commit 2") + shell.EmptyCommit("commit 3") + + shell.CreateFile("myfile", "myfile content") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + PressPrimaryAction(). // stage file + Press(keys.Files.CommitChanges) + + t.ExpectPopup().CommitMessagePanel(). + InitialText(Equals("")). + Type("my commit message"). + SelectPreviousMessage(). + Content(Equals("commit 3")). + SelectPreviousMessage(). + Content(Equals("commit 2")). + SelectPreviousMessage(). + Content(Equals("initial commit")). + SelectPreviousMessage(). + Content(Equals("initial commit")). // we hit the end + SelectNextMessage(). + Content(Equals("commit 2")). + SelectNextMessage(). + Content(Equals("commit 3")). + SelectNextMessage(). + Content(Equals("my commit message")). + SelectNextMessage(). + Content(Equals("my commit message")). // we hit the beginning + Type(" with extra added"). + Confirm() + + t.Views().Commits(). + TopLines( + Contains("my commit message with extra added").IsSelected(), + ) + }, +}) diff --git a/pkg/integration/tests/commit/history_complex.go b/pkg/integration/tests/commit/history_complex.go new file mode 100644 index 000000000..e88da4416 --- /dev/null +++ b/pkg/integration/tests/commit/history_complex.go @@ -0,0 +1,59 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var HistoryComplex = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "More complex flow for cycling commit message history", + ExtraCmdArgs: "", + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("initial commit") + shell.EmptyCommit("commit 2") + shell.EmptyCommit("commit 3") + + shell.CreateFileAndAdd("myfile", "myfile content") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // We're going to start a new commit message, + // then leave and try to reword a commit, then + // come back to original message and confirm we haven't lost our message. + // This shows that we're storing the preserved message for a new commit separately + // to the message when cycling history. + + t.Views().Files(). + IsFocused(). + Press(keys.Files.CommitChanges) + + t.ExpectPopup().CommitMessagePanel(). + InitialText(Equals("")). + Type("my commit message"). + Cancel() + + t.Views().Commits(). + Focus(). + SelectedLine(Contains("commit 3")). + Press(keys.Commits.RenameCommit) + + t.ExpectPopup().CommitMessagePanel(). + InitialText(Equals("commit 3")). + SelectNextMessage(). + Content(Equals("")). + Type("reworded message"). + SelectPreviousMessage(). + Content(Equals("commit 3")). + SelectNextMessage(). + Content(Equals("reworded message")). + Cancel() + + t.Views().Files(). + Focus(). + Press(keys.Files.CommitChanges) + + t.ExpectPopup().CommitMessagePanel(). + InitialText(Equals("my commit message")) + }, +}) diff --git a/pkg/integration/tests/commit/reword.go b/pkg/integration/tests/commit/reword.go new file mode 100644 index 000000000..f488977ed --- /dev/null +++ b/pkg/integration/tests/commit/reword.go @@ -0,0 +1,66 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var Reword = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Staging a couple files and committing", + ExtraCmdArgs: "", + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFile("myfile", "myfile content") + shell.CreateFile("myfile2", "myfile2 content") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsEmpty() + + t.Views().Files(). + IsFocused(). + PressPrimaryAction(). + Press(keys.Files.CommitChanges) + + commitMessage := "my commit message" + + t.ExpectPopup().CommitMessagePanel().Type(commitMessage).Confirm() + t.Views().Commits(). + Lines( + Contains(commitMessage), + ) + + t.Views().Files(). + IsFocused(). + PressPrimaryAction(). + Press(keys.Files.CommitChanges) + + wipCommitMessage := "my commit message wip" + + t.ExpectPopup().CommitMessagePanel().Type(wipCommitMessage).Close() + + t.Views().Commits().Focus(). + Lines( + Contains(commitMessage), + ).Press(keys.Commits.RenameCommit) + + t.ExpectPopup().CommitMessagePanel(). + SwitchToDescription(). + Type("some description"). + SwitchToSummary(). + Confirm() + + t.Views().Main().Content(MatchesRegexp("my commit message\n\\s*some description")) + + t.Views().Files(). + Focus(). + Press(keys.Files.CommitChanges) + + t.ExpectPopup().CommitMessagePanel().Confirm() + t.Views().Commits(). + Lines( + Contains(wipCommitMessage), + ) + }, +}) diff --git a/pkg/integration/tests/interactive_rebase/reword_first_commit.go b/pkg/integration/tests/interactive_rebase/reword_first_commit.go index c85293cec..50ca2fb0e 100644 --- a/pkg/integration/tests/interactive_rebase/reword_first_commit.go +++ b/pkg/integration/tests/interactive_rebase/reword_first_commit.go @@ -27,8 +27,8 @@ var RewordFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains("commit 01")). Press(keys.Commits.RenameCommit). Tap(func() { - t.ExpectPopup().Prompt(). - Title(Equals("reword commit")). + t.ExpectPopup().CommitMessagePanel(). + Title(Equals("Reword commit")). InitialText(Equals("commit 01")). Clear(). Type("renamed 01"). diff --git a/pkg/integration/tests/interactive_rebase/reword_last_commit.go b/pkg/integration/tests/interactive_rebase/reword_last_commit.go index 9a4329219..742b250d4 100644 --- a/pkg/integration/tests/interactive_rebase/reword_last_commit.go +++ b/pkg/integration/tests/interactive_rebase/reword_last_commit.go @@ -23,8 +23,8 @@ var RewordLastCommit = NewIntegrationTest(NewIntegrationTestArgs{ ). Press(keys.Commits.RenameCommit). Tap(func() { - t.ExpectPopup().Prompt(). - Title(Equals("reword commit")). + t.ExpectPopup().CommitMessagePanel(). + Title(Equals("Reword commit")). InitialText(Equals("commit 02")). Clear(). Type("renamed 02"). diff --git a/pkg/integration/tests/interactive_rebase/reword_you_are_here_commit.go b/pkg/integration/tests/interactive_rebase/reword_you_are_here_commit.go index 921e1a016..c7431d059 100644 --- a/pkg/integration/tests/interactive_rebase/reword_you_are_here_commit.go +++ b/pkg/integration/tests/interactive_rebase/reword_you_are_here_commit.go @@ -31,8 +31,8 @@ var RewordYouAreHereCommit = NewIntegrationTest(NewIntegrationTestArgs{ ). Press(keys.Commits.RenameCommit). Tap(func() { - t.ExpectPopup().Prompt(). - Title(Equals("reword commit")). + t.ExpectPopup().CommitMessagePanel(). + Title(Equals("Reword commit")). InitialText(Equals("commit 02")). Clear(). Type("renamed 02"). diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index d4da79732..8c1d75c6e 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -49,10 +49,13 @@ var tests = []*components.IntegrationTest{ commit.CommitMultiline, commit.CreateTag, commit.DiscardOldFileChange, + commit.History, + commit.HistoryComplex, commit.NewBranch, commit.ResetAuthor, commit.Revert, commit.RevertMerge, + commit.Reword, commit.Search, commit.SetAuthor, commit.StageRangeOfLines,