@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 CHANGED
@@ -93,9 +93,6 @@ ger rebase 12345
93
93
  ger submit 12345
94
94
  ger restore 12345
95
95
 
96
- # AI-powered code review (requires claude, llm, or opencode CLI)
97
- ger review 12345
98
- ger review 12345 --dry-run # Preview without posting
99
96
  ```
100
97
 
101
98
  ## Commands
@@ -711,29 +708,6 @@ ger groups-members project-reviewers --xml
711
708
  - Group IDs can be names, numeric IDs, or UUIDs
712
709
  - Use groups with `ger add-reviewer --group` to add entire teams as reviewers
713
710
 
714
- ### AI-Powered Review
715
-
716
- The `ger review` command provides automated code review using AI tools (claude, llm, or opencode CLI).
717
-
718
- ```bash
719
- # Full AI review with inline and overall comments
720
- ger review 12345
721
-
722
- # Preview what would be posted without actually posting
723
- ger review 12345 --dry-run
724
-
725
- # Show debug output including AI responses
726
- ger review 12345 --debug
727
- ```
728
-
729
- The review command performs a two-stage review process:
730
- 1. **Inline comments**: Specific code issues with line-by-line feedback
731
- 2. **Overall review**: High-level assessment and recommendations
732
-
733
- Requirements:
734
- - One of these AI tools must be installed: `claude`, `llm`, or `opencode`
735
- - Gerrit credentials must be configured (`ger setup`)
736
-
737
711
  ## Claude Code Skill
738
712
 
739
713
  This repository includes a Claude Code Agent Skill that teaches Claude how to work effectively with Gerrit using the ger CLI. The skill provides Claude with expertise in Gerrit workflows, command usage, and best practices.
@@ -6,16 +6,16 @@
6
6
  ┌─────────────────────────────────────────────────────────────┐
7
7
  │ CLI Layer │
8
8
  │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
9
- │ │ show │ │ comment │ │ push │ │ review │ ... │
9
+ │ │ show │ │ comment │ │ push │ │ vote │ ... │
10
10
  │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
11
11
  └───────┼───────────┼───────────┼───────────┼─────────────────┘
12
12
  │ │ │ │
13
13
  ┌───────┴───────────┴───────────┴───────────┴─────────────────┐
14
14
  │ Service Layer │
15
- │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
16
- │ │ GerritApi │ │ ConfigService│ ReviewStrategy│ │
17
- │ │ Service │ │ │ │ │
18
- │ └──────────────┘ └──────────────┘ └──────────────┘
15
+ │ ┌──────────────┐ ┌──────────────┐
16
+ │ │ GerritApi │ │ ConfigService│
17
+ │ │ Service │ │ │
18
+ │ └──────────────┘ └──────────────┘
19
19
  └───────┬───────────────────────────────────────────────────────┘
20
20
 
21
21
  ┌───────┴─────────────────────────────────────────────────────┐
@@ -46,7 +46,6 @@ src/
46
46
 
47
47
  ├── services/ # Business logic services
48
48
  │ ├── config.ts # Configuration management
49
- │ ├── review-strategy.ts # AI tool strategies
50
49
  │ ├── git-worktree.ts # Git worktree operations
51
50
  │ └── commit-hook.ts # Gerrit hook installation
52
51
 
@@ -59,12 +58,7 @@ src/
59
58
  │ ├── git-commit.ts # Git operations
60
59
  │ ├── formatters.ts # Output formatting
61
60
  │ ├── shell-safety.ts # XML/CDATA handling
62
- │ └── ... (diff, comment, review utils)
63
-
64
- ├── prompts/ # AI review prompts
65
- │ ├── default-review.md
66
- │ ├── system-inline-review.md
67
- │ └── system-overall-review.md
61
+ │ └── ... (diff, comment utils)
68
62
 
69
63
  └── i18n/ # Internationalization (planned)
70
64
 
@@ -207,42 +201,6 @@ User: echo '...' | ger comment 12345
207
201
  Success/Error
208
202
  ```
209
203
 
