Enhance support for GPG signed tags (#4394)

- **PR Description**

When using gpg signing on tags via the git config `tag.gpgSign=true`, it
is not possible to do a lightweight tag. You must do an annotated tag
(Which is allowed to have an empty description).

This PR augments the existing `WithGpgHandling` helper to allow it to
specifically target commit signing and tag signing. It then uses that
handler, and creates an annotated tag, when the git config demands tag
GPG signing. By default it will launch the tag signing in a new
subprocess. This does require that users click an extra `<enter>` after
creating a signed tag, but this is currently the behavior for signed
commits, so I don't feel too bad about that. If they want, they can use
the LazyGit configuration option of `git.overrideGpg: true` to promise
to LazyGit that they do not need a GPG sub-process (because they
reliably already have gpg-agent or similar running in the background).
This again matches the current behavior for GPG signed commits.

This has no integration test because we don't have the machinery set up
in place to set up a GPG key inside of our integration test framework.

Fixes https://github.com/jesseduffield/lazygit/issues/2955
This commit is contained in:
Stefan Haller 2025-03-24 08:43:41 +01:00 committed by GitHub
commit 7d26a751bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 69 additions and 46 deletions

View file

@ -57,15 +57,31 @@ func (self *ConfigCommands) GetPager(width int) string {
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
// UsingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (self *ConfigCommands) UsingGpg() bool {
type GpgConfigKey string
const (
CommitGpgSign GpgConfigKey = "commit.gpgSign"
TagGpgSign GpgConfigKey = "tag.gpgSign"
)
// NeedsGpgSubprocess tells us whether the user has gpg enabled for the specified action type
// and needs a subprocess because they have a process where they manually
// enter their password every time a GPG action is taken
func (self *ConfigCommands) NeedsGpgSubprocess(key GpgConfigKey) bool {
overrideGpg := self.UserConfig().Git.OverrideGpg
if overrideGpg {
return false
}
return self.gitConfig.GetBool("commit.gpgsign")
return self.gitConfig.GetBool(string(key))
}
func (self *ConfigCommands) NeedsGpgSubprocessForCommit() bool {
return self.NeedsGpgSubprocess(CommitGpgSign)
}
func (self *ConfigCommands) GetGpgTagSign() bool {
return self.gitConfig.GetBool(string(TagGpgSign))
}
func (self *ConfigCommands) GetCoreEditor() string {

View file

@ -150,7 +150,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if self.config.UsingGpg() {
if self.config.NeedsGpgSubprocessForCommit() {
return errors.New(self.Tr.DisabledForGPG)
}

View file

@ -35,7 +35,7 @@ func NewRebaseCommands(
}
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, summary string, description string) error {
if self.config.UsingGpg() {
if self.config.NeedsGpgSubprocessForCommit() {
return errors.New(self.Tr.DisabledForGPG)
}
@ -413,7 +413,7 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommitRange(
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if self.config.UsingGpg() {
if self.config.NeedsGpgSubprocessForCommit() {
return errors.New(self.Tr.DisabledForGPG)
}

View file

@ -128,7 +128,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
},
{
testName: "returns error when using gpg",
gitConfigMockResponses: map[string]string{"commit.gpgsign": "true"},
gitConfigMockResponses: map[string]string{"commit.gpgSign": "true"},
commits: []*models.Commit{{Name: "commit", Hash: "123456"}},
commitIndex: 0,
fileName: []string{"test999.txt"},

View file

@ -1,6 +1,9 @@
package git_commands
import "github.com/jesseduffield/gocui"
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
type TagCommands struct {
*GitCommon
@ -12,24 +15,24 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
}
}
func (self *TagCommands) CreateLightweight(tagName string, ref string, force bool) error {
func (self *TagCommands) CreateLightweightObj(tagName string, ref string, force bool) oscommands.ICmdObj {
cmdArgs := NewGitCmd("tag").
ArgIf(force, "--force").
Arg("--", tagName).
ArgIf(len(ref) > 0, ref).
ToArgv()
return self.cmd.New(cmdArgs).Run()
return self.cmd.New(cmdArgs)
}
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string, force bool) error {
func (self *TagCommands) CreateAnnotatedObj(tagName, ref, msg string, force bool) oscommands.ICmdObj {
cmdArgs := NewGitCmd("tag").Arg(tagName).
ArgIf(force, "--force").
ArgIf(len(ref) > 0, ref).
Arg("-m", msg).
ToArgv()
return self.cmd.New(cmdArgs).Run()
return self.cmd.New(cmdArgs)
}
func (self *TagCommands) HasTag(tagName string) bool {

View file

@ -106,7 +106,7 @@ func (gui *Gui) resetHelpersAndControllers() {
Suggestions: suggestionsHelper,
Files: helpers.NewFilesHelper(helperCommon),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper),
Tags: helpers.NewTagsHelper(helperCommon, commitsHelper),
Tags: helpers.NewTagsHelper(helperCommon, commitsHelper, gpgHelper),
BranchesHelper: helpers.NewBranchesHelper(helperCommon, worktreeHelper),
GPG: helpers.NewGpgHelper(helperCommon),
MergeAndRebase: rebaseHelper,

View file

@ -1,5 +1,7 @@
package helpers
import "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
type AmendHelper struct {
c *HelperCommon
gpg *GpgHelper
@ -18,5 +20,5 @@ func NewAmendHelper(
func (self *AmendHelper) AmendHead() error {
cmdObj := self.c.Git().Commit.AmendHeadCmdObj()
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
return self.gpg.WithGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil)
return self.gpg.WithGpgHandling(cmdObj, git_commands.CommitGpgSign, self.c.Tr.AmendingStatus, nil, nil)
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
@ -22,8 +23,8 @@ func NewGpgHelper(c *HelperCommon) *GpgHelper {
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
// fix this bug, or just stop running subprocesses from within there, given that
// we don't need to see a loading status if we're in a subprocess.
func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
useSubprocess := self.c.Git().Config.UsingGpg()
func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, configKey git_commands.GpgConfigKey, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
useSubprocess := self.c.Git().Config.NeedsGpgSubprocess(configKey)
if useSubprocess {
success, err := self.c.RunSubprocess(cmdObj)
if success && onSuccess != nil {
@ -31,20 +32,20 @@ func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus
return err
}
}
if err := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
if err := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: refreshScope}); err != nil {
return err
}
return err
} else {
return self.runAndStream(cmdObj, waitingStatus, onSuccess)
return self.runAndStream(cmdObj, waitingStatus, onSuccess, refreshScope)
}
}
func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error, refreshScope []types.RefreshableView) error {
return self.c.WithWaitingStatus(waitingStatus, func(gocui.Task) error {
if err := cmdObj.StreamOutput().Run(); err != nil {
_ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
_ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: refreshScope})
return fmt.Errorf(
self.c.Tr.GitCommandFailed, self.c.UserConfig().Keybinding.Universal.ExtrasMenu,
)
@ -56,6 +57,6 @@ func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus str
}
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: refreshScope})
})
}

View file

@ -1,7 +1,8 @@
package helpers
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
@ -10,36 +11,32 @@ import (
type TagsHelper struct {
c *HelperCommon
commitsHelper *CommitsHelper
gpg *GpgHelper
}
func NewTagsHelper(c *HelperCommon, commitsHelper *CommitsHelper) *TagsHelper {
func NewTagsHelper(c *HelperCommon, commitsHelper *CommitsHelper, gpg *GpgHelper) *TagsHelper {
return &TagsHelper{
c: c,
commitsHelper: commitsHelper,
gpg: gpg,
}
}
func (self *TagsHelper) OpenCreateTagPrompt(ref string, onCreate func()) error {
doCreateTag := func(tagName string, description string, force bool) error {
return self.c.WithWaitingStatus(self.c.Tr.CreatingTag, func(gocui.Task) error {
if description != "" {
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, description, force); err != nil {
return err
}
} else {
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
if err := self.c.Git().Tag.CreateLightweight(tagName, ref, force); err != nil {
return err
}
}
var command oscommands.ICmdObj
if description != "" || self.c.Git().Config.GetGpgTagSign() {
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
command = self.c.Git().Tag.CreateAnnotatedObj(tagName, ref, description, force)
} else {
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
command = self.c.Git().Tag.CreateLightweightObj(tagName, ref, force)
}
return self.gpg.WithGpgHandling(command, git_commands.TagGpgSign, self.c.Tr.CreatingTag, func() error {
self.commitsHelper.OnCommitSuccess()
return self.c.Refresh(types.RefreshOptions{
Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS},
})
})
return nil
}, []types.RefreshableView{types.COMMITS, types.TAGS})
}
onConfirm := func(tagName string, description string) error {

View file

@ -5,6 +5,7 @@ import (
"fmt"
"regexp"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/context"
@ -111,10 +112,11 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin
func (self *WorkingTreeHelper) handleCommit(summary string, description string, forceSkipHooks bool) error {
cmdObj := self.c.Git().Commit.CommitCmdObj(summary, description, forceSkipHooks)
self.c.LogAction(self.c.Tr.Actions.Commit)
return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
self.commitsHelper.OnCommitSuccess()
return nil
})
return self.gpgHelper.WithGpgHandling(cmdObj, git_commands.CommitGpgSign, self.c.Tr.CommittingStatus,
func() error {
self.commitsHelper.OnCommitSuccess()
return nil
}, nil)
}
func (self *WorkingTreeHelper) switchFromCommitMessagePanelToEditor(filepath string, forceSkipHooks bool) error {

View file

@ -6,6 +6,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/context"
@ -412,7 +413,8 @@ func (self *LocalCommitsController) handleReword(summary string, description str
if models.IsHeadCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx()) {
// we've selected the top commit so no rebase is required
return self.c.Helpers().GPG.WithGpgHandling(self.c.Git().Commit.RewordLastCommit(summary, description),
self.c.Tr.RewordingStatus, nil)
git_commands.CommitGpgSign,
self.c.Tr.RewordingStatus, nil, nil)
}
return self.c.WithWaitingStatus(self.c.Tr.RewordingStatus, func(gocui.Task) error {