@aaronshaf/ger 2.0.10 → 3.0.2
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/README.md +0 -26
- package/docs/prd/architecture.md +6 -48
- package/docs/prd/commands.md +85 -21
- package/index.ts +0 -19
- package/llms.txt +0 -14
- package/package.json +1 -5
- package/src/api/gerrit.ts +37 -40
- package/src/cli/commands/files.ts +86 -0
- package/src/cli/commands/reviewers.ts +95 -0
- package/src/cli/commands/set-ready.ts +76 -0
- package/src/cli/commands/set-wip.ts +76 -0
- package/src/cli/commands/setup.ts +0 -1
- package/src/cli/index.ts +1 -0
- package/src/cli/register-commands.ts +43 -102
- package/src/cli/register-state-commands.ts +106 -0
- package/src/schemas/gerrit.ts +2 -0
- package/src/schemas/reviewer.ts +16 -0
- package/src/utils/diff-formatters.ts +32 -0
- package/tests/files.test.ts +223 -0
- package/tests/reviewers.test.ts +259 -0
- package/src/cli/commands/review.ts +0 -486
- package/src/prompts/default-review.md +0 -86
- package/src/prompts/system-inline-review.md +0 -135
- package/src/prompts/system-overall-review.md +0 -206
- package/src/services/review-strategy.ts +0 -292
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
3
|
+
import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
4
|
+
|
|
5
|
+
interface SetReadyOptions {
|
|
6
|
+
message?: string
|
|
7
|
+
xml?: boolean
|
|
8
|
+
json?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const setReadyCommand = (
|
|
12
|
+
changeId?: string,
|
|
13
|
+
options: SetReadyOptions = {},
|
|
14
|
+
): Effect.Effect<void, ApiError, GerritApiService> =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const gerritApi = yield* GerritApiService
|
|
17
|
+
|
|
18
|
+
if (!changeId) {
|
|
19
|
+
console.error('✗ Change ID is required')
|
|
20
|
+
console.error(' Usage: ger set-ready <change-id>')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Try to fetch change details for richer output, but don't let it block the mutation
|
|
25
|
+
let changeNumber: number | undefined
|
|
26
|
+
let subject: string | undefined
|
|
27
|
+
try {
|
|
28
|
+
const change = yield* gerritApi.getChange(changeId)
|
|
29
|
+
changeNumber = change._number
|
|
30
|
+
subject = change.subject
|
|
31
|
+
} catch {
|
|
32
|
+
// Proceed without change details
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
yield* gerritApi.setReady(changeId, options.message)
|
|
36
|
+
|
|
37
|
+
if (options.json) {
|
|
38
|
+
console.log(
|
|
39
|
+
JSON.stringify(
|
|
40
|
+
{
|
|
41
|
+
status: 'success',
|
|
42
|
+
...(changeNumber !== undefined
|
|
43
|
+
? { change_number: changeNumber }
|
|
44
|
+
: { change_id: changeId }),
|
|
45
|
+
...(subject !== undefined ? { subject } : {}),
|
|
46
|
+
...(options.message ? { message: options.message } : {}),
|
|
47
|
+
},
|
|
48
|
+
null,
|
|
49
|
+
2,
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
} else if (options.xml) {
|
|
53
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
54
|
+
console.log(`<set_ready_result>`)
|
|
55
|
+
console.log(` <status>success</status>`)
|
|
56
|
+
if (changeNumber !== undefined) {
|
|
57
|
+
console.log(` <change_number>${changeNumber}</change_number>`)
|
|
58
|
+
} else {
|
|
59
|
+
console.log(` <change_id>${changeId}</change_id>`)
|
|
60
|
+
}
|
|
61
|
+
if (subject !== undefined) {
|
|
62
|
+
console.log(` <subject><![CDATA[${sanitizeCDATA(subject)}]]></subject>`)
|
|
63
|
+
}
|
|
64
|
+
if (options.message) {
|
|
65
|
+
console.log(` <message><![CDATA[${sanitizeCDATA(options.message)}]]></message>`)
|
|
66
|
+
}
|
|
67
|
+
console.log(`</set_ready_result>`)
|
|
68
|
+
} else {
|
|
69
|
+
const label = changeNumber !== undefined ? `${changeNumber}` : changeId
|
|
70
|
+
const suffix = subject !== undefined ? `: ${subject}` : ''
|
|
71
|
+
console.log(`✓ Marked change ${label} as ready for review${suffix}`)
|
|
72
|
+
if (options.message) {
|
|
73
|
+
console.log(` Message: ${options.message}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
3
|
+
import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
4
|
+
|
|
5
|
+
interface SetWipOptions {
|
|
6
|
+
message?: string
|
|
7
|
+
xml?: boolean
|
|
8
|
+
json?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const setWipCommand = (
|
|
12
|
+
changeId?: string,
|
|
13
|
+
options: SetWipOptions = {},
|
|
14
|
+
): Effect.Effect<void, ApiError, GerritApiService> =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const gerritApi = yield* GerritApiService
|
|
17
|
+
|
|
18
|
+
if (!changeId) {
|
|
19
|
+
console.error('✗ Change ID is required')
|
|
20
|
+
console.error(' Usage: ger set-wip <change-id>')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Try to fetch change details for richer output, but don't let it block the mutation
|
|
25
|
+
let changeNumber: number | undefined
|
|
26
|
+
let subject: string | undefined
|
|
27
|
+
try {
|
|
28
|
+
const change = yield* gerritApi.getChange(changeId)
|
|
29
|
+
changeNumber = change._number
|
|
30
|
+
subject = change.subject
|
|
31
|
+
} catch {
|
|
32
|
+
// Proceed without change details
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
yield* gerritApi.setWip(changeId, options.message)
|
|
36
|
+
|
|
37
|
+
if (options.json) {
|
|
38
|
+
console.log(
|
|
39
|
+
JSON.stringify(
|
|
40
|
+
{
|
|
41
|
+
status: 'success',
|
|
42
|
+
...(changeNumber !== undefined
|
|
43
|
+
? { change_number: changeNumber }
|
|
44
|
+
: { change_id: changeId }),
|
|
45
|
+
...(subject !== undefined ? { subject } : {}),
|
|
46
|
+
...(options.message ? { message: options.message } : {}),
|
|
47
|
+
},
|
|
48
|
+
null,
|
|
49
|
+
2,
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
} else if (options.xml) {
|
|
53
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
54
|
+
console.log(`<set_wip_result>`)
|
|
55
|
+
console.log(` <status>success</status>`)
|
|
56
|
+
if (changeNumber !== undefined) {
|
|
57
|
+
console.log(` <change_number>${changeNumber}</change_number>`)
|
|
58
|
+
} else {
|
|
59
|
+
console.log(` <change_id>${changeId}</change_id>`)
|
|
60
|
+
}
|
|
61
|
+
if (subject !== undefined) {
|
|
62
|
+
console.log(` <subject><![CDATA[${sanitizeCDATA(subject)}]]></subject>`)
|
|
63
|
+
}
|
|
64
|
+
if (options.message) {
|
|
65
|
+
console.log(` <message><![CDATA[${sanitizeCDATA(options.message)}]]></message>`)
|
|
66
|
+
}
|
|
67
|
+
console.log(`</set_wip_result>`)
|
|
68
|
+
} else {
|
|
69
|
+
const label = changeNumber !== undefined ? `${changeNumber}` : changeId
|
|
70
|
+
const suffix = subject !== undefined ? `: ${subject}` : ''
|
|
71
|
+
console.log(`✓ Marked change ${label} as work-in-progress${suffix}`)
|
|
72
|
+
if (options.message) {
|
|
73
|
+
console.log(` Message: ${options.message}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
@@ -260,7 +260,6 @@ const setupEffect = (configService: ConfigServiceImpl) =>
|
|
|
260
260
|
Effect.tap(() => Console.log('You can now use:')),
|
|
261
261
|
Effect.tap(() => Console.log(' • "ger mine" to view your changes')),
|
|
262
262
|
Effect.tap(() => Console.log(' • "ger show <change-id>" to view change details')),
|
|
263
|
-
Effect.tap(() => Console.log(' • "ger review <change-id>" to review with AI')),
|
|
264
263
|
Effect.catchAll((error) =>
|
|
265
264
|
pipe(
|
|
266
265
|
Console.error(
|
package/src/cli/index.ts
CHANGED
|
@@ -75,6 +75,7 @@ COMMON LLM WORKFLOWS
|
|
|
75
75
|
Review a change: ger show <id> → ger diff <id> → ger comments <id>
|
|
76
76
|
Post a review: ger comment <id> -m "..." → ger vote <id> <label> <score>
|
|
77
77
|
Manage changes: ger push, ger checkout <id>, ger abandon <id>, ger submit <id>
|
|
78
|
+
WIP toggle: ger set-wip <id>, ger set-ready <id> [-m "message"]
|
|
78
79
|
Check CI: ger build-status <id> --exit-status
|
|
79
80
|
|
|
80
81
|
EXIT CODES
|
|
@@ -2,11 +2,8 @@ import type { Command } from 'commander'
|
|
|
2
2
|
import { Effect } from 'effect'
|
|
3
3
|
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
4
4
|
import { ConfigServiceLive } from '@/services/config'
|
|
5
|
-
import { ReviewStrategyServiceLive } from '@/services/review-strategy'
|
|
6
|
-
import { GitWorktreeServiceLive } from '@/services/git-worktree'
|
|
7
5
|
import { CommitHookServiceLive } from '@/services/commit-hook'
|
|
8
|
-
import {
|
|
9
|
-
import { restoreCommand } from './commands/restore'
|
|
6
|
+
import { registerStateCommands } from './register-state-commands'
|
|
10
7
|
import { rebaseCommand } from './commands/rebase'
|
|
11
8
|
import { submitCommand } from './commands/submit'
|
|
12
9
|
import { topicCommand, TOPIC_HELP_TEXT } from './commands/topic'
|
|
@@ -23,7 +20,6 @@ import { installHookCommand } from './commands/install-hook'
|
|
|
23
20
|
import { mineCommand } from './commands/mine'
|
|
24
21
|
import { openCommand } from './commands/open'
|
|
25
22
|
import { pushCommand, PUSH_HELP_TEXT } from './commands/push'
|
|
26
|
-
import { reviewCommand } from './commands/review'
|
|
27
23
|
import { searchCommand, SEARCH_HELP_TEXT } from './commands/search'
|
|
28
24
|
import { setup } from './commands/setup'
|
|
29
25
|
import { showCommand, SHOW_HELP_TEXT } from './commands/show'
|
|
@@ -32,6 +28,8 @@ import { workspaceCommand } from './commands/workspace'
|
|
|
32
28
|
import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
33
29
|
import { registerGroupCommands } from './register-group-commands'
|
|
34
30
|
import { registerReviewerCommands } from './register-reviewer-commands'
|
|
31
|
+
import { filesCommand } from './commands/files'
|
|
32
|
+
import { reviewersCommand } from './commands/reviewers'
|
|
35
33
|
|
|
36
34
|
// Helper function to output error in plain text, JSON, or XML format
|
|
37
35
|
function outputError(
|
|
@@ -240,43 +238,8 @@ export function registerCommands(program: Command): void {
|
|
|
240
238
|
)
|
|
241
239
|
})
|
|
242
240
|
|
|
243
|
-
// abandon
|
|
244
|
-
program
|
|
245
|
-
.command('abandon [change-id]')
|
|
246
|
-
.description(
|
|
247
|
-
'Abandon a change (interactive mode if no change-id provided; accepts change number or Change-ID)',
|
|
248
|
-
)
|
|
249
|
-
.option('-m, --message <message>', 'Abandon message')
|
|
250
|
-
.option('--xml', 'XML output for LLM consumption')
|
|
251
|
-
.option('--json', 'JSON output for programmatic consumption')
|
|
252
|
-
.action(async (changeId, options) => {
|
|
253
|
-
await executeEffect(
|
|
254
|
-
abandonCommand(changeId, options).pipe(
|
|
255
|
-
Effect.provide(GerritApiServiceLive),
|
|
256
|
-
Effect.provide(ConfigServiceLive),
|
|
257
|
-
),
|
|
258
|
-
options,
|
|
259
|
-
'abandon_result',
|
|
260
|
-
)
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
// restore command
|
|
264
|
-
program
|
|
265
|
-
.command('restore <change-id>')
|
|
266
|
-
.description('Restore an abandoned change (accepts change number or Change-ID)')
|
|
267
|
-
.option('-m, --message <message>', 'Restoration message')
|
|
268
|
-
.option('--xml', 'XML output for LLM consumption')
|
|
269
|
-
.option('--json', 'JSON output for programmatic consumption')
|
|
270
|
-
.action(async (changeId, options) => {
|
|
271
|
-
await executeEffect(
|
|
272
|
-
restoreCommand(changeId, options).pipe(
|
|
273
|
-
Effect.provide(GerritApiServiceLive),
|
|
274
|
-
Effect.provide(ConfigServiceLive),
|
|
275
|
-
),
|
|
276
|
-
options,
|
|
277
|
-
'restore_result',
|
|
278
|
-
)
|
|
279
|
-
})
|
|
241
|
+
// abandon / restore / set-ready / set-wip commands
|
|
242
|
+
registerStateCommands(program)
|
|
280
243
|
|
|
281
244
|
// rebase command
|
|
282
245
|
program
|
|
@@ -617,6 +580,44 @@ Note:
|
|
|
617
580
|
}
|
|
618
581
|
})
|
|
619
582
|
|
|
583
|
+
// files command
|
|
584
|
+
program
|
|
585
|
+
.command('files [change-id]')
|
|
586
|
+
.description(
|
|
587
|
+
'List files changed in a Gerrit change (auto-detects from HEAD commit if not specified)',
|
|
588
|
+
)
|
|
589
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
590
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
591
|
+
.action(async (changeId, options) => {
|
|
592
|
+
await executeEffect(
|
|
593
|
+
filesCommand(changeId, options).pipe(
|
|
594
|
+
Effect.provide(GerritApiServiceLive),
|
|
595
|
+
Effect.provide(ConfigServiceLive),
|
|
596
|
+
),
|
|
597
|
+
options,
|
|
598
|
+
'files_result',
|
|
599
|
+
)
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
// reviewers command
|
|
603
|
+
program
|
|
604
|
+
.command('reviewers [change-id]')
|
|
605
|
+
.description(
|
|
606
|
+
'List reviewers on a Gerrit change (auto-detects from HEAD commit if not specified)',
|
|
607
|
+
)
|
|
608
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
609
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
610
|
+
.action(async (changeId, options) => {
|
|
611
|
+
await executeEffect(
|
|
612
|
+
reviewersCommand(changeId, options).pipe(
|
|
613
|
+
Effect.provide(GerritApiServiceLive),
|
|
614
|
+
Effect.provide(ConfigServiceLive),
|
|
615
|
+
),
|
|
616
|
+
options,
|
|
617
|
+
'reviewers_result',
|
|
618
|
+
)
|
|
619
|
+
})
|
|
620
|
+
|
|
620
621
|
// checkout command
|
|
621
622
|
program
|
|
622
623
|
.command('checkout <change-id>')
|
|
@@ -636,64 +637,4 @@ Note:
|
|
|
636
637
|
process.exit(1)
|
|
637
638
|
}
|
|
638
639
|
})
|
|
639
|
-
|
|
640
|
-
// review command
|
|
641
|
-
program
|
|
642
|
-
.command('review <change-id>')
|
|
643
|
-
.description(
|
|
644
|
-
'AI-powered code review that analyzes changes and optionally posts comments (accepts change number or Change-ID)',
|
|
645
|
-
)
|
|
646
|
-
.option('--comment', 'Post the review as comments (prompts for confirmation)')
|
|
647
|
-
.option('-y, --yes', 'Skip confirmation prompts when posting comments')
|
|
648
|
-
.option('--debug', 'Show debug output including AI responses')
|
|
649
|
-
.option('--prompt <file>', 'Path to custom review prompt file (e.g., ~/prompts/review.md)')
|
|
650
|
-
.option('--tool <tool>', 'Preferred AI tool (claude, gemini, opencode)')
|
|
651
|
-
.option('--system-prompt <prompt>', 'Custom system prompt for the AI')
|
|
652
|
-
.addHelpText(
|
|
653
|
-
'after',
|
|
654
|
-
`
|
|
655
|
-
This command uses AI (claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
|
|
656
|
-
It performs a two-stage review process:
|
|
657
|
-
1. Generates inline comments for specific code issues
|
|
658
|
-
2. Generates an overall review comment
|
|
659
|
-
By default, the review is only displayed in the terminal.
|
|
660
|
-
Use --comment to post the review to Gerrit (with confirmation prompts).
|
|
661
|
-
Use --comment --yes to post without confirmation.
|
|
662
|
-
|
|
663
|
-
Requirements:
|
|
664
|
-
- One of these AI tools must be available: claude CLI, gemini CLI, or opencode CLI
|
|
665
|
-
- Gerrit credentials must be configured (run 'ger setup' first)
|
|
666
|
-
|
|
667
|
-
Examples:
|
|
668
|
-
$ ger review 12345
|
|
669
|
-
$ ger review If5a3ae8cb5a107e187447802358417f311d0c4b1
|
|
670
|
-
$ ger review 12345 --comment
|
|
671
|
-
$ ger review 12345 --comment --yes
|
|
672
|
-
$ ger review 12345 --tool gemini
|
|
673
|
-
$ ger review 12345 --debug
|
|
674
|
-
|
|
675
|
-
Note: Both change number (e.g., 12345) and Change-ID (e.g., If5a3ae8...) formats are accepted
|
|
676
|
-
`,
|
|
677
|
-
)
|
|
678
|
-
.action(async (changeId, options) => {
|
|
679
|
-
try {
|
|
680
|
-
const effect = reviewCommand(changeId, {
|
|
681
|
-
comment: options.comment,
|
|
682
|
-
yes: options.yes,
|
|
683
|
-
debug: options.debug,
|
|
684
|
-
prompt: options.prompt,
|
|
685
|
-
tool: options.tool,
|
|
686
|
-
systemPrompt: options.systemPrompt,
|
|
687
|
-
}).pipe(
|
|
688
|
-
Effect.provide(ReviewStrategyServiceLive),
|
|
689
|
-
Effect.provide(GerritApiServiceLive),
|
|
690
|
-
Effect.provide(ConfigServiceLive),
|
|
691
|
-
Effect.provide(GitWorktreeServiceLive),
|
|
692
|
-
)
|
|
693
|
-
await Effect.runPromise(effect)
|
|
694
|
-
} catch (error) {
|
|
695
|
-
console.error('✗ Error:', error instanceof Error ? error.message : String(error))
|
|
696
|
-
process.exit(1)
|
|
697
|
-
}
|
|
698
|
-
})
|
|
699
640
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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 { abandonCommand } from './commands/abandon'
|
|
6
|
+
import { restoreCommand } from './commands/restore'
|
|
7
|
+
import { setReadyCommand } from './commands/set-ready'
|
|
8
|
+
import { setWipCommand } from './commands/set-wip'
|
|
9
|
+
|
|
10
|
+
type StateOptions = { message?: string; xml?: boolean; json?: boolean }
|
|
11
|
+
|
|
12
|
+
async function executeStateEffect(
|
|
13
|
+
effect: Effect.Effect<void, unknown, never>,
|
|
14
|
+
options: StateOptions,
|
|
15
|
+
resultTag: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
await Effect.runPromise(effect)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
21
|
+
if (options.xml) {
|
|
22
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
23
|
+
console.log(`<${resultTag}>`)
|
|
24
|
+
console.log(` <status>error</status>`)
|
|
25
|
+
console.log(` <error><![CDATA[${errorMessage}]]></error>`)
|
|
26
|
+
console.log(`</${resultTag}>`)
|
|
27
|
+
} else if (options.json) {
|
|
28
|
+
console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
|
|
29
|
+
} else {
|
|
30
|
+
console.error('✗ Error:', errorMessage)
|
|
31
|
+
}
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function registerStateCommands(program: Command): void {
|
|
37
|
+
program
|
|
38
|
+
.command('abandon [change-id]')
|
|
39
|
+
.description(
|
|
40
|
+
'Abandon a change (interactive mode if no change-id provided; accepts change number or Change-ID)',
|
|
41
|
+
)
|
|
42
|
+
.option('-m, --message <message>', 'Abandon message')
|
|
43
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
44
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
45
|
+
.action(async (changeId, options) => {
|
|
46
|
+
await executeStateEffect(
|
|
47
|
+
abandonCommand(changeId, options).pipe(
|
|
48
|
+
Effect.provide(GerritApiServiceLive),
|
|
49
|
+
Effect.provide(ConfigServiceLive),
|
|
50
|
+
),
|
|
51
|
+
options,
|
|
52
|
+
'abandon_result',
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('restore <change-id>')
|
|
58
|
+
.description('Restore an abandoned change (accepts change number or Change-ID)')
|
|
59
|
+
.option('-m, --message <message>', 'Restoration message')
|
|
60
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
61
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
62
|
+
.action(async (changeId, options) => {
|
|
63
|
+
await executeStateEffect(
|
|
64
|
+
restoreCommand(changeId, options).pipe(
|
|
65
|
+
Effect.provide(GerritApiServiceLive),
|
|
66
|
+
Effect.provide(ConfigServiceLive),
|
|
67
|
+
),
|
|
68
|
+
options,
|
|
69
|
+
'restore_result',
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command('set-ready <change-id>')
|
|
75
|
+
.description('Mark a WIP change as ready for review (accepts change number or Change-ID)')
|
|
76
|
+
.option('-m, --message <message>', 'Message to include with the status change')
|
|
77
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
78
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
79
|
+
.action(async (changeId, options) => {
|
|
80
|
+
await executeStateEffect(
|
|
81
|
+
setReadyCommand(changeId, options).pipe(
|
|
82
|
+
Effect.provide(GerritApiServiceLive),
|
|
83
|
+
Effect.provide(ConfigServiceLive),
|
|
84
|
+
),
|
|
85
|
+
options,
|
|
86
|
+
'set_ready_result',
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command('set-wip <change-id>')
|
|
92
|
+
.description('Mark a change as work-in-progress (accepts change number or Change-ID)')
|
|
93
|
+
.option('-m, --message <message>', 'Message to include with the status change')
|
|
94
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
95
|
+
.option('--json', 'JSON output for programmatic consumption')
|
|
96
|
+
.action(async (changeId, options) => {
|
|
97
|
+
await executeStateEffect(
|
|
98
|
+
setWipCommand(changeId, options).pipe(
|
|
99
|
+
Effect.provide(GerritApiServiceLive),
|
|
100
|
+
Effect.provide(ConfigServiceLive),
|
|
101
|
+
),
|
|
102
|
+
options,
|
|
103
|
+
'set_wip_result',
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
}
|
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>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { colors } from './formatters'
|
|
2
|
+
import type { FileDiffContent } from '@/schemas/gerrit'
|
|
2
3
|
|
|
3
4
|
interface DiffStats {
|
|
4
5
|
additions: number
|
|
@@ -139,3 +140,34 @@ export const extractDiffStats = (diffContent: string): DiffStats => {
|
|
|
139
140
|
|
|
140
141
|
return { additions, deletions, files }
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
export const convertToUnifiedDiff = (diff: FileDiffContent, filePath: string): string => {
|
|
145
|
+
const lines: string[] = []
|
|
146
|
+
|
|
147
|
+
if (diff.diff_header) {
|
|
148
|
+
lines.push(...diff.diff_header)
|
|
149
|
+
} else {
|
|
150
|
+
lines.push(`--- a/${filePath}`)
|
|
151
|
+
lines.push(`+++ b/${filePath}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const section of diff.content) {
|
|
155
|
+
if (section.ab) {
|
|
156
|
+
for (const line of section.ab) {
|
|
157
|
+
lines.push(` ${line}`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (section.a) {
|
|
161
|
+
for (const line of section.a) {
|
|
162
|
+
lines.push(`-${line}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (section.b) {
|
|
166
|
+
for (const line of section.b) {
|
|
167
|
+
lines.push(`+${line}`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return lines.join('\n')
|
|
173
|
+
}
|