diff --git a/docs/Config.md b/docs/Config.md index 4b26c5f84..6998b2296 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -254,6 +254,7 @@ keybinding: moveDownCommit: '' # move commit down one moveUpCommit: '' # move commit up one amendToCommit: 'A' + amendAttributeMenu: 'a' pickCommit: 'p' # pick commit (when mid-rebase) revertCommit: 't' cherryPickCopy: 'C' @@ -276,6 +277,12 @@ keybinding: init: 'i' update: 'u' bulkMenu: 'b' + commitMessage: + commitMenu: '' + amendAttribute: + addCoAuthor: 'c' + resetAuthor: 'a' + setAuthor: 'A' ``` ## Platform Defaults diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 960fab811..400cd7608 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -39,13 +39,13 @@ func (self *CommitCommands) SetAuthor(value string) error { } // Add a commit's coauthor using Github/Gitlab Co-authored-by metadata. Value is expected to be of the form 'Name ' -func (self *CommitCommands) AddCoAuthor(sha string, value string) error { +func (self *CommitCommands) AddCoAuthor(sha string, author string) error { message, err := self.GetCommitMessage(sha) if err != nil { return err } - message = message + fmt.Sprintf("\nCo-authored-by: %s", value) + message = AddCoAuthorToMessage(message, author) cmdArgs := NewGitCmd("commit"). Arg("--allow-empty", "--amend", "--only", "-m", message). @@ -54,6 +54,25 @@ func (self *CommitCommands) AddCoAuthor(sha string, value string) error { return self.cmd.New(cmdArgs).Run() } +func AddCoAuthorToMessage(message string, author string) string { + subject, body, _ := strings.Cut(message, "\n") + + return strings.TrimSpace(subject) + "\n\n" + AddCoAuthorToDescription(strings.TrimSpace(body), author) +} + +func AddCoAuthorToDescription(description string, author string) string { + if description != "" { + lines := strings.Split(description, "\n") + if strings.HasPrefix(lines[len(lines)-1], "Co-authored-by:") { + description += "\n" + } else { + description += "\n\n" + } + } + + return description + fmt.Sprintf("Co-authored-by: %s", author) +} + // ResetToCommit reset to commit func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error { cmdArgs := NewGitCmd("reset").Arg("--"+strength, sha).ToArgv() diff --git a/pkg/commands/git_commands/commit_test.go b/pkg/commands/git_commands/commit_test.go index 34b064d67..d6e3397e3 100644 --- a/pkg/commands/git_commands/commit_test.go +++ b/pkg/commands/git_commands/commit_test.go @@ -333,3 +333,70 @@ func TestGetCommitMessageFromHistory(t *testing.T) { }) } } + +func TestAddCoAuthorToMessage(t *testing.T) { + scenarios := []struct { + name string + message string + expectedResult string + }{ + { + // This never happens, I think it isn't possible to create a commit + // with an empty message. Just including it for completeness. + name: "Empty message", + message: "", + expectedResult: "\n\nCo-authored-by: John Doe ", + }, + { + name: "Just a subject, no body", + message: "Subject", + expectedResult: "Subject\n\nCo-authored-by: John Doe ", + }, + { + name: "Subject and body", + message: "Subject\n\nBody", + expectedResult: "Subject\n\nBody\n\nCo-authored-by: John Doe ", + }, + { + name: "Body already ending with a Co-authored-by line", + message: "Subject\n\nBody\n\nCo-authored-by: Jane Smith ", + expectedResult: "Subject\n\nBody\n\nCo-authored-by: Jane Smith \nCo-authored-by: John Doe ", + }, + } + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := AddCoAuthorToMessage(s.message, "John Doe ") + assert.Equal(t, s.expectedResult, result) + }) + } +} + +func TestAddCoAuthorToDescription(t *testing.T) { + scenarios := []struct { + name string + description string + expectedResult string + }{ + { + name: "Empty description", + description: "", + expectedResult: "Co-authored-by: John Doe ", + }, + { + name: "Non-empty description", + description: "Body", + expectedResult: "Body\n\nCo-authored-by: John Doe ", + }, + { + name: "Description already ending with a Co-authored-by line", + description: "Body\n\nCo-authored-by: Jane Smith ", + expectedResult: "Body\n\nCo-authored-by: Jane Smith \nCo-authored-by: John Doe ", + }, + } + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := AddCoAuthorToDescription(s.description, "John Doe ") + assert.Equal(t, s.expectedResult, result) + }) + } +} diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 6a4efe78a..2cbb15ce4 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -281,17 +281,18 @@ type UpdateConfig struct { } type KeybindingConfig struct { - Universal KeybindingUniversalConfig `yaml:"universal"` - Status KeybindingStatusConfig `yaml:"status"` - Files KeybindingFilesConfig `yaml:"files"` - Branches KeybindingBranchesConfig `yaml:"branches"` - Worktrees KeybindingWorktreesConfig `yaml:"worktrees"` - Commits KeybindingCommitsConfig `yaml:"commits"` - Stash KeybindingStashConfig `yaml:"stash"` - CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"` - Main KeybindingMainConfig `yaml:"main"` - Submodules KeybindingSubmodulesConfig `yaml:"submodules"` - CommitMessage KeybindingCommitMessageConfig `yaml:"commitMessage"` + Universal KeybindingUniversalConfig `yaml:"universal"` + Status KeybindingStatusConfig `yaml:"status"` + Files KeybindingFilesConfig `yaml:"files"` + Branches KeybindingBranchesConfig `yaml:"branches"` + Worktrees KeybindingWorktreesConfig `yaml:"worktrees"` + Commits KeybindingCommitsConfig `yaml:"commits"` + AmendAttribute KeybindingAmendAttributeConfig `yaml:"amendAttribute"` + Stash KeybindingStashConfig `yaml:"stash"` + CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"` + Main KeybindingMainConfig `yaml:"main"` + Submodules KeybindingSubmodulesConfig `yaml:"submodules"` + CommitMessage KeybindingCommitMessageConfig `yaml:"commitMessage"` } // damn looks like we have some inconsistencies here with -alt and -alt1 @@ -440,6 +441,12 @@ type KeybindingCommitsConfig struct { StartInteractiveRebase string `yaml:"startInteractiveRebase"` } +type KeybindingAmendAttributeConfig struct { + ResetAuthor string `yaml:"resetAuthor"` + SetAuthor string `yaml:"setAuthor"` + AddCoAuthor string `yaml:"addCoAuthor"` +} + type KeybindingStashConfig struct { PopStash string `yaml:"popStash"` RenameStash string `yaml:"renameStash"` @@ -462,7 +469,7 @@ type KeybindingSubmodulesConfig struct { } type KeybindingCommitMessageConfig struct { - SwitchToEditor string `yaml:"switchToEditor"` + CommitMenu string `yaml:"commitMenu"` } // OSConfig contains config on the level of the os @@ -836,6 +843,11 @@ func GetDefaultConfig() *UserConfig { ViewBisectOptions: "b", StartInteractiveRebase: "i", }, + AmendAttribute: KeybindingAmendAttributeConfig{ + ResetAuthor: "a", + SetAuthor: "A", + AddCoAuthor: "c", + }, Stash: KeybindingStashConfig{ PopStash: "g", RenameStash: "r", @@ -854,7 +866,7 @@ func GetDefaultConfig() *UserConfig { BulkMenu: "b", }, CommitMessage: KeybindingCommitMessageConfig{ - SwitchToEditor: "", + CommitMenu: "", }, }, OS: OSConfig{}, diff --git a/pkg/gui/context/commit_message_context.go b/pkg/gui/context/commit_message_context.go index 1ac158839..0cea8e6b3 100644 --- a/pkg/gui/context/commit_message_context.go +++ b/pkg/gui/context/commit_message_context.go @@ -115,8 +115,8 @@ func (self *CommitMessageContext) SetPanelState( subtitleTemplate := lo.Ternary(onSwitchToEditor != nil, self.c.Tr.CommitDescriptionSubTitle, self.c.Tr.CommitDescriptionSubTitleNoSwitch) self.c.Views().CommitDescription.Subtitle = utils.ResolvePlaceholderString(subtitleTemplate, map[string]string{ - "togglePanelKeyBinding": keybindings.Label(self.c.UserConfig.Keybinding.Universal.TogglePanel), - "switchToEditorKeyBinding": keybindings.Label(self.c.UserConfig.Keybinding.CommitMessage.SwitchToEditor), + "togglePanelKeyBinding": keybindings.Label(self.c.UserConfig.Keybinding.Universal.TogglePanel), + "commitMenuKeybinding": keybindings.Label(self.c.UserConfig.Keybinding.CommitMessage.CommitMenu), }) } diff --git a/pkg/gui/controllers/commit_description_controller.go b/pkg/gui/controllers/commit_description_controller.go index 8f07cecfc..0c078382b 100644 --- a/pkg/gui/controllers/commit_description_controller.go +++ b/pkg/gui/controllers/commit_description_controller.go @@ -36,8 +36,8 @@ func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOp Handler: self.confirm, }, { - Key: opts.GetKey(opts.Config.CommitMessage.SwitchToEditor), - Handler: self.switchToEditor, + Key: opts.GetKey(opts.Config.CommitMessage.CommitMenu), + Handler: self.openCommitMenu, }, } @@ -64,6 +64,7 @@ func (self *CommitDescriptionController) confirm() error { return self.c.Helpers().Commits.HandleCommitConfirm() } -func (self *CommitDescriptionController) switchToEditor() error { - return self.c.Helpers().Commits.SwitchToEditor() +func (self *CommitDescriptionController) openCommitMenu() error { + authorSuggestion := self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc() + return self.c.Helpers().Commits.OpenCommitMenu(authorSuggestion) } diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index 756b240e6..84e553d87 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -48,8 +48,8 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) Handler: self.switchToCommitDescription, }, { - Key: opts.GetKey(opts.Config.CommitMessage.SwitchToEditor), - Handler: self.switchToEditor, + Key: opts.GetKey(opts.Config.CommitMessage.CommitMenu), + Handler: self.openCommitMenu, }, } @@ -89,10 +89,6 @@ func (self *CommitMessageController) switchToCommitDescription() error { return nil } -func (self *CommitMessageController) switchToEditor() error { - return self.c.Helpers().Commits.SwitchToEditor() -} - func (self *CommitMessageController) handleCommitIndexChange(value int) error { currentIndex := self.context().GetSelectedIndex() newIndex := currentIndex + value @@ -134,3 +130,8 @@ func (self *CommitMessageController) confirm() error { func (self *CommitMessageController) close() error { return self.c.Helpers().Commits.CloseCommitMessagePanel() } + +func (self *CommitMessageController) openCommitMenu() error { + authorSuggestion := self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc() + return self.c.Helpers().Commits.OpenCommitMenu(authorSuggestion) +} diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go index 0801d5742..568c07726 100644 --- a/pkg/gui/controllers/helpers/commits_helper.go +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -6,6 +6,7 @@ import ( "time" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/samber/lo" ) @@ -215,3 +216,39 @@ func (self *CommitsHelper) commitMessageContexts() []types.Context { self.c.Contexts().CommitMessage, } } + +func (self *CommitsHelper) OpenCommitMenu(suggestionFunc func(string) []*types.Suggestion) error { + menuItems := []*types.MenuItem{ + { + Label: self.c.Tr.OpenInEditor, + OnPress: func() error { + return self.SwitchToEditor() + }, + Key: 'e', + }, + { + Label: self.c.Tr.AddCoAuthor, + OnPress: func() error { + return self.addCoAuthor(suggestionFunc) + }, + Key: 'c', + }, + } + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.CommitMenuTitle, + Items: menuItems, + }) +} + +func (self *CommitsHelper) addCoAuthor(suggestionFunc func(string) []*types.Suggestion) error { + return self.c.Prompt(types.PromptOpts{ + Title: self.c.Tr.AddCoAuthorPromptTitle, + FindSuggestionsFunc: suggestionFunc, + HandleConfirm: func(value string) error { + commitDescription := self.getCommitDescription() + commitDescription = git_commands.AddCoAuthorToDescription(commitDescription, value) + self.setCommitDescription(commitDescription) + return nil + }, + }) +} diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 8c99d7586..085504662 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -673,25 +673,26 @@ func (self *LocalCommitsController) canAmend(commit *models.Commit) *types.Disab } func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error { + opts := self.c.KeybindingsOpts() return self.c.Menu(types.CreateMenuOptions{ Title: "Amend commit attribute", Items: []*types.MenuItem{ { Label: self.c.Tr.ResetAuthor, OnPress: self.resetAuthor, - Key: 'a', - Tooltip: "Reset the commit's author to the currently configured user. This will also renew the author timestamp", + Key: opts.GetKey(opts.Config.AmendAttribute.ResetAuthor), + Tooltip: self.c.Tr.ResetAuthorTooltip, }, { Label: self.c.Tr.SetAuthor, OnPress: self.setAuthor, - Key: 'A', - Tooltip: "Set the author based on a prompt", + Key: opts.GetKey(opts.Config.AmendAttribute.SetAuthor), + Tooltip: self.c.Tr.SetAuthorTooltip, }, { Label: self.c.Tr.AddCoAuthor, OnPress: self.addCoAuthor, - Key: 'c', + Key: opts.GetKey(opts.Config.AmendAttribute.AddCoAuthor), Tooltip: self.c.Tr.AddCoAuthorTooltip, }, }, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index b1d0e01eb..818783e93 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -147,7 +147,9 @@ type TranslationSet struct { AmendCommitTooltip string Amend string ResetAuthor string + ResetAuthorTooltip string SetAuthor string + SetAuthorTooltip string AddCoAuthor string AmendCommitAttribute string AmendCommitAttributeTooltip string @@ -270,6 +272,7 @@ type TranslationSet struct { SearchTitle string TagsTitle string MenuTitle string + CommitMenuTitle string RemotesTitle string RemoteBranchesTitle string PatchBuildingTitle string @@ -1093,7 +1096,9 @@ func EnglishTranslationSet() TranslationSet { AmendCommitTooltip: "Amend commit with staged changes. If the selected commit is the HEAD commit, this will perform `git commit --amend`. Otherwise the commit will be amended via a rebase.", Amend: "Amend", ResetAuthor: "Reset author", + ResetAuthorTooltip: "Reset the commit's author to the currently configured user. This will also renew the author timestamp", SetAuthor: "Set author", + SetAuthorTooltip: "Set the author based on a prompt", AddCoAuthor: "Add co-author", AmendCommitAttribute: "Amend commit attribute", AmendCommitAttributeTooltip: "Set/Reset commit author or set co-author.", @@ -1209,12 +1214,13 @@ func EnglishTranslationSet() TranslationSet { RebaseOptionsTitle: "Rebase options", CommitSummaryTitle: "Commit summary", CommitDescriptionTitle: "Commit description", - CommitDescriptionSubTitle: "Press {{.togglePanelKeyBinding}} to toggle focus, {{.switchToEditorKeyBinding}} to switch to editor", + CommitDescriptionSubTitle: "Press {{.togglePanelKeyBinding}} to toggle focus, {{.commitMenuKeybinding}} to open menu", CommitDescriptionSubTitleNoSwitch: "Press {{.togglePanelKeyBinding}} to toggle focus", LocalBranchesTitle: "Local branches", SearchTitle: "Search", TagsTitle: "Tags", MenuTitle: "Menu", + CommitMenuTitle: "Commit Menu", RemotesTitle: "Remotes", RemoteBranchesTitle: "Remote branches", PatchBuildingTitle: "Main panel (patch building)", diff --git a/pkg/integration/components/commit_description_panel_driver.go b/pkg/integration/components/commit_description_panel_driver.go index eddc53533..253dc6f87 100644 --- a/pkg/integration/components/commit_description_panel_driver.go +++ b/pkg/integration/components/commit_description_panel_driver.go @@ -41,6 +41,17 @@ func (self *CommitDescriptionPanelDriver) GoToBeginning() *CommitDescriptionPane return self } +func (self *CommitDescriptionPanelDriver) AddCoAuthor(author string) *CommitDescriptionPanelDriver { + self.t.press(self.t.keys.CommitMessage.CommitMenu) + self.t.ExpectPopup().Menu().Title(Equals("Commit Menu")). + Select(Contains("Add co-author")). + Confirm() + self.t.ExpectPopup().Prompt().Title(Contains("Add co-author")). + Type(author). + Confirm() + return self +} + func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver { self.getViewDriver().Title(expected) diff --git a/pkg/integration/components/commit_message_panel_driver.go b/pkg/integration/components/commit_message_panel_driver.go index b3dda6a04..68e1c639b 100644 --- a/pkg/integration/components/commit_message_panel_driver.go +++ b/pkg/integration/components/commit_message_panel_driver.go @@ -69,7 +69,10 @@ func (self *CommitMessagePanelDriver) Cancel() { } func (self *CommitMessagePanelDriver) SwitchToEditor() { - self.getViewDriver().Press(self.t.keys.CommitMessage.SwitchToEditor) + self.OpenCommitMenu() + self.t.ExpectPopup().Menu().Title(Equals("Commit Menu")). + Select(Contains("Open in editor")). + Confirm() } func (self *CommitMessagePanelDriver) SelectPreviousMessage() *CommitMessagePanelDriver { @@ -81,3 +84,8 @@ func (self *CommitMessagePanelDriver) SelectNextMessage() *CommitMessagePanelDri self.getViewDriver().SelectNextItem() return self } + +func (self *CommitMessagePanelDriver) OpenCommitMenu() *CommitMessagePanelDriver { + self.t.press(self.t.keys.CommitMessage.CommitMenu) + return self +} diff --git a/pkg/integration/tests/commit/add_co_author.go b/pkg/integration/tests/commit/add_co_author.go index 3a2021dac..f4c8d2c52 100644 --- a/pkg/integration/tests/commit/add_co_author.go +++ b/pkg/integration/tests/commit/add_co_author.go @@ -33,8 +33,9 @@ var AddCoAuthor = NewIntegrationTest(NewIntegrationTestArgs{ }) t.Views().Main().ContainsLines( - Contains("initial commit"), - Contains("Co-authored-by: John Smith "), + Equals(" initial commit"), + Equals(" "), + Equals(" Co-authored-by: John Smith "), ) }, }) diff --git a/pkg/integration/tests/commit/add_co_author_while_committing.go b/pkg/integration/tests/commit/add_co_author_while_committing.go new file mode 100644 index 000000000..d817f00f0 --- /dev/null +++ b/pkg/integration/tests/commit/add_co_author_while_committing.go @@ -0,0 +1,51 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var AddCoAuthorWhileCommitting = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Add co-author while typing the commit message", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + }, + SetupRepo: func(shell *Shell) { + shell.CreateFile("file", "file content") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + PressPrimaryAction(). // stage file + Press(keys.Files.CommitChanges) + + t.ExpectPopup().CommitMessagePanel(). + Type("Subject"). + SwitchToDescription(). + Type("Here's my message."). + AddCoAuthor("John Doe "). + Content(Equals("Here's my message.\n\nCo-authored-by: John Doe ")). + AddCoAuthor("Jane Smith "). + // Second co-author doesn't add a blank line: + Content(Equals("Here's my message.\n\nCo-authored-by: John Doe \nCo-authored-by: Jane Smith ")). + SwitchToSummary(). + Confirm() + + t.Views().Commits(). + Lines( + Contains("Subject"), + ). + Focus(). + Tap(func() { + t.Views().Main().ContainsLines( + Equals(" Subject"), + Equals(" "), + Equals(" Here's my message."), + Equals(" "), + Equals(" Co-authored-by: John Doe "), + Equals(" Co-authored-by: Jane Smith "), + ) + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 8c2891d47..e5a540e8b 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -64,6 +64,7 @@ var tests = []*components.IntegrationTest{ cherry_pick.CherryPickDuringRebase, cherry_pick.CherryPickRange, commit.AddCoAuthor, + commit.AddCoAuthorWhileCommitting, commit.Amend, commit.AutoWrapMessage, commit.Commit, diff --git a/schema/config.json b/schema/config.json index 44d8e0cb4..3816dcea6 100644 --- a/schema/config.json +++ b/schema/config.json @@ -1163,6 +1163,24 @@ "additionalProperties": false, "type": "object" }, + "amendAttribute": { + "properties": { + "resetAuthor": { + "type": "string", + "default": "a" + }, + "setAuthor": { + "type": "string", + "default": "A" + }, + "addCoAuthor": { + "type": "string", + "default": "c" + } + }, + "additionalProperties": false, + "type": "object" + }, "stash": { "properties": { "popStash": { @@ -1225,7 +1243,7 @@ }, "commitMessage": { "properties": { - "switchToEditor": { + "commitMenu": { "type": "string", "default": "\u003cc-o\u003e" }