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
6 changes: 4 additions & 2 deletions doc/includes/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,10 @@ See https://abhinav.github.io/git-spice/cli/json/ for details.
* `-a`, `--all` ([:material-wrench:{ .middle title="spice.log.all" }](/cli/config.md#spicelogall)): Show all tracked branches, not just the current stack.
* `-S`, `--[no-]cr-status` ([:material-wrench:{ .middle title="spice.log.crStatus" }](/cli/config.md#spicelogcrstatus)): Request and include information about the Change Request
* `-c`, `--[no-]cr-comments` ([:material-wrench:{ .middle title="spice.log.crComments" }](/cli/config.md#spicelogcrcomments)): Include comment resolution counts for changes
* `--[no-]cr-checks` ([:material-wrench:{ .middle title="spice.log.crChecks" }](/cli/config.md#spicelogcrchecks)): Include forge CI/check rollup and per-run detail for changes
* `--json`: Write to stdout as a stream of JSON objects in an unspecified order <span class="mdx-badge"><span class="mdx-badge__icon">:material-tag:{ title="Released in version" }</span><span class="mdx-badge__text">[v0.18.0](/changelog.md#v0.18.0)</span>

**Configuration**: [spice.log.all](/cli/config.md#spicelogall), [spice.log.crComments](/cli/config.md#spicelogcrcomments), [spice.log.crFormat](/cli/config.md#spicelogcrformat), [spice.log.crStatus](/cli/config.md#spicelogcrstatus), [spice.log.pushStatusFormat](/cli/config.md#spicelogpushstatusformat), [spice.logLong.crFormat](/cli/config.md#spiceloglongcrformat), [spice.logShort.crFormat](/cli/config.md#spicelogshortcrformat)
**Configuration**: [spice.log.all](/cli/config.md#spicelogall), [spice.log.crChecks](/cli/config.md#spicelogcrchecks), [spice.log.crComments](/cli/config.md#spicelogcrcomments), [spice.log.crFormat](/cli/config.md#spicelogcrformat), [spice.log.crStatus](/cli/config.md#spicelogcrstatus), [spice.log.pushStatusFormat](/cli/config.md#spicelogpushstatusformat), [spice.logLong.crFormat](/cli/config.md#spiceloglongcrformat), [spice.logShort.crFormat](/cli/config.md#spicelogshortcrformat)

### git-spice log long {#gs-log-long}

Expand All @@ -227,9 +228,10 @@ See https://abhinav.github.io/git-spice/cli/json/ for details.
* `-a`, `--all` ([:material-wrench:{ .middle title="spice.log.all" }](/cli/config.md#spicelogall)): Show all tracked branches, not just the current stack.
* `-S`, `--[no-]cr-status` ([:material-wrench:{ .middle title="spice.log.crStatus" }](/cli/config.md#spicelogcrstatus)): Request and include information about the Change Request
* `-c`, `--[no-]cr-comments` ([:material-wrench:{ .middle title="spice.log.crComments" }](/cli/config.md#spicelogcrcomments)): Include comment resolution counts for changes
* `--[no-]cr-checks` ([:material-wrench:{ .middle title="spice.log.crChecks" }](/cli/config.md#spicelogcrchecks)): Include forge CI/check rollup and per-run detail for changes
* `--json`: Write to stdout as a stream of JSON objects in an unspecified order <span class="mdx-badge"><span class="mdx-badge__icon">:material-tag:{ title="Released in version" }</span><span class="mdx-badge__text">[v0.18.0](/changelog.md#v0.18.0)</span>

**Configuration**: [spice.log.all](/cli/config.md#spicelogall), [spice.log.crComments](/cli/config.md#spicelogcrcomments), [spice.log.crFormat](/cli/config.md#spicelogcrformat), [spice.log.crStatus](/cli/config.md#spicelogcrstatus), [spice.log.pushStatusFormat](/cli/config.md#spicelogpushstatusformat), [spice.logLong.crFormat](/cli/config.md#spiceloglongcrformat), [spice.logShort.crFormat](/cli/config.md#spicelogshortcrformat)
**Configuration**: [spice.log.all](/cli/config.md#spicelogall), [spice.log.crChecks](/cli/config.md#spicelogcrchecks), [spice.log.crComments](/cli/config.md#spicelogcrcomments), [spice.log.crFormat](/cli/config.md#spicelogcrformat), [spice.log.crStatus](/cli/config.md#spicelogcrstatus), [spice.log.pushStatusFormat](/cli/config.md#spicelogpushstatusformat), [spice.logLong.crFormat](/cli/config.md#spiceloglongcrformat), [spice.logShort.crFormat](/cli/config.md#spicelogshortcrformat)

## Stack

Expand Down
28 changes: 26 additions & 2 deletions internal/handler/list/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ const (
// branches that have an associated ChangeID.
IncludeCommentCounts

needsRemoteID = IncludeChangeURL | IncludeChangeState | IncludeCommentCounts
// IncludeChecks includes per-change rolled-up and per-run check
// state for branches that have an associated ChangeID.
IncludeChecks

needsRemoteID = IncludeChangeURL | IncludeChangeState |
IncludeCommentCounts | IncludeChecks
)

// BranchesRequest holds the parameters for the log command.
Expand Down Expand Up @@ -141,6 +146,7 @@ type BranchItem struct {
ChangeURL string // only if IncludeChangeURL is set
ChangeState forge.ChangeState // populated if RemoteRepository is available
CommentCounts *forge.CommentCounts // only if IncludeCommentCounts is set
Checks *forge.ChecksReport // only if IncludeChecks is set
PushStatus *PushStatus // only if IncludePushStatus is set

// Worktree is the absolute path to the worktree where this branch is checked out.
Expand Down Expand Up @@ -365,7 +371,7 @@ func (h *Handler) ListBranches(ctx context.Context, req *BranchesRequest) (*Bran
}

// If requested and possible, batch-resolve remote change metadata.
if req.Include&(IncludeChangeState|IncludeCommentCounts) != 0 && remoteForge != nil {
if req.Include&(IncludeChangeState|IncludeCommentCounts|IncludeChecks) != 0 && remoteForge != nil {
// Try to load remote metadata, but don't fail the whole operation
// if something goes wrong.
if err := h.loadRemoteChangeData(ctx, remoteForge, remoteRepoID, req.Include, items); err != nil {
Expand Down Expand Up @@ -445,6 +451,24 @@ func (h *Handler) loadRemoteChangeData(
})
}

if include&IncludeChecks != 0 {
var checks []*forge.ChecksReport
updates = append(updates, func() {
for j, idx := range branchesIdx {
branches[idx].Checks = checks[j]
}
})

wg.Go(func() error {
var err error
checks, err = remoteRepo.ChecksByChange(ctx, changeIDs)
if err != nil {
return fmt.Errorf("retrieve checks: %w", err)
}
return nil
})
}

if err := wg.Wait(); err != nil {
return err
}
Expand Down
66 changes: 64 additions & 2 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type branchLogCmd struct {

Comments bool `name:"cr-comments" short:"c" config:"log.crComments" help:"Include comment resolution counts for changes" default:"false" negatable:""`

Checks bool `name:"cr-checks" config:"log.crChecks" help:"Include forge CI/check rollup and per-run detail for changes" default:"false" negatable:""`

PushStatusFormat pushStatusFormat `config:"log.pushStatusFormat" help:"Show indicator for branches that are out of sync with their remotes. One of 'true', 'false' and 'aheadbehind'." hidden:"" default:"true"`

JSON bool `name:"json" released:"v0.18.0" help:"Write to stdout as a stream of JSON objects in an unspecified order"`
Expand All @@ -100,12 +102,13 @@ func (cmd *branchLogCmd) run(
}

var presenter logPresenter
var wantPushStatus, wantChangeState, wantCommentCounts bool
var wantPushStatus, wantChangeState, wantCommentCounts, wantChecks bool
if cmd.JSON {
// JSON always wants URLs and push status, but respects flags for change state/comments.
// JSON always wants URLs and push status, but respects flags for change state/comments/checks.
wantPushStatus = true
wantChangeState = cmd.CRStatus
wantCommentCounts = cmd.Comments
wantChecks = cmd.Checks

presenter = &jsonLogPresenter{
Stdout: kctx.Stdout,
Expand All @@ -124,6 +127,11 @@ func (cmd *branchLogCmd) run(
wantPushStatus = cmd.PushStatusFormat.Enabled()
wantChangeState = cmd.CRStatus
wantCommentCounts = cmd.Comments
// Graph presenter doesn't surface checks yet — UI surface
// for the rollup is tracked in the extension. The flag still
// fetches and includes the data so other consumers
// (e.g. piped tooling) can use it via --json.
_ = wantChecks

stderrView := ui.NewFileView(kctx.Stderr)
presenter = &graphLogPresenter{
Expand Down Expand Up @@ -154,6 +162,9 @@ func (cmd *branchLogCmd) run(
if wantCommentCounts {
req.Include |= list.IncludeCommentCounts
}
if wantChecks {
req.Include |= list.IncludeChecks
}

res, err := listHandler.ListBranches(ctx, &req)
if err != nil {
Expand Down Expand Up @@ -335,6 +346,23 @@ func (p *jsonLogPresenter) Present(res *list.BranchesResponse, currentBranch str
Unresolved: cc.Unresolved,
}
}
if checks := branch.Checks; checks != nil {
jchecks := &jsonLogChecks{
Rollup: checks.Rollup.String(),
URL: checks.URL,
}
if len(checks.Runs) > 0 {
jchecks.Runs = make([]jsonLogCheckRun, len(checks.Runs))
for i, r := range checks.Runs {
jchecks.Runs[i] = jsonLogCheckRun{
Name: r.Name,
State: r.State,
URL: r.URL,
}
}
}
jc.Checks = jchecks
}
logBranch.Change = jc
}

Expand Down Expand Up @@ -435,6 +463,40 @@ type jsonLogChange struct {

// Comments contains comment resolution counts for the change.
Comments *jsonLogComments `json:"comments,omitempty"`

// Checks contains the forge CI/check rollup and per-run detail
// for the change. Unset when --cr-checks is not enabled or when
// no checks are reported.
Checks *jsonLogChecks `json:"checks,omitempty"`
}

// jsonLogChecks is the forge check status payload for a change.
//
// Rollup is the single rolled-up state. Runs preserves forge-native
// per-check state strings. URL is the forge's summary page for the
// change's checks (click target).
type jsonLogChecks struct {
// Rollup is one of "pending", "passed", "failed", or "none".
Rollup string `json:"rollup"`

// Runs is the per-check detail in forge-defined order.
// Omitted when no per-check data is available.
Runs []jsonLogCheckRun `json:"runs,omitempty"`

// URL is the forge's summary page for the change's checks.
URL string `json:"url,omitempty"`
}

// jsonLogCheckRun is a single check reported by the forge.
type jsonLogCheckRun struct {
// Name is the forge-defined display name of the check.
Name string `json:"name"`

// State is the forge-native state for the run.
State string `json:"state"`

// URL is the forge's detail page for the run.
URL string `json:"url,omitempty"`
}

type jsonLogComments struct {
Expand Down
2 changes: 2 additions & 0 deletions testdata/help/log_long.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Flags:
Request (🔧 spice.log.crStatus)
-c, --[no-]cr-comments Include comment resolution counts for changes (🔧
spice.log.crComments)
--[no-]cr-checks Include forge CI/check rollup and per-run detail for
changes (🔧 spice.log.crChecks)
--json Write to stdout as a stream of JSON objects in an
unspecified order

Expand Down
2 changes: 2 additions & 0 deletions testdata/help/log_short.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Flags:
Request (🔧 spice.log.crStatus)
-c, --[no-]cr-comments Include comment resolution counts for changes (🔧
spice.log.crComments)
--[no-]cr-checks Include forge CI/check rollup and per-run detail for
changes (🔧 spice.log.crChecks)
--json Write to stdout as a stream of JSON objects in an
unspecified order

Expand Down
67 changes: 67 additions & 0 deletions testdata/script/log_checks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 'gs log --json' emits forge check status when --cr-checks is set.

as 'Test <test@example.com>'
at '2026-06-12T17:00:00Z'

mkdir repo
cd repo
git init
git commit --allow-empty -m 'Initial commit'
gs repo init

shamhub init
shamhub register alice
shamhub new origin alice/example.git
git push origin main

env SHAMHUB_USERNAME=alice
gs auth login

git add feat1.txt
gs bc feat1 -m 'feat1'
git add feat2.txt
gs bc feat2 -m 'feat2'
git add feat3.txt
gs bc feat3 -m 'feat3'

gs bco feat3
gs dss --fill

# feat1: failing build with one passed + one failed run.
shamhub set-checks alice/example 1 '{"rollup":"failed","runs":[{"name":"unit","state":"success","url":"https://ci.example/feat1/unit"},{"name":"lint","state":"failure","url":"https://ci.example/feat1/lint"}],"url":"https://ci.example/feat1"}'

# feat2: passing rollup with one neutral run preserved in the per-run state.
shamhub set-checks alice/example 2 '{"rollup":"passed","runs":[{"name":"unit","state":"neutral","url":"https://ci.example/feat2/unit"}],"url":"https://ci.example/feat2"}'

# feat3 has no checks seeded -> rollup is "none".

# Without --cr-checks: no checks field in any JSON output.
gs ls --json --no-cr-checks
cmpenv stdout $WORK/golden/ls-without-checks.json

# With --cr-checks: checks appear for changes with seeded data.
gs ls --json --cr-checks
cmpenv stdout $WORK/golden/ls-with-checks.json

# Config also enables checks.
git config spice.log.crChecks true
gs ls --json
cmpenv stdout $WORK/golden/ls-with-checks.json

-- repo/feat1.txt --
feat1
-- repo/feat2.txt --
feat2
-- repo/feat3.txt --
feat3

-- golden/ls-without-checks.json --
{"name":"feat1","down":{"name":"main"},"ups":[{"name":"feat2"}],"change":{"id":"#1","url":"$SHAMHUB_URL/alice/example/changes/1"},"push":{"ahead":0,"behind":0}}
{"name":"feat2","down":{"name":"feat1"},"ups":[{"name":"feat3"}],"change":{"id":"#2","url":"$SHAMHUB_URL/alice/example/changes/2"},"push":{"ahead":0,"behind":0}}
{"name":"feat3","current":true,"down":{"name":"feat2"},"change":{"id":"#3","url":"$SHAMHUB_URL/alice/example/changes/3"},"push":{"ahead":0,"behind":0}}
{"name":"main","ups":[{"name":"feat1"}]}
-- golden/ls-with-checks.json --
{"name":"feat1","down":{"name":"main"},"ups":[{"name":"feat2"}],"change":{"id":"#1","url":"$SHAMHUB_URL/alice/example/changes/1","checks":{"rollup":"failed","runs":[{"name":"unit","state":"success","url":"https://ci.example/feat1/unit"},{"name":"lint","state":"failure","url":"https://ci.example/feat1/lint"}],"url":"https://ci.example/feat1"}},"push":{"ahead":0,"behind":0}}
{"name":"feat2","down":{"name":"feat1"},"ups":[{"name":"feat3"}],"change":{"id":"#2","url":"$SHAMHUB_URL/alice/example/changes/2","checks":{"rollup":"passed","runs":[{"name":"unit","state":"neutral","url":"https://ci.example/feat2/unit"}],"url":"https://ci.example/feat2"}},"push":{"ahead":0,"behind":0}}
{"name":"feat3","current":true,"down":{"name":"feat2"},"change":{"id":"#3","url":"$SHAMHUB_URL/alice/example/changes/3","checks":{"rollup":"none"}},"push":{"ahead":0,"behind":0}}
{"name":"main","ups":[{"name":"feat1"}]}
Loading