@aaronshaf/ger 3.0.2 → 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/package.json +1 -1
- package/src/api/gerrit-types.ts +121 -0
- package/src/api/gerrit.ts +55 -96
- 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/list.ts +239 -0
- package/src/cli/commands/rebase.ts +5 -1
- package/src/cli/commands/retrigger.ts +91 -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 +56 -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/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/helpers/config-mock.ts +4 -0
- package/tests/list.test.ts +220 -0
- package/tests/retrigger.test.ts +159 -0
- package/tests/tree.test.ts +517 -0
- package/tests/update.test.ts +86 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { Console, Effect } from 'effect'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
export interface UpdateOptions {
|
|
5
|
+
skipPull?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const PACKAGE_NAME = '@aaronshaf/ger'
|
|
9
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
|
|
10
|
+
|
|
11
|
+
const readCurrentVersion = (): string => {
|
|
12
|
+
try {
|
|
13
|
+
// Bun.file is available at runtime; use dynamic require as fallback
|
|
14
|
+
const raw = require('../../package.json') as { version: string }
|
|
15
|
+
return raw.version
|
|
16
|
+
} catch {
|
|
17
|
+
return '0.0.0'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class UpdateError extends Error {
|
|
22
|
+
readonly _tag = 'UpdateError' as const
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message)
|
|
25
|
+
this.name = 'UpdateError'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fetchLatestVersion = (): Effect.Effect<string, UpdateError> =>
|
|
30
|
+
Effect.tryPromise({
|
|
31
|
+
try: async () => {
|
|
32
|
+
const res = await fetch(REGISTRY_URL)
|
|
33
|
+
if (!res.ok) throw new Error(`Registry returned ${res.status}`)
|
|
34
|
+
const data = (await res.json()) as { version: string }
|
|
35
|
+
return data.version
|
|
36
|
+
},
|
|
37
|
+
catch: (e) =>
|
|
38
|
+
new UpdateError(
|
|
39
|
+
`Failed to check latest version: ${e instanceof Error ? e.message : String(e)}`,
|
|
40
|
+
),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const updateCommand = (options: UpdateOptions): Effect.Effect<void, UpdateError, never> =>
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
if (!options.skipPull) {
|
|
46
|
+
yield* Console.log(chalk.dim('Checking for updates...'))
|
|
47
|
+
|
|
48
|
+
const latest = yield* fetchLatestVersion()
|
|
49
|
+
const current = readCurrentVersion()
|
|
50
|
+
|
|
51
|
+
yield* Console.log(` Current: ${chalk.cyan(current)}`)
|
|
52
|
+
yield* Console.log(` Latest: ${chalk.cyan(latest)}`)
|
|
53
|
+
|
|
54
|
+
if (current === latest) {
|
|
55
|
+
yield* Console.log(chalk.green(`✓ Already up to date (${current})`))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
yield* Console.log('')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
yield* Console.log(chalk.dim(`Installing ${PACKAGE_NAME}@latest...`))
|
|
63
|
+
yield* Effect.try({
|
|
64
|
+
try: () => {
|
|
65
|
+
execSync(`bun install -g ${PACKAGE_NAME}@latest`, { stdio: 'inherit', timeout: 60000 })
|
|
66
|
+
},
|
|
67
|
+
catch: (e) =>
|
|
68
|
+
new UpdateError(`Install failed: ${e instanceof Error ? e.message : String(e)}`),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
yield* Console.log('')
|
|
72
|
+
yield* Console.log(chalk.green('✓ ger updated successfully'))
|
|
73
|
+
})
|
|
@@ -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,8 +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'
|
|
31
30
|
import { filesCommand } from './commands/files'
|
|
32
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'
|
|
33
36
|
|
|
34
37
|
// Helper function to output error in plain text, JSON, or XML format
|
|
35
38
|
function outputError(
|
|
@@ -154,22 +157,7 @@ export function registerCommands(program: Command): void {
|
|
|
154
157
|
)
|
|
155
158
|
})
|
|
156
159
|
|
|
157
|
-
|
|
158
|
-
program
|
|
159
|
-
.command('mine')
|
|
160
|
-
.description('Show your open changes')
|
|
161
|
-
.option('--xml', 'XML output for LLM consumption')
|
|
162
|
-
.option('--json', 'JSON output for programmatic consumption')
|
|
163
|
-
.action(async (options) => {
|
|
164
|
-
await executeEffect(
|
|
165
|
-
mineCommand(options).pipe(
|
|
166
|
-
Effect.provide(GerritApiServiceLive),
|
|
167
|
-
Effect.provide(ConfigServiceLive),
|
|
168
|
-
),
|
|
169
|
-
options,
|
|
170
|
-
'mine_result',
|
|
171
|
-
)
|
|
172
|
-
})
|
|
160
|
+
registerListCommands(program)
|
|
173
161
|
|
|
174
162
|
// search command
|
|
175
163
|
program
|
|
@@ -201,15 +189,16 @@ export function registerCommands(program: Command): void {
|
|
|
201
189
|
})
|
|
202
190
|
})
|
|
203
191
|
|
|
204
|
-
// workspace command
|
|
192
|
+
// workspace command (deprecated — use 'ger tree setup' instead)
|
|
205
193
|
program
|
|
206
194
|
.command('workspace <change-id>')
|
|
207
|
-
.description(
|
|
208
|
-
'Create or switch to a git worktree for a Gerrit change (accepts change number or Change-ID)',
|
|
209
|
-
)
|
|
195
|
+
.description('[deprecated: use "ger tree setup"] Create a git worktree for a Gerrit change')
|
|
210
196
|
.option('--xml', 'XML output for LLM consumption')
|
|
211
197
|
.option('--json', 'JSON output for programmatic consumption')
|
|
212
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
|
+
}
|
|
213
202
|
await executeEffect(
|
|
214
203
|
workspaceCommand(changeId, options).pipe(
|
|
215
204
|
Effect.provide(GerritApiServiceLive),
|
|
@@ -220,23 +209,7 @@ export function registerCommands(program: Command): void {
|
|
|
220
209
|
)
|
|
221
210
|
})
|
|
222
211
|
|
|
223
|
-
|
|
224
|
-
program
|
|
225
|
-
.command('incoming')
|
|
226
|
-
.description('Show incoming changes for review (where you are a reviewer)')
|
|
227
|
-
.option('--xml', 'XML output for LLM consumption')
|
|
228
|
-
.option('--json', 'JSON output for programmatic consumption')
|
|
229
|
-
.option('-i, --interactive', 'Interactive mode with detailed view and diff')
|
|
230
|
-
.action(async (options) => {
|
|
231
|
-
await executeEffect(
|
|
232
|
-
incomingCommand(options).pipe(
|
|
233
|
-
Effect.provide(GerritApiServiceLive),
|
|
234
|
-
Effect.provide(ConfigServiceLive),
|
|
235
|
-
),
|
|
236
|
-
options,
|
|
237
|
-
'incoming_result',
|
|
238
|
-
)
|
|
239
|
-
})
|
|
212
|
+
registerTreeCommands(program)
|
|
240
213
|
|
|
241
214
|
// abandon / restore / set-ready / set-wip commands
|
|
242
215
|
registerStateCommands(program)
|
|
@@ -246,11 +219,12 @@ export function registerCommands(program: Command): void {
|
|
|
246
219
|
.command('rebase [change-id]')
|
|
247
220
|
.description('Rebase a change onto target branch (auto-detects from HEAD if not provided)')
|
|
248
221
|
.option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
|
|
222
|
+
.option('--allow-conflicts', 'Allow rebasing even if conflicts exist')
|
|
249
223
|
.option('--xml', 'XML output for LLM consumption')
|
|
250
224
|
.option('--json', 'JSON output for programmatic consumption')
|
|
251
225
|
.action(async (changeId, options) => {
|
|
252
226
|
await executeEffect(
|
|
253
|
-
rebaseCommand(changeId, options).pipe(
|
|
227
|
+
rebaseCommand(changeId, { ...options, allowConflicts: options.allowConflicts }).pipe(
|
|
254
228
|
Effect.provide(GerritApiServiceLive),
|
|
255
229
|
Effect.provide(ConfigServiceLive),
|
|
256
230
|
),
|
|
@@ -340,6 +314,26 @@ export function registerCommands(program: Command): void {
|
|
|
340
314
|
// Register all group-related commands
|
|
341
315
|
registerGroupCommands(program)
|
|
342
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
|
+
|
|
343
337
|
// comments command
|
|
344
338
|
program
|
|
345
339
|
.command('comments <change-id>')
|
|
@@ -637,4 +631,26 @@ Note:
|
|
|
637
631
|
process.exit(1)
|
|
638
632
|
}
|
|
639
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
|
+
})
|
|
640
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/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
|
)
|