Add amend menu entry "Sign off"

This builds on the native 'git commit --signoff' feature to append the
committer's Signed-off-by trailer to commit messages. Such a trailer is
mandatory in certain projects like the Linux kernel and can now be
easily applied to ranges of commits in hindsight with lazygit.

The change is implemented as new CommitCommands/RebaseCommands methods
that hook into GenericAmend to operate on range selections. It supports
a configurable keybinding (default "s") and integrates with the command
log.
This commit is contained in:
Tobias Deiminger 2026-09-05 12:32:19 +02:00
parent e0b2a5081d
commit e27f7bdf9a
15 changed files with 186 additions and 6 deletions

View file

@ -809,6 +809,7 @@ keybinding:
resetAuthor: a
setAuthor: A
addCoAuthor: c
signOff: s
stash:
popStash: g
renameStash: r

View file

@ -102,7 +102,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` V `` | Paste (cherry-pick) | |
| `` B `` | Mark as base commit for rebase | Select a base commit for the next rebase. When you rebase onto a branch, only commits above the base commit will be brought across. This uses the `git rebase --onto` command. |
| `` A `` | Amend | 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. |
| `` a `` | Amend commit attribute | Set/Reset commit author or set co-author. |
| `` a `` | Amend commit attribute | Set/Reset commit author, set co-author, or sign off the commit. |
| `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
| `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
| `` <ctrl+l> `` | View log options | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |

View file

@ -313,7 +313,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` V `` | 커밋을 붙여넣기 (cherry-pick) | |
| `` B `` | Mark as base commit for rebase | Select a base commit for the next rebase. When you rebase onto a branch, only commits above the base commit will be brought across. This uses the `git rebase --onto` command. |
| `` A `` | Amend | Amend commit with staged changes |
| `` a `` | Amend commit attribute | Set/Reset commit author or set co-author. |
| `` a `` | Amend commit attribute | Set/Reset commit author, set co-author, or sign off the commit. |
| `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
| `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
| `` <ctrl+l> `` | 로그 메뉴 열기 | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |

View file

@ -174,7 +174,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` V `` | Plak commits (cherry-pick) | |
| `` B `` | Markeer als basiscommit voor rebase | Selecteer een basiscommit voor de volgende rebase. Als je rebased op een branch worden alleen commits boven de basiscommit meegenomen. Hiervoor wordt het `git rebase --onto` commando gebruikt. |
| `` A `` | Amend | Wijzig commit met staged veranderingen |
| `` a `` | Amend commit attribute | Set/Reset commit author or set co-author. |
| `` a `` | Amend commit attribute | Set/Reset commit author, set co-author, or sign off the commit. |
| `` t `` | Revert | Maak een revert commit voor de geselecteerde commit, die de wijzigingen in deze commit terugdraait. |
| `` T `` | Tag commit | Maak een nieuwe tag die naar de geselecteerde commit wijst. Je wordt gevraagd om een tag naam en optionele omschrijving. |
| `` <ctrl+l> `` | Log opties weergeven | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |

View file

@ -184,7 +184,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` V `` | Вставить отобранные коммиты (cherry-pick) | |
| `` B `` | Mark as base commit for rebase | Select a base commit for the next rebase. When you rebase onto a branch, only commits above the base commit will be brought across. This uses the `git rebase --onto` command. |
| `` A `` | Amend | Править последний коммит с проиндексированными изменениями |
| `` a `` | Установить/убрать автора коммита | Set/Reset commit author or set co-author. |
| `` a `` | Установить/убрать автора коммита | Set/Reset commit author, set co-author, or sign off the commit. |
| `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
| `` T `` | Пометить коммит тегом | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
| `` <ctrl+l> `` | Открыть меню журнала | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |

View file

@ -198,7 +198,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` V `` | 貼上提交 (揀選) | |
| `` B `` | 為了變基已標注提交為基準提交 | 請為了下一次變基選擇一項基準提交;此將執行 `git rebase --onto`。 |
| `` A `` | 修改 | 使用已預存的更改修正提交 |
| `` a `` | 設定/重設提交作者 | Set/Reset commit author or set co-author. |
| `` a `` | 設定/重設提交作者 | Set/Reset commit author, set co-author, or sign off the commit. |
| `` t `` | 還原 | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
| `` T `` | 打標籤到提交 | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
| `` <ctrl+l> `` | 開啟記錄選單 | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |

