@aaronshaf/ger 3.0.1 → 4.0.0
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/prd/commands.md +59 -1
- package/package.json +1 -1
- package/src/api/gerrit-types.ts +121 -0
- package/src/api/gerrit.ts +68 -102
- package/src/cli/commands/analyze.ts +284 -0
- package/src/cli/commands/cherry.ts +268 -0
- package/src/cli/commands/failures.ts +74 -0
- package/src/cli/commands/files.ts +86 -0
- package/src/cli/commands/list.ts +239 -0
- package/src/cli/commands/rebase.ts +5 -1
- package/src/cli/commands/retrigger.ts +91 -0
- package/src/cli/commands/reviewers.ts +95 -0
- package/src/cli/commands/setup.ts +19 -6
- package/src/cli/commands/tree-cleanup.ts +139 -0
- package/src/cli/commands/tree-rebase.ts +168 -0
- package/src/cli/commands/tree-setup.ts +202 -0
- package/src/cli/commands/trees.ts +107 -0
- package/src/cli/commands/update.ts +73 -0
- package/src/cli/register-analytics-commands.ts +90 -0
- package/src/cli/register-commands.ts +96 -40
- package/src/cli/register-list-commands.ts +105 -0
- package/src/cli/register-tree-commands.ts +128 -0
- package/src/schemas/config.ts +3 -0
- package/src/schemas/gerrit.ts +2 -0
- package/src/schemas/reviewer.ts +16 -0
- package/src/services/config.ts +15 -0
- package/tests/analyze.test.ts +197 -0
- package/tests/cherry.test.ts +208 -0
- package/tests/failures.test.ts +212 -0
- package/tests/files.test.ts +223 -0
- package/tests/helpers/config-mock.ts +4 -0
- package/tests/list.test.ts +220 -0
- package/tests/retrigger.test.ts +159 -0
- package/tests/reviewers.test.ts +259 -0
- package/tests/tree.test.ts +517 -0
- package/tests/update.test.ts +86 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
4
|
+
import { ConfigServiceLive } from '@/services/config'
|
|
5
|
+
import { analyzeCommand } from './commands/analyze'
|
|
6
|
+
import { failuresCommand } from './commands/failures'
|
|
7
|
+
import { updateCommand } from './commands/update'
|
|
8
|
+
|
|
9
|
+
function executeEffect<E>(
|
|
10
|
+
effect: Effect.Effect<void, E, never>,
|
|
11
|
+
options: { xml?: boolean; json?: boolean },
|
|
12
|
+
resultTag: string,
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
if (options.xml && options.json) {
|
|
15
|
+
console.error('✗ Error: --xml and --json are mutually exclusive')
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
return Effect.runPromise(effect).catch((error: unknown) => {
|
|
19
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
20
|
+
if (options.json) {
|
|
21
|
+
console.log(JSON.stringify({ status: 'error', error: msg }, null, 2))
|
|
22
|
+
} else if (options.xml) {
|
|
23
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
24
|
+
console.log(`<${resultTag}>`)
|
|
25
|
+
console.log(` <status>error</status>`)
|
|
26
|
+
console.log(` <error><![CDATA[${msg}]]></error>`)
|
|
27
|
+
console.log(`</${resultTag}>`)
|
|
28
|
+
} else {
|
|
29
|
+
console.error('✗ Error:', msg)
|
|
30
|
+
}
|
|
31
|
+
process.exit(1)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerAnalyticsCommands(program: Command): void {
|
|
36
|
+
// update command
|
|
37
|
+
program
|
|
38
|
+
.command('update')
|
|
39
|
+
.description('Update ger to the latest version')
|
|
40
|
+
.option('--skip-pull', 'Skip version check and install directly')
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
await executeEffect(updateCommand({ skipPull: options.skipPull }), options, 'update_result')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// failures command
|
|
46
|
+
program
|
|
47
|
+
.command('failures <change-id>')
|
|
48
|
+
.description('Get the most recent build failure link from Service Cloud Jenkins')
|
|
49
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
50
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
51
|
+
.action(async (changeId, options) => {
|
|
52
|
+
await executeEffect(
|
|
53
|
+
failuresCommand(changeId, options).pipe(
|
|
54
|
+
Effect.provide(GerritApiServiceLive),
|
|
55
|
+
Effect.provide(ConfigServiceLive),
|
|
56
|
+
),
|
|
57
|
+
options,
|
|
58
|
+
'failures_result',
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// analyze command
|
|
63
|
+
program
|
|
64
|
+
.command('analyze')
|
|
65
|
+
.description('Show contribution analytics for merged changes')
|
|
66
|
+
.option('--start-date <date>', 'Start date (YYYY-MM-DD, default: Jan 1 of current year)')
|
|
67
|
+
.option('--end-date <date>', 'End date (YYYY-MM-DD, default: today)')
|
|
68
|
+
.option('--repo <project>', 'Filter by Gerrit project name')
|
|
69
|
+
.option('--json', 'JSON output')
|
|
70
|
+
.option('--xml', 'XML output')
|
|
71
|
+
.option('--markdown', 'Markdown output')
|
|
72
|
+
.option('--csv', 'CSV output')
|
|
73
|
+
.option('--output <file>', 'Write output to file')
|
|
74
|
+
.action(async (options) => {
|
|
75
|
+
await executeEffect(
|
|
76
|
+
analyzeCommand({
|
|
77
|
+
startDate: options.startDate,
|
|
78
|
+
endDate: options.endDate,
|
|
79
|
+
repo: options.repo,
|
|
80
|
+
json: options.json,
|
|
81
|
+
xml: options.xml,
|
|
82
|
+
markdown: options.markdown,
|
|
83
|
+
csv: options.csv,
|
|
84
|
+
output: options.output,
|
|
85
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive)),
|
|
86
|
+
options,
|
|
87
|
+
'analyze_result',
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
@@ -15,9 +15,7 @@ import { commentCommand, COMMENT_HELP_TEXT } from './commands/comment'
|
|
|
15
15
|
import { commentsCommand } from './commands/comments'
|
|
16
16
|
import { diffCommand } from './commands/diff'
|
|
17
17
|
import { extractUrlCommand } from './commands/extract-url'
|
|
18
|
-
import { incomingCommand } from './commands/incoming'
|
|
19
18
|
import { installHookCommand } from './commands/install-hook'
|
|
20
|
-
import { mineCommand } from './commands/mine'
|
|
21
19
|
import { openCommand } from './commands/open'
|
|
22
20
|
import { pushCommand, PUSH_HELP_TEXT } from './commands/push'
|
|
23
21
|
import { searchCommand, SEARCH_HELP_TEXT } from './commands/search'
|
|
@@ -28,6 +26,13 @@ import { workspaceCommand } from './commands/workspace'
|
|
|
28
26
|
import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
29
27
|
import { registerGroupCommands } from './register-group-commands'
|
|
30
28
|
import { registerReviewerCommands } from './register-reviewer-commands'
|
|
29
|
+
import { registerTreeCommands } from './register-tree-commands'
|
|
30
|
+
import { filesCommand } from './commands/files'
|
|
31
|
+
import { reviewersCommand } from './commands/reviewers'
|
|
32
|
+
import { retriggerCommand, RETRIGGER_HELP_TEXT } from './commands/retrigger'
|
|
33
|
+
import { cherryCommand, CHERRY_HELP_TEXT } from './commands/cherry'
|
|
34
|
+
import { registerListCommands } from './register-list-commands'
|
|
35
|
+
import { registerAnalyticsCommands } from './register-analytics-commands'
|
|
31
36
|
|
|
32
37
|
// Helper function to output error in plain text, JSON, or XML format
|
|
33
38
|
function outputError(
|
|
@@ -152,22 +157,7 @@ export function registerCommands(program: Command): void {
|
|
|
152
157
|
)
|
|
153
158
|
})
|
|
154
159
|
|
|
155
|
-
|
|
156
|
-
program
|
|
157
|
-
.command('mine')
|
|
158
|
-
.description('Show your open changes')
|
|
159
|
-
.option('--xml', 'XML output for LLM consumption')
|
|
160
|
-
.option('--json', 'JSON output for programmatic consumption')
|
|
161
|
-
.action(async (options) => {
|
|
162
|
-
await executeEffect(
|
|
163
|
-
mineCommand(options).pipe(
|
|
164
|
-
Effect.provide(GerritApiServiceLive),
|
|
165
|
-
Effect.provide(ConfigServiceLive),
|
|
166
|
-
),
|
|
167
|
-
options,
|
|
168
|
-
'mine_result',
|
|
169
|
-
)
|
|
170
|
-
})
|
|
160
|
+
registerListCommands(program)
|
|
171
161
|
|
|
172
162
|
// search command
|
|
173
163
|
program
|
|
@@ -199,15 +189,16 @@ export function registerCommands(program: Command): void {
|
|
|
199
189
|
})
|
|
200
190
|
})
|
|
201
191
|
|
|
202
|
-
// workspace command
|
|
192
|
+
// workspace command (deprecated — use 'ger tree setup' instead)
|
|
203
193
|
program
|
|
204
194
|
.command('workspace <change-id>')
|
|
205
|
-
.description(
|
|
206
|
-
'Create or switch to a git worktree for a Gerrit change (accepts change number or Change-ID)',
|
|
207
|
-
)
|
|
195
|
+
.description('[deprecated: use "ger tree setup"] Create a git worktree for a Gerrit change')
|
|
208
196
|
.option('--xml', 'XML output for LLM consumption')
|
|
209
197
|
.option('--json', 'JSON output for programmatic consumption')
|
|
210
198
|
.action(async (changeId, options) => {
|
|
199
|
+
if (!options.xml && !options.json) {
|
|
200
|
+
console.error('Note: "ger workspace" is deprecated. Use "ger tree setup" instead.')
|
|
201
|
+
}
|
|
211
202
|
await executeEffect(
|
|
212
203
|
workspaceCommand(changeId, options).pipe(
|
|
213
204
|
Effect.provide(GerritApiServiceLive),
|
|
@@ -218,23 +209,7 @@ export function registerCommands(program: Command): void {
|
|
|
218
209
|
)
|
|
219
210
|
})
|
|
220
211
|
|
|
221
|
-
|
|
222
|
-
program
|
|
223
|
-
.command('incoming')
|
|
224
|
-
.description('Show incoming changes for review (where you are a reviewer)')
|
|
225
|
-
.option('--xml', 'XML output for LLM consumption')
|
|
226
|
-
.option('--json', 'JSON output for programmatic consumption')
|
|
227
|
-
.option('-i, --interactive', 'Interactive mode with detailed view and diff')
|
|
228
|
-
.action(async (options) => {
|
|
229
|
-
await executeEffect(
|
|
230
|
-
incomingCommand(options).pipe(
|
|
231
|
-
Effect.provide(GerritApiServiceLive),
|
|
232
|
-
Effect.provide(ConfigServiceLive),
|
|
233
|
-
),
|
|
234
|
-
options,
|
|
235
|
-
'incoming_result',
|
|
236
|
-
)
|
|
237
|
-
})
|
|
212
|
+
registerTreeCommands(program)
|
|
238
213
|
|
|
239
214
|
// abandon / restore / set-ready / set-wip commands
|
|
240
215
|
registerStateCommands(program)
|
|
@@ -244,11 +219,12 @@ export function registerCommands(program: Command): void {
|
|
|
244
219
|
.command('rebase [change-id]')
|
|
245
220
|
.description('Rebase a change onto target branch (auto-detects from HEAD if not provided)')
|
|
246
221
|
.option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
|
|
222
|
+
.option('--allow-conflicts', 'Allow rebasing even if conflicts exist')
|
|
247
223
|
.option('--xml', 'XML output for LLM consumption')
|
|
248
224
|
.option('--json', 'JSON output for programmatic consumption')
|
|
249
225
|
.action(async (changeId, options) => {
|
|
250
226
|
await executeEffect(
|
|
251
|
-
rebaseCommand(changeId, options).pipe(
|
|
227
|
+
rebaseCommand(changeId, { ...options, allowConflicts: options.allowConflicts }).pipe(
|
|
252
228
|
Effect.provide(GerritApiServiceLive),
|
|
253
229
|
Effect.provide(ConfigServiceLive),
|
|
254
230
|
),
|
|
@@ -338,6 +314,26 @@ export function registerCommands(program: Command): void {
|
|
|
338
314
|
// Register all group-related commands
|
|
339
315
|
registerGroupCommands(program)
|
|
340
316
|
|
|
317
|
+
// retrigger command
|
|
318
|
+
program
|
|
319
|
+
.command('retrigger [change-id]')
|
|
320
|
+
.description(
|
|
321
|
+
'Post the CI retrigger comment on a change (auto-detects from HEAD if no change-id given)',
|
|
322
|
+
)
|
|
323
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
324
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
325
|
+
.addHelpText('after', RETRIGGER_HELP_TEXT)
|
|
326
|
+
.action(async (changeId, options) => {
|
|
327
|
+
await executeEffect(
|
|
328
|
+
retriggerCommand(changeId as string | undefined, options).pipe(
|
|
329
|
+
Effect.provide(GerritApiServiceLive),
|
|
330
|
+
Effect.provide(ConfigServiceLive),
|
|
331
|
+
),
|
|
332
|
+
options,
|
|
333
|
+
'retrigger_result',
|
|
334
|
+
)
|
|
335
|
+
})
|
|
336
|
+
|
|
341
337
|
// comments command
|
|
342
338
|
program
|
|
343
339
|
.command('comments <change-id>')
|
|
@@ -578,6 +574,44 @@ Note:
|
|
|
578
574
|
}
|
|
579
575
|
})
|
|
580
576
|
|
|
577
|
+
// files command
|
|
578
|
+
program
|
|
579
|
+
.command('files [change-id]')
|
|
580
|
+
.description(
|
|
581
|
+
'List files changed in a Gerrit change (auto-detects from HEAD commit if not specified)',
|
|
582
|
+
)
|
|
583
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
584
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
585
|
+
.action(async (changeId, options) => {
|
|
586
|
+
await executeEffect(
|
|
587
|
+
filesCommand(changeId, options).pipe(
|
|
588
|
+
Effect.provide(GerritApiServiceLive),
|
|
589
|
+
Effect.provide(ConfigServiceLive),
|
|
590
|
+
),
|
|
591
|
+
options,
|
|
592
|
+
'files_result',
|
|
593
|
+
)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
// reviewers command
|
|
597
|
+
program
|
|
598
|
+
.command('reviewers [change-id]')
|
|
599
|
+
.description(
|
|
600
|
+
'List reviewers on a Gerrit change (auto-detects from HEAD commit if not specified)',
|
|
601
|
+
)
|
|
602
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
603
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
604
|
+
.action(async (changeId, options) => {
|
|
605
|
+
await executeEffect(
|
|
606
|
+
reviewersCommand(changeId, options).pipe(
|
|
607
|
+
Effect.provide(GerritApiServiceLive),
|
|
608
|
+
Effect.provide(ConfigServiceLive),
|
|
609
|
+
),
|
|
610
|
+
options,
|
|
611
|
+
'reviewers_result',
|
|
612
|
+
)
|
|
613
|
+
})
|
|
614
|
+
|
|
581
615
|
// checkout command
|
|
582
616
|
program
|
|
583
617
|
.command('checkout <change-id>')
|
|
@@ -597,4 +631,26 @@ Note:
|
|
|
597
631
|
process.exit(1)
|
|
598
632
|
}
|
|
599
633
|
})
|
|
634
|
+
|
|
635
|
+
registerAnalyticsCommands(program)
|
|
636
|
+
|
|
637
|
+
// cherry command
|
|
638
|
+
program
|
|
639
|
+
.command('cherry <change-id>')
|
|
640
|
+
.description('Fetch and cherry-pick a Gerrit change onto the current branch')
|
|
641
|
+
.option('-n, --no-commit', 'Stage changes without committing')
|
|
642
|
+
.option('--no-verify', 'Bypass git commit hooks during cherry-pick')
|
|
643
|
+
.option('--remote <name>', 'Use specific git remote (default: auto-detect)')
|
|
644
|
+
.addHelpText('after', CHERRY_HELP_TEXT)
|
|
645
|
+
.action(async (changeId, options) => {
|
|
646
|
+
await executeEffect(
|
|
647
|
+
cherryCommand(changeId, {
|
|
648
|
+
noCommit: options.noCommit,
|
|
649
|
+
noVerify: options.noVerify,
|
|
650
|
+
remote: options.remote,
|
|
651
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive)),
|
|
652
|
+
options,
|
|
653
|
+
'cherry_result',
|
|
654
|
+
)
|
|
655
|
+
})
|
|
600
656
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
4
|
+
import { ConfigServiceLive } from '@/services/config'
|
|
5
|
+
import { listCommand } from './commands/list'
|
|
6
|
+
|
|
7
|
+
function executeEffect<E>(
|
|
8
|
+
effect: Effect.Effect<void, E, never>,
|
|
9
|
+
options: { xml?: boolean; json?: boolean },
|
|
10
|
+
resultTag: string,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
if (options.xml && options.json) {
|
|
13
|
+
console.error('✗ Error: --xml and --json are mutually exclusive')
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
return Effect.runPromise(effect).catch((error: unknown) => {
|
|
17
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
18
|
+
if (options.json) {
|
|
19
|
+
console.log(JSON.stringify({ status: 'error', error: msg }, null, 2))
|
|
20
|
+
} else if (options.xml) {
|
|
21
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
22
|
+
console.log(`<${resultTag}>`)
|
|
23
|
+
console.log(` <status>error</status>`)
|
|
24
|
+
console.log(` <error><![CDATA[${msg}]]></error>`)
|
|
25
|
+
console.log(`</${resultTag}>`)
|
|
26
|
+
} else {
|
|
27
|
+
console.error('✗ Error:', msg)
|
|
28
|
+
}
|
|
29
|
+
process.exit(1)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function registerListCommands(program: Command): void {
|
|
34
|
+
// list command (primary)
|
|
35
|
+
program
|
|
36
|
+
.command('list')
|
|
37
|
+
.description('List your changes or changes needing your review')
|
|
38
|
+
.option('--status <status>', 'Filter by status: open, merged, abandoned (default: open)')
|
|
39
|
+
.option('-n, --limit <number>', 'Maximum number of changes to show (default: 25)', parseInt)
|
|
40
|
+
.option('--detailed', 'Show detailed information for each change')
|
|
41
|
+
.option('--reviewer', 'Show changes where you are a reviewer')
|
|
42
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
43
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
44
|
+
.action(async (options) => {
|
|
45
|
+
await executeEffect(
|
|
46
|
+
listCommand(options).pipe(
|
|
47
|
+
Effect.provide(GerritApiServiceLive),
|
|
48
|
+
Effect.provide(ConfigServiceLive),
|
|
49
|
+
),
|
|
50
|
+
options,
|
|
51
|
+
'list_result',
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// mine command (alias for list)
|
|
56
|
+
program
|
|
57
|
+
.command('mine')
|
|
58
|
+
.description('Show your open changes (alias for "ger list")')
|
|
59
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
60
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
61
|
+
.action(async (options) => {
|
|
62
|
+
await executeEffect(
|
|
63
|
+
listCommand(options).pipe(
|
|
64
|
+
Effect.provide(GerritApiServiceLive),
|
|
65
|
+
Effect.provide(ConfigServiceLive),
|
|
66
|
+
),
|
|
67
|
+
options,
|
|
68
|
+
'list_result',
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const registerReviewerListCommand = (name: string, description: string): void => {
|
|
73
|
+
program
|
|
74
|
+
.command(name)
|
|
75
|
+
.description(description)
|
|
76
|
+
.option('--status <status>', 'Filter by status: open, merged, abandoned (default: open)')
|
|
77
|
+
.option('-n, --limit <number>', 'Maximum number of changes to show (default: 25)', parseInt)
|
|
78
|
+
.option('--detailed', 'Show detailed information for each change')
|
|
79
|
+
.option('--all-verified', 'Include all verification states (default: open only)')
|
|
80
|
+
.option('-f, --filter <query>', 'Append custom Gerrit query syntax')
|
|
81
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
82
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
83
|
+
.action(async (options) => {
|
|
84
|
+
await executeEffect(
|
|
85
|
+
listCommand({
|
|
86
|
+
...options,
|
|
87
|
+
reviewer: true,
|
|
88
|
+
allVerified: options.allVerified,
|
|
89
|
+
filter: options.filter,
|
|
90
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive)),
|
|
91
|
+
options,
|
|
92
|
+
'list_result',
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
registerReviewerListCommand(
|
|
98
|
+
'incoming',
|
|
99
|
+
'Show changes where you are a reviewer or CC\'d (alias for "ger list --reviewer")',
|
|
100
|
+
)
|
|
101
|
+
registerReviewerListCommand(
|
|
102
|
+
'team',
|
|
103
|
+
'Show changes where you are a reviewer or CC\'d (alias for "ger list --reviewer")',
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import { Effect } from 'effect'
|
|
3
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
4
|
+
import { ConfigServiceLive } from '@/services/config'
|
|
5
|
+
import { treeSetupCommand, TREE_SETUP_HELP_TEXT } from './commands/tree-setup'
|
|
6
|
+
import { treesCommand } from './commands/trees'
|
|
7
|
+
import { treeCleanupCommand } from './commands/tree-cleanup'
|
|
8
|
+
import { treeRebaseCommand } from './commands/tree-rebase'
|
|
9
|
+
|
|
10
|
+
async function executeEffect<E>(
|
|
11
|
+
effect: Effect.Effect<void, E, never>,
|
|
12
|
+
options: { xml?: boolean; json?: boolean },
|
|
13
|
+
resultTag: string,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
if (options.xml && options.json) {
|
|
16
|
+
console.error('--xml and --json are mutually exclusive')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
await Effect.runPromise(effect)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
23
|
+
if (options.json) {
|
|
24
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
25
|
+
} else if (options.xml) {
|
|
26
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
27
|
+
console.log(`<${resultTag}>`)
|
|
28
|
+
console.log(` <status>error</status>`)
|
|
29
|
+
console.log(` <error><![CDATA[${errorMessage}]]></error>`)
|
|
30
|
+
console.log(`</${resultTag}>`)
|
|
31
|
+
} else {
|
|
32
|
+
console.error('✗ Error:', errorMessage)
|
|
33
|
+
}
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function registerTreeCommands(program: Command): void {
|
|
39
|
+
const tree = program
|
|
40
|
+
.command('tree')
|
|
41
|
+
.description('Manage git worktrees for reviewing Gerrit changes')
|
|
42
|
+
|
|
43
|
+
tree
|
|
44
|
+
.command('setup <change-id>')
|
|
45
|
+
.description('Create a git worktree for reviewing a change')
|
|
46
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
47
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
48
|
+
.addHelpText('after', TREE_SETUP_HELP_TEXT)
|
|
49
|
+
.action(async (changeId: string, options: { xml?: boolean; json?: boolean }) => {
|
|
50
|
+
await executeEffect(
|
|
51
|
+
treeSetupCommand(changeId, options).pipe(
|
|
52
|
+
Effect.provide(GerritApiServiceLive),
|
|
53
|
+
Effect.provide(ConfigServiceLive),
|
|
54
|
+
),
|
|
55
|
+
options,
|
|
56
|
+
'tree_setup_result',
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
tree
|
|
61
|
+
.command('cleanup [change-id]')
|
|
62
|
+
.description('Remove ger-managed worktrees (all, or a specific one by change number)')
|
|
63
|
+
.option('--force', 'Force removal even with uncommitted changes')
|
|
64
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
65
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
66
|
+
.addHelpText(
|
|
67
|
+
'after',
|
|
68
|
+
`
|
|
69
|
+
Examples:
|
|
70
|
+
# Remove all ger-managed worktrees
|
|
71
|
+
$ ger tree cleanup
|
|
72
|
+
|
|
73
|
+
# Remove worktree for a specific change
|
|
74
|
+
$ ger tree cleanup 12345
|
|
75
|
+
|
|
76
|
+
# Force removal (discards uncommitted changes)
|
|
77
|
+
$ ger tree cleanup 12345 --force`,
|
|
78
|
+
)
|
|
79
|
+
.action(
|
|
80
|
+
async (
|
|
81
|
+
changeId: string | undefined,
|
|
82
|
+
options: { force?: boolean; xml?: boolean; json?: boolean },
|
|
83
|
+
) => {
|
|
84
|
+
await executeEffect(treeCleanupCommand(changeId, options), options, 'tree_cleanup_result')
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
tree
|
|
89
|
+
.command('rebase')
|
|
90
|
+
.description('Fetch origin and rebase the current worktree')
|
|
91
|
+
.option('--onto <branch>', 'Branch to rebase onto (default: auto-detect)')
|
|
92
|
+
.option('-i, --interactive', 'Interactive rebase')
|
|
93
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
94
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
95
|
+
.addHelpText(
|
|
96
|
+
'after',
|
|
97
|
+
`
|
|
98
|
+
Examples:
|
|
99
|
+
# Rebase onto auto-detected upstream
|
|
100
|
+
$ ger tree rebase
|
|
101
|
+
|
|
102
|
+
# Rebase onto a specific branch
|
|
103
|
+
$ ger tree rebase --onto origin/main
|
|
104
|
+
|
|
105
|
+
# Interactive rebase
|
|
106
|
+
$ ger tree rebase -i`,
|
|
107
|
+
)
|
|
108
|
+
.action(
|
|
109
|
+
async (options: { onto?: string; interactive?: boolean; xml?: boolean; json?: boolean }) => {
|
|
110
|
+
await executeEffect(
|
|
111
|
+
treeRebaseCommand(options).pipe(Effect.provide(ConfigServiceLive)),
|
|
112
|
+
options,
|
|
113
|
+
'tree_rebase_result',
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// 'trees' is a top-level command (matches gerry's naming)
|
|
119
|
+
program
|
|
120
|
+
.command('trees')
|
|
121
|
+
.description('List ger-managed git worktrees in the current repository')
|
|
122
|
+
.option('--all', 'Show all worktrees including the main checkout')
|
|
123
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
124
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
125
|
+
.action(async (options: { all?: boolean; xml?: boolean; json?: boolean }) => {
|
|
126
|
+
await executeEffect(treesCommand(options), options, 'trees_result')
|
|
127
|
+
})
|
|
128
|
+
}
|
package/src/schemas/config.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const AppConfig: Schema.Struct<{
|
|
|
7
7
|
password: typeof Schema.String
|
|
8
8
|
aiTool: Schema.optional<Schema.Literal<['claude', 'llm', 'opencode', 'gemini']>>
|
|
9
9
|
aiAutoDetect: Schema.optionalWith<typeof Schema.Boolean, { default: () => boolean }>
|
|
10
|
+
retriggerComment: Schema.optional<typeof Schema.String>
|
|
10
11
|
}> = Schema.Struct({
|
|
11
12
|
// Gerrit credentials (flattened)
|
|
12
13
|
host: Schema.String.pipe(
|
|
@@ -24,6 +25,8 @@ export const AppConfig: Schema.Struct<{
|
|
|
24
25
|
// AI configuration (flattened)
|
|
25
26
|
aiTool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
|
|
26
27
|
aiAutoDetect: Schema.optionalWith(Schema.Boolean, { default: (): boolean => true }),
|
|
28
|
+
// CI retrigger comment
|
|
29
|
+
retriggerComment: Schema.optional(Schema.String),
|
|
27
30
|
})
|
|
28
31
|
|
|
29
32
|
export type AppConfig = Schema.Schema.Type<typeof AppConfig>
|
package/src/schemas/gerrit.ts
CHANGED
|
@@ -570,6 +570,8 @@ const ReviewerAccountInfo = Schema.Struct({
|
|
|
570
570
|
username: Schema.optional(Schema.String),
|
|
571
571
|
})
|
|
572
572
|
|
|
573
|
+
export type { ReviewerListItem } from './reviewer'
|
|
574
|
+
|
|
573
575
|
export const ReviewerResult: Schema.Schema<{
|
|
574
576
|
readonly input: string
|
|
575
577
|
readonly reviewers?: ReadonlyArray<{
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Schema } from '@effect/schema'
|
|
2
|
+
|
|
3
|
+
export const ReviewerListItem: Schema.Schema<{
|
|
4
|
+
readonly _account_id?: number
|
|
5
|
+
readonly name?: string
|
|
6
|
+
readonly email?: string
|
|
7
|
+
readonly username?: string
|
|
8
|
+
readonly approvals?: { readonly [x: string]: string }
|
|
9
|
+
}> = Schema.Struct({
|
|
10
|
+
_account_id: Schema.optional(Schema.Number),
|
|
11
|
+
name: Schema.optional(Schema.String),
|
|
12
|
+
email: Schema.optional(Schema.String),
|
|
13
|
+
username: Schema.optional(Schema.String),
|
|
14
|
+
approvals: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
|
|
15
|
+
})
|
|
16
|
+
export type ReviewerListItem = Schema.Schema.Type<typeof ReviewerListItem>
|
package/src/services/config.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface ConfigServiceImpl {
|
|
|
14
14
|
readonly saveAiConfig: (config: AiConfig) => Effect.Effect<void, ConfigError>
|
|
15
15
|
readonly getFullConfig: Effect.Effect<AppConfig, ConfigError>
|
|
16
16
|
readonly saveFullConfig: (config: AppConfig) => Effect.Effect<void, ConfigError>
|
|
17
|
+
readonly getRetriggerComment: Effect.Effect<string | undefined, ConfigError>
|
|
18
|
+
readonly saveRetriggerComment: (comment: string) => Effect.Effect<void, ConfigError>
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
// Export both the tag value and the type for use in Effect requirements
|
|
@@ -237,6 +239,17 @@ export const ConfigServiceLive: Layer.Layer<ConfigService, never, never> = Layer
|
|
|
237
239
|
yield* saveFullConfig(updatedConfig)
|
|
238
240
|
})
|
|
239
241
|
|
|
242
|
+
const getRetriggerComment = Effect.gen(function* () {
|
|
243
|
+
const config = yield* getFullConfig.pipe(Effect.orElseSucceed(() => null))
|
|
244
|
+
return config?.retriggerComment
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
const saveRetriggerComment = (comment: string) =>
|
|
248
|
+
Effect.gen(function* () {
|
|
249
|
+
const existingConfig = yield* getFullConfig
|
|
250
|
+
yield* saveFullConfig({ ...existingConfig, retriggerComment: comment })
|
|
251
|
+
})
|
|
252
|
+
|
|
240
253
|
return {
|
|
241
254
|
getCredentials,
|
|
242
255
|
saveCredentials,
|
|
@@ -245,6 +258,8 @@ export const ConfigServiceLive: Layer.Layer<ConfigService, never, never> = Layer
|
|
|
245
258
|
saveAiConfig,
|
|
246
259
|
getFullConfig,
|
|
247
260
|
saveFullConfig,
|
|
261
|
+
getRetriggerComment,
|
|
262
|
+
saveRetriggerComment,
|
|
248
263
|
}
|
|
249
264
|
}),
|
|
250
265
|
)
|