@aaronshaf/ger 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/adr/0023-show-reviewer-list.md +42 -0
- package/docs/adr/README.md +1 -0
- package/docs/prd/commands.md +3 -1
- package/docs/prd/data-model.md +19 -8
- package/package.json +1 -1
- package/src/cli/commands/abandon.ts +28 -2
- package/src/cli/commands/add-reviewer.ts +37 -5
- package/src/cli/commands/comment.ts +93 -6
- package/src/cli/commands/comments.ts +34 -2
- package/src/cli/commands/diff.ts +13 -1
- package/src/cli/commands/groups-members.ts +28 -3
- package/src/cli/commands/groups-show.ts +38 -2
- package/src/cli/commands/groups.ts +32 -3
- package/src/cli/commands/incoming.ts +21 -1
- package/src/cli/commands/install-hook.ts +27 -6
- package/src/cli/commands/mine.ts +18 -1
- package/src/cli/commands/projects.ts +22 -2
- package/src/cli/commands/rebase.ts +19 -2
- package/src/cli/commands/remove-reviewer.ts +32 -4
- package/src/cli/commands/restore.ts +15 -1
- package/src/cli/commands/search.ts +22 -1
- package/src/cli/commands/show.ts +114 -1
- package/src/cli/commands/status.ts +11 -1
- package/src/cli/commands/submit.ts +30 -2
- package/src/cli/commands/topic.ts +33 -3
- package/src/cli/commands/vote.ts +15 -1
- package/src/cli/commands/workspace.ts +23 -4
- package/src/cli/register-commands.ts +40 -22
- package/src/cli/register-group-commands.ts +17 -2
- package/src/cli/register-reviewer-commands.ts +16 -2
- package/src/schemas/gerrit.ts +29 -14
- package/src/services/commit-hook.ts +6 -8
- package/tests/show.test.ts +126 -0
- package/tests/submit.test.ts +28 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ADR 0023: Surface Reviewers and CCs in `ger show`
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`ger` supports reviewer management (`add-reviewer`, `remove-reviewer`) but did not expose a reliable way to view current reviewers for a change in one command.
|
|
10
|
+
|
|
11
|
+
This created a workflow gap:
|
|
12
|
+
|
|
13
|
+
- Users could mutate reviewer state but not inspect it from the CLI.
|
|
14
|
+
- `show` already served as the canonical "single change detail" command and was the best place to surface reviewer assignments.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
Add reviewer visibility to `ger show` by:
|
|
19
|
+
|
|
20
|
+
1. Keeping `getChange` lightweight and using a `listChanges` fallback (with detailed account/label options) when reviewer state is not present in the base change response.
|
|
21
|
+
2. Extending `ChangeInfo` schema with Gerrit reviewer state maps (`REVIEWER`, `CC`, `REMOVED`).
|
|
22
|
+
3. Rendering reviewers and CCs in all `show` output formats (pretty, JSON, XML).
|
|
23
|
+
|
|
24
|
+
## Rationale
|
|
25
|
+
|
|
26
|
+
- **Single source of truth**: `show` remains the canonical command for full change context.
|
|
27
|
+
- **No new command surface**: avoids adding a narrowly scoped `list-reviewers` command.
|
|
28
|
+
- **Automation-friendly**: JSON/XML consumers can parse reviewer state without scraping text output.
|
|
29
|
+
- **Backward compatible**: reviewer fields are optional and do not break servers or older data shapes.
|
|
30
|
+
|
|
31
|
+
## Consequences
|
|
32
|
+
|
|
33
|
+
### Positive
|
|
34
|
+
|
|
35
|
+
- Users can verify reviewer assignment directly after add/remove operations.
|
|
36
|
+
- Better parity between mutation commands and read visibility.
|
|
37
|
+
- More complete machine-readable change payloads.
|
|
38
|
+
|
|
39
|
+
### Negative
|
|
40
|
+
|
|
41
|
+
- Extra `listChanges` request when reviewer data is absent from `getChange`.
|
|
42
|
+
- Additional schema/output maintenance for reviewer state rendering.
|
package/docs/adr/README.md
CHANGED
|
@@ -28,3 +28,4 @@ Records of significant architectural decisions with context and rationale. Each
|
|
|
28
28
|
| [0020](0020-code-coverage-enforcement.md) | 80% coverage threshold in pre-commit |
|
|
29
29
|
| [0021](0021-typescript-isolated-declarations.md) | Explicit return types on exports |
|
|
30
30
|
| [0022](0022-biome-oxlint-tooling.md) | Biome formatter + oxlint linter |
|
|
31
|
+
| [0023](0023-show-reviewer-list.md) | Surface reviewers and CCs in `ger show` |
|
package/docs/prd/commands.md
CHANGED
|
@@ -23,11 +23,13 @@ ger show # Auto-detect from HEAD
|
|
|
23
23
|
|
|
24
24
|
**Output includes:**
|
|
25
25
|
- Change metadata (number, project, branch, status)
|
|
26
|
-
- Owner and
|
|
26
|
+
- Owner, reviewers, and CC information
|
|
27
27
|
- Submit requirements
|
|
28
28
|
- Full diff
|
|
29
29
|
- All comments with context
|
|
30
30
|
|
|
31
|
+
Reviewer listing for a specific change is provided by `show` (there is no separate `list-reviewers` command).
|
|
32
|
+
|
|
31
33
|
### diff
|
|
32
34
|
|
|
33
35
|
Get change diff in various formats.
|
package/docs/prd/data-model.md
CHANGED
|
@@ -14,25 +14,36 @@ const ChangeInfo = Schema.Struct({
|
|
|
14
14
|
_number: Schema.Number, // Numeric change ID
|
|
15
15
|
project: Schema.String, // Project name
|
|
16
16
|
branch: Schema.String, // Target branch
|
|
17
|
+
change_id: Schema.String, // Gerrit Change-Id
|
|
17
18
|
topic: Schema.optional(Schema.String),
|
|
18
19
|
subject: Schema.String, // First line of commit message
|
|
19
20
|
status: Schema.Literal('NEW', 'MERGED', 'ABANDONED', 'DRAFT'),
|
|
20
|
-
created: Schema.String,
|
|
21
|
-
updated: Schema.String,
|
|
22
|
-
|
|
23
|
-
submitter: Schema.optional(AccountInfo),
|
|
24
|
-
owner: AccountInfo,
|
|
21
|
+
created: Schema.optional(Schema.String),
|
|
22
|
+
updated: Schema.optional(Schema.String),
|
|
23
|
+
owner: Schema.optional(AccountInfo),
|
|
25
24
|
current_revision: Schema.optional(Schema.String),
|
|
26
25
|
revisions: Schema.optional(Schema.Record(Schema.String, RevisionInfo)),
|
|
27
26
|
labels: Schema.optional(Schema.Record(Schema.String, LabelInfo)),
|
|
28
|
-
reviewers: Schema.optional(
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
reviewers: Schema.optional(ReviewerStateMap),
|
|
28
|
+
submittable: Schema.optional(Schema.Boolean),
|
|
29
|
+
work_in_progress: Schema.optional(Schema.Boolean),
|
|
31
30
|
insertions: Schema.optional(Schema.Number),
|
|
32
31
|
deletions: Schema.optional(Schema.Number),
|
|
33
32
|
})
|
|
34
33
|
```
|
|
35
34
|
|
|
35
|
+
### ReviewerStateMap
|
|
36
|
+
|
|
37
|
+
Reviewer assignments grouped by Gerrit reviewer state.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const ReviewerStateMap = Schema.Struct({
|
|
41
|
+
REVIEWER: Schema.optional(Schema.Array(AccountInfo)),
|
|
42
|
+
CC: Schema.optional(Schema.Array(AccountInfo)),
|
|
43
|
+
REMOVED: Schema.optional(Schema.Array(AccountInfo)),
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
36
47
|
### AccountInfo
|
|
37
48
|
|
|
38
49
|
User account information.
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
|
4
4
|
interface AbandonOptions {
|
|
5
5
|
message?: string
|
|
6
6
|
xml?: boolean
|
|
7
|
+
json?: boolean
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export const abandonCommand = (
|
|
@@ -26,7 +27,20 @@ export const abandonCommand = (
|
|
|
26
27
|
// Perform the abandon
|
|
27
28
|
yield* gerritApi.abandonChange(changeId, options.message)
|
|
28
29
|
|
|
29
|
-
if (options.
|
|
30
|
+
if (options.json) {
|
|
31
|
+
console.log(
|
|
32
|
+
JSON.stringify(
|
|
33
|
+
{
|
|
34
|
+
status: 'success',
|
|
35
|
+
change_number: change._number,
|
|
36
|
+
subject: change.subject,
|
|
37
|
+
...(options.message ? { message: options.message } : {}),
|
|
38
|
+
},
|
|
39
|
+
null,
|
|
40
|
+
2,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
} else if (options.xml) {
|
|
30
44
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
31
45
|
console.log(`<abandon_result>`)
|
|
32
46
|
console.log(` <status>success</status>`)
|
|
@@ -46,7 +60,19 @@ export const abandonCommand = (
|
|
|
46
60
|
// If we can't get change details, still try to abandon with just the ID
|
|
47
61
|
yield* gerritApi.abandonChange(changeId, options.message)
|
|
48
62
|
|
|
49
|
-
if (options.
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.log(
|
|
65
|
+
JSON.stringify(
|
|
66
|
+
{
|
|
67
|
+
status: 'success',
|
|
68
|
+
change_id: changeId,
|
|
69
|
+
...(options.message ? { message: options.message } : {}),
|
|
70
|
+
},
|
|
71
|
+
null,
|
|
72
|
+
2,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
} else if (options.xml) {
|
|
50
76
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
51
77
|
console.log(`<abandon_result>`)
|
|
52
78
|
console.log(` <status>success</status>`)
|
|
@@ -6,6 +6,7 @@ interface AddReviewerOptions {
|
|
|
6
6
|
cc?: boolean
|
|
7
7
|
notify?: string
|
|
8
8
|
xml?: boolean
|
|
9
|
+
json?: boolean
|
|
9
10
|
group?: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -24,6 +25,10 @@ const outputXmlError = (message: string): void => {
|
|
|
24
25
|
console.log(`</add_reviewer_result>`)
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
const outputJsonError = (message: string): void => {
|
|
29
|
+
console.log(JSON.stringify({ status: 'error', error: message }, null, 2))
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
class ValidationError extends Error {
|
|
28
33
|
readonly _tag = 'ValidationError'
|
|
29
34
|
}
|
|
@@ -40,7 +45,9 @@ export const addReviewerCommand = (
|
|
|
40
45
|
if (!changeId) {
|
|
41
46
|
const message =
|
|
42
47
|
'Change ID is required. Use -c <change-id> or run from a branch with an active change.'
|
|
43
|
-
if (options.
|
|
48
|
+
if (options.json) {
|
|
49
|
+
outputJsonError(message)
|
|
50
|
+
} else if (options.xml) {
|
|
44
51
|
outputXmlError(message)
|
|
45
52
|
} else {
|
|
46
53
|
console.error(`✗ ${message}`)
|
|
@@ -51,7 +58,9 @@ export const addReviewerCommand = (
|
|
|
51
58
|
if (reviewers.length === 0) {
|
|
52
59
|
const entityType = options.group ? 'group' : 'reviewer'
|
|
53
60
|
const message = `At least one ${entityType} is required.`
|
|
54
|
-
if (options.
|
|
61
|
+
if (options.json) {
|
|
62
|
+
outputJsonError(message)
|
|
63
|
+
} else if (options.xml) {
|
|
55
64
|
outputXmlError(message)
|
|
56
65
|
} else {
|
|
57
66
|
console.error(`✗ ${message}`)
|
|
@@ -67,7 +76,9 @@ export const addReviewerCommand = (
|
|
|
67
76
|
const emailLikeInputs = reviewers.filter((r) => r.includes('@'))
|
|
68
77
|
if (emailLikeInputs.length > 0) {
|
|
69
78
|
const message = `The --group flag expects group identifiers, but received email-like input: ${emailLikeInputs.join(', ')}. Did you mean to omit --group?`
|
|
70
|
-
if (options.
|
|
79
|
+
if (options.json) {
|
|
80
|
+
outputJsonError(message)
|
|
81
|
+
} else if (options.xml) {
|
|
71
82
|
outputXmlError(message)
|
|
72
83
|
} else {
|
|
73
84
|
console.error(`✗ ${message}`)
|
|
@@ -85,7 +96,9 @@ export const addReviewerCommand = (
|
|
|
85
96
|
const upperNotify = options.notify.toUpperCase()
|
|
86
97
|
if (!VALID_NOTIFY_LEVELS.includes(upperNotify as NotifyLevel)) {
|
|
87
98
|
const message = `Invalid notify level: ${options.notify}. Valid values: none, owner, owner_reviewers, all`
|
|
88
|
-
if (options.
|
|
99
|
+
if (options.json) {
|
|
100
|
+
outputJsonError(message)
|
|
101
|
+
} else if (options.xml) {
|
|
89
102
|
outputXmlError(message)
|
|
90
103
|
} else {
|
|
91
104
|
console.error(`✗ ${message}`)
|
|
@@ -120,7 +133,26 @@ export const addReviewerCommand = (
|
|
|
120
133
|
}
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
if (options.
|
|
136
|
+
if (options.json) {
|
|
137
|
+
const allSuccess = results.every((r) => r.success)
|
|
138
|
+
console.log(
|
|
139
|
+
JSON.stringify(
|
|
140
|
+
{
|
|
141
|
+
status: allSuccess ? 'success' : 'partial_failure',
|
|
142
|
+
change_id: changeId,
|
|
143
|
+
state,
|
|
144
|
+
entity_type: entityType,
|
|
145
|
+
reviewers: results.map((r) =>
|
|
146
|
+
r.success
|
|
147
|
+
? { input: r.reviewer, name: r.name, status: 'added' }
|
|
148
|
+
: { input: r.reviewer, error: r.error, status: 'failed' },
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
null,
|
|
152
|
+
2,
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
} else if (options.xml) {
|
|
124
156
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
125
157
|
console.log(`<add_reviewer_result>`)
|
|
126
158
|
console.log(` <change_id>${escapeXml(changeId)}</change_id>`)
|
|
@@ -14,18 +14,27 @@ Examples:
|
|
|
14
14
|
# Post a line-specific comment
|
|
15
15
|
$ ger comment 12345 --file src/main.js --line 42 -m "Consider using const here"
|
|
16
16
|
|
|
17
|
+
# Reply to a specific comment thread (resolves the thread by default)
|
|
18
|
+
$ ger comment 12345 --file src/main.js --line 42 --reply-to 37935b71_9e79a76c -m "Done, fixed"
|
|
19
|
+
|
|
20
|
+
# Reply but keep the thread unresolved
|
|
21
|
+
$ ger comment 12345 --file src/main.js --line 42 --reply-to 37935b71_9e79a76c --unresolved -m "What do you think?"
|
|
22
|
+
|
|
17
23
|
# Post multiple comments using batch mode
|
|
18
24
|
$ echo '{"message": "Review complete", "comments": [
|
|
19
25
|
{"file": "src/main.js", "line": 10, "message": "Good refactor"}
|
|
20
26
|
]}' | ger comment 12345 --batch
|
|
21
27
|
|
|
22
|
-
Note: Line numbers refer to the NEW version of the file, not diff line numbers
|
|
28
|
+
Note: Line numbers refer to the NEW version of the file, not diff line numbers.
|
|
29
|
+
Note: Comment IDs for --reply-to can be found in \`ger comments --xml\` or \`ger comments --json\` output (<id> / "id" field).`
|
|
23
30
|
|
|
24
31
|
interface CommentOptions {
|
|
25
32
|
message?: string
|
|
26
33
|
xml?: boolean
|
|
34
|
+
json?: boolean
|
|
27
35
|
file?: string
|
|
28
36
|
line?: number
|
|
37
|
+
replyTo?: string
|
|
29
38
|
unresolved?: boolean
|
|
30
39
|
batch?: boolean
|
|
31
40
|
}
|
|
@@ -185,6 +194,21 @@ export const createReviewInputFromString = (
|
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
const createReviewInput = (options: CommentOptions): Effect.Effect<ReviewInput, Error> => {
|
|
197
|
+
// Validate --reply-to constraints early
|
|
198
|
+
if (options.replyTo !== undefined) {
|
|
199
|
+
if (options.batch) {
|
|
200
|
+
return Effect.fail(new Error('--reply-to cannot be used with --batch'))
|
|
201
|
+
}
|
|
202
|
+
if (!(options.file && options.line)) {
|
|
203
|
+
return Effect.fail(new Error('--reply-to requires --file and --line'))
|
|
204
|
+
}
|
|
205
|
+
if (options.replyTo.trim().length === 0) {
|
|
206
|
+
return Effect.fail(new Error('--reply-to comment ID cannot be empty'))
|
|
207
|
+
}
|
|
208
|
+
// Normalize to trimmed value so the payload never contains leading/trailing whitespace
|
|
209
|
+
options = { ...options, replyTo: options.replyTo.trim() }
|
|
210
|
+
}
|
|
211
|
+
|
|
188
212
|
// Batch mode
|
|
189
213
|
if (options.batch) {
|
|
190
214
|
return pipe(
|
|
@@ -226,7 +250,13 @@ const createReviewInput = (options: CommentOptions): Effect.Effect<ReviewInput,
|
|
|
226
250
|
{
|
|
227
251
|
line: options.line,
|
|
228
252
|
message: options.message,
|
|
229
|
-
|
|
253
|
+
...(options.replyTo !== undefined ? { in_reply_to: options.replyTo } : {}),
|
|
254
|
+
// When replying, default unresolved to false (resolves the thread) unless explicitly set
|
|
255
|
+
...(options.replyTo !== undefined
|
|
256
|
+
? { unresolved: options.unresolved ?? false }
|
|
257
|
+
: options.unresolved !== undefined
|
|
258
|
+
? { unresolved: options.unresolved }
|
|
259
|
+
: {}),
|
|
230
260
|
},
|
|
231
261
|
],
|
|
232
262
|
},
|
|
@@ -428,8 +458,14 @@ const formatXmlOutput = (
|
|
|
428
458
|
lines.push(` <comment>`)
|
|
429
459
|
lines.push(` <file>${options.file}</file>`)
|
|
430
460
|
lines.push(` <line>${options.line}</line>`)
|
|
461
|
+
if (options.replyTo) lines.push(` <in_reply_to>${options.replyTo}</in_reply_to>`)
|
|
431
462
|
lines.push(` <message><![CDATA[${options.message}]]></message>`)
|
|
432
|
-
|
|
463
|
+
// Always emit unresolved when replying so callers know thread resolution state
|
|
464
|
+
if (options.replyTo !== undefined) {
|
|
465
|
+
lines.push(` <unresolved>${(options.unresolved ?? false).toString()}</unresolved>`)
|
|
466
|
+
} else if (options.unresolved) {
|
|
467
|
+
lines.push(` <unresolved>true</unresolved>`)
|
|
468
|
+
}
|
|
433
469
|
lines.push(` </comment>`)
|
|
434
470
|
} else {
|
|
435
471
|
lines.push(` <message><![CDATA[${options.message}]]></message>`)
|
|
@@ -459,6 +495,10 @@ const formatHumanOutput = (
|
|
|
459
495
|
console.log(`Posted ${totalComments} line comment(s)`)
|
|
460
496
|
} else if (options.file && options.line) {
|
|
461
497
|
console.log(`File: ${options.file}, Line: ${options.line}`)
|
|
498
|
+
if (options.replyTo) {
|
|
499
|
+
const resolved = !(options.unresolved ?? false)
|
|
500
|
+
console.log(`Reply to: ${options.replyTo} (thread ${resolved ? 'resolved' : 'unresolved'})`)
|
|
501
|
+
}
|
|
462
502
|
console.log(`Message: ${options.message}`)
|
|
463
503
|
if (options.unresolved) console.log(`Status: Unresolved`)
|
|
464
504
|
}
|
|
@@ -466,6 +506,51 @@ const formatHumanOutput = (
|
|
|
466
506
|
// since it was already shown in the "OVERALL REVIEW TO POST" section
|
|
467
507
|
})
|
|
468
508
|
|
|
509
|
+
// Helper to format JSON output
|
|
510
|
+
const formatJsonOutput = (
|
|
511
|
+
change: ChangeInfo,
|
|
512
|
+
review: ReviewInput,
|
|
513
|
+
options: CommentOptions,
|
|
514
|
+
changeId: string,
|
|
515
|
+
): Effect.Effect<void> =>
|
|
516
|
+
Effect.sync(() => {
|
|
517
|
+
const output: Record<string, unknown> = {
|
|
518
|
+
status: 'success',
|
|
519
|
+
change_id: changeId,
|
|
520
|
+
change_number: change._number,
|
|
521
|
+
change_subject: change.subject,
|
|
522
|
+
change_status: change.status,
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (options.batch && review.comments) {
|
|
526
|
+
output.comments = Object.entries(review.comments).flatMap(([file, comments]) =>
|
|
527
|
+
comments.map((comment) => ({
|
|
528
|
+
file,
|
|
529
|
+
...(comment.line ? { line: comment.line } : {}),
|
|
530
|
+
message: comment.message,
|
|
531
|
+
...(comment.unresolved ? { unresolved: true } : {}),
|
|
532
|
+
})),
|
|
533
|
+
)
|
|
534
|
+
} else if (options.file && options.line) {
|
|
535
|
+
output.comment = {
|
|
536
|
+
file: options.file,
|
|
537
|
+
line: options.line,
|
|
538
|
+
...(options.replyTo ? { in_reply_to: options.replyTo } : {}),
|
|
539
|
+
message: options.message,
|
|
540
|
+
// Always include unresolved when replying so callers know thread resolution state
|
|
541
|
+
...(options.replyTo !== undefined
|
|
542
|
+
? { unresolved: options.unresolved ?? false }
|
|
543
|
+
: options.unresolved
|
|
544
|
+
? { unresolved: true }
|
|
545
|
+
: {}),
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
output.message = options.message
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
console.log(JSON.stringify(output, null, 2))
|
|
552
|
+
})
|
|
553
|
+
|
|
469
554
|
// Main output formatter
|
|
470
555
|
const formatOutput = (
|
|
471
556
|
change: ChangeInfo,
|
|
@@ -473,6 +558,8 @@ const formatOutput = (
|
|
|
473
558
|
options: CommentOptions,
|
|
474
559
|
changeId: string,
|
|
475
560
|
): Effect.Effect<void> =>
|
|
476
|
-
options.
|
|
477
|
-
?
|
|
478
|
-
:
|
|
561
|
+
options.json
|
|
562
|
+
? formatJsonOutput(change, review, options, changeId)
|
|
563
|
+
: options.xml
|
|
564
|
+
? formatXmlOutput(change, review, options, changeId)
|
|
565
|
+
: formatHumanOutput(change, review, options)
|
|
@@ -6,6 +6,7 @@ import { getDiffContext } from '@/utils/diff-context'
|
|
|
6
6
|
|
|
7
7
|
interface CommentsOptions {
|
|
8
8
|
xml?: boolean
|
|
9
|
+
json?: boolean
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const getCommentsForChange = (
|
|
@@ -63,7 +64,36 @@ export const commentsCommand = (
|
|
|
63
64
|
})
|
|
64
65
|
|
|
65
66
|
// Format output
|
|
66
|
-
if (options.
|
|
67
|
+
if (options.json) {
|
|
68
|
+
const jsonOutput = {
|
|
69
|
+
status: 'success',
|
|
70
|
+
change_id: changeId,
|
|
71
|
+
comment_count: commentsWithContext.length,
|
|
72
|
+
comments: commentsWithContext.map(({ comment, context }) => ({
|
|
73
|
+
id: comment.id,
|
|
74
|
+
...(comment.path ? { path: comment.path } : {}),
|
|
75
|
+
...(comment.line ? { line: comment.line } : {}),
|
|
76
|
+
...(comment.range ? { range: comment.range } : {}),
|
|
77
|
+
...(comment.author
|
|
78
|
+
? {
|
|
79
|
+
author: {
|
|
80
|
+
...(comment.author.name ? { name: comment.author.name } : {}),
|
|
81
|
+
...(comment.author.email ? { email: comment.author.email } : {}),
|
|
82
|
+
...(comment.author._account_id !== undefined
|
|
83
|
+
? { account_id: comment.author._account_id }
|
|
84
|
+
: {}),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
...(comment.updated ? { updated: comment.updated } : {}),
|
|
89
|
+
...(comment.unresolved !== undefined ? { unresolved: comment.unresolved } : {}),
|
|
90
|
+
...(comment.in_reply_to ? { in_reply_to: comment.in_reply_to } : {}),
|
|
91
|
+
message: comment.message,
|
|
92
|
+
...(context ? { diff_context: context } : {}),
|
|
93
|
+
})),
|
|
94
|
+
}
|
|
95
|
+
console.log(JSON.stringify(jsonOutput, null, 2))
|
|
96
|
+
} else if (options.xml) {
|
|
67
97
|
formatCommentsXml(changeId, commentsWithContext)
|
|
68
98
|
} else {
|
|
69
99
|
formatCommentsPretty(commentsWithContext)
|
|
@@ -71,7 +101,9 @@ export const commentsCommand = (
|
|
|
71
101
|
}).pipe(
|
|
72
102
|
// Regional error boundary for the entire command
|
|
73
103
|
Effect.catchTag('ApiError', (error) => {
|
|
74
|
-
if (options.
|
|
104
|
+
if (options.json) {
|
|
105
|
+
console.log(JSON.stringify({ status: 'error', error: error.message }, null, 2))
|
|
106
|
+
} else if (options.xml) {
|
|
75
107
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
76
108
|
console.log(`<comments_result>`)
|
|
77
109
|
console.log(` <status>error</status>`)
|
package/src/cli/commands/diff.ts
CHANGED
|
@@ -35,7 +35,19 @@ export const diffCommand = (
|
|
|
35
35
|
),
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
if (validatedOptions.
|
|
38
|
+
if (validatedOptions.json) {
|
|
39
|
+
// JSON output
|
|
40
|
+
const jsonOutput: Record<string, unknown> = {
|
|
41
|
+
status: 'success',
|
|
42
|
+
change_id: changeId,
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(diff)) {
|
|
45
|
+
jsonOutput.files = diff
|
|
46
|
+
} else {
|
|
47
|
+
jsonOutput.content = diff
|
|
48
|
+
}
|
|
49
|
+
console.log(JSON.stringify(jsonOutput, null, 2))
|
|
50
|
+
} else if (validatedOptions.xml) {
|
|
39
51
|
// XML output for LLM consumption
|
|
40
52
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
41
53
|
console.log(`<diff_result>`)
|
|
@@ -4,6 +4,7 @@ import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
|
4
4
|
|
|
5
5
|
interface GroupsMembersOptions {
|
|
6
6
|
xml?: boolean
|
|
7
|
+
json?: boolean
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -25,7 +26,9 @@ export const groupsMembersCommand = (
|
|
|
25
26
|
const members = yield* gerritApi.getGroupMembers(groupId).pipe(
|
|
26
27
|
Effect.catchTag('ApiError', (error) =>
|
|
27
28
|
Effect.gen(function* () {
|
|
28
|
-
if (options.
|
|
29
|
+
if (options.json) {
|
|
30
|
+
console.log(JSON.stringify({ status: 'error', error: error.message }, null, 2))
|
|
31
|
+
} else if (options.xml) {
|
|
29
32
|
console.log('<?xml version="1.0" encoding="UTF-8"?>')
|
|
30
33
|
console.log('<group_members_result>')
|
|
31
34
|
console.log(' <status>error</status>')
|
|
@@ -47,7 +50,11 @@ export const groupsMembersCommand = (
|
|
|
47
50
|
|
|
48
51
|
// Handle empty results
|
|
49
52
|
if (members.length === 0) {
|
|
50
|
-
if (options.
|
|
53
|
+
if (options.json) {
|
|
54
|
+
console.log(
|
|
55
|
+
JSON.stringify({ status: 'success', group_id: groupId, count: 0, members: [] }, null, 2),
|
|
56
|
+
)
|
|
57
|
+
} else if (options.xml) {
|
|
51
58
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
52
59
|
console.log(`<group_members_result>`)
|
|
53
60
|
console.log(` <status>success</status>`)
|
|
@@ -62,7 +69,25 @@ export const groupsMembersCommand = (
|
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
// Output results
|
|
65
|
-
if (options.
|
|
72
|
+
if (options.json) {
|
|
73
|
+
console.log(
|
|
74
|
+
JSON.stringify(
|
|
75
|
+
{
|
|
76
|
+
status: 'success',
|
|
77
|
+
group_id: groupId,
|
|
78
|
+
count: members.length,
|
|
79
|
+
members: members.map((member) => ({
|
|
80
|
+
account_id: member._account_id,
|
|
81
|
+
...(member.name ? { name: member.name } : {}),
|
|
82
|
+
...(member.email ? { email: member.email } : {}),
|
|
83
|
+
...(member.username ? { username: member.username } : {}),
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
null,
|
|
87
|
+
2,
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
} else if (options.xml) {
|
|
66
91
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
67
92
|
console.log(`<group_members_result>`)
|
|
68
93
|
console.log(` <status>success</status>`)
|
|
@@ -4,6 +4,7 @@ import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
|
4
4
|
|
|
5
5
|
interface GroupsShowOptions {
|
|
6
6
|
xml?: boolean
|
|
7
|
+
json?: boolean
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -25,7 +26,9 @@ export const groupsShowCommand = (
|
|
|
25
26
|
const group = yield* gerritApi.getGroupDetail(groupId).pipe(
|
|
26
27
|
Effect.catchTag('ApiError', (error) =>
|
|
27
28
|
Effect.gen(function* () {
|
|
28
|
-
if (options.
|
|
29
|
+
if (options.json) {
|
|
30
|
+
console.log(JSON.stringify({ status: 'error', error: error.message }, null, 2))
|
|
31
|
+
} else if (options.xml) {
|
|
29
32
|
console.log('<?xml version="1.0" encoding="UTF-8"?>')
|
|
30
33
|
console.log('<group_detail_result>')
|
|
31
34
|
console.log(' <status>error</status>')
|
|
@@ -46,7 +49,40 @@ export const groupsShowCommand = (
|
|
|
46
49
|
)
|
|
47
50
|
|
|
48
51
|
// Output results
|
|
49
|
-
if (options.
|
|
52
|
+
if (options.json) {
|
|
53
|
+
console.log(
|
|
54
|
+
JSON.stringify(
|
|
55
|
+
{
|
|
56
|
+
status: 'success',
|
|
57
|
+
group: {
|
|
58
|
+
id: group.id,
|
|
59
|
+
...(group.name ? { name: group.name } : {}),
|
|
60
|
+
...(group.description ? { description: group.description } : {}),
|
|
61
|
+
...(group.owner ? { owner: group.owner } : {}),
|
|
62
|
+
...(group.owner_id ? { owner_id: group.owner_id } : {}),
|
|
63
|
+
...(group.group_id !== undefined ? { group_id: group.group_id } : {}),
|
|
64
|
+
...(group.options?.visible_to_all !== undefined
|
|
65
|
+
? { visible_to_all: group.options.visible_to_all }
|
|
66
|
+
: {}),
|
|
67
|
+
...(group.created_on ? { created_on: group.created_on } : {}),
|
|
68
|
+
...(group.url ? { url: group.url } : {}),
|
|
69
|
+
members: (group.members ?? []).map((member) => ({
|
|
70
|
+
account_id: member._account_id,
|
|
71
|
+
...(member.name ? { name: member.name } : {}),
|
|
72
|
+
...(member.email ? { email: member.email } : {}),
|
|
73
|
+
...(member.username ? { username: member.username } : {}),
|
|
74
|
+
})),
|
|
75
|
+
subgroups: (group.includes ?? []).map((subgroup) => ({
|
|
76
|
+
id: subgroup.id,
|
|
77
|
+
...(subgroup.name ? { name: subgroup.name } : {}),
|
|
78
|
+
})),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
null,
|
|
82
|
+
2,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
} else if (options.xml) {
|
|
50
86
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
51
87
|
console.log(`<group_detail_result>`)
|
|
52
88
|
console.log(` <status>success</status>`)
|
|
@@ -9,6 +9,7 @@ interface GroupsOptions {
|
|
|
9
9
|
user?: string
|
|
10
10
|
limit?: string
|
|
11
11
|
xml?: boolean
|
|
12
|
+
json?: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -44,7 +45,9 @@ export const groupsCommand = (
|
|
|
44
45
|
.pipe(
|
|
45
46
|
Effect.catchTag('ApiError', (error) =>
|
|
46
47
|
Effect.gen(function* () {
|
|
47
|
-
if (options.
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify({ status: 'error', error: error.message }, null, 2))
|
|
50
|
+
} else if (options.xml) {
|
|
48
51
|
console.log('<?xml version="1.0" encoding="UTF-8"?>')
|
|
49
52
|
console.log('<groups_result>')
|
|
50
53
|
console.log(' <status>error</status>')
|
|
@@ -64,7 +67,9 @@ export const groupsCommand = (
|
|
|
64
67
|
|
|
65
68
|
// Handle empty results
|
|
66
69
|
if (groups.length === 0) {
|
|
67
|
-
if (options.
|
|
70
|
+
if (options.json) {
|
|
71
|
+
console.log(JSON.stringify({ status: 'success', count: 0, groups: [] }, null, 2))
|
|
72
|
+
} else if (options.xml) {
|
|
68
73
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
69
74
|
console.log(`<groups_result>`)
|
|
70
75
|
console.log(` <status>success</status>`)
|
|
@@ -78,7 +83,31 @@ export const groupsCommand = (
|
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
// Output results
|
|
81
|
-
if (options.
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(
|
|
88
|
+
JSON.stringify(
|
|
89
|
+
{
|
|
90
|
+
status: 'success',
|
|
91
|
+
count: groups.length,
|
|
92
|
+
groups: groups.map((group) => ({
|
|
93
|
+
id: group.id,
|
|
94
|
+
...(group.name ? { name: group.name } : {}),
|
|
95
|
+
...(group.description ? { description: group.description } : {}),
|
|
96
|
+
...(group.owner ? { owner: group.owner } : {}),
|
|
97
|
+
...(group.owner_id ? { owner_id: group.owner_id } : {}),
|
|
98
|
+
...(group.group_id !== undefined ? { group_id: group.group_id } : {}),
|
|
99
|
+
...(group.options?.visible_to_all !== undefined
|
|
100
|
+
? { visible_to_all: group.options.visible_to_all }
|
|
101
|
+
: {}),
|
|
102
|
+
...(group.created_on ? { created_on: group.created_on } : {}),
|
|
103
|
+
...(group.url ? { url: group.url } : {}),
|
|
104
|
+
})),
|
|
105
|
+
},
|
|
106
|
+
null,
|
|
107
|
+
2,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
} else if (options.xml) {
|
|
82
111
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
83
112
|
console.log(`<groups_result>`)
|
|
84
113
|
console.log(` <status>success</status>`)
|