210
- ## AI Integration Flow
211
-
212
- ```
213
- User: ger review 12345
214
-
215
-
216
- ┌─────────────────────┐
217
- │ Fetch change diff │
218
- │ (API: GET /patch) │
219
- └─────────┬───────────┘
220
-
221
-
222
- ┌─────────────────────┐
223
- │ Detect AI tool │
224
- │ (claude/llm/etc) │
225
- └─────────┬───────────┘
226
-
227
-
228
- ┌─────────────────────┐ ┌─────────────────────┐
229
- │ Stage 1: Inline │────►│ AI: Generate inline │
230
- │ comments │ │ comments JSON │
231
- └─────────┬───────────┘ └─────────────────────┘
232
-
233
-
234
- ┌─────────────────────┐ ┌─────────────────────┐
235
- │ Stage 2: Overall │────►│ AI: Generate │
236
- │ review │ │ summary │
237
- └─────────┬───────────┘ └─────────────────────┘
238
-
239
-
240
- ┌─────────────────────┐
241
- │ API: POST /review │
242
- │ (all comments) │
243
- └─────────────────────┘
244
- ```
245
-
246
204
  ## Configuration Architecture
247
205
 
248
206
  ```
@@ -28,7 +28,65 @@ ger show # Auto-detect from HEAD
28
28
  - Full diff
29
29
  - All comments with context
30
30
 
31
- Reviewer listing for a specific change is provided by `show` (there is no separate `list-reviewers` command).
31
+ ### files
32
+
33
+ List files changed in a change.
34
+
35
+ ```bash
36
+ ger files [change-id]
37
+ ger files 12345
38
+ ger files # Auto-detect from HEAD
39
+ ger files 12345 --json
40
+ ger files 12345 --xml
41
+ ```
42
+
43
+ | Option | Description |
44
+ |--------|-------------|
45
+ | `--json` | Output as JSON |
46
+ | `--xml` | Output as XML for LLM consumption |
47
+
48
+ **Output:** One file per line with status prefix (`M` modified, `A` added, `D` deleted, `R` renamed). Magic files (`/COMMIT_MSG`, `/MERGE_LIST`, `/PATCHSET_LEVEL`) are filtered out.
49
+
50
+ **JSON output:**
51
+ ```json
52
+ {
53
+ "status": "success",
54
+ "change_id": "12345",
55
+ "files": [
56
+ { "path": "src/foo.ts", "status": "M", "lines_inserted": 10, "lines_deleted": 2 }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ ### reviewers
62
+
63
+ List reviewers on a change.
64
+
65
+ ```bash
66
+ ger reviewers [change-id]
67
+ ger reviewers 12345
68
+ ger reviewers # Auto-detect from HEAD
69
+ ger reviewers 12345 --json
70
+ ger reviewers 12345 --xml
71
+ ```
72
+
73
+ | Option | Description |
74
+ |--------|-------------|
75
+ | `--json` | Output as JSON |
76
+ | `--xml` | Output as XML for LLM consumption |
77
+
78
+ **Output:** One reviewer per line in `Name <email>` format (or email alone for email-only accounts).
79
+
80
+ **JSON output:**
81
+ ```json
82
+ {
83
+ "status": "success",
84
+ "change_id": "12345",
85
+ "reviewers": [
86
+ { "account_id": 1001, "name": "Alice Smith", "email": "alice@example.com", "username": "alice" }
87
+ ]
88
+ }
89
+ ```
32
90
 
33
91
  ### diff
34
92
 
@@ -121,6 +179,32 @@ ger restore <change-id>
121
179
  ger restore <change-id> -m "Needed after all"
122
180
  ```
123
181
 
182
+ ### set-ready
183
+
184
+ Mark a WIP change as ready for review via the Gerrit REST API. Does not require a git push.
185
+
186
+ ```bash
187
+ ger set-ready <change-id>
188
+ ger set-ready <change-id> -m "Ready for another look"
189
+ ```
190
+
191
+ | Option | Description |
192
+ |--------|-------------|
193
+ | `-m <message>` | Optional message to include with the status change |
194
+
195
+ ### set-wip
196
+
197
+ Mark a change as work-in-progress via the Gerrit REST API. Does not require a git push.
198
+
199
+ ```bash
200
+ ger set-wip <change-id>
201
+ ger set-wip <change-id> -m "Still in progress"
202
+ ```
203
+
204
+ | Option | Description |
205
+ |--------|-------------|
206
+ | `-m <message>` | Optional message to include with the status change |
207
+
124
208
  ### workspace
125
209
 
126
210
  View local git branch tracking information.
@@ -236,26 +320,6 @@ ger vote <change-id> --label "Custom-Label" +1
236
320
  | `--label <name> <score>` | Custom label vote |
237
321
  | `-m <message>` | Optional message with vote |
238
322
 
239
- ### review
240
-
241
- AI-powered code review (multi-stage).
242
-
243
- ```bash
244
- ger review <change-id>
245
- ger review <change-id> --tool claude # Specific AI tool
246
- ger review # Auto-detect change from HEAD
247
- ```
248
-
249
- | Option | Description |
250
- |--------|-------------|
251
- | `--tool <name>` | AI tool (claude, llm, opencode, gemini) |
252
- | `--inline-only` | Only post inline comments |
253
- | `--overall-only` | Only post overall review |
254
-
255
- **Stages:**
256
- 1. **Inline**: Generate line-specific comments
257
- 2. **Overall**: Generate high-level assessment
258
-
259
323
  ### add-reviewer
260
324
 
261
325
  Add reviewers or groups to a change.
package/index.ts CHANGED
@@ -61,25 +61,6 @@ export {
61
61
  type ConfigErrorFields,
62
62
  } from './src/services/config'
63
63
 
64
- // ============================================================================
65
- // Review Strategy Service
66
- // ============================================================================
67
-
68
- export {
69
- // Strategy types
70
- type ReviewStrategy,
71
- // Built-in strategies
72
- claudeCliStrategy,
73
- geminiCliStrategy,
74
- openCodeCliStrategy,
75
- // Service
76
- ReviewStrategyService,
77
- ReviewStrategyServiceLive,
78
- type ReviewStrategyServiceImpl,
79
- // Errors
80
- ReviewStrategyError,
81
- type ReviewStrategyErrorFields,
82
- } from './src/services/review-strategy'
83
64
 
84
65
  // ============================================================================
85
66
  // Git Worktree Service
package/llms.txt CHANGED
@@ -114,14 +114,6 @@ ger extract-url "build-summary-report" | tail -1 # Latest Jenkins URL
114
114
  ger extract-url "jenkins" --json | jq -r '.urls[-1]'
115
115
  ```
116
116
 
117
- ### AI Review
118
-
119
- ```bash
120
- ger review <change-id> # AI-powered review (requires claude/gemini/opencode CLI)
121
- ger review <change-id> --tool claude
122
- ger review <change-id> --comment --yes # Post review comments
123
- ```
124
-
125
117
  ### Groups
126
118
 
127
119
  ```bash
@@ -179,12 +171,6 @@ ger comment 12345 -m "LGTM" # Add comment
179
171
  ger vote 12345 --code-review +2 # Approve
180
172
  ```
181
173
 
182
- ### Post AI review
183
-
184
- ```bash
185
- ger review 12345 --comment --yes
186
- ```
187
-
188
174
  ### Check build and get failures
189
175
 
190
176
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "2.0.10",
3
+ "version": "3.0.2",
4
4
  "description": "Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS",
5
5
  "keywords": [
6
6
  "gerrit",
@@ -30,10 +30,6 @@
30
30
  "import": "./src/services/config.ts",
31
31
  "types": "./src/services/config.ts"
32
32
  },
33
- "./services/review-strategy": {
34
- "import": "./src/services/review-strategy.ts",
35
- "types": "./src/services/review-strategy.ts"
36
- },
37
33
  "./services/git-worktree": {
38
34
  "import": "./src/services/git-worktree.ts",
39
35
  "types": "./src/services/git-worktree.ts"
package/src/api/gerrit.ts CHANGED
@@ -18,7 +18,9 @@ import {
18
18
  GroupDetailInfo,
19
19
  AccountInfo,
20
20
  } from '@/schemas/gerrit'
21
+ import { ReviewerListItem } from '@/schemas/reviewer'
21
22
  import { filterMeaningfulMessages } from '@/utils/message-filters'
23
+ import { convertToUnifiedDiff } from '@/utils/diff-formatters'
22
24
  import { ConfigService } from '@/services/config'
23
25
  import { normalizeChangeIdentifier } from '@/utils/change-id'
24
26
 
@@ -85,6 +87,7 @@ export interface GerritApiServiceImpl {
85
87
  readonly getGroup: (groupId: string) => Effect.Effect<GroupInfo, ApiError>
86
88
  readonly getGroupDetail: (groupId: string) => Effect.Effect<GroupDetailInfo, ApiError>
87
89
  readonly getGroupMembers: (groupId: string) => Effect.Effect<readonly AccountInfo[], ApiError>
90
+ readonly getReviewers: (changeId: string) => Effect.Effect<readonly ReviewerListItem[], ApiError>
88
91
  readonly removeReviewer: (
89
92
  changeId: string,
90
93
  accountId: string,
@@ -93,6 +96,8 @@ export interface GerritApiServiceImpl {
93
96
  readonly getTopic: (changeId: string) => Effect.Effect<string | null, ApiError>
94
97
  readonly setTopic: (changeId: string, topic: string) => Effect.Effect<string, ApiError>
95
98
  readonly deleteTopic: (changeId: string) => Effect.Effect<void, ApiError>
99
+ readonly setReady: (changeId: string, message?: string) => Effect.Effect<void, ApiError>
100
+ readonly setWip: (changeId: string, message?: string) => Effect.Effect<void, ApiError>
96
101
  }
97
102
 
98
103
  export const GerritApiService: Context.Tag<GerritApiServiceImpl, GerritApiServiceImpl> =
@@ -224,13 +229,8 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
224
229
  const { credentials, authHeader } = yield* getCredentialsAndAuth
225
230
  let url = `${credentials.host}/a/projects/`
226
231
  if (options?.pattern) url += `?p=${encodeURIComponent(options.pattern)}`
227
- const projectsRecord = yield* makeRequest(
228
- url,
229
- authHeader,
230
- 'GET',
231
- undefined,
232
- Schema.Record({ key: Schema.String, value: ProjectInfo }),
233
- )
232
+ const schema = Schema.Record({ key: Schema.String, value: ProjectInfo })
233
+ const projectsRecord = yield* makeRequest(url, authHeader, 'GET', undefined, schema)
234
234
  return Object.values(projectsRecord).sort((a, b) => a.name.localeCompare(b.name))
235
235
  })
236
236
 
@@ -455,39 +455,6 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
455
455
  return yield* getPatch(changeId, revisionId)
456
456
  })
457
457
 
458
- const convertToUnifiedDiff = (diff: FileDiffContent, filePath: string): string => {
459
- const lines: string[] = []
460
-
461
- if (diff.diff_header) {
462
- lines.push(...diff.diff_header)
463
- } else {
464
- lines.push(`--- a/${filePath}`)
465
- lines.push(`+++ b/${filePath}`)
466
- }
467
-
468
- for (const section of diff.content) {
469
- if (section.ab) {
470
- for (const line of section.ab) {
471
- lines.push(` ${line}`)
472
- }
473
- }
474
-
475
- if (section.a) {
476
- for (const line of section.a) {
477
- lines.push(`-${line}`)
478
- }
479
- }
480
-
481
- if (section.b) {
482
- for (const line of section.b) {
483
- lines.push(`+${line}`)
484
- }
485
- }
486
- }
487
-
488
- return lines.join('\n')
489
- }
490
-
491
458
  const getComments = (changeId: string, revisionId = 'current') =>
492
459
  Effect.gen(function* () {
493
460
  const { credentials, authHeader } = yield* getCredentialsAndAuth
@@ -611,6 +578,15 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
611
578
  return yield* makeRequest(url, authHeader, 'GET', undefined, Schema.Array(AccountInfo))
612
579
  })
613
580
 
581
+ const getReviewers = (changeId: string) =>
582
+ Effect.gen(function* () {
583
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
584
+ const normalized = yield* normalizeAndValidate(changeId)
585
+ const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}/reviewers`
586
+ const schema = Schema.Array(ReviewerListItem)
587
+ return yield* makeRequest(url, authHeader, 'GET', undefined, schema)
588
+ })
589
+
614
590
  const removeReviewer = (
615
591
  changeId: string,
616
592
  accountId: string,
@@ -667,6 +643,24 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
667
643
  yield* makeRequest(getTopicUrl(credentials.host, normalized), authHeader, 'DELETE')
668
644
  })
669
645
 
646
+ const setReady = (changeId: string, message?: string) =>
647
+ Effect.gen(function* () {
648
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
649
+ const normalized = yield* normalizeAndValidate(changeId)
650
+ const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}/ready`
651
+ const body = message ? { message } : {}
652
+ yield* makeRequest(url, authHeader, 'POST', body)
653
+ })
654
+
655
+ const setWip = (changeId: string, message?: string) =>
656
+ Effect.gen(function* () {
657
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
658
+ const normalized = yield* normalizeAndValidate(changeId)
659
+ const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}/wip`
660
+ const body = message ? { message } : {}
661
+ yield* makeRequest(url, authHeader, 'POST', body)
662
+ })
663
+
670
664
  return {
671
665
  getChange,
672
666
  listChanges,
@@ -686,6 +680,7 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
686
680
  getComments,
687
681
  getMessages,
688
682
  addReviewer,
683
+ getReviewers,
689
684
  listGroups,
690
685
  getGroup,
691
686
  getGroupDetail,
@@ -694,6 +689,8 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
694
689
  getTopic,
695
690
  setTopic,
696
691
  deleteTopic,
692
+ setReady,
693
+ setWip,
697
694
  }
698
695
  }),
699
696
  )
@@ -0,0 +1,86 @@
1
+ import { Effect } from 'effect'
2
+ import { type ApiError, GerritApiService } from '@/api/gerrit'
3
+ import { GitError, NoChangeIdError, getChangeIdFromHead } from '@/utils/git-commit'
4
+ import { escapeXML, sanitizeCDATA } from '@/utils/shell-safety'
5
+
6
+ const MAGIC_FILES = new Set(['/COMMIT_MSG', '/MERGE_LIST', '/PATCHSET_LEVEL'])
7
+
8
+ interface FilesOptions {
9
+ xml?: boolean
10
+ json?: boolean
11
+ }
12
+
13
+ export const filesCommand = (
14
+ changeId?: string,
15
+ options: FilesOptions = {},
16
+ ): Effect.Effect<void, never, GerritApiService> =>
17
+ Effect.gen(function* () {
18
+ const gerritApi = yield* GerritApiService
19
+ const resolvedChangeId = changeId || (yield* getChangeIdFromHead())
20
+
21
+ const filesRecord = yield* gerritApi.getFiles(resolvedChangeId)
22
+ const files = Object.entries(filesRecord)
23
+ .filter(([path]) => !MAGIC_FILES.has(path))
24
+ .map(([path, info]) => ({
25
+ path,
26
+ status: info.status ?? 'M',
27
+ lines_inserted: info.lines_inserted ?? 0,
28
+ lines_deleted: info.lines_deleted ?? 0,
29
+ }))
30
+
31
+ if (options.json) {
32
+ console.log(
33
+ JSON.stringify(
34
+ {
35
+ status: 'success',
36
+ change_id: resolvedChangeId,
37
+ files,
38
+ },
39
+ null,
40
+ 2,
41
+ ),
42
+ )
43
+ } else if (options.xml) {
44
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
45
+ console.log(`<files_result>`)
46
+ console.log(` <status>success</status>`)
47
+ console.log(` <change_id><![CDATA[${sanitizeCDATA(resolvedChangeId)}]]></change_id>`)
48
+ console.log(` <files>`)
49
+ for (const file of files) {
50
+ console.log(` <file>`)
51
+ console.log(` <path><![CDATA[${sanitizeCDATA(file.path)}]]></path>`)
52
+ console.log(` <status>${escapeXML(file.status)}</status>`)
53
+ console.log(` <lines_inserted>${file.lines_inserted}</lines_inserted>`)
54
+ console.log(` <lines_deleted>${file.lines_deleted}</lines_deleted>`)
55
+ console.log(` </file>`)
56
+ }
57
+ console.log(` </files>`)
58
+ console.log(`</files_result>`)
59
+ } else {
60
+ for (const file of files) {
61
+ console.log(`${file.status} ${file.path}`)
62
+ }
63
+ }
64
+ }).pipe(
65
+ Effect.catchAll((error: ApiError | GitError | NoChangeIdError) =>
66
+ Effect.sync(() => {
67
+ const errorMessage =
68
+ error instanceof GitError || error instanceof NoChangeIdError || error instanceof Error
69
+ ? error.message
70
+ : String(error)
71
+
72
+ if (options.json) {
73
+ console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
74
+ } else if (options.xml) {
75
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
76
+ console.log(`<files_result>`)
77
+ console.log(` <status>error</status>`)
78
+ console.log(` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
79
+ console.log(`</files_result>`)
80
+ } else {
81
+ console.error(`✗ Error: ${errorMessage}`)
82
+ }
83
+ process.exit(1)
84
+ }),
85
+ ),
86
+ )
@@ -0,0 +1,95 @@
1
+ import { Effect } from 'effect'
2
+ import { type ApiError, GerritApiService } from '@/api/gerrit'
3
+ import { GitError, NoChangeIdError, getChangeIdFromHead } from '@/utils/git-commit'
4
+ import { sanitizeCDATA } from '@/utils/shell-safety'
5
+ import type { ReviewerListItem } from '@/schemas/reviewer'
6
+
7
+ interface ReviewersOptions {
8
+ xml?: boolean
9
+ json?: boolean
10
+ }
11
+
12
+ function formatReviewer(r: ReviewerListItem): string {
13
+ const name =
14
+ r.name ?? r.username ?? (r._account_id !== undefined ? `#${r._account_id}` : undefined)
15
+ if (name !== undefined) return r.email ? `${name} <${r.email}>` : name
16
+ return r.email ?? 'unknown'
17
+ }
18
+
19
+ export const reviewersCommand = (
20
+ changeId?: string,
21
+ options: ReviewersOptions = {},
22
+ ): Effect.Effect<void, never, GerritApiService> =>
23
+ Effect.gen(function* () {
24
+ const gerritApi = yield* GerritApiService
25
+ const resolvedChangeId = changeId || (yield* getChangeIdFromHead())
26
+
27
+ const reviewers = yield* gerritApi.getReviewers(resolvedChangeId)
28
+
29
+ if (options.json) {
30
+ console.log(
31
+ JSON.stringify(
32
+ {
33
+ status: 'success',
34
+ change_id: resolvedChangeId,
35
+ reviewers: reviewers.map((r) => ({
36
+ ...(r._account_id !== undefined ? { account_id: r._account_id } : {}),
37
+ name: r.name,
38
+ email: r.email,
39
+ username: r.username,
40
+ })),
41
+ },
42
+ null,
43
+ 2,
44
+ ),
45
+ )
46
+ } else if (options.xml) {
47
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
48
+ console.log(`<reviewers_result>`)
49
+ console.log(` <status>success</status>`)
50
+ console.log(` <change_id><![CDATA[${sanitizeCDATA(resolvedChangeId)}]]></change_id>`)
51
+ console.log(` <reviewers>`)
52
+ for (const r of reviewers) {
53
+ console.log(` <reviewer>`)
54
+ if (r._account_id !== undefined)
55
+ console.log(` <account_id>${r._account_id}</account_id>`)
56
+ if (r.name) console.log(` <name><![CDATA[${sanitizeCDATA(r.name)}]]></name>`)
57
+ if (r.email) console.log(` <email><![CDATA[${sanitizeCDATA(r.email)}]]></email>`)
58
+ if (r.username)
59
+ console.log(` <username><![CDATA[${sanitizeCDATA(r.username)}]]></username>`)
60
+ console.log(` </reviewer>`)
61
+ }
62
+ console.log(` </reviewers>`)
63
+ console.log(`</reviewers_result>`)
64
+ } else {
65
+ if (reviewers.length === 0) {
66
+ console.log('No reviewers')
67
+ } else {
68
+ for (const r of reviewers) {
69
+ console.log(formatReviewer(r))
70
+ }
71
+ }
72
+ }
73
+ }).pipe(
74
+ Effect.catchAll((error: ApiError | GitError | NoChangeIdError) =>
75
+ Effect.sync(() => {
76
+ const errorMessage =
77
+ error instanceof GitError || error instanceof NoChangeIdError || error instanceof Error
78
+ ? error.message
79
+ : String(error)
80
+
81
+ if (options.json) {
82
+ console.log(JSON.stringify({ status: 'error', error: errorMessage }, null, 2))
83
+ } else if (options.xml) {
84
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
85
+ console.log(`<reviewers_result>`)
86
+ console.log(` <status>error</status>`)
87
+ console.log(` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
88
+ console.log(`</reviewers_result>`)
89
+ } else {
90
+ console.error(`✗ Error: ${errorMessage}`)
91
+ }
92
+ process.exit(1)
93
+ }),
94
+ ),
95
+ )