Skip to content
Closed
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.messageGenerator config option, activated with --fill. Scripts receive 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
38 changes: 37 additions & 1 deletion branch_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ 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/silog"
"go.abhg.dev/gs/internal/spice"
"go.abhg.dev/gs/internal/spice/state"
Expand All @@ -29,6 +31,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" help:"Fill the commit message using the configured message generator."`
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 +103,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 +115,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 +218,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 +346,37 @@ 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
}

env := msggenEnv("commit", false,
"GS_BASE="+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
42 changes: 41 additions & 1 deletion commit_amend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ 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/silog"
"go.abhg.dev/gs/internal/spice"
"go.abhg.dev/gs/internal/spice/state"
Expand All @@ -20,6 +22,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" help:"Fill the commit message using the configured message updater."`
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 +59,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 +132,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 @@ -185,6 +189,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, 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
29 changes: 29 additions & 0 deletions commit_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ 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/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" help:"Fill the commit message using the configured message generator."`
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 +51,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, 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
42 changes: 42 additions & 0 deletions commit_fill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"context"
"strconv"

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

// msggenEnv builds the base environment variables
// for message generator scripts.
// kind is the message kind (e.g. "commit" or "branch").
// update indicates whether this is an update
// of an existing message.
// extras are appended as additional environment variables.
func msggenEnv(
kind string, update bool, extras ...string,
) []string {
env := make([]string, 0, 2+len(extras))
env = append(env,
"GS_MESSAGE_KIND="+kind,
"GS_MESSAGE_UPDATE="+strconv.FormatBool(update),
)
return append(env, extras...)
}

// commitEnv builds environment variables
// for commit message scripts.
// It calls msggenEnv and appends GS_BRANCH
// when available.
func commitEnv(
ctx context.Context,
wt *git.Worktree,
update bool,
extras ...string,
) []string {
env := msggenEnv("commit", update, extras...)
if branch, err := wt.CurrentBranch(ctx); err == nil {
env = append(env, "GS_BRANCH="+branch)
}
return env
}
4 changes: 4 additions & 0 deletions doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ target (A) to the specified branch:
* `--below`: Place the branch below the target branch and restack its upstack
* `-t`, `--target=BRANCH`: Branch to create the new branch above/below
* `-a`, `--all`: Automatically stage modified and deleted files
* `-c`, `--fill`: Fill the commit message using the configured message generator.
* `-m`, `--message=MSG`: Commit message
* `-F`, `--message-file=FILE`: Read the commit message from the given file.
* `--no-verify`: Bypass pre-commit and commit-msg hooks.
Expand Down Expand Up @@ -855,6 +856,7 @@ to specify a commit message without editing.

**Flags**

* `-c`, `--fill`: Fill the commit message using the configured message generator.
* `--no-verify`: Bypass pre-commit and commit-msg hooks.
* `--no-edit`: Do not open an editor to edit the squashed commit message. Only applicable if --message is not used. <span class="mdx-badge"><span class="mdx-badge__icon">:material-tag:{ title="Released in version" }</span><span class="mdx-badge__text">[v0.16.0](/changelog.md#v0.16.0)</span>
* `-m`, `--message=MSG`: Use the given message as the commit message.
Expand Down Expand Up @@ -1070,6 +1072,7 @@ when you want to apply changes to an older commit.

* `-a`, `--all`: Stage all changes before committing.
* `--allow-empty`: Create a new commit even if it contains no changes.
* `-c`, `--fill`: Fill the commit message using the configured message generator.
* `--fixup=COMMIT`: Create a fixup commit. See also 'git-spice commit fixup'.
* `-m`, `--message=MSG`: Use the given message as the commit message.
* `-F`, `--message-file=FILE`: Read the commit message from the given file.
Expand Down Expand Up @@ -1110,6 +1113,7 @@ The --no-prompt flag can be used to skip this prompt in scripts.

* `-a`, `--all`: Stage all changes before committing.
* `--allow-empty`: Create a commit even if it contains no changes.
* `-c`, `--fill`: Fill the commit message using the configured message updater.
* `-m`, `--message=MSG`: Use the given message as the commit message.
* `-F`, `--message-file=FILE`: Read the commit message from the given file.
* `--no-edit`: Don't edit the commit message
Expand Down
1 change: 1 addition & 0 deletions doc/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ nav:
- guide/concepts.md
- guide/branch.md
- guide/cr.md
- guide/ai-messages.md
- guide/limits.md
- guide/troubleshooting.md
- guide/internals.md
Expand Down
43 changes: 43 additions & 0 deletions doc/src/cli/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,49 @@ The maximum length of branch names which are automatically generated by $$gs bra

- Any integer (defaults to 32)

### spice.messageGenerator

<!-- gs:version unreleased -->

Command to generate or update messages.
When set and `--fill` is passed to a supported command,
this script runs to produce the message.

Supported commands:
$$gs commit create$$, $$gs commit amend$$,
$$gs branch create$$, $$gs branch submit$$,
and $$gs branch squash$$.

The command runs in the repository root
and receives the following environment variables:

- `GS_MESSAGE_KIND`: `commit` or `branch`
- `GS_MESSAGE_UPDATE`: `true` if updating, `false` if new
- `GS_BRANCH`: current or submitting branch name
- `GS_BASE`: base branch name (when applicable)
- `GS_MESSAGE`: existing commit message
(commit amend, squash)
- `GS_TITLE`: existing CR title (branch updater)
- `GS_BODY`: existing CR body (branch updater)

The invoking process's argument vector
is forwarded to the script as positional parameters.

For commit messages,
the output is used as the full commit message.
For branch messages (CR title and body),
the first line of output becomes the title
and everything after the first blank line becomes the body.

If the command fails or produces empty output,
git-spice falls back to the default behavior
(opening the editor or using commit messages).

If the value starts with `#!`,
it is written to a temporary file and executed
with the interpreter specified in the shebang line.
Otherwise, it is passed to `sh -c`.

### spice.commit.signoff

<!-- gs:version v0.20.0 -->
Expand Down
Loading