@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
package/src/cli/commands/vote.ts
CHANGED
|
@@ -8,6 +8,7 @@ interface VoteOptions {
|
|
|
8
8
|
label?: string[]
|
|
9
9
|
message?: string
|
|
10
10
|
xml?: boolean
|
|
11
|
+
json?: boolean
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -92,7 +93,20 @@ export const voteCommand = (
|
|
|
92
93
|
yield* gerritApi.postReview(changeId, reviewInput)
|
|
93
94
|
|
|
94
95
|
// Output success
|
|
95
|
-
if (options.
|
|
96
|
+
if (options.json) {
|
|
97
|
+
console.log(
|
|
98
|
+
JSON.stringify(
|
|
99
|
+
{
|
|
100
|
+
status: 'success',
|
|
101
|
+
change_id: changeId,
|
|
102
|
+
labels,
|
|
103
|
+
...(options.message ? { message: options.message } : {}),
|
|
104
|
+
},
|
|
105
|
+
null,
|
|
106
|
+
2,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
} else if (options.xml) {
|
|
96
110
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
97
111
|
console.log(`<vote_result>`)
|
|
98
112
|
console.log(` <status>success</status>`)
|
|
@@ -7,6 +7,7 @@ import { type ConfigError, ConfigService } from '@/services/config'
|
|
|
7
7
|
|
|
8
8
|
interface WorkspaceOptions {
|
|
9
9
|
xml?: boolean
|
|
10
|
+
json?: boolean
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const parseChangeSpec = (changeSpec: string): { changeId: string; patchset?: string } => {
|
|
@@ -129,7 +130,11 @@ export const workspaceCommand = (
|
|
|
129
130
|
|
|
130
131
|
// Check if worktree already exists
|
|
131
132
|
if (fs.existsSync(workspaceDir)) {
|
|
132
|
-
if (options.
|
|
133
|
+
if (options.json) {
|
|
134
|
+
console.log(
|
|
135
|
+
JSON.stringify({ status: 'success', path: workspaceDir, exists: true }, null, 2),
|
|
136
|
+
)
|
|
137
|
+
} else if (options.xml) {
|
|
133
138
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
134
139
|
console.log(`<workspace>`)
|
|
135
140
|
console.log(` <path>${workspaceDir}</path>`)
|
|
@@ -150,7 +155,7 @@ export const workspaceCommand = (
|
|
|
150
155
|
|
|
151
156
|
// Fetch the change ref
|
|
152
157
|
const changeRef = revision.ref
|
|
153
|
-
if (!options.xml) {
|
|
158
|
+
if (!options.xml && !options.json) {
|
|
154
159
|
console.log(`Fetching change ${change._number}: ${change.subject}`)
|
|
155
160
|
}
|
|
156
161
|
|
|
@@ -168,7 +173,7 @@ export const workspaceCommand = (
|
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
// Create worktree
|
|
171
|
-
if (!options.xml) {
|
|
176
|
+
if (!options.xml && !options.json) {
|
|
172
177
|
console.log(`Creating worktree at: ${workspaceDir}`)
|
|
173
178
|
}
|
|
174
179
|
|
|
@@ -185,7 +190,21 @@ export const workspaceCommand = (
|
|
|
185
190
|
throw new Error(`Failed to create worktree: ${error}`)
|
|
186
191
|
}
|
|
187
192
|
|
|
188
|
-
if (options.
|
|
193
|
+
if (options.json) {
|
|
194
|
+
console.log(
|
|
195
|
+
JSON.stringify(
|
|
196
|
+
{
|
|
197
|
+
status: 'success',
|
|
198
|
+
path: workspaceDir,
|
|
199
|
+
change_number: change._number,
|
|
200
|
+
subject: change.subject,
|
|
201
|
+
created: true,
|
|
202
|
+
},
|
|
203
|
+
null,
|
|
204
|
+
2,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
} else if (options.xml) {
|
|
189
208
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
190
209
|
console.log(`<workspace>`)
|
|
191
210
|
console.log(` <path>${workspaceDir}</path>`)
|
|
@@ -33,10 +33,16 @@ import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
|
33
33
|
import { registerGroupCommands } from './register-group-commands'
|
|
34
34
|
import { registerReviewerCommands } from './register-reviewer-commands'
|
|
35
35
|
|
|
36
|
-
// Helper function to output error in plain text or XML format
|
|
37
|
-
function outputError(
|
|
36
|
+
// Helper function to output error in plain text, JSON, or XML format
|
|
37
|
+
function outputError(
|
|
38
|
+
error: unknown,
|
|
39
|
+
options: { xml?: boolean; json?: boolean },
|
|
40
|
+
resultTag: string,
|
|
41
|
+
): void {
|
|
38
42
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
39
|
-
if (options.
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
45
|
+
} else if (options.xml) {
|
|
40
46
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
41
47
|
console.log(`<${resultTag}>`)
|
|
42
48
|
console.log(` <status>error</status>`)
|
|
@@ -50,9 +56,13 @@ function outputError(error: unknown, options: { xml?: boolean }, resultTag: stri
|
|
|
50
56
|
// Helper function to execute Effect with standard error handling
|
|
51
57
|
async function executeEffect<E>(
|
|
52
58
|
effect: Effect.Effect<void, E, never>,
|
|
53
|
-
options: { xml?: boolean },
|
|
59
|
+
options: { xml?: boolean; json?: boolean },
|
|
54
60
|
resultTag: string,
|
|
55
61
|
): Promise<void> {
|
|
62
|
+
if (options.xml && options.json) {
|
|
63
|
+
outputError(new Error('--xml and --json are mutually exclusive'), options, resultTag)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
56
66
|
try {
|
|
57
67
|
await Effect.runPromise(effect)
|
|
58
68
|
} catch (error) {
|
|
@@ -83,6 +93,7 @@ export function registerCommands(program: Command): void {
|
|
|
83
93
|
.command('status')
|
|
84
94
|
.description('Check connection status')
|
|
85
95
|
.option('--xml', 'XML output for LLM consumption')
|
|
96
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
86
97
|
.action(async (options) => {
|
|
87
98
|
await executeEffect(
|
|
88
99
|
statusCommand(options).pipe(
|
|
@@ -105,9 +116,14 @@ export function registerCommands(program: Command): void {
|
|
|
105
116
|
'Line number in the NEW version of the file (not diff line numbers)',
|
|
106
117
|
parseInt,
|
|
107
118
|
)
|
|
119
|
+
.option(
|
|
120
|
+
'--reply-to <comment-id>',
|
|
121
|
+
'Reply to a comment thread (requires --file and --line; resolves thread by default)',
|
|
122
|
+
)
|
|
108
123
|
.option('--unresolved', 'Mark comment as unresolved (requires human attention)')
|
|
109
124
|
.option('--batch', 'Read batch comments from stdin as JSON (see examples below)')
|
|
110
125
|
.option('--xml', 'XML output for LLM consumption')
|
|
126
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
111
127
|
.addHelpText('after', COMMENT_HELP_TEXT)
|
|
112
128
|
.action(async (changeId, options) => {
|
|
113
129
|
await executeEffect(
|
|
@@ -125,6 +141,7 @@ export function registerCommands(program: Command): void {
|
|
|
125
141
|
.command('diff <change-id>')
|
|
126
142
|
.description('Get diff for a change (accepts change number or Change-ID)')
|
|
127
143
|
.option('--xml', 'XML output for LLM consumption')
|
|
144
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
128
145
|
.option('--file <file>', 'Specific file to diff')
|
|
129
146
|
.option('--files-only', 'List changed files only')
|
|
130
147
|
.option('--format <format>', 'Output format (unified, json, files)')
|
|
@@ -144,6 +161,7 @@ export function registerCommands(program: Command): void {
|
|
|
144
161
|
.command('mine')
|
|
145
162
|
.description('Show your open changes')
|
|
146
163
|
.option('--xml', 'XML output for LLM consumption')
|
|
164
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
147
165
|
.action(async (options) => {
|
|
148
166
|
await executeEffect(
|
|
149
167
|
mineCommand(options).pipe(
|
|
@@ -160,6 +178,7 @@ export function registerCommands(program: Command): void {
|
|
|
160
178
|
.command('search [query]')
|
|
161
179
|
.description('Search changes using Gerrit query syntax')
|
|
162
180
|
.option('--xml', 'XML output for LLM consumption')
|
|
181
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
163
182
|
.option('-n, --limit <number>', 'Limit results (default: 25)')
|
|
164
183
|
.addHelpText('after', SEARCH_HELP_TEXT)
|
|
165
184
|
.action(async (query, options) => {
|
|
@@ -168,16 +187,17 @@ export function registerCommands(program: Command): void {
|
|
|
168
187
|
Effect.provide(ConfigServiceLive),
|
|
169
188
|
)
|
|
170
189
|
await Effect.runPromise(effect).catch((error: unknown) => {
|
|
171
|
-
|
|
190
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
191
|
+
if (options.json) {
|
|
192
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
193
|
+
} else if (options.xml) {
|
|
172
194
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
173
195
|
console.log(`<search_result>`)
|
|
174
196
|
console.log(` <status>error</status>`)
|
|
175
|
-
console.log(
|
|
176
|
-
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
177
|
-
)
|
|
197
|
+
console.log(` <error><![CDATA[${errorMessage}]]></error>`)
|
|
178
198
|
console.log(`</search_result>`)
|
|
179
199
|
} else {
|
|
180
|
-
console.error('✗ Error:',
|
|
200
|
+
console.error('✗ Error:', errorMessage)
|
|
181
201
|
}
|
|
182
202
|
process.exit(1)
|
|
183
203
|
})
|
|
@@ -190,6 +210,7 @@ export function registerCommands(program: Command): void {
|
|
|
190
210
|
'Create or switch to a git worktree for a Gerrit change (accepts change number or Change-ID)',
|
|
191
211
|
)
|
|
192
212
|
.option('--xml', 'XML output for LLM consumption')
|
|
213
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
193
214
|
.action(async (changeId, options) => {
|
|
194
215
|
await executeEffect(
|
|
195
216
|
workspaceCommand(changeId, options).pipe(
|
|
@@ -206,6 +227,7 @@ export function registerCommands(program: Command): void {
|
|
|
206
227
|
.command('incoming')
|
|
207
228
|
.description('Show incoming changes for review (where you are a reviewer)')
|
|
208
229
|
.option('--xml', 'XML output for LLM consumption')
|
|
230
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
209
231
|
.option('-i, --interactive', 'Interactive mode with detailed view and diff')
|
|
210
232
|
.action(async (options) => {
|
|
211
233
|
await executeEffect(
|
|
@@ -226,6 +248,7 @@ export function registerCommands(program: Command): void {
|
|
|
226
248
|
)
|
|
227
249
|
.option('-m, --message <message>', 'Abandon message')
|
|
228
250
|
.option('--xml', 'XML output for LLM consumption')
|
|
251
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
229
252
|
.action(async (changeId, options) => {
|
|
230
253
|
await executeEffect(
|
|
231
254
|
abandonCommand(changeId, options).pipe(
|
|
@@ -243,6 +266,7 @@ export function registerCommands(program: Command): void {
|
|
|
243
266
|
.description('Restore an abandoned change (accepts change number or Change-ID)')
|
|
244
267
|
.option('-m, --message <message>', 'Restoration message')
|
|
245
268
|
.option('--xml', 'XML output for LLM consumption')
|
|
269
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
246
270
|
.action(async (changeId, options) => {
|
|
247
271
|
await executeEffect(
|
|
248
272
|
restoreCommand(changeId, options).pipe(
|
|
@@ -260,6 +284,7 @@ export function registerCommands(program: Command): void {
|
|
|
260
284
|
.description('Rebase a change onto target branch (auto-detects from HEAD if not provided)')
|
|
261
285
|
.option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
|
|
262
286
|
.option('--xml', 'XML output for LLM consumption')
|
|
287
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
263
288
|
.action(async (changeId, options) => {
|
|
264
289
|
await executeEffect(
|
|
265
290
|
rebaseCommand(changeId, options).pipe(
|
|
@@ -276,6 +301,7 @@ export function registerCommands(program: Command): void {
|
|
|
276
301
|
.command('submit <change-id>')
|
|
277
302
|
.description('Submit a change for merging (accepts change number or Change-ID)')
|
|
278
303
|
.option('--xml', 'XML output for LLM consumption')
|
|
304
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
279
305
|
.action(async (changeId, options) => {
|
|
280
306
|
await executeEffect(
|
|
281
307
|
submitCommand(changeId, options).pipe(
|
|
@@ -293,6 +319,7 @@ export function registerCommands(program: Command): void {
|
|
|
293
319
|
.description('Get, set, or remove topic for a change (auto-detects from HEAD if not specified)')
|
|
294
320
|
.option('--delete', 'Remove the topic from the change')
|
|
295
321
|
.option('--xml', 'XML output for LLM consumption')
|
|
322
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
296
323
|
.addHelpText('after', TOPIC_HELP_TEXT)
|
|
297
324
|
.action(async (changeId, topic, options) => {
|
|
298
325
|
await executeEffect(
|
|
@@ -314,6 +341,7 @@ export function registerCommands(program: Command): void {
|
|
|
314
341
|
.option('--label <name> <value>', 'Custom label vote (can be used multiple times)')
|
|
315
342
|
.option('-m, --message <message>', 'Comment with vote')
|
|
316
343
|
.option('--xml', 'XML output for LLM consumption')
|
|
344
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
317
345
|
.action(async (changeId, options) => {
|
|
318
346
|
await executeEffect(
|
|
319
347
|
voteCommand(changeId, options).pipe(
|
|
@@ -334,6 +362,7 @@ export function registerCommands(program: Command): void {
|
|
|
334
362
|
.description('List Gerrit projects')
|
|
335
363
|
.option('--pattern <regex>', 'Filter projects by name pattern')
|
|
336
364
|
.option('--xml', 'XML output for LLM consumption')
|
|
365
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
337
366
|
.action(async (options) => {
|
|
338
367
|
await executeEffect(
|
|
339
368
|
projectsCommand(options).pipe(
|
|
@@ -355,6 +384,7 @@ export function registerCommands(program: Command): void {
|
|
|
355
384
|
'Show all comments on a change with diff context (accepts change number or Change-ID)',
|
|
356
385
|
)
|
|
357
386
|
.option('--xml', 'XML output for LLM consumption')
|
|
387
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
358
388
|
.action(async (changeId, options) => {
|
|
359
389
|
await executeEffect(
|
|
360
390
|
commentsCommand(changeId, options).pipe(
|
|
@@ -523,6 +553,7 @@ Note:
|
|
|
523
553
|
.description('Install the Gerrit commit-msg hook for automatic Change-Id generation')
|
|
524
554
|
.option('--force', 'Overwrite existing hook')
|
|
525
555
|
.option('--xml', 'XML output for LLM consumption')
|
|
556
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
526
557
|
.addHelpText(
|
|
527
558
|
'after',
|
|
528
559
|
`
|
|
@@ -623,10 +654,8 @@ Note:
|
|
|
623
654
|
`
|
|
624
655
|
This command uses AI (claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
|
|
625
656
|
It performs a two-stage review process:
|
|
626
|
-
|
|
627
657
|
1. Generates inline comments for specific code issues
|
|
628
658
|
2. Generates an overall review comment
|
|
629
|
-
|
|
630
659
|
By default, the review is only displayed in the terminal.
|
|
631
660
|
Use --comment to post the review to Gerrit (with confirmation prompts).
|
|
632
661
|
Use --comment --yes to post without confirmation.
|
|
@@ -636,22 +665,11 @@ Requirements:
|
|
|
636
665
|
- Gerrit credentials must be configured (run 'ger setup' first)
|
|
637
666
|
|
|
638
667
|
Examples:
|
|
639
|
-
# Review a change using change number (display only)
|
|
640
668
|
$ ger review 12345
|
|
641
|
-
|
|
642
|
-
# Review using Change-ID
|
|
643
669
|
$ ger review If5a3ae8cb5a107e187447802358417f311d0c4b1
|
|
644
|
-
|
|
645
|
-
# Review and prompt to post comments
|
|
646
670
|
$ ger review 12345 --comment
|
|
647
|
-
|
|
648
|
-
# Review and auto-post comments without prompting
|
|
649
671
|
$ ger review 12345 --comment --yes
|
|
650
|
-
|
|
651
|
-
# Use specific AI tool
|
|
652
672
|
$ ger review 12345 --tool gemini
|
|
653
|
-
|
|
654
|
-
# Show debug output to troubleshoot issues
|
|
655
673
|
$ ger review 12345 --debug
|
|
656
674
|
|
|
657
675
|
Note: Both change number (e.g., 12345) and Change-ID (e.g., If5a3ae8...) formats are accepted
|
|
@@ -9,14 +9,26 @@ import { groupsMembersCommand } from './commands/groups-members'
|
|
|
9
9
|
// Helper function to execute Effect with standard error handling
|
|
10
10
|
async function executeEffect<E>(
|
|
11
11
|
effect: Effect.Effect<void, E, never>,
|
|
12
|
-
options: { xml?: boolean },
|
|
12
|
+
options: { xml?: boolean; json?: boolean },
|
|
13
13
|
resultTag: string,
|
|
14
14
|
): Promise<void> {
|
|
15
|
+
if (options.xml && options.json) {
|
|
16
|
+
console.log(
|
|
17
|
+
JSON.stringify(
|
|
18
|
+
{ status: 'error', error: '--xml and --json are mutually exclusive' },
|
|
19
|
+
null,
|
|
20
|
+
2,
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
15
25
|
try {
|
|
16
26
|
await Effect.runPromise(effect)
|
|
17
27
|
} catch (error) {
|
|
18
28
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
19
|
-
if (options.
|
|
29
|
+
if (options.json) {
|
|
30
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
31
|
+
} else if (options.xml) {
|
|
20
32
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
21
33
|
console.log(`<${resultTag}>`)
|
|
22
34
|
console.log(` <status>error</status>`)
|
|
@@ -43,6 +55,7 @@ export function registerGroupCommands(program: Command): void {
|
|
|
43
55
|
.option('--user <account>', 'Show groups a user belongs to')
|
|
44
56
|
.option('--limit <n>', 'Maximum number of results (default: 25)')
|
|
45
57
|
.option('--xml', 'XML output for LLM consumption')
|
|
58
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
46
59
|
.action(async (options) => {
|
|
47
60
|
await executeEffect(
|
|
48
61
|
groupsCommand(options).pipe(
|
|
@@ -59,6 +72,7 @@ export function registerGroupCommands(program: Command): void {
|
|
|
59
72
|
.command('groups-show <group-id>')
|
|
60
73
|
.description('Show detailed information about a Gerrit group')
|
|
61
74
|
.option('--xml', 'XML output for LLM consumption')
|
|
75
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
62
76
|
.action(async (groupId, options) => {
|
|
63
77
|
await executeEffect(
|
|
64
78
|
groupsShowCommand(groupId, options).pipe(
|
|
@@ -75,6 +89,7 @@ export function registerGroupCommands(program: Command): void {
|
|
|
75
89
|
.command('groups-members <group-id>')
|
|
76
90
|
.description('List all members of a Gerrit group')
|
|
77
91
|
.option('--xml', 'XML output for LLM consumption')
|
|
92
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
78
93
|
.action(async (groupId, options) => {
|
|
79
94
|
await executeEffect(
|
|
80
95
|
groupsMembersCommand(groupId, options).pipe(
|
|
@@ -8,14 +8,26 @@ import { removeReviewerCommand } from './commands/remove-reviewer'
|
|
|
8
8
|
// Helper function to execute Effect with standard error handling
|
|
9
9
|
async function executeEffect<E>(
|
|
10
10
|
effect: Effect.Effect<void, E, never>,
|
|
11
|
-
options: { xml?: boolean },
|
|
11
|
+
options: { xml?: boolean; json?: boolean },
|
|
12
12
|
resultTag: string,
|
|
13
13
|
): Promise<void> {
|
|
14
|
+
if (options.xml && options.json) {
|
|
15
|
+
console.log(
|
|
16
|
+
JSON.stringify(
|
|
17
|
+
{ status: 'error', error: '--xml and --json are mutually exclusive' },
|
|
18
|
+
null,
|
|
19
|
+
2,
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
14
24
|
try {
|
|
15
25
|
await Effect.runPromise(effect)
|
|
16
26
|
} catch (error) {
|
|
17
27
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
18
|
-
if (options.
|
|
28
|
+
if (options.json) {
|
|
29
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
30
|
+
} else if (options.xml) {
|
|
19
31
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
20
32
|
console.log(`<${resultTag}>`)
|
|
21
33
|
console.log(` <status>error</status>`)
|
|
@@ -44,6 +56,7 @@ export function registerReviewerCommands(program: Command): void {
|
|
|
44
56
|
'Notification level: none, owner, owner_reviewers, all (default: all)',
|
|
45
57
|
)
|
|
46
58
|
.option('--xml', 'XML output for LLM consumption')
|
|
59
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
47
60
|
.addHelpText(
|
|
48
61
|
'after',
|
|
49
62
|
`
|
|
@@ -76,6 +89,7 @@ Examples:
|
|
|
76
89
|
'Notification level: none, owner, owner_reviewers, all (default: all)',
|
|
77
90
|
)
|
|
78
91
|
.option('--xml', 'XML output for LLM consumption')
|
|
92
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
79
93
|
.addHelpText(
|
|
80
94
|
'after',
|
|
81
95
|
`
|
package/src/schemas/gerrit.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Schema } from '@effect/schema'
|
|
2
2
|
|
|
3
|
-
// Authentication schemas
|
|
4
3
|
export const GerritCredentials: Schema.Schema<{
|
|
5
4
|
readonly host: string
|
|
6
5
|
readonly username: string
|
|
@@ -21,7 +20,6 @@ export const GerritCredentials: Schema.Schema<{
|
|
|
21
20
|
})
|
|
22
21
|
export type GerritCredentials = Schema.Schema.Type<typeof GerritCredentials>
|
|
23
22
|
|
|
24
|
-
// Forward declare RevisionInfo type for use in ChangeInfo
|
|
25
23
|
export interface RevisionInfoType {
|
|
26
24
|
readonly kind?: string
|
|
27
25
|
readonly _number: number
|
|
@@ -65,7 +63,22 @@ export interface RevisionInfoType {
|
|
|
65
63
|
>
|
|
66
64
|
}
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
type ChangeReviewerAccount = {
|
|
67
|
+
readonly _account_id?: number
|
|
68
|
+
readonly name?: string
|
|
69
|
+
readonly email?: string
|
|
70
|
+
readonly username?: string
|
|
71
|
+
}
|
|
72
|
+
const ChangeReviewerAccountInfo: Schema.Schema<ChangeReviewerAccount> = Schema.Struct({
|
|
73
|
+
_account_id: Schema.optional(Schema.Number),
|
|
74
|
+
name: Schema.optional(Schema.String),
|
|
75
|
+
email: Schema.optional(Schema.String),
|
|
76
|
+
username: Schema.optional(Schema.String),
|
|
77
|
+
})
|
|
78
|
+
type ChangeReviewerMap = Partial<
|
|
79
|
+
Record<'REVIEWER' | 'CC' | 'REMOVED', ReadonlyArray<ChangeReviewerAccount>>
|
|
80
|
+
>
|
|
81
|
+
|
|
69
82
|
export const ChangeInfo: Schema.Schema<{
|
|
70
83
|
readonly id: string
|
|
71
84
|
readonly project: string
|
|
@@ -119,6 +132,7 @@ export const ChangeInfo: Schema.Schema<{
|
|
|
119
132
|
readonly current_revision?: string
|
|
120
133
|
readonly revisions?: Record<string, RevisionInfoType>
|
|
121
134
|
readonly topic?: string
|
|
135
|
+
readonly reviewers?: ChangeReviewerMap
|
|
122
136
|
}> = Schema.Struct({
|
|
123
137
|
id: Schema.String,
|
|
124
138
|
project: Schema.String,
|
|
@@ -184,10 +198,16 @@ export const ChangeInfo: Schema.Schema<{
|
|
|
184
198
|
current_revision: Schema.optional(Schema.String),
|
|
185
199
|
revisions: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Any })),
|
|
186
200
|
topic: Schema.optional(Schema.String),
|
|
201
|
+
reviewers: Schema.optional(
|
|
202
|
+
Schema.Struct({
|
|
203
|
+
REVIEWER: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
|
|
204
|
+
CC: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
|
|
205
|
+
REMOVED: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
187
208
|
})
|
|
188
209
|
export type ChangeInfo = Schema.Schema.Type<typeof ChangeInfo>
|
|
189
210
|
|
|
190
|
-
// Comment schemas
|
|
191
211
|
export const CommentInput: Schema.Schema<{
|
|
192
212
|
readonly message: string
|
|
193
213
|
readonly unresolved?: boolean
|
|
@@ -200,7 +220,6 @@ export const CommentInput: Schema.Schema<{
|
|
|
200
220
|
})
|
|
201
221
|
export type CommentInput = Schema.Schema.Type<typeof CommentInput>
|
|
202
222
|
|
|
203
|
-
// Comment info returned from API
|
|
204
223
|
export const CommentInfo: Schema.Schema<{
|
|
205
224
|
readonly id: string
|
|
206
225
|
readonly path?: string
|
|
@@ -246,7 +265,6 @@ export const CommentInfo: Schema.Schema<{
|
|
|
246
265
|
})
|
|
247
266
|
export type CommentInfo = Schema.Schema.Type<typeof CommentInfo>
|
|
248
267
|
|
|
249
|
-
// Message info for review messages
|
|
250
268
|
export const MessageInfo: Schema.Schema<{
|
|
251
269
|
readonly id: string
|
|
252
270
|
readonly message: string
|
|
@@ -290,6 +308,7 @@ export const ReviewInput: Schema.Schema<{
|
|
|
290
308
|
readonly message: string
|
|
291
309
|
readonly side?: 'PARENT' | 'REVISION'
|
|
292
310
|
readonly unresolved?: boolean
|
|
311
|
+
readonly in_reply_to?: string
|
|
293
312
|
}>
|
|
294
313
|
>
|
|
295
314
|
}> = Schema.Struct({
|
|
@@ -312,6 +331,7 @@ export const ReviewInput: Schema.Schema<{
|
|
|
312
331
|
message: Schema.String,
|
|
313
332
|
side: Schema.optional(Schema.Literal('PARENT', 'REVISION')),
|
|
314
333
|
unresolved: Schema.optional(Schema.Boolean),
|
|
334
|
+
in_reply_to: Schema.optional(Schema.String),
|
|
315
335
|
}),
|
|
316
336
|
),
|
|
317
337
|
}),
|
|
@@ -319,7 +339,6 @@ export const ReviewInput: Schema.Schema<{
|
|
|
319
339
|
})
|
|
320
340
|
export type ReviewInput = Schema.Schema.Type<typeof ReviewInput>
|
|
321
341
|
|
|
322
|
-
// Project schema
|
|
323
342
|
export const ProjectInfo: Schema.Schema<{
|
|
324
343
|
readonly id: string
|
|
325
344
|
readonly name: string
|
|
@@ -333,7 +352,6 @@ export const ProjectInfo: Schema.Schema<{
|
|
|
333
352
|
})
|
|
334
353
|
export type ProjectInfo = Schema.Schema.Type<typeof ProjectInfo>
|
|
335
354
|
|
|
336
|
-
// File and diff schemas
|
|
337
355
|
export const FileInfo: Schema.Schema<{
|
|
338
356
|
readonly status?: 'A' | 'D' | 'R' | 'C' | 'M'
|
|
339
357
|
readonly lines_inserted?: number
|
|
@@ -503,17 +521,14 @@ export type DiffOptions = Schema.Schema.Type<typeof DiffOptions>
|
|
|
503
521
|
// Command options schemas
|
|
504
522
|
export const DiffCommandOptions: Schema.Schema<{
|
|
505
523
|
readonly xml?: boolean
|
|
524
|
+
readonly json?: boolean
|
|
506
525
|
readonly file?: string
|
|
507
526
|
readonly filesOnly?: boolean
|
|
508
527
|
readonly format?: 'unified' | 'json' | 'files'
|
|
509
528
|
}> = Schema.Struct({
|
|
510
529
|
xml: Schema.optional(Schema.Boolean),
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
Schema.minLength(1),
|
|
514
|
-
Schema.annotations({ description: 'File path for diff (relative to repo root)' }),
|
|
515
|
-
),
|
|
516
|
-
),
|
|
530
|
+
json: Schema.optional(Schema.Boolean),
|
|
531
|
+
file: Schema.optional(Schema.String.pipe(Schema.minLength(1))),
|
|
517
532
|
filesOnly: Schema.optional(Schema.Boolean),
|
|
518
533
|
format: Schema.optional(DiffFormat),
|
|
519
534
|
})
|
|
@@ -135,11 +135,9 @@ export const getHooksDir = (): string => {
|
|
|
135
135
|
export interface CommitHookServiceImpl {
|
|
136
136
|
readonly hasHook: () => Effect.Effect<boolean, NotGitRepoError>
|
|
137
137
|
readonly hasChangeId: (commit?: string) => Effect.Effect<boolean, NotGitRepoError>
|
|
138
|
-
readonly installHook: (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
ConfigServiceImpl
|
|
142
|
-
>
|
|
138
|
+
readonly installHook: (
|
|
139
|
+
quiet?: boolean,
|
|
140
|
+
) => Effect.Effect<void, HookInstallError | NotGitRepoError, ConfigServiceImpl>
|
|
143
141
|
readonly ensureChangeId: () => Effect.Effect<
|
|
144
142
|
void,
|
|
145
143
|
HookInstallError | MissingChangeIdError | NotGitRepoError,
|
|
@@ -161,7 +159,7 @@ const CommitHookServiceImplLive: CommitHookServiceImpl = {
|
|
|
161
159
|
catch: () => new NotGitRepoError({ message: 'Not in a git repository' }),
|
|
162
160
|
}),
|
|
163
161
|
|
|
164
|
-
installHook: () =>
|
|
162
|
+
installHook: (quiet = false) =>
|
|
165
163
|
Effect.gen(function* () {
|
|
166
164
|
const configService = yield* ConfigService
|
|
167
165
|
|
|
@@ -177,7 +175,7 @@ const CommitHookServiceImplLive: CommitHookServiceImpl = {
|
|
|
177
175
|
const normalizedHost = config.host.replace(/\/$/, '')
|
|
178
176
|
const hookUrl = `${normalizedHost}/tools/hooks/commit-msg`
|
|
179
177
|
|
|
180
|
-
yield* Console.log(`Installing commit-msg hook from ${config.host}...`)
|
|
178
|
+
if (!quiet) yield* Console.log(`Installing commit-msg hook from ${config.host}...`)
|
|
181
179
|
|
|
182
180
|
const hookContent = yield* Effect.tryPromise({
|
|
183
181
|
try: async () => {
|
|
@@ -236,7 +234,7 @@ const CommitHookServiceImplLive: CommitHookServiceImpl = {
|
|
|
236
234
|
}),
|
|
237
235
|
})
|
|
238
236
|
|
|
239
|
-
yield* Console.log('commit-msg hook installed successfully')
|
|
237
|
+
if (!quiet) yield* Console.log('commit-msg hook installed successfully')
|
|
240
238
|
}),
|
|
241
239
|
|
|
242
240
|
ensureChangeId: () =>
|