mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Merge 3343a3a364 into d0ede21e9c
This commit is contained in:
commit
d961b2e130
|
|
@ -405,6 +405,13 @@ git:
|
|||
# If autoWrapCommitMessage is true, the width to wrap to
|
||||
autoWrapWidth: 72
|
||||
|
||||
# Shell command that generates a commit message from a diff piped to stdin.
|
||||
# The command's stdout is used as the commit message (markdown fences are
|
||||
# stripped automatically).
|
||||
# Accessible via the commit menu (<c-o> then 'g').
|
||||
# Example: "claude -p 'Generate a conventional commit message for this diff:'"
|
||||
generateCommand: ""
|
||||
|
||||
# Config relating to merging
|
||||
merging:
|
||||
# If true, run merges in a subprocess so that if a commit message is required,
|
||||
|
|
|
|||
|
|
@ -397,6 +397,11 @@ type CommitConfig struct {
|
|||
AutoWrapCommitMessage bool `yaml:"autoWrapCommitMessage"`
|
||||
// If autoWrapCommitMessage is true, the width to wrap to
|
||||
AutoWrapWidth int `yaml:"autoWrapWidth"`
|
||||
// Shell command that generates a commit message from a diff piped to stdin.
|
||||
// The command's stdout is used as the commit message (markdown fences are stripped automatically).
|
||||
// Accessible via the commit menu (<c-o> then 'g').
|
||||
// Example: "claude -p 'Generate a conventional commit message for this diff:'"
|
||||
GenerateCommand string `yaml:"generateCommand"`
|
||||
}
|
||||
|
||||
type MergingConfig struct {
|
||||
|
|
|
|||
|
|
@ -174,6 +174,10 @@ func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, e
|
|||
}
|
||||
|
||||
func (self *CommitMessageController) confirm() error {
|
||||
if self.c.Helpers().Commits.IsGenerating() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The default keybinding for this action is "<enter>", which means that we
|
||||
// also get here when pasting multi-line text that contains newlines. In
|
||||
// that case we don't want to confirm the commit, but switch to the
|
||||
|
|
@ -192,6 +196,10 @@ func (self *CommitMessageController) confirm() error {
|
|||
}
|
||||
|
||||
func (self *CommitMessageController) close() error {
|
||||
if self.c.Helpers().Commits.IsGenerating() {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.Helpers().Commits.CloseCommitMessagePanel()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
|
|
@ -20,6 +21,13 @@ type CommitsHelper struct {
|
|||
getCommitDescription func() string
|
||||
getUnwrappedCommitDescription func() string
|
||||
setCommitDescription func(string)
|
||||
|
||||
// set to 1 while commit message generation is in progress
|
||||
generating atomic.Int32
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) IsGenerating() bool {
|
||||
return self.generating.Load() == 1
|
||||
}
|
||||
|
||||
func NewCommitsHelper(
|
||||
|
|
@ -222,6 +230,13 @@ func (self *CommitsHelper) OpenCommitMenu(suggestionFunc func(string) []*types.S
|
|||
}
|
||||
}
|
||||
|
||||
var disabledReasonForGenerate *types.DisabledReason
|
||||
if self.c.UserConfig().Git.Commit.GenerateCommand == "" {
|
||||
disabledReasonForGenerate = &types.DisabledReason{
|
||||
Text: self.c.Tr.NoGenerateCommandConfigured,
|
||||
}
|
||||
}
|
||||
|
||||
menuItems := []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.OpenInEditor,
|
||||
|
|
@ -245,6 +260,14 @@ func (self *CommitsHelper) OpenCommitMenu(suggestionFunc func(string) []*types.S
|
|||
},
|
||||
Keys: menuKey('p'),
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.GenerateCommitMessage,
|
||||
OnPress: func() error {
|
||||
return self.generateCommitMessage()
|
||||
},
|
||||
Key: 'g',
|
||||
DisabledReason: disabledReasonForGenerate,
|
||||
},
|
||||
}
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.CommitMenuTitle,
|
||||
|
|
@ -286,3 +309,70 @@ func (self *CommitsHelper) pasteCommitMessageFromClipboard() error {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) generateCommitMessage() error {
|
||||
self.generating.Store(1)
|
||||
self.c.Views().CommitMessage.Editable = false
|
||||
self.c.Views().CommitDescription.Editable = false
|
||||
originalTitle := self.c.Views().CommitMessage.Title
|
||||
self.c.Views().CommitMessage.Title = self.c.Tr.GeneratingCommitMessageStatus
|
||||
|
||||
restore := func() {
|
||||
self.c.OnUIThread(func() error {
|
||||
self.generating.Store(0)
|
||||
self.c.Views().CommitMessage.Editable = true
|
||||
self.c.Views().CommitDescription.Editable = true
|
||||
self.c.Views().CommitMessage.Title = originalTitle
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return self.c.WithWaitingStatus(self.c.Tr.GeneratingCommitMessageStatus, func(_ gocui.Task) error {
|
||||
defer restore()
|
||||
|
||||
diff, err := self.c.Git().Diff.GetDiff(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
self.c.OnUIThread(func() error {
|
||||
self.c.ErrorToast(self.c.Tr.NoStagedChangesForGenerate)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
command := self.c.UserConfig().Git.Commit.GenerateCommand
|
||||
message, err := self.c.OS().Cmd.NewShell(command, "").SetStdin(diff).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message = parseGenerateOutput(message)
|
||||
if message == "" {
|
||||
return errors.New("generate command returned an empty commit message")
|
||||
}
|
||||
|
||||
self.c.OnUIThread(func() error {
|
||||
self.SetMessageAndDescriptionInView(message)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// parseGenerateOutput strips markdown code fences and any preamble before them.
|
||||
func parseGenerateOutput(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if start := strings.Index(s, "```"); start >= 0 {
|
||||
rest := s[start:]
|
||||
if idx := strings.Index(rest, "\n"); idx >= 0 {
|
||||
rest = rest[idx+1:]
|
||||
}
|
||||
if idx := strings.LastIndex(rest, "```"); idx >= 0 {
|
||||
rest = rest[:idx]
|
||||
}
|
||||
s = strings.TrimSpace(rest)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,50 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseAIOutput(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "plain text unchanged",
|
||||
input: "feat: add login button",
|
||||
expected: "feat: add login button",
|
||||
},
|
||||
{
|
||||
name: "strips surrounding whitespace",
|
||||
input: " feat: add login button ",
|
||||
expected: "feat: add login button",
|
||||
},
|
||||
{
|
||||
name: "strips plain code fence",
|
||||
input: "```\nfeat: add login button\n```",
|
||||
expected: "feat: add login button",
|
||||
},
|
||||
{
|
||||
name: "strips fenced block with language tag",
|
||||
input: "```text\nfeat: add login button\n```",
|
||||
expected: "feat: add login button",
|
||||
},
|
||||
{
|
||||
name: "strips fenced block with surrounding text",
|
||||
input: "Here is your commit:\n```\nfeat: add login button\n```\nDone.",
|
||||
expected: "feat: add login button",
|
||||
},
|
||||
{
|
||||
name: "multiline commit message in fence",
|
||||
input: "```\nfeat: add login button\n\nCloses #123\n```",
|
||||
expected: "feat: add login button\n\nCloses #123",
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, parseGenerateOutput(s.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryRemoveHardLineBreaks(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -698,6 +698,10 @@ type TranslationSet struct {
|
|||
CommitURL string
|
||||
PasteCommitMessageFromClipboard string
|
||||
SurePasteCommitMessage string
|
||||
GenerateCommitMessage string
|
||||
GeneratingCommitMessageStatus string
|
||||
NoStagedChangesForGenerate string
|
||||
NoGenerateCommandConfigured string
|
||||
CommitMessage string
|
||||
CommitMessageBody string
|
||||
CommitSubject string
|
||||
|
|
@ -1861,6 +1865,10 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
CommitURL: "Commit URL",
|
||||
PasteCommitMessageFromClipboard: "Paste commit message from clipboard",
|
||||
SurePasteCommitMessage: "Pasting will overwrite the current commit message, continue?",
|
||||
GenerateCommitMessage: "Generate commit message",
|
||||
GeneratingCommitMessageStatus: "Generating commit message...",
|
||||
NoStagedChangesForGenerate: "No staged changes to generate a commit message from",
|
||||
NoGenerateCommandConfigured: "No generate command configured. Set git.commit.generateCommand in your config",
|
||||
CommitMessage: "Commit message (subject and body)",
|
||||
CommitMessageBody: "Commit message body",
|
||||
CommitSubject: "Commit subject",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@
|
|||
"type": "integer",
|
||||
"description": "If autoWrapCommitMessage is true, the width to wrap to",
|
||||
"default": 72
|
||||
},
|
||||
"generateCommand": {
|
||||
"type": "string",
|
||||
"description": "Shell command that generates a commit message from a diff piped to stdin.\nThe command's stdout is used as the commit message (markdown fences are stripped automatically).\nAccessible via the commit menu (\u003cc-o\u003e then 'g').\nExample: \"claude -p 'Generate a conventional commit message for this diff:'\""
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
|
|
|||
Loading…
Reference in a new issue