@aaronshaf/ger 1.2.10 → 2.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/.ast-grep/rules/no-as-casting.yml +13 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +83 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/dependency-update.yml +84 -0
- package/.github/workflows/release.yml +166 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/security.yml +96 -0
- package/.husky/pre-commit +16 -0
- package/.husky/pre-push +25 -0
- package/.lintstagedrc.json +6 -0
- package/.tool-versions +1 -0
- package/CLAUDE.md +105 -0
- package/DEVELOPMENT.md +361 -0
- package/EXAMPLES.md +457 -0
- package/README.md +831 -16
- package/bin/ger +3 -18
- package/biome.json +36 -0
- package/bun.lock +678 -0
- package/bunfig.toml +8 -0
- package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
- package/docs/adr/0002-use-bun-runtime.md +64 -0
- package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
- package/docs/adr/0004-use-commander-for-cli.md +76 -0
- package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
- package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
- package/docs/adr/0007-git-hooks-for-quality.md +94 -0
- package/docs/adr/0008-no-as-typecasting.md +83 -0
- package/docs/adr/0009-file-size-limits.md +82 -0
- package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
- package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
- package/docs/adr/0012-build-status-message-parsing.md +94 -0
- package/docs/adr/0013-git-subprocess-integration.md +98 -0
- package/docs/adr/0014-group-management-support.md +95 -0
- package/docs/adr/0015-batch-comment-processing.md +111 -0
- package/docs/adr/0016-flexible-change-identifiers.md +94 -0
- package/docs/adr/0017-git-worktree-support.md +102 -0
- package/docs/adr/0018-auto-install-commit-hook.md +103 -0
- package/docs/adr/0019-sdk-package-exports.md +95 -0
- package/docs/adr/0020-code-coverage-enforcement.md +105 -0
- package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
- package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
- package/docs/adr/README.md +30 -0
- package/docs/prd/README.md +12 -0
- package/docs/prd/architecture.md +325 -0
- package/docs/prd/commands.md +425 -0
- package/docs/prd/data-model.md +349 -0
- package/docs/prd/overview.md +124 -0
- package/index.ts +219 -0
- package/oxlint.json +24 -0
- package/package.json +82 -15
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/skills/gerrit-workflow/SKILL.md +247 -0
- package/skills/gerrit-workflow/examples.md +572 -0
- package/skills/gerrit-workflow/reference.md +728 -0
- package/src/api/gerrit.ts +696 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/add-reviewer.ts +156 -0
- package/src/cli/commands/build-status.ts +282 -0
- package/src/cli/commands/checkout.ts +422 -0
- package/src/cli/commands/comment.ts +460 -0
- package/src/cli/commands/comments.ts +85 -0
- package/src/cli/commands/diff.ts +71 -0
- package/src/cli/commands/extract-url.ts +266 -0
- package/src/cli/commands/groups-members.ts +104 -0
- package/src/cli/commands/groups-show.ts +169 -0
- package/src/cli/commands/groups.ts +137 -0
- package/src/cli/commands/incoming.ts +226 -0
- package/src/cli/commands/init.ts +164 -0
- package/src/cli/commands/mine.ts +115 -0
- package/src/cli/commands/open.ts +57 -0
- package/src/cli/commands/projects.ts +68 -0
- package/src/cli/commands/push.ts +430 -0
- package/src/cli/commands/rebase.ts +52 -0
- package/src/cli/commands/remove-reviewer.ts +123 -0
- package/src/cli/commands/restore.ts +50 -0
- package/src/cli/commands/review.ts +486 -0
- package/src/cli/commands/search.ts +162 -0
- package/src/cli/commands/setup.ts +286 -0
- package/src/cli/commands/show.ts +491 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/submit.ts +108 -0
- package/src/cli/commands/vote.ts +119 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +53 -0
- package/src/cli/register-commands.ts +659 -0
- package/src/cli/register-group-commands.ts +88 -0
- package/src/cli/register-reviewer-commands.ts +97 -0
- package/src/prompts/default-review.md +86 -0
- package/src/prompts/system-inline-review.md +135 -0
- package/src/prompts/system-overall-review.md +206 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +84 -0
- package/src/schemas/gerrit.ts +681 -0
- package/src/services/commit-hook.ts +314 -0
- package/src/services/config.test.ts +150 -0
- package/src/services/config.ts +250 -0
- package/src/services/git-worktree.ts +342 -0
- package/src/services/review-strategy.ts +292 -0
- package/src/test-utils/mock-generator.ts +138 -0
- package/src/utils/change-id.test.ts +98 -0
- package/src/utils/change-id.ts +63 -0
- package/src/utils/comment-formatters.ts +153 -0
- package/src/utils/diff-context.ts +103 -0
- package/src/utils/diff-formatters.ts +141 -0
- package/src/utils/formatters.ts +85 -0
- package/src/utils/git-commit.test.ts +277 -0
- package/src/utils/git-commit.ts +122 -0
- package/src/utils/index.ts +55 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/review-formatters.ts +89 -0
- package/src/utils/review-prompt-builder.ts +110 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +271 -0
- package/src/utils/url-parser.ts +118 -0
- package/tests/abandon.test.ts +230 -0
- package/tests/add-reviewer.test.ts +579 -0
- package/tests/build-status-watch.test.ts +344 -0
- package/tests/build-status.test.ts +789 -0
- package/tests/change-id-formats.test.ts +268 -0
- package/tests/checkout/integration.test.ts +653 -0
- package/tests/checkout/parse-input.test.ts +55 -0
- package/tests/checkout/validation.test.ts +178 -0
- package/tests/comment-batch-advanced.test.ts +431 -0
- package/tests/comment-gerrit-api-compliance.test.ts +414 -0
- package/tests/comment.test.ts +708 -0
- package/tests/comments.test.ts +323 -0
- package/tests/config-service-simple.test.ts +100 -0
- package/tests/diff.test.ts +419 -0
- package/tests/extract-url.test.ts +517 -0
- package/tests/groups-members.test.ts +256 -0
- package/tests/groups-show.test.ts +323 -0
- package/tests/groups.test.ts +334 -0
- package/tests/helpers/build-status-test-setup.ts +83 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/init.test.ts +70 -0
- package/tests/integration/commit-hook.test.ts +246 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +285 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/projects.test.ts +259 -0
- package/tests/rebase.test.ts +271 -0
- package/tests/remove-reviewer.test.ts +357 -0
- package/tests/restore.test.ts +237 -0
- package/tests/review.test.ts +135 -0
- package/tests/search.test.ts +712 -0
- package/tests/setup.test.ts +63 -0
- package/tests/show-auto-detect.test.ts +324 -0
- package/tests/show.test.ts +813 -0
- package/tests/status.test.ts +145 -0
- package/tests/submit.test.ts +316 -0
- package/tests/unit/commands/push.test.ts +194 -0
- package/tests/unit/git-branch-detection.test.ts +82 -0
- package/tests/unit/git-worktree.test.ts +55 -0
- package/tests/unit/patterns/push-patterns.test.ts +148 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/services/commit-hook.test.ts +132 -0
- package/tests/unit/services/review-strategy.test.ts +349 -0
- package/tests/unit/test-utils/mock-generator.test.ts +154 -0
- package/tests/unit/utils/comment-formatters.test.ts +415 -0
- package/tests/unit/utils/diff-context.test.ts +171 -0
- package/tests/unit/utils/diff-formatters.test.ts +165 -0
- package/tests/unit/utils/formatters.test.ts +411 -0
- package/tests/unit/utils/message-filters.test.ts +227 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tests/vote.test.ts +317 -0
- package/tests/workspace.test.ts +295 -0
- package/tsconfig.json +36 -5
- package/src/commands/branch.ts +0 -180
- package/src/ger.ts +0 -22
- package/src/types.d.ts +0 -35
- package/src/utils.ts +0 -130
|
@@ -0,0 +1,659 @@
|
|
|
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 { ReviewStrategyServiceLive } from '@/services/review-strategy'
|
|
6
|
+
import { GitWorktreeServiceLive } from '@/services/git-worktree'
|
|
7
|
+
import { CommitHookServiceLive } from '@/services/commit-hook'
|
|
8
|
+
import { abandonCommand } from './commands/abandon'
|
|
9
|
+
import { restoreCommand } from './commands/restore'
|
|
10
|
+
import { rebaseCommand } from './commands/rebase'
|
|
11
|
+
import { submitCommand } from './commands/submit'
|
|
12
|
+
import { voteCommand } from './commands/vote'
|
|
13
|
+
import { projectsCommand } from './commands/projects'
|
|
14
|
+
import { buildStatusCommand, BUILD_STATUS_HELP_TEXT } from './commands/build-status'
|
|
15
|
+
import { checkoutCommand, CHECKOUT_HELP_TEXT } from './commands/checkout'
|
|
16
|
+
import { commentCommand } from './commands/comment'
|
|
17
|
+
import { commentsCommand } from './commands/comments'
|
|
18
|
+
import { diffCommand } from './commands/diff'
|
|
19
|
+
import { extractUrlCommand } from './commands/extract-url'
|
|
20
|
+
import { incomingCommand } from './commands/incoming'
|
|
21
|
+
import { mineCommand } from './commands/mine'
|
|
22
|
+
import { openCommand } from './commands/open'
|
|
23
|
+
import { pushCommand, PUSH_HELP_TEXT } from './commands/push'
|
|
24
|
+
import { reviewCommand } from './commands/review'
|
|
25
|
+
import { searchCommand, SEARCH_HELP_TEXT } from './commands/search'
|
|
26
|
+
import { setup } from './commands/setup'
|
|
27
|
+
import { showCommand, SHOW_HELP_TEXT } from './commands/show'
|
|
28
|
+
import { statusCommand } from './commands/status'
|
|
29
|
+
import { workspaceCommand } from './commands/workspace'
|
|
30
|
+
import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
31
|
+
import { registerGroupCommands } from './register-group-commands'
|
|
32
|
+
import { registerReviewerCommands } from './register-reviewer-commands'
|
|
33
|
+
|
|
34
|
+
// Helper function to output error in plain text or XML format
|
|
35
|
+
function outputError(error: unknown, options: { xml?: boolean }, resultTag: string): void {
|
|
36
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
37
|
+
if (options.xml) {
|
|
38
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
39
|
+
console.log(`<${resultTag}>`)
|
|
40
|
+
console.log(` <status>error</status>`)
|
|
41
|
+
console.log(` <error><![CDATA[${errorMessage}]]></error>`)
|
|
42
|
+
console.log(`</${resultTag}>`)
|
|
43
|
+
} else {
|
|
44
|
+
console.error('✗ Error:', errorMessage)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Helper function to execute Effect with standard error handling
|
|
49
|
+
async function executeEffect<E>(
|
|
50
|
+
effect: Effect.Effect<void, E, never>,
|
|
51
|
+
options: { xml?: boolean },
|
|
52
|
+
resultTag: string,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
try {
|
|
55
|
+
await Effect.runPromise(effect)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
outputError(error, options, resultTag)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function registerCommands(program: Command): void {
|
|
63
|
+
// setup command (new primary command)
|
|
64
|
+
program
|
|
65
|
+
.command('setup')
|
|
66
|
+
.description('Configure Gerrit credentials and AI tools')
|
|
67
|
+
.action(async () => {
|
|
68
|
+
await setup()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// init command (kept for backward compatibility, redirects to setup)
|
|
72
|
+
program
|
|
73
|
+
.command('init')
|
|
74
|
+
.description('Initialize Gerrit credentials (alias for setup)')
|
|
75
|
+
.action(async () => {
|
|
76
|
+
await setup()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// status command
|
|
80
|
+
program
|
|
81
|
+
.command('status')
|
|
82
|
+
.description('Check connection status')
|
|
83
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
84
|
+
.action(async (options) => {
|
|
85
|
+
await executeEffect(
|
|
86
|
+
statusCommand(options).pipe(
|
|
87
|
+
Effect.provide(GerritApiServiceLive),
|
|
88
|
+
Effect.provide(ConfigServiceLive),
|
|
89
|
+
),
|
|
90
|
+
options,
|
|
91
|
+
'status_result',
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// comment command
|
|
96
|
+
program
|
|
97
|
+
.command('comment <change-id>')
|
|
98
|
+
.description('Post a comment on a change (accepts change number or Change-ID)')
|
|
99
|
+
.option('-m, --message <message>', 'Comment message')
|
|
100
|
+
.option('--file <file>', 'File path for line-specific comment (relative to repo root)')
|
|
101
|
+
.option(
|
|
102
|
+
'--line <line>',
|
|
103
|
+
'Line number in the NEW version of the file (not diff line numbers)',
|
|
104
|
+
parseInt,
|
|
105
|
+
)
|
|
106
|
+
.option('--unresolved', 'Mark comment as unresolved (requires human attention)')
|
|
107
|
+
.option('--batch', 'Read batch comments from stdin as JSON (see examples below)')
|
|
108
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
109
|
+
.addHelpText(
|
|
110
|
+
'after',
|
|
111
|
+
`
|
|
112
|
+
Examples:
|
|
113
|
+
# Post a general comment on a change (using change number)
|
|
114
|
+
$ ger comment 12345 -m "Looks good to me!"
|
|
115
|
+
|
|
116
|
+
# Post a comment using Change-ID
|
|
117
|
+
$ ger comment If5a3ae8cb5a107e187447802358417f311d0c4b1 -m "LGTM"
|
|
118
|
+
|
|
119
|
+
# Post a comment using piped input (useful for multi-line comments or scripts)
|
|
120
|
+
$ echo "This is a comment from stdin!" | ger comment 12345
|
|
121
|
+
$ cat review-notes.txt | ger comment 12345
|
|
122
|
+
|
|
123
|
+
# Post a line-specific comment (line number from NEW file version)
|
|
124
|
+
$ ger comment 12345 --file src/main.js --line 42 -m "Consider using const here"
|
|
125
|
+
|
|
126
|
+
# Post an unresolved comment requiring human attention
|
|
127
|
+
$ ger comment 12345 --file src/api.js --line 15 -m "Security concern" --unresolved
|
|
128
|
+
|
|
129
|
+
# Post multiple comments using batch mode
|
|
130
|
+
$ echo '{"message": "Review complete", "comments": [
|
|
131
|
+
{"file": "src/main.js", "line": 10, "message": "Good refactor"},
|
|
132
|
+
{"file": "src/api.js", "line": 25, "message": "Check error handling", "unresolved": true}
|
|
133
|
+
]}' | ger comment 12345 --batch
|
|
134
|
+
|
|
135
|
+
Note:
|
|
136
|
+
- Both change number (e.g., 12345) and Change-ID (e.g., If5a3ae8...) formats are accepted
|
|
137
|
+
- Line numbers refer to the actual line numbers in the NEW version of the file,
|
|
138
|
+
NOT the line numbers shown in the diff view. To find the correct line number,
|
|
139
|
+
look at the file after all changes have been applied.`,
|
|
140
|
+
)
|
|
141
|
+
.action(async (changeId, options) => {
|
|
142
|
+
await executeEffect(
|
|
143
|
+
commentCommand(changeId, options).pipe(
|
|
144
|
+
Effect.provide(GerritApiServiceLive),
|
|
145
|
+
Effect.provide(ConfigServiceLive),
|
|
146
|
+
),
|
|
147
|
+
options,
|
|
148
|
+
'comment_result',
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// diff command
|
|
153
|
+
program
|
|
154
|
+
.command('diff <change-id>')
|
|
155
|
+
.description('Get diff for a change (accepts change number or Change-ID)')
|
|
156
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
157
|
+
.option('--file <file>', 'Specific file to diff')
|
|
158
|
+
.option('--files-only', 'List changed files only')
|
|
159
|
+
.option('--format <format>', 'Output format (unified, json, files)')
|
|
160
|
+
.action(async (changeId, options) => {
|
|
161
|
+
await executeEffect(
|
|
162
|
+
diffCommand(changeId, options).pipe(
|
|
163
|
+
Effect.provide(GerritApiServiceLive),
|
|
164
|
+
Effect.provide(ConfigServiceLive),
|
|
165
|
+
),
|
|
166
|
+
options,
|
|
167
|
+
'diff_result',
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// mine command
|
|
172
|
+
program
|
|
173
|
+
.command('mine')
|
|
174
|
+
.description('Show your open changes')
|
|
175
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
176
|
+
.action(async (options) => {
|
|
177
|
+
await executeEffect(
|
|
178
|
+
mineCommand(options).pipe(
|
|
179
|
+
Effect.provide(GerritApiServiceLive),
|
|
180
|
+
Effect.provide(ConfigServiceLive),
|
|
181
|
+
),
|
|
182
|
+
options,
|
|
183
|
+
'mine_result',
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// search command
|
|
188
|
+
program
|
|
189
|
+
.command('search [query]')
|
|
190
|
+
.description('Search changes using Gerrit query syntax')
|
|
191
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
192
|
+
.option('-n, --limit <number>', 'Limit results (default: 25)')
|
|
193
|
+
.addHelpText('after', SEARCH_HELP_TEXT)
|
|
194
|
+
.action(async (query, options) => {
|
|
195
|
+
const effect = searchCommand(query, options).pipe(
|
|
196
|
+
Effect.provide(GerritApiServiceLive),
|
|
197
|
+
Effect.provide(ConfigServiceLive),
|
|
198
|
+
)
|
|
199
|
+
await Effect.runPromise(effect).catch((error: unknown) => {
|
|
200
|
+
if (options.xml) {
|
|
201
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
202
|
+
console.log(`<search_result>`)
|
|
203
|
+
console.log(` <status>error</status>`)
|
|
204
|
+
console.log(
|
|
205
|
+
` <error><![CDATA[${error instanceof Error ? error.message : String(error)}]]></error>`,
|
|
206
|
+
)
|
|
207
|
+
console.log(`</search_result>`)
|
|
208
|
+
} else {
|
|
209
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
210
|
+
}
|
|
211
|
+
process.exit(1)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// workspace command
|
|
216
|
+
program
|
|
217
|
+
.command('workspace <change-id>')
|
|
218
|
+
.description(
|
|
219
|
+
'Create or switch to a git worktree for a Gerrit change (accepts change number or Change-ID)',
|
|
220
|
+
)
|
|
221
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
222
|
+
.action(async (changeId, options) => {
|
|
223
|
+
await executeEffect(
|
|
224
|
+
workspaceCommand(changeId, options).pipe(
|
|
225
|
+
Effect.provide(GerritApiServiceLive),
|
|
226
|
+
Effect.provide(ConfigServiceLive),
|
|
227
|
+
),
|
|
228
|
+
options,
|
|
229
|
+
'workspace_result',
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// incoming command
|
|
234
|
+
program
|
|
235
|
+
.command('incoming')
|
|
236
|
+
.description('Show incoming changes for review (where you are a reviewer)')
|
|
237
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
238
|
+
.option('-i, --interactive', 'Interactive mode with detailed view and diff')
|
|
239
|
+
.action(async (options) => {
|
|
240
|
+
await executeEffect(
|
|
241
|
+
incomingCommand(options).pipe(
|
|
242
|
+
Effect.provide(GerritApiServiceLive),
|
|
243
|
+
Effect.provide(ConfigServiceLive),
|
|
244
|
+
),
|
|
245
|
+
options,
|
|
246
|
+
'incoming_result',
|
|
247
|
+
)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// abandon command
|
|
251
|
+
program
|
|
252
|
+
.command('abandon [change-id]')
|
|
253
|
+
.description(
|
|
254
|
+
'Abandon a change (interactive mode if no change-id provided; accepts change number or Change-ID)',
|
|
255
|
+
)
|
|
256
|
+
.option('-m, --message <message>', 'Abandon message')
|
|
257
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
258
|
+
.action(async (changeId, options) => {
|
|
259
|
+
await executeEffect(
|
|
260
|
+
abandonCommand(changeId, options).pipe(
|
|
261
|
+
Effect.provide(GerritApiServiceLive),
|
|
262
|
+
Effect.provide(ConfigServiceLive),
|
|
263
|
+
),
|
|
264
|
+
options,
|
|
265
|
+
'abandon_result',
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// restore command
|
|
270
|
+
program
|
|
271
|
+
.command('restore <change-id>')
|
|
272
|
+
.description('Restore an abandoned change (accepts change number or Change-ID)')
|
|
273
|
+
.option('-m, --message <message>', 'Restoration message')
|
|
274
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
275
|
+
.action(async (changeId, options) => {
|
|
276
|
+
await executeEffect(
|
|
277
|
+
restoreCommand(changeId, options).pipe(
|
|
278
|
+
Effect.provide(GerritApiServiceLive),
|
|
279
|
+
Effect.provide(ConfigServiceLive),
|
|
280
|
+
),
|
|
281
|
+
options,
|
|
282
|
+
'restore_result',
|
|
283
|
+
)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// rebase command
|
|
287
|
+
program
|
|
288
|
+
.command('rebase <change-id>')
|
|
289
|
+
.description('Rebase a change onto target branch (accepts change number or Change-ID)')
|
|
290
|
+
.option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
|
|
291
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
292
|
+
.action(async (changeId, options) => {
|
|
293
|
+
await executeEffect(
|
|
294
|
+
rebaseCommand(changeId, options).pipe(
|
|
295
|
+
Effect.provide(GerritApiServiceLive),
|
|
296
|
+
Effect.provide(ConfigServiceLive),
|
|
297
|
+
),
|
|
298
|
+
options,
|
|
299
|
+
'rebase_result',
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// submit command
|
|
304
|
+
program
|
|
305
|
+
.command('submit <change-id>')
|
|
306
|
+
.description('Submit a change for merging (accepts change number or Change-ID)')
|
|
307
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
308
|
+
.action(async (changeId, options) => {
|
|
309
|
+
await executeEffect(
|
|
310
|
+
submitCommand(changeId, options).pipe(
|
|
311
|
+
Effect.provide(GerritApiServiceLive),
|
|
312
|
+
Effect.provide(ConfigServiceLive),
|
|
313
|
+
),
|
|
314
|
+
options,
|
|
315
|
+
'submit_result',
|
|
316
|
+
)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// vote command
|
|
320
|
+
program
|
|
321
|
+
.command('vote <change-id>')
|
|
322
|
+
.description('Cast votes on a change (accepts change number or Change-ID)')
|
|
323
|
+
.option('--code-review <value>', 'Code-Review vote (-2 to +2)', parseInt)
|
|
324
|
+
.option('--verified <value>', 'Verified vote (-1 to +1)', parseInt)
|
|
325
|
+
.option('--label <name> <value>', 'Custom label vote (can be used multiple times)')
|
|
326
|
+
.option('-m, --message <message>', 'Comment with vote')
|
|
327
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
328
|
+
.action(async (changeId, options) => {
|
|
329
|
+
await executeEffect(
|
|
330
|
+
voteCommand(changeId, options).pipe(
|
|
331
|
+
Effect.provide(GerritApiServiceLive),
|
|
332
|
+
Effect.provide(ConfigServiceLive),
|
|
333
|
+
),
|
|
334
|
+
options,
|
|
335
|
+
'vote_result',
|
|
336
|
+
)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// Register all reviewer-related commands
|
|
340
|
+
registerReviewerCommands(program)
|
|
341
|
+
|
|
342
|
+
// projects command
|
|
343
|
+
program
|
|
344
|
+
.command('projects')
|
|
345
|
+
.description('List Gerrit projects')
|
|
346
|
+
.option('--pattern <regex>', 'Filter projects by name pattern')
|
|
347
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
348
|
+
.action(async (options) => {
|
|
349
|
+
await executeEffect(
|
|
350
|
+
projectsCommand(options).pipe(
|
|
351
|
+
Effect.provide(GerritApiServiceLive),
|
|
352
|
+
Effect.provide(ConfigServiceLive),
|
|
353
|
+
),
|
|
354
|
+
options,
|
|
355
|
+
'projects_result',
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// Register all group-related commands
|
|
360
|
+
registerGroupCommands(program)
|
|
361
|
+
|
|
362
|
+
// comments command
|
|
363
|
+
program
|
|
364
|
+
.command('comments <change-id>')
|
|
365
|
+
.description(
|
|
366
|
+
'Show all comments on a change with diff context (accepts change number or Change-ID)',
|
|
367
|
+
)
|
|
368
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
369
|
+
.action(async (changeId, options) => {
|
|
370
|
+
await executeEffect(
|
|
371
|
+
commentsCommand(changeId, options).pipe(
|
|
372
|
+
Effect.provide(GerritApiServiceLive),
|
|
373
|
+
Effect.provide(ConfigServiceLive),
|
|
374
|
+
),
|
|
375
|
+
options,
|
|
376
|
+
'comments_result',
|
|
377
|
+
)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// open command
|
|
381
|
+
program
|
|
382
|
+
.command('open <change-id>')
|
|
383
|
+
.description('Open a change in the browser (accepts change number or Change-ID)')
|
|
384
|
+
.action(async (changeId, options) => {
|
|
385
|
+
try {
|
|
386
|
+
const effect = openCommand(changeId, options).pipe(
|
|
387
|
+
Effect.provide(GerritApiServiceLive),
|
|
388
|
+
Effect.provide(ConfigServiceLive),
|
|
389
|
+
)
|
|
390
|
+
await Effect.runPromise(effect)
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
393
|
+
process.exit(1)
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// show command
|
|
398
|
+
program
|
|
399
|
+
.command('show [change-id]')
|
|
400
|
+
.description(
|
|
401
|
+
'Show comprehensive change information (auto-detects from HEAD commit if not specified)',
|
|
402
|
+
)
|
|
403
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
404
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
405
|
+
.addHelpText('after', SHOW_HELP_TEXT)
|
|
406
|
+
.action(async (changeId, options) => {
|
|
407
|
+
try {
|
|
408
|
+
const effect = showCommand(changeId, options).pipe(
|
|
409
|
+
Effect.provide(GerritApiServiceLive),
|
|
410
|
+
Effect.provide(ConfigServiceLive),
|
|
411
|
+
)
|
|
412
|
+
await Effect.runPromise(effect)
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
415
|
+
if (options.json) {
|
|
416
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
417
|
+
} else if (options.xml) {
|
|
418
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
419
|
+
console.log(`<show_result>`)
|
|
420
|
+
console.log(` <status>error</status>`)
|
|
421
|
+
console.log(` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
|
|
422
|
+
console.log(`</show_result>`)
|
|
423
|
+
} else {
|
|
424
|
+
console.error('✗ Error:', errorMessage)
|
|
425
|
+
}
|
|
426
|
+
process.exit(1)
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
// build-status command
|
|
431
|
+
program
|
|
432
|
+
.command('build-status [change-id]')
|
|
433
|
+
.description(
|
|
434
|
+
'Check build status from Gerrit messages (auto-detects from HEAD commit if not specified)',
|
|
435
|
+
)
|
|
436
|
+
.option('--watch', 'Watch build status until completion (mimics gh run watch)')
|
|
437
|
+
.option('-i, --interval <seconds>', 'Refresh interval in seconds (default: 10)', '10')
|
|
438
|
+
.option('--timeout <seconds>', 'Maximum wait time in seconds (default: 1800 / 30min)', '1800')
|
|
439
|
+
.option('--exit-status', 'Exit with non-zero status if build fails')
|
|
440
|
+
.addHelpText('after', BUILD_STATUS_HELP_TEXT)
|
|
441
|
+
.action(async (changeId, cmdOptions) => {
|
|
442
|
+
try {
|
|
443
|
+
const effect = buildStatusCommand(changeId, {
|
|
444
|
+
watch: cmdOptions.watch,
|
|
445
|
+
interval: Number.parseInt(cmdOptions.interval, 10),
|
|
446
|
+
timeout: Number.parseInt(cmdOptions.timeout, 10),
|
|
447
|
+
exitStatus: cmdOptions.exitStatus,
|
|
448
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive))
|
|
449
|
+
await Effect.runPromise(effect)
|
|
450
|
+
} catch (error) {
|
|
451
|
+
// Errors are handled within the command itself
|
|
452
|
+
// This catch is just for any unexpected errors
|
|
453
|
+
if (error instanceof Error && error.message !== 'Process exited') {
|
|
454
|
+
console.error('✗ Unexpected error:', error.message)
|
|
455
|
+
process.exit(3)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
// extract-url command
|
|
461
|
+
program
|
|
462
|
+
.command('extract-url <pattern> [change-id]')
|
|
463
|
+
.description(
|
|
464
|
+
'Extract URLs from change messages and comments (auto-detects from HEAD commit if not specified)',
|
|
465
|
+
)
|
|
466
|
+
.option('--include-comments', 'Also search inline comments (default: messages only)')
|
|
467
|
+
.option('--regex', 'Treat pattern as regex instead of substring match')
|
|
468
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
469
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
470
|
+
.addHelpText(
|
|
471
|
+
'after',
|
|
472
|
+
`
|
|
473
|
+
Examples:
|
|
474
|
+
# Extract all Jenkins build-summary-report URLs (substring match)
|
|
475
|
+
$ ger extract-url "build-summary-report"
|
|
476
|
+
|
|
477
|
+
# Get the latest build URL using tail
|
|
478
|
+
$ ger extract-url "build-summary-report" | tail -1
|
|
479
|
+
|
|
480
|
+
# Get the first build URL using head
|
|
481
|
+
$ ger extract-url "jenkins.inst-ci.net" | head -1
|
|
482
|
+
|
|
483
|
+
# For a specific change (using change number)
|
|
484
|
+
$ ger extract-url "build-summary" 391831
|
|
485
|
+
|
|
486
|
+
# For a specific change (using Change-ID)
|
|
487
|
+
$ ger extract-url "jenkins" If5a3ae8cb5a107e187447802358417f311d0c4b1
|
|
488
|
+
|
|
489
|
+
# Use regex for precise matching
|
|
490
|
+
$ ger extract-url "job/MyProject/job/main/\\d+/" --regex
|
|
491
|
+
|
|
492
|
+
# Search both messages and inline comments
|
|
493
|
+
$ ger extract-url "github.com" --include-comments
|
|
494
|
+
|
|
495
|
+
# JSON output for scripting
|
|
496
|
+
$ ger extract-url "jenkins" --json | jq -r '.urls[-1]'
|
|
497
|
+
|
|
498
|
+
# XML output
|
|
499
|
+
$ ger extract-url "jenkins" --xml
|
|
500
|
+
|
|
501
|
+
Note:
|
|
502
|
+
- URLs are output in chronological order (oldest first)
|
|
503
|
+
- Use tail -1 to get the latest URL, head -1 for the oldest
|
|
504
|
+
- When no change-id is provided, it will be automatically extracted from the
|
|
505
|
+
Change-ID footer in your HEAD commit`,
|
|
506
|
+
)
|
|
507
|
+
.action(async (pattern, changeId, options) => {
|
|
508
|
+
try {
|
|
509
|
+
const effect = extractUrlCommand(pattern, changeId, options).pipe(
|
|
510
|
+
Effect.provide(GerritApiServiceLive),
|
|
511
|
+
Effect.provide(ConfigServiceLive),
|
|
512
|
+
)
|
|
513
|
+
await Effect.runPromise(effect)
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
516
|
+
if (options.json) {
|
|
517
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
518
|
+
} else if (options.xml) {
|
|
519
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
520
|
+
console.log(`<extract_url_result>`)
|
|
521
|
+
console.log(` <status>error</status>`)
|
|
522
|
+
console.log(` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
|
|
523
|
+
console.log(`</extract_url_result>`)
|
|
524
|
+
} else {
|
|
525
|
+
console.error('✗ Error:', errorMessage)
|
|
526
|
+
}
|
|
527
|
+
process.exit(1)
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
// push command
|
|
532
|
+
program
|
|
533
|
+
.command('push')
|
|
534
|
+
.description('Push commits to Gerrit for code review')
|
|
535
|
+
.option('-b, --branch <branch>', 'Target branch (default: auto-detect)')
|
|
536
|
+
.option('-t, --topic <topic>', 'Set change topic')
|
|
537
|
+
.option('-r, --reviewer <email...>', 'Add reviewer(s)')
|
|
538
|
+
.option('--cc <email...>', 'Add CC recipient(s)')
|
|
539
|
+
.option('--wip', 'Mark as work-in-progress')
|
|
540
|
+
.option('--ready', 'Mark as ready for review')
|
|
541
|
+
.option('--hashtag <tag...>', 'Add hashtag(s)')
|
|
542
|
+
.option('--private', 'Mark change as private')
|
|
543
|
+
.option('--draft', 'Alias for --wip')
|
|
544
|
+
.option('--dry-run', 'Show what would be pushed without pushing')
|
|
545
|
+
.addHelpText('after', PUSH_HELP_TEXT)
|
|
546
|
+
.action(async (options) => {
|
|
547
|
+
try {
|
|
548
|
+
const effect = pushCommand({
|
|
549
|
+
branch: options.branch,
|
|
550
|
+
topic: options.topic,
|
|
551
|
+
reviewer: options.reviewer,
|
|
552
|
+
cc: options.cc,
|
|
553
|
+
wip: options.wip,
|
|
554
|
+
ready: options.ready,
|
|
555
|
+
hashtag: options.hashtag,
|
|
556
|
+
private: options.private,
|
|
557
|
+
draft: options.draft,
|
|
558
|
+
dryRun: options.dryRun,
|
|
559
|
+
}).pipe(Effect.provide(CommitHookServiceLive), Effect.provide(ConfigServiceLive))
|
|
560
|
+
await Effect.runPromise(effect)
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error('Error:', error instanceof Error ? error.message : String(error))
|
|
563
|
+
process.exit(1)
|
|
564
|
+
}
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
// checkout command
|
|
568
|
+
program
|
|
569
|
+
.command('checkout <change-id>')
|
|
570
|
+
.description('Fetch and checkout a Gerrit change')
|
|
571
|
+
.option('--detach', 'Checkout as detached HEAD without creating branch')
|
|
572
|
+
.option('--remote <name>', 'Use specific git remote (default: auto-detect)')
|
|
573
|
+
.addHelpText('after', CHECKOUT_HELP_TEXT)
|
|
574
|
+
.action(async (changeId, options) => {
|
|
575
|
+
try {
|
|
576
|
+
const effect = checkoutCommand(changeId, {
|
|
577
|
+
detach: options.detach,
|
|
578
|
+
remote: options.remote,
|
|
579
|
+
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(ConfigServiceLive))
|
|
580
|
+
await Effect.runPromise(effect)
|
|
581
|
+
} catch (error) {
|
|
582
|
+
console.error('Error:', error instanceof Error ? error.message : String(error))
|
|
583
|
+
process.exit(1)
|
|
584
|
+
}
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
// review command
|
|
588
|
+
program
|
|
589
|
+
.command('review <change-id>')
|
|
590
|
+
.description(
|
|
591
|
+
'AI-powered code review that analyzes changes and optionally posts comments (accepts change number or Change-ID)',
|
|
592
|
+
)
|
|
593
|
+
.option('--comment', 'Post the review as comments (prompts for confirmation)')
|
|
594
|
+
.option('-y, --yes', 'Skip confirmation prompts when posting comments')
|
|
595
|
+
.option('--debug', 'Show debug output including AI responses')
|
|
596
|
+
.option('--prompt <file>', 'Path to custom review prompt file (e.g., ~/prompts/review.md)')
|
|
597
|
+
.option('--tool <tool>', 'Preferred AI tool (claude, gemini, opencode)')
|
|
598
|
+
.option('--system-prompt <prompt>', 'Custom system prompt for the AI')
|
|
599
|
+
.addHelpText(
|
|
600
|
+
'after',
|
|
601
|
+
`
|
|
602
|
+
This command uses AI (claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
|
|
603
|
+
It performs a two-stage review process:
|
|
604
|
+
|
|
605
|
+
1. Generates inline comments for specific code issues
|
|
606
|
+
2. Generates an overall review comment
|
|
607
|
+
|
|
608
|
+
By default, the review is only displayed in the terminal.
|
|
609
|
+
Use --comment to post the review to Gerrit (with confirmation prompts).
|
|
610
|
+
Use --comment --yes to post without confirmation.
|
|
611
|
+
|
|
612
|
+
Requirements:
|
|
613
|
+
- One of these AI tools must be available: claude CLI, gemini CLI, or opencode CLI
|
|
614
|
+
- Gerrit credentials must be configured (run 'ger setup' first)
|
|
615
|
+
|
|
616
|
+
Examples:
|
|
617
|
+
# Review a change using change number (display only)
|
|
618
|
+
$ ger review 12345
|
|
619
|
+
|
|
620
|
+
# Review using Change-ID
|
|
621
|
+
$ ger review If5a3ae8cb5a107e187447802358417f311d0c4b1
|
|
622
|
+
|
|
623
|
+
# Review and prompt to post comments
|
|
624
|
+
$ ger review 12345 --comment
|
|
625
|
+
|
|
626
|
+
# Review and auto-post comments without prompting
|
|
627
|
+
$ ger review 12345 --comment --yes
|
|
628
|
+
|
|
629
|
+
# Use specific AI tool
|
|
630
|
+
$ ger review 12345 --tool gemini
|
|
631
|
+
|
|
632
|
+
# Show debug output to troubleshoot issues
|
|
633
|
+
$ ger review 12345 --debug
|
|
634
|
+
|
|
635
|
+
Note: Both change number (e.g., 12345) and Change-ID (e.g., If5a3ae8...) formats are accepted
|
|
636
|
+
`,
|
|
637
|
+
)
|
|
638
|
+
.action(async (changeId, options) => {
|
|
639
|
+
try {
|
|
640
|
+
const effect = reviewCommand(changeId, {
|
|
641
|
+
comment: options.comment,
|
|
642
|
+
yes: options.yes,
|
|
643
|
+
debug: options.debug,
|
|
644
|
+
prompt: options.prompt,
|
|
645
|
+
tool: options.tool,
|
|
646
|
+
systemPrompt: options.systemPrompt,
|
|
647
|
+
}).pipe(
|
|
648
|
+
Effect.provide(ReviewStrategyServiceLive),
|
|
649
|
+
Effect.provide(GerritApiServiceLive),
|
|
650
|
+
Effect.provide(ConfigServiceLive),
|
|
651
|
+
Effect.provide(GitWorktreeServiceLive),
|
|
652
|
+
)
|
|
653
|
+
await Effect.runPromise(effect)
|
|
654
|
+
} catch (error) {
|
|
655
|
+
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
656
|
+
process.exit(1)
|
|
657
|
+
}
|
|
658
|
+
})
|
|
659
|
+
}
|