From 52eb1984712c8e730b04d5eedf3e584723611ebb Mon Sep 17 00:00:00 2001 From: Edmund Kohlwey Date: Tue, 2 Jun 2026 15:39:51 -0400 Subject: [PATCH] branch comment: extend list --json shape per extension contract --- .../unreleased/Changed-20260601-132341.yaml | 3 + branch_comment_add.go | 3 + branch_comment_list.go | 73 ++++++++++++++++++- internal/forge/forge.go | 8 +- internal/forge/shamhub/inline_comment.go | 4 + testdata/script/branch_comment_list_json.txt | 2 +- .../branch_comment_list_json_extended.txt | 66 +++++++++++++++++ 7 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 .changes/unreleased/Changed-20260601-132341.yaml create mode 100644 testdata/script/branch_comment_list_json_extended.txt diff --git a/.changes/unreleased/Changed-20260601-132341.yaml b/.changes/unreleased/Changed-20260601-132341.yaml new file mode 100644 index 000000000..a064cb1ca --- /dev/null +++ b/.changes/unreleased/Changed-20260601-132341.yaml @@ -0,0 +1,3 @@ +kind: Changed +body: '''branch comment list --json'': Extend output shape with scope, side, range, commitSHA, resolved/stale booleans, and accept multi-line ranges via file:start-end on ''branch comment add''.' +time: 2026-06-01T13:23:41.325779-04:00 diff --git a/branch_comment_add.go b/branch_comment_add.go index 6f02392b1..f103c649e 100644 --- a/branch_comment_add.go +++ b/branch_comment_add.go @@ -151,6 +151,9 @@ func (cmd *branchCommentAddCmd) Run( req.Line = diffLine req.Side = side + // For a range comment, also resolve the end line in diff + // coordinates so the forge anchors both ends to the same + // side. if rangeEnd > 0 { _, endDiffLine, _, err := mapper.Map(file, rangeEnd) if err != nil { diff --git a/branch_comment_list.go b/branch_comment_list.go index c0ef9f57b..94059854b 100644 --- a/branch_comment_list.go +++ b/branch_comment_list.go @@ -296,14 +296,19 @@ func (cmd *branchCommentListCmd) writeJSON( } func stagedToJSON(c *state.StagedComment) jsonComment { - return jsonComment{ + jc := jsonComment{ Kind: "staged", ID: fmt.Sprintf("sc-%d", c.ID), + Scope: "line", Path: c.File, Line: c.Line, Body: c.Body, ThreadID: c.ThreadID, } + if c.File == "" && c.Line == 0 { + jc.Scope = "pr" + } + return jc } func forgeToJSON(c *forge.InlineComment) jsonComment { @@ -311,17 +316,40 @@ func forgeToJSON(c *forge.InlineComment) jsonComment { if !c.CreatedAt.IsZero() { createdAt = &c.CreatedAt } - return jsonComment{ + scope := c.Scope.String() + if c.Scope == forge.CommentScopeUnknown { + // Legacy callers that did not set Scope produced line + // comments; preserve that default. + scope = "line" + if c.Path == "" { + scope = "pr" + } + } + resolved := c.Resolved + stale := c.Outdated + jc := jsonComment{ Kind: "forge", ID: c.ID.String(), + Scope: scope, Path: c.Path, Line: c.Line, + Side: c.Side, + CommitSHA: c.CommitSHA, Body: c.Body, ThreadID: c.ThreadID, Author: c.Author, + Resolved: &resolved, + Stale: &stale, Status: commentStatus(c), CreatedAt: createdAt, } + if c.Range != nil { + jc.Range = &jsonCommentRange{ + Start: c.Range.Start, + End: c.Range.End, + } + } + return jc } // jsonComment is the JSON representation @@ -335,12 +363,29 @@ type jsonComment struct { // For forge comments: forge-specific ID. ID string `json:"id"` + // Scope is "pr", "file", or "line". + Scope string `json:"scope,omitempty"` + // Path is the file path relative to the repo root. + // Empty for "pr" scope. Path string `json:"path,omitempty"` // Line is the line number in the file. + // Omitted for "pr" and "file" scopes. Line int `json:"line,omitempty"` + // Range is the multi-line range, if the comment spans + // more than one line. Omitted for single-line comments. + Range *jsonCommentRange `json:"range,omitempty"` + + // Side is "LEFT" or "RIGHT" for the diff side. + // Empty for non-line scopes. + Side string `json:"side,omitempty"` + + // CommitSHA is the commit the comment was authored against. + // Empty when the forge does not track per-commit comments. + CommitSHA string `json:"commitSHA,omitempty"` + // Body is the full markdown body of the comment. Body string `json:"body"` @@ -351,11 +396,31 @@ type jsonComment struct { // Only set for forge comments. Author string `json:"author,omitempty"` - // Status is "open", "resolved", or "outdated". - // Only set for forge comments. + // Resolved indicates the thread is resolved. + // A nil pointer (the zero value) omits the field — used + // for staged comments where the concept doesn't apply. + // Forge comments always emit a non-nil pointer so + // consumers see both true and false. + Resolved *bool `json:"resolved,omitempty"` + + // Stale indicates the comment is anchored to a line no + // longer present in the change's current head diff. Same + // pointer semantics as Resolved. + Stale *bool `json:"stale,omitempty"` + + // Status is "open", "resolved", or "outdated". Retained + // for backward compatibility with existing consumers; + // new consumers should prefer Resolved and Stale. Status string `json:"status,omitempty"` // CreatedAt is the time the comment was created. // Only set for forge comments. CreatedAt *time.Time `json:"createdAt,omitempty"` } + +// jsonCommentRange is the JSON representation of a +// multi-line comment range. +type jsonCommentRange struct { + Start int `json:"start"` + End int `json:"end"` +} diff --git a/internal/forge/forge.go b/internal/forge/forge.go index bf93cbb24..7cdfea744 100644 --- a/internal/forge/forge.go +++ b/internal/forge/forge.go @@ -1053,6 +1053,10 @@ type InlineComment struct { // version. Empty for non-line scopes. Side string + // CommitSHA is the commit the comment was authored against. + // Empty when the forge does not track per-commit comments. + CommitSHA string + // Body is the markdown body of the comment. Body string @@ -1062,7 +1066,9 @@ type InlineComment struct { // Resolved indicates the comment thread is resolved. Resolved bool - // Outdated indicates the comment is on an outdated diff. + // Outdated indicates the comment is on an outdated diff: + // the anchored line is no longer present in the change's + // current head diff. Surfaced to the extension as "stale". Outdated bool // CreatedAt is the time the comment was created. diff --git a/internal/forge/shamhub/inline_comment.go b/internal/forge/shamhub/inline_comment.go index ca2ca6547..90854e999 100644 --- a/internal/forge/shamhub/inline_comment.go +++ b/internal/forge/shamhub/inline_comment.go @@ -68,6 +68,7 @@ type inlineCommentItem struct { RangeLo int `json:"rangeStart,omitempty"` RangeHi int `json:"rangeEnd,omitempty"` Side string `json:"side,omitempty"` + CommitSHA string `json:"commitSHA,omitempty"` Body string `json:"body"` Author string `json:"author"` Resolved bool `json:"resolved"` @@ -119,6 +120,7 @@ func (sh *ShamHub) handleListInlineComments( RangeLo: c.RangeStart, RangeHi: c.RangeEnd, Side: c.Side, + CommitSHA: c.CommitSHA, Body: c.Body, Author: c.Author, Resolved: c.Resolved, @@ -215,6 +217,7 @@ func (r *forgeRepository) ListInlineComments( Path: item.Path, Line: item.Line, Side: item.Side, + CommitSHA: item.CommitSHA, Body: item.Body, Author: item.Author, Resolved: item.Resolved, @@ -260,6 +263,7 @@ type postInlineCommentRequest struct { RangeEnd int `json:"rangeEnd,omitempty"` Body string `json:"body"` Side string `json:"side,omitempty"` + CommitSHA string `json:"commitSHA,omitempty"` ThreadID string `json:"threadID,omitempty"` } diff --git a/testdata/script/branch_comment_list_json.txt b/testdata/script/branch_comment_list_json.txt index aa894d0e6..d2a221d76 100644 --- a/testdata/script/branch_comment_list_json.txt +++ b/testdata/script/branch_comment_list_json.txt @@ -60,4 +60,4 @@ func main() { } -- golden/staged_only.json -- -{"kind":"staged","id":"sc-1","path":"main.go","line":5,"body":"Add error handling here."} +{"kind":"staged","id":"sc-1","scope":"line","path":"main.go","line":5,"body":"Add error handling here."} diff --git a/testdata/script/branch_comment_list_json_extended.txt b/testdata/script/branch_comment_list_json_extended.txt new file mode 100644 index 000000000..8a3a0a0cb --- /dev/null +++ b/testdata/script/branch_comment_list_json_extended.txt @@ -0,0 +1,66 @@ +# Verifies the extended 'gs branch comment list --json' shape that +# the VSCode extension consumes. Covers: +# - scope, side, resolved (always emitted as bool), stale, +# range (multi-line), kind, status (legacy) + +as 'Test ' +at '2024-04-05T16:40:32Z' + +cd repo +git init +git commit --allow-empty -m 'Initial commit' + +shamhub init +shamhub new origin alice/example.git +shamhub register alice +git push origin main + +env SHAMHUB_USERNAME=alice +gs auth login + +git add main.go +gs bc -m 'Add main' feature1 +gs branch submit --fill +stderr 'Created #' + +# Post a single-line comment. +gs branch comment add main.go:3 -m 'Single-line comment.' +stderr 'Posted comment' + +# Post a multi-line range comment spanning lines 3-5. +gs branch comment add main.go:3-5 -m 'Range comment.' +stderr 'Posted comment' + +# Single-line comment: scope, side, resolved (false), stale (false). +gs branch comment list --json +stdout '"scope":"line"' +stdout '"side":"RIGHT"' +stdout '"resolved":false' +stdout '"stale":false' +stdout '"status":"open"' +stdout '"body":"Single-line comment."' +stdout '"body":"Range comment."' + +# Range comment: range:{start,end} present. +gs branch comment list --json +stdout '"range":{"start":3,"end":5}' + +# Resolve the first inline comment's thread. +shamhub comment edit -resolved=true 2 +gs branch comment list --json +stdout '"resolved":true' +stdout '"status":"resolved"' + +# Mark the second comment outdated/stale. +shamhub comment edit -outdated=true 3 +gs branch comment list --json +stdout '"stale":true' +stdout '"status":"outdated"' + +-- repo/main.go -- +package main + +func main() { + println("hello") + println("world") +}