View file

@ -29,6 +29,15 @@ func (self *CommitCommands) ResetAuthor() error {
return self.cmd.New(cmdArgs).Run()
}
// Adds a Signed-off-by trailer to the topmost commit
func (self *CommitCommands) SignOff() error {
cmdArgs := NewGitCmd("commit").
Arg("--allow-empty", "--allow-empty-message", "--only", "--no-edit", "--amend", "--signoff").
ToArgv()
return self.cmd.New(cmdArgs).Run()
}
// Sets the commit's author to the supplied value. Value is expected to be of the form 'Name <Email>'
func (self *CommitCommands) SetAuthor(value string) error {
cmdArgs := NewGitCmd("commit").

View file

@ -80,6 +80,12 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, start, end
})
}
func (self *RebaseCommands) SignOffCommit(commits []*models.Commit, start, end int) error {
return self.GenericAmend(commits, start, end, func(_ *models.Commit) error {
return self.commit.SignOff()
})
}
func (self *RebaseCommands) AddCommitCoAuthor(commits []*models.Commit, start, end int, value string) error {
return self.GenericAmend(commits, start, end, func(commit *models.Commit) error {
return self.commit.AddCoAuthor(commit.Hash(), value)

View file

@ -646,6 +646,7 @@ type KeybindingAmendAttributeConfig struct {
ResetAuthor Keybinding `yaml:"resetAuthor"`
SetAuthor Keybinding `yaml:"setAuthor"`
AddCoAuthor Keybinding `yaml:"addCoAuthor"`
SignOff Keybinding `yaml:"signOff"`
}
type KeybindingStashConfig struct {
@ -1159,6 +1160,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
ResetAuthor: Keybinding{"a"},
SetAuthor: Keybinding{"A"},
AddCoAuthor: Keybinding{"c"},
SignOff: Keybinding{"s"},
},
Stash: KeybindingStashConfig{
PopStash: Keybinding{"g"},

View file

@ -1301,10 +1301,31 @@ func (self *LocalCommitsController) amendAttribute(_ []*models.Commit, start, en
Keys: opts.GetKeys(opts.Config.AmendAttribute.AddCoAuthor),
Tooltip: self.c.Tr.AddCoAuthorTooltip,
},
{
Label: self.c.Tr.SignOffCommit,
OnPress: func() error { return self.signOffCommit(commits, start, end) },
Keys: opts.GetKeys(opts.Config.AmendAttribute.SignOff),
Tooltip: self.c.Tr.SignOffCommitTooltip,
},
},
})
}
func (self *LocalCommitsController) signOffCommit(commits []*models.Commit, start, end int) error {
return self.c.WithWaitingStatusBlockingInput(types.WaitingStatusOpts{
Message: self.c.Tr.AmendingStatus,
HideWorkingTreeState: true,
}, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.SignOffCommit)
if err := self.c.Git().Rebase.SignOffCommit(commits, start, end); err != nil {
return err
}
self.c.RefreshFromWorker(types.RefreshOptions{})
return nil
})
}
func (self *LocalCommitsController) resetAuthor(commits []*models.Commit, start, end int) error {
return self.c.WithWaitingStatusBlockingInput(types.WaitingStatusOpts{
Message: self.c.Tr.AmendingStatus,

View file

@ -197,6 +197,8 @@ type TranslationSet struct {
ResetAuthorTooltip string
SetAuthor string
SetAuthorTooltip string
SignOffCommit string
SignOffCommitTooltip string
AddCoAuthor string
AmendCommitAttribute string
AmendCommitAttributeTooltip string
@ -1037,6 +1039,7 @@ type Actions struct {
ResetCommitAuthor string
SetCommitAuthor string
AddCommitCoAuthor string
SignOffCommit string
RevertCommit string
CreateFixupCommit string
SquashAllAboveFixupCommits string
@ -1355,9 +1358,11 @@ func EnglishTranslationSet() *TranslationSet {
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",
SignOffCommit: "Sign off",
SignOffCommitTooltip: "Add a Signed-off-by trailer to the commit.",
AddCoAuthor: "Add co-author",
AmendCommitAttribute: "Amend commit attribute",
AmendCommitAttributeTooltip: "Set/Reset commit author or set co-author.",
AmendCommitAttributeTooltip: "Set/Reset commit author, set co-author, or sign off the commit.",
SetAuthorPromptTitle: "Set author (must look like 'Name <Email>')",
AddCoAuthorPromptTitle: "Add co-author (must look like 'Name <Email>')",
AddCoAuthorTooltip: "Add co-author using the Github/Gitlab metadata Co-authored-by.",
@ -2154,6 +2159,7 @@ func EnglishTranslationSet() *TranslationSet {
ResetCommitAuthor: "Reset commit author",
SetCommitAuthor: "Set commit author",
AddCommitCoAuthor: "Add commit co-author",
SignOffCommit: "Sign off commit",
RevertCommit: "Revert commit",
CreateFixupCommit: "Create fixup commit",
SquashAllAboveFixupCommits: "Squash all above fixup commits",

View file

@ -0,0 +1,36 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SignOff = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Sign off a commit",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("initial commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("initial commit").IsSelected(),
).
Press(keys.Commits.ResetCommitAuthor).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Amend commit attribute")).
Select(Contains("Sign off")).
Confirm()
})
t.Views().Main().ContainsLines(
Equals(" initial commit"),
Equals(" "),
Equals(" Signed-off-by: CI <CI@example.com>"),
)
},
})

View file

@ -0,0 +1,83 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SignOffRange = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Sign off a range of commits",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("fourth commit")
shell.EmptyCommit("third commit")
shell.EmptyCommit("second commit")
shell.EmptyCommit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("first commit").IsSelected(),
Contains("second commit"),
Contains("third commit"),
Contains("fourth commit"),
).
SelectNextItem().
Press(keys.Universal.ToggleRangeSelect).
SelectNextItem().
SelectedLines(
Contains("second commit"),
Contains("third commit"),
).
Press(keys.Commits.ResetCommitAuthor).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Amend commit attribute")).
Select(Contains("Sign off")).
Confirm()
}).
PressEscape().
SelectNextItem().
SelectedLine(Contains("fourth commit"))
t.Views().Main().Content(
Contains("fourth commit").
DoesNotContain("Signed-off-by: CI <CI@example.com>"),
)
t.Views().Commits().
IsFocused().
SelectPreviousItem().
SelectedLine(Contains("third commit"))
t.Views().Main().ContainsLines(
Equals(" third commit"),
Equals(" "),
Equals(" Signed-off-by: CI <CI@example.com>"),
)
t.Views().Commits().
IsFocused().
SelectPreviousItem().
SelectedLine(Contains("second commit"))
t.Views().Main().ContainsLines(
Equals(" second commit"),
Equals(" "),
Equals(" Signed-off-by: CI <CI@example.com>"),
)
t.Views().Commits().
IsFocused().
SelectPreviousItem().
SelectedLine(Contains("first commit"))
t.Views().Main().Content(
Contains("first commit").
DoesNotContain("Signed-off-by: CI <CI@example.com>"),
)
},
})

View file

@ -158,6 +158,8 @@ var tests = []*components.IntegrationTest{
commit.Search,
commit.SetAuthor,
commit.SetAuthorRange,
commit.SignOff,
commit.SignOffRange,
commit.StageRangeOfLines,
commit.Staged,
commit.StagedWithoutHooks,

View file

@ -986,6 +986,20 @@
}
],
"default": "c"
},
"signOff": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string"
},
"type": "array"
}
],
"default": "s"
}
},
"additionalProperties": false,