diff --git a/.changes/unreleased/Added-20260513-113740.yaml b/.changes/unreleased/Added-20260513-113740.yaml new file mode 100644 index 000000000..b5fb1e582 --- /dev/null +++ b/.changes/unreleased/Added-20260513-113740.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Add --fill-from parameter to upstack, downstack, and stack commands to allow autofilling of CR titles/bodies when submitting whole stacks. +time: 2026-05-13T11:37:40.724695-07:00 diff --git a/doc/includes/cli-reference.md b/doc/includes/cli-reference.md index c1a50066e..6f0e45652 100644 --- a/doc/includes/cli-reference.md +++ b/doc/includes/cli-reference.md @@ -277,6 +277,7 @@ only if there are multiple CRs in the stack. * `-r`, `--reviewer=REVIEWER,...`: Add reviewers to the change request. Pass multiple times or separate with commas. :material-tag:{ title="Released in version" }[v0.21.0](/changelog.md#v0.21.0) * `-a`, `--assign=ASSIGNEE,...`: Assign the change request to these users. Pass multiple times or separate with commas. :material-tag:{ title="Released in version" }[v0.21.0](/changelog.md#v0.21.0) * `--no-web`: Alias for --web=false. +* `--fill-from=FILE`: JSON file mapping branch names to {title, body} for CR creation. Use '-' for stdin. **Configuration**: [spice.submit.assignees](/cli/config.md#spicesubmitassignees), [spice.submit.draft](/cli/config.md#spicesubmitdraft), [spice.submit.label](/cli/config.md#spicesubmitlabel), [spice.submit.label.addWhen](/cli/config.md#spicesubmitlabeladdwhen), [spice.submit.listTemplatesTimeout](/cli/config.md#spicesubmitlisttemplatestimeout), [spice.submit.navigationComment](/cli/config.md#spicesubmitnavigationcomment), [spice.submit.navigationComment.downstack](/cli/config.md#spicesubmitnavigationcommentdownstack), [spice.submit.navigationCommentStyle.marker](/cli/config.md#spicesubmitnavigationcommentstylemarker), [spice.submit.navigationCommentSync](/cli/config.md#spicesubmitnavigationcommentsync), [spice.submit.publish](/cli/config.md#spicesubmitpublish), [spice.submit.reviewers](/cli/config.md#spicesubmitreviewers), [spice.submit.reviewers.addWhen](/cli/config.md#spicesubmitreviewersaddwhen), [spice.submit.skipRestackCheck](/cli/config.md#spicesubmitskiprestackcheck), [spice.submit.template](/cli/config.md#spicesubmittemplate), [spice.submit.updateOnly](/cli/config.md#spicesubmitupdateonly), [spice.submit.web](/cli/config.md#spicesubmitweb) @@ -400,6 +401,7 @@ only if there are multiple CRs in the stack. * `-a`, `--assign=ASSIGNEE,...`: Assign the change request to these users. Pass multiple times or separate with commas. :material-tag:{ title="Released in version" }[v0.21.0](/changelog.md#v0.21.0) * `--no-web`: Alias for --web=false. * `--branch=NAME`: Branch to start at +* `--fill-from=FILE`: JSON file mapping branch names to {title, body} for CR creation. Use '-' for stdin. **Configuration**: [spice.submit.assignees](/cli/config.md#spicesubmitassignees), [spice.submit.draft](/cli/config.md#spicesubmitdraft), [spice.submit.label](/cli/config.md#spicesubmitlabel), [spice.submit.label.addWhen](/cli/config.md#spicesubmitlabeladdwhen), [spice.submit.listTemplatesTimeout](/cli/config.md#spicesubmitlisttemplatestimeout), [spice.submit.navigationComment](/cli/config.md#spicesubmitnavigationcomment), [spice.submit.navigationComment.downstack](/cli/config.md#spicesubmitnavigationcommentdownstack), [spice.submit.navigationCommentStyle.marker](/cli/config.md#spicesubmitnavigationcommentstylemarker), [spice.submit.navigationCommentSync](/cli/config.md#spicesubmitnavigationcommentsync), [spice.submit.publish](/cli/config.md#spicesubmitpublish), [spice.submit.reviewers](/cli/config.md#spicesubmitreviewers), [spice.submit.reviewers.addWhen](/cli/config.md#spicesubmitreviewersaddwhen), [spice.submit.skipRestackCheck](/cli/config.md#spicesubmitskiprestackcheck), [spice.submit.template](/cli/config.md#spicesubmittemplate), [spice.submit.updateOnly](/cli/config.md#spicesubmitupdateonly), [spice.submit.web](/cli/config.md#spicesubmitweb) @@ -555,6 +557,7 @@ only if there are multiple CRs in the stack. * `-a`, `--assign=ASSIGNEE,...`: Assign the change request to these users. Pass multiple times or separate with commas. :material-tag:{ title="Released in version" }[v0.21.0](/changelog.md#v0.21.0) * `--no-web`: Alias for --web=false. * `--branch=NAME`: Branch to start at +* `--fill-from=FILE`: JSON file mapping branch names to {title, body} for CR creation. Use '-' for stdin. **Configuration**: [spice.submit.assignees](/cli/config.md#spicesubmitassignees), [spice.submit.draft](/cli/config.md#spicesubmitdraft), [spice.submit.label](/cli/config.md#spicesubmitlabel), [spice.submit.label.addWhen](/cli/config.md#spicesubmitlabeladdwhen), [spice.submit.listTemplatesTimeout](/cli/config.md#spicesubmitlisttemplatestimeout), [spice.submit.navigationComment](/cli/config.md#spicesubmitnavigationcomment), [spice.submit.navigationComment.downstack](/cli/config.md#spicesubmitnavigationcommentdownstack), [spice.submit.navigationCommentStyle.marker](/cli/config.md#spicesubmitnavigationcommentstylemarker), [spice.submit.navigationCommentSync](/cli/config.md#spicesubmitnavigationcommentsync), [spice.submit.publish](/cli/config.md#spicesubmitpublish), [spice.submit.reviewers](/cli/config.md#spicesubmitreviewers), [spice.submit.reviewers.addWhen](/cli/config.md#spicesubmitreviewersaddwhen), [spice.submit.skipRestackCheck](/cli/config.md#spicesubmitskiprestackcheck), [spice.submit.template](/cli/config.md#spicesubmittemplate), [spice.submit.updateOnly](/cli/config.md#spicesubmitupdateonly), [spice.submit.web](/cli/config.md#spicesubmitweb) diff --git a/downstack_submit.go b/downstack_submit.go index fc8fc7d61..8fe378a0e 100644 --- a/downstack_submit.go +++ b/downstack_submit.go @@ -19,6 +19,8 @@ type downstackSubmitCmd struct { submit.BatchOptions Branch string `placeholder:"NAME" help:"Branch to start at" predictor:"trackedBranches"` + + FillFrom string `name:"fill-from" help:"JSON file mapping branch names to {title, body} for CR creation. Use '-' for stdin." placeholder:"FILE"` } func (*downstackSubmitCmd) Help() string { @@ -57,6 +59,14 @@ func (cmd *downstackSubmitCmd) Run( // TODO: separate preparation of the stack from submission + if cmd.FillFrom != "" { + meta, err := parseFillFrom(cmd.FillFrom) + if err != nil { + return fmt.Errorf("--fill-from: %w", err) + } + cmd.BranchMetadata = meta + } + return submitHandler.SubmitBatch(ctx, &submit.BatchRequest{ Branches: downstacks, Options: &cmd.Options, diff --git a/fill_from.go b/fill_from.go new file mode 100644 index 000000000..155b537cd --- /dev/null +++ b/fill_from.go @@ -0,0 +1,32 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "go.abhg.dev/gs/internal/handler/submit" +) + +// parseFillFrom reads per-branch CR metadata from a file or stdin. +// If path is "-", it reads from stdin. +// The expected format is a JSON object mapping branch names +// to objects with "title" and "body" fields. +func parseFillFrom(path string) (map[string]submit.BranchMeta, error) { + var data []byte + var err error + if path == "-" { + data, err = io.ReadAll(os.Stdin) + } else { + data, err = os.ReadFile(path) + } + if err != nil { + return nil, fmt.Errorf("read: %w", err) + } + var meta map[string]submit.BranchMeta + if err := json.Unmarshal(data, &meta); err != nil { + return nil, fmt.Errorf("parse JSON: %w", err) + } + return meta, nil +} diff --git a/internal/handler/submit/handler.go b/internal/handler/submit/handler.go index 402b43861..d5f4c1785 100644 --- a/internal/handler/submit/handler.go +++ b/internal/handler/submit/handler.go @@ -463,6 +463,20 @@ func (l *LabelsAddWhen) UnmarshalText(bs []byte) error { // that are only available to batch submit operations. type BatchOptions struct { UpdateOnlyDefault bool `config:"submit.updateOnly" help:"Default value for --update-only in batch submit operations." hidden:"" default:"false"` + + // BranchMetadata provides per-branch title and body overrides + // for CR creation. When a branch has an entry here, + // its title and body are used directly + // instead of deriving from commits. + BranchMetadata map[string]BranchMeta `kong:"-"` +} + +// BranchMeta holds per-branch CR metadata for batch submissions. +// When provided via --fill-from, these values are used directly +// instead of deriving title and body from commit messages. +type BranchMeta struct { + Title string `json:"title"` + Body string `json:"body"` } // BatchRequest is a request to submit one or more change requests. @@ -489,10 +503,19 @@ func (h *Handler) SubmitBatch(ctx context.Context, req *BatchRequest) error { for _, branch := range req.Branches { // Shallow copy the options because submitBranch may modify them. opts := *opts + sopts := &submitOptions{Options: &opts} + + // Use per-branch metadata if available. + if batchOpts.BranchMetadata != nil { + if meta, ok := batchOpts.BranchMetadata[branch]; ok { + sopts.Title = meta.Title + sopts.Body = meta.Body + } + } status, err := h.submitBranch( ctx, branch, - &submitOptions{Options: &opts}, + sopts, ) if err != nil { return fmt.Errorf("submit branch %s: %w", branch, err) diff --git a/stack_submit.go b/stack_submit.go index 9f857eeae..e2225190f 100644 --- a/stack_submit.go +++ b/stack_submit.go @@ -14,6 +14,8 @@ import ( type stackSubmitCmd struct { submitOptions submit.BatchOptions + + FillFrom string `name:"fill-from" help:"JSON file mapping branch names to {title, body} for CR creation. Use '-' for stdin." placeholder:"FILE"` } func (*stackSubmitCmd) Help() string { @@ -49,6 +51,14 @@ func (cmd *stackSubmitCmd) Run( // TODO: separate preparation of the stack from submission + if cmd.FillFrom != "" { + meta, err := parseFillFrom(cmd.FillFrom) + if err != nil { + return fmt.Errorf("--fill-from: %w", err) + } + cmd.BranchMetadata = meta + } + return submitHandler.SubmitBatch(ctx, &submit.BatchRequest{ Branches: toSubmit, Options: &cmd.Options, diff --git a/testdata/help/downstack_submit.txt b/testdata/help/downstack_submit.txt index 23dc2e516..d13bd9ab4 100644 --- a/testdata/help/downstack_submit.txt +++ b/testdata/help/downstack_submit.txt @@ -51,6 +51,8 @@ Flags: Pass multiple times or separate with commas. --no-web Alias for --web=false. --branch=NAME Branch to start at + --fill-from=FILE JSON file mapping branch names to {title, + body} for CR creation. Use '-' for stdin. Global Flags: -h, --help Show help for the command diff --git a/testdata/help/stack_submit.txt b/testdata/help/stack_submit.txt index e428fd686..f97f5a028 100644 --- a/testdata/help/stack_submit.txt +++ b/testdata/help/stack_submit.txt @@ -49,6 +49,8 @@ Flags: -a, --assign=ASSIGNEE,... Assign the change request to these users. Pass multiple times or separate with commas. --no-web Alias for --web=false. + --fill-from=FILE JSON file mapping branch names to {title, + body} for CR creation. Use '-' for stdin. Global Flags: -h, --help Show help for the command diff --git a/testdata/help/upstack_submit.txt b/testdata/help/upstack_submit.txt index 24f328bd0..060b5d029 100644 --- a/testdata/help/upstack_submit.txt +++ b/testdata/help/upstack_submit.txt @@ -53,6 +53,8 @@ Flags: Pass multiple times or separate with commas. --no-web Alias for --web=false. --branch=NAME Branch to start at + --fill-from=FILE JSON file mapping branch names to {title, + body} for CR creation. Use '-' for stdin. Global Flags: -h, --help Show help for the command diff --git a/upstack_submit.go b/upstack_submit.go index 1c93d8865..4bb063cc5 100644 --- a/upstack_submit.go +++ b/upstack_submit.go @@ -18,6 +18,8 @@ type upstackSubmitCmd struct { submit.BatchOptions Branch string `placeholder:"NAME" help:"Branch to start at" predictor:"trackedBranches"` + + FillFrom string `name:"fill-from" help:"JSON file mapping branch names to {title, body} for CR creation. Use '-' for stdin." placeholder:"FILE"` } func (*upstackSubmitCmd) Help() string { @@ -78,6 +80,14 @@ func (cmd *upstackSubmitCmd) Run( // TODO: separate preparation of the stack from submission + if cmd.FillFrom != "" { + meta, err := parseFillFrom(cmd.FillFrom) + if err != nil { + return fmt.Errorf("--fill-from: %w", err) + } + cmd.BranchMetadata = meta + } + return submitHandler.SubmitBatch(ctx, &submit.BatchRequest{ Branches: upstacks, Options: &cmd.Options,