Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20260321-072917.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: 'commit/submit/squash: Add AI message generation via spice.message.generator config option, activated with --fill. For branch submit, --fill and spice.message.autoFill only generate messages for new change requests; use --regenerate-message to rewrite an existing description. Scripts receive GS_OPERATION, GS_MESSAGE_KIND, and GS_MESSAGE_UPDATE environment variables, plus the full gs command as positional args.'
time: 2026-03-21T07:29:17.2847-04:00
41 changes: 40 additions & 1 deletion branch_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"encoding/json"
"errors"
"fmt"
"os"

"go.abhg.dev/gs/internal/cli"
"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/msggen"
"go.abhg.dev/gs/internal/scriptrun"
"go.abhg.dev/gs/internal/silog"
"go.abhg.dev/gs/internal/spice"
"go.abhg.dev/gs/internal/spice/state"
Expand All @@ -29,6 +32,7 @@ type branchCreateCmd struct {
Target string `short:"t" placeholder:"BRANCH" help:"Branch to create the new branch above/below"`

All bool `short:"a" help:"Automatically stage modified and deleted files"`
Fill bool `short:"c" negatable:"" config:"message.autoFill" help:"Fill the commit message using the configured message generator. Defaults to spice.message.autoFill."`
Message string `short:"m" xor:"commit-message-source" placeholder:"MSG" help:"Commit message"`
MessageFile string `short:"F" xor:"commit-message-source" placeholder:"FILE" help:"Read the commit message from the given file."`

Expand Down Expand Up @@ -100,6 +104,7 @@ func (*branchCreateCmd) Help() string {
func (cmd *branchCreateCmd) Run(
ctx context.Context,
log *silog.Logger,
cfg *spice.Config,
repo *git.Repository,
wt *git.Worktree,
store *state.Store,
Expand All @@ -111,6 +116,11 @@ func (cmd *branchCreateCmd) Run(
cmd.Commit = true
}

// If --fill is set, automatically enable commits.
if cmd.Fill {
cmd.Commit = true
}

if cmd.Name == "" && !cmd.Commit {
return errors.New("a branch name is required with --no-commit")
}
Expand Down Expand Up @@ -209,7 +219,7 @@ func (cmd *branchCreateCmd) Run(
)
branchAt := baseHash
if cmd.Commit {
commitHash, restore, err := cmd.commit(ctx, wt, baseName, log)
commitHash, restore, err := cmd.commit(ctx, cfg, wt, baseName, log)
if err != nil {
return err
}
Expand Down Expand Up @@ -337,10 +347,39 @@ func (cmd *branchCreateCmd) Run(
// the repository to its original state if an error occurs.
func (cmd *branchCreateCmd) commit(
ctx context.Context,
cfg *spice.Config,
wt *git.Worktree,
baseName string,
log *silog.Logger,
) (commitHash git.Hash, restore func() error, err error) {
// If --fill is set and no message was provided,
// try to generate one using the configured script.
if cmd.Fill && cmd.Message == "" {
script := cfg.MessageGenerator()
if script == "" {
return "", nil, msggen.ErrNoGenerator
}

branch := ""
if b, err := wt.CurrentBranch(ctx); err == nil {
branch = b
}
env := msggenEnv(scriptrun.OpBranchCreate, false, branch, baseName)
result, err := (&msggen.Runner{
Log: log,
Args: os.Args,
}).Run(
ctx, script, wt.RootDir(), env,
)
if err != nil {
log.Warn("Message generator failed, "+
"falling back to editor",
"error", err)
} else {
cmd.Message = result.Message()
}
}

// We'll need --allow-empty if there are no staged changes.
diff, err := wt.DiffIndex(ctx, "HEAD")
if err != nil {
Expand Down
43 changes: 42 additions & 1 deletion commit_amend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"context"
"errors"
"fmt"
"os"

"go.abhg.dev/gs/internal/cli"
"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/handler/restack"
"go.abhg.dev/gs/internal/msggen"
"go.abhg.dev/gs/internal/scriptrun"
"go.abhg.dev/gs/internal/silog"
"go.abhg.dev/gs/internal/spice"
"go.abhg.dev/gs/internal/spice/state"
Expand All @@ -20,6 +23,7 @@ type commitAmendCmd struct {

All bool `short:"a" help:"Stage all changes before committing."`
AllowEmpty bool `help:"Create a commit even if it contains no changes."`
Fill bool `short:"c" negatable:"" config:"message.autoFill" help:"Fill the commit message using the configured message updater. Defaults to spice.message.autoFill."`
Message string `short:"m" xor:"commit-message-source" placeholder:"MSG" help:"Use the given message as the commit message."`
MessageFile string `short:"F" xor:"commit-message-source" placeholder:"FILE" help:"Read the commit message from the given file."`

Expand Down Expand Up @@ -56,6 +60,7 @@ func (*commitAmendCmd) Help() string {
func (cmd *commitAmendCmd) Run(
ctx context.Context,
log *silog.Logger,
cfg *spice.Config,
view ui.View,
repo *git.Repository,
wt *git.Worktree,
Expand Down Expand Up @@ -128,7 +133,7 @@ func (cmd *commitAmendCmd) Run(
MessageFile: cmd.MessageFile,
Signoff: cmd.Signoff,
Commit: true,
}).Run(ctx, log, repo, wt, store, svc, restackHandler)
}).Run(ctx, log, cfg, repo, wt, store, svc, restackHandler)
}
}
}
Expand Down Expand Up @@ -218,6 +223,42 @@ func (cmd *commitAmendCmd) Run(
}
}

// If --fill is set and no message was provided,
// try to update the existing message
// using the configured script.
if cmd.Fill && cmd.Message == "" {
script := cfg.MessageGenerator()
if script == "" {
return msggen.ErrNoGenerator
}

var extras []string
existingMsg, err := repo.CommitMessageFull(
ctx, "HEAD",
)
if err == nil {
extras = append(extras,
"GS_MESSAGE="+existingMsg,
)
}
env := commitEnv(ctx, wt, scriptrun.OpCommitAmend, true, extras...)

result, err := (&msggen.Runner{
Log: log,
Args: os.Args,
}).Run(
ctx, script, wt.RootDir(), env,
)
if err != nil {
log.Warn("Message generator failed, "+
"falling back to editor",
"error", err)
} else {
cmd.Message = result.Message()
cmd.NoEdit = true
}
}

if err := wt.Commit(ctx, git.CommitRequest{
Message: cmd.Message,
MessageFile: cmd.MessageFile,
Expand Down
30 changes: 30 additions & 0 deletions commit_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import (
"context"
"errors"
"fmt"
"os"

"go.abhg.dev/gs/internal/cli"
"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/handler/restack"
"go.abhg.dev/gs/internal/msggen"
"go.abhg.dev/gs/internal/scriptrun"
"go.abhg.dev/gs/internal/silog"
"go.abhg.dev/gs/internal/spice"
"go.abhg.dev/gs/internal/text"
)

type commitCreateCmd struct {
All bool `short:"a" help:"Stage all changes before committing."`
AllowEmpty bool `help:"Create a new commit even if it contains no changes."`
Fill bool `short:"c" negatable:"" config:"message.autoFill" help:"Fill the commit message using the configured message generator. Defaults to spice.message.autoFill."`
Fixup string `help:"Create a fixup commit. See also 'git-spice commit fixup'." placeholder:"COMMIT"`
Message string `short:"m" xor:"commit-message-source" placeholder:"MSG" help:"Use the given message as the commit message."`
MessageFile string `short:"F" xor:"commit-message-source" placeholder:"FILE" help:"Read the commit message from the given file."`
Expand Down Expand Up @@ -47,9 +52,34 @@ func (*commitCreateCmd) Help() string {
func (cmd *commitCreateCmd) Run(
ctx context.Context,
log *silog.Logger,
cfg *spice.Config,
wt *git.Worktree,
restackHandler RestackHandler,
) error {
// If --fill is set and no message was provided,
// try to generate one using the configured script.
if cmd.Fill && cmd.Message == "" {
script := cfg.MessageGenerator()
if script == "" {
return msggen.ErrNoGenerator
}

result, err := (&msggen.Runner{
Log: log,
Args: os.Args,
}).Run(
ctx, script, wt.RootDir(),
commitEnv(ctx, wt, scriptrun.OpCommitCreate, false),
)
if err != nil {
log.Warn("Message generator failed, "+
"falling back to editor",
"error", err)
} else {
cmd.Message = result.Message()
}
}

if err := wt.Commit(ctx, git.CommitRequest{
Message: cmd.Message,
MessageFile: cmd.MessageFile,
Expand Down
46 changes: 46 additions & 0 deletions commit_fill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"context"
"strconv"

"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/scriptrun"
)

// msggenEnv builds the base environment for a message-generation
// script. It layers the message-specific GS_MESSAGE_* variables on
// top of the shared GS_OPERATION / GS_BRANCH / GS_BASE env that every
// gs-driven script receives.
//
// kind names the operation (one of [scriptrun.Operation]). update
// indicates whether this is an update of an existing message (e.g.
// 'gs commit amend --fill', 'gs branch submit --fill' updating an
// existing CR). branch is the active branch when known; base is the
// branch's base when applicable. extras are appended verbatim.
func msggenEnv(
op scriptrun.Operation,
update bool,
branch, base string,
extras ...string,
) []string {
env := scriptrun.EnvFor(op, branch, base)
env = append(env, "GS_MESSAGE_UPDATE="+strconv.FormatBool(update))
return append(env, extras...)
}

// commitEnv builds the environment for a commit-message script.
// update is true when amending an existing message.
func commitEnv(
ctx context.Context,
wt *git.Worktree,
op scriptrun.Operation,
update bool,
extras ...string,
) []string {
branch := ""
if b, err := wt.CurrentBranch(ctx); err == nil {
branch = b
}
return msggenEnv(op, update, branch, "", extras...)
}
Loading
Loading