Add Co-Author support to new commits (#3097)

- Adds Co-Author support to commit menu (`<C-o>` by default) 
  - `e` Opens up the commit message in your editor
  - `c` Lets you add a co author to your commit
- Cleans up and amend commit attribute menu related code
This commit is contained in:
Stefan Haller 2024-03-12 08:40:47 +01:00 committed by GitHub
commit d12ceeb1ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 278 additions and 37 deletions

View file

@ -254,6 +254,7 @@ keybinding:
moveDownCommit: '<c-j>' # move commit down one
moveUpCommit: '<c-k>' # 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: '<c-o>'
amendAttribute:
addCoAuthor: 'c'
resetAuthor: 'a'
setAuthor: 'A'
```
## Platform Defaults

View file

@ -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 <Email>'
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()

View file

@ -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 <john@doe.com>",
},
{
name: "Just a subject, no body",
message: "Subject",
expectedResult: "Subject\n\nCo-authored-by: John Doe <john@doe.com>",
},
{
name: "Subject and body",
message: "Subject\n\nBody",
expectedResult: "Subject\n\nBody\n\nCo-authored-by: John Doe <john@doe.com>",
},
{
name: "Body already ending with a Co-authored-by line",
message: "Subject\n\nBody\n\nCo-authored-by: Jane Smith <jane@smith.com>",
expectedResult: "Subject\n\nBody\n\nCo-authored-by: Jane Smith <jane@smith.com>\nCo-authored-by: John Doe <john@doe.com>",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := AddCoAuthorToMessage(s.message, "John Doe <john@doe.com>")
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 <john@doe.com>",
},
{
name: "Non-empty description",
description: "Body",
expectedResult: "Body\n\nCo-authored-by: John Doe <john@doe.com>",
},
{
name: "Description already ending with a Co-authored-by line",
description: "Body\n\nCo-authored-by: Jane Smith <jane@smith.com>",
expectedResult: "Body\n\nCo-authored-by: Jane Smith <jane@smith.com>\nCo-authored-by: John Doe <john@doe.com>",
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := AddCoAuthorToDescription(s.description, "John Doe <john@doe.com>")
assert.Equal(t, s.expectedResult, result)
})
}
}

View file

@ -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: "<c-o>",
CommitMenu: "<c-o>",
},
},
OS: OSConfig{},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -33,8 +33,9 @@ var AddCoAuthor = NewIntegrationTest(NewIntegrationTestArgs{
})
t.Views().Main().ContainsLines(
Contains("initial commit"),
Contains("Co-authored-by: John Smith <jsmith@gmail.com>"),
Equals(" initial commit"),
Equals(" "),
Equals(" Co-authored-by: John Smith <jsmith@gmail.com>"),
)
},
})

View file

@ -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 <john@doe.com>").
Content(Equals("Here's my message.\n\nCo-authored-by: John Doe <john@doe.com>")).
AddCoAuthor("Jane Smith <jane@smith.com>").
// Second co-author doesn't add a blank line:
Content(Equals("Here's my message.\n\nCo-authored-by: John Doe <john@doe.com>\nCo-authored-by: Jane Smith <jane@smith.com>")).
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 <john@doe.com>"),
Equals(" Co-authored-by: Jane Smith <jane@smith.com>"),
)
})
},
})

View file

@ -64,6 +64,7 @@ var tests = []*components.IntegrationTest{
cherry_pick.CherryPickDuringRebase,
cherry_pick.CherryPickRange,
commit.AddCoAuthor,
commit.AddCoAuthorWhileCommitting,
commit.Amend,
commit.AutoWrapMessage,
commit.Commit,

View file

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