Skip to content
Draft
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/Changed-20260601-132341.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions branch_comment_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
73 changes: 69 additions & 4 deletions branch_comment_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,32 +296,60 @@ 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 {
var createdAt *time.Time
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
Expand All @@ -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"`

Expand All @@ -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"`
}
8 changes: 7 additions & 1 deletion internal/forge/forge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions internal/forge/shamhub/inline_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"`
}

Expand Down
2 changes: 1 addition & 1 deletion testdata/script/branch_comment_list_json.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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."}
66 changes: 66 additions & 0 deletions testdata/script/branch_comment_list_json_extended.txt
Original file line number Diff line number Diff line change
@@ -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 <test@example.com>'
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")
}
Loading