@aaronshaf/ger 2.0.1 → 2.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
@@ -421,6 +421,22 @@ ger diff 12345 --file src/main.ts
421
421
 
422
422
  ### Change Management
423
423
 
424
+ #### Install Commit-Msg Hook
425
+ ```bash
426
+ # Install the Gerrit commit-msg hook
427
+ ger install-hook
428
+
429
+ # Force reinstall (overwrite existing)
430
+ ger install-hook --force
431
+ ```
432
+
433
+ **What it does:**
434
+ - Downloads the commit-msg hook from your configured Gerrit server
435
+ - Installs to `.git/hooks/commit-msg` with executable permissions
436
+ - Required for commits to have Change-Id footers
437
+
438
+ **Note:** The `push` command auto-installs the hook if missing, but you can use this command to manually install or update it.
439
+
424
440
  #### Checkout Changes
425
441
  ```bash
426
442
  # Checkout latest patchset
@@ -129,6 +129,55 @@ ger workspace
129
129
 
130
130
  **Output:** Current branch and associated Gerrit change.
131
131
 
132
+ ### topic
133
+
134
+ Get, set, or remove topic for a change.
135
+
136
+ ```bash
137
+ ger topic [change-id] # View current topic (auto-detect from HEAD)
138
+ ger topic [change-id] <topic> # Set topic
139
+ ger topic [change-id] --delete # Remove topic
140
+ ger topic [change-id] --xml # XML output
141
+ ```
142
+
143
+ | Option | Description |
144
+ |--------|-------------|
145
+ | `--delete` | Remove the topic from the change |
146
+ | `--xml` | Output as XML for LLM consumption |
147
+
148
+ **Output formats:**
149
+
150
+ Text (get):
151
+ ```
152
+ my-feature
153
+ ```
154
+
155
+ Text (set):
156
+ ```
157
+ ✓ Set topic on change 12345: my-feature
158
+ ```
159
+
160
+ Text (delete):
161
+ ```
162
+ ✓ Removed topic from change 12345
163
+ ```
164
+
165
+ XML:
166
+ ```xml
167
+ <?xml version="1.0" encoding="UTF-8"?>
168
+ <topic_result>
169
+ <status>success</status>
170
+ <action>get|set|deleted</action>
171
+ <change_id><![CDATA[12345]]></change_id>
172
+ <topic><![CDATA[my-feature]]></topic>
173
+ </topic_result>
174
+ ```
175
+
176
+ **Use cases:**
177
+ - Group related changes under a common topic
178
+ - Filter changes by topic in Gerrit UI
179
+ - Organize work for releases or features
180
+
132
181
  ## Code Review
133
182
 
134
183
  ### comment
@@ -370,6 +419,28 @@ ger init # Alias
370
419
 
371
420
  **Creates:** `~/.ger/config.json` with secure permissions.
372
421
 
422
+ ### install-hook
423
+
424
+ Install the Gerrit commit-msg hook for automatic Change-Id generation.
425
+
426
+ ```bash
427
+ ger install-hook
428
+ ger install-hook --force # Overwrite existing hook
429
+ ```
430
+
431
+ | Option | Description |
432
+ |--------|-------------|
433
+ | `--force` | Overwrite existing hook |
434
+ | `--xml` | Output as XML |
435
+
436
+ **Downloads:** Hook from configured Gerrit server.
437
+ **Installs to:** `.git/hooks/commit-msg` (executable).
438
+
439
+ **Use cases:**
440
+ - Set up a new clone before first push
441
+ - Repair corrupted hook
442
+ - Update hook after Gerrit upgrade
443
+
373
444
  ### open
374
445
 
375
446
  Open change in browser.
package/llms.txt ADDED
@@ -0,0 +1,217 @@
1
+ # ger
2
+
3
+ > CLI for Gerrit code review. Query changes, post comments, vote, manage reviewers, checkout/push code, check build status. Supports XML output for LLM pipelines.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun install -g @aaronshaf/ger
9
+ ger setup # Configure credentials interactively
10
+ ```
11
+
12
+ ## Change Identifiers
13
+
14
+ Commands accept change number (12345) or Change-ID (If5a3ae8...). Many commands auto-detect from HEAD commit when omitted.
15
+
16
+ ## Commands
17
+
18
+ ### View Changes
19
+
20
+ ```bash
21
+ ger show [change-id] # Full change info: metadata, diff, comments
22
+ ger show --xml # XML output for LLM consumption
23
+ ger diff <change-id> # Get diff
24
+ ger diff <change-id> --files-only # List changed files only
25
+ ger comments <change-id> # View all comments with context
26
+ ger search "owner:self status:open" # Gerrit query syntax
27
+ ger search "project:foo" -n 10 # Limit results
28
+ ```
29
+
30
+ ### List Changes
31
+
32
+ ```bash
33
+ ger mine # Your open changes
34
+ ger incoming # Changes awaiting your review
35
+ ```
36
+
37
+ ### Post Comments
38
+
39
+ ```bash
40
+ ger comment <change-id> -m "LGTM" # Overall comment
41
+ echo "Review text" | ger comment <change-id> # Piped input
42
+ ger comment <change-id> --file src/foo.ts --line 42 -m "Fix this" # Line comment
43
+ ger comment <change-id> --file src/foo.ts --line 42 -m "Bug" --unresolved
44
+ ```
45
+
46
+ Batch comments via JSON stdin:
47
+
48
+ ```bash
49
+ echo '[
50
+ {"file": "src/main.ts", "line": 10, "message": "Add type annotation"},
51
+ {"file": "src/api.ts", "line": 25, "message": "Handle error", "unresolved": true}
52
+ ]' | ger comment <change-id> --batch
53
+ ```
54
+
55
+ JSON schema: `{"file": string, "line": number, "message": string, "unresolved"?: boolean, "side"?: "PARENT"|"REVISION", "range"?: {start_line, end_line, start_character?, end_character?}}`
56
+
57
+ ### Vote
58
+
59
+ ```bash
60
+ ger vote <change-id> --code-review +2
61
+ ger vote <change-id> --code-review +2 --verified +1 -m "LGTM"
62
+ ger vote <change-id> --label "Custom-Label" +1
63
+ ```
64
+
65
+ ### Manage Reviewers
66
+
67
+ ```bash
68
+ ger add-reviewer user@example.com -c <change-id>
69
+ ger add-reviewer user1 user2 -c <change-id>
70
+ ger add-reviewer --group team-name -c <change-id>
71
+ ger add-reviewer --cc user@example.com -c <change-id>
72
+ ger remove-reviewer user@example.com -c <change-id>
73
+ ```
74
+
75
+ ### Topic Management
76
+
77
+ ```bash
78
+ ger topic <change-id> # View current topic
79
+ ger topic <change-id> my-feature # Set topic
80
+ ger topic <change-id> --delete # Remove topic
81
+ ```
82
+
83
+ ### Git Operations
84
+
85
+ ```bash
86
+ ger checkout <change-id> # Fetch and checkout change
87
+ ger checkout <change-id> --detach # Detached HEAD
88
+ ger push # Push for review
89
+ ger push -r alice@example.com -t my-topic --wip
90
+ ger push --dry-run # Preview push
91
+ ```
92
+
93
+ ### Change Lifecycle
94
+
95
+ ```bash
96
+ ger rebase [change-id] # Rebase onto target branch
97
+ ger submit <change-id> # Submit for merge
98
+ ger abandon <change-id> -m "reason"
99
+ ger restore <change-id>
100
+ ```
101
+
102
+ ### Build Status
103
+
104
+ ```bash
105
+ ger build-status [change-id] # Check status: pending|running|success|failure
106
+ ger build-status --watch # Poll until completion
107
+ ger build-status --watch --exit-status && deploy.sh # Exit 1 on failure
108
+ ```
109
+
110
+ ### Extract URLs
111
+
112
+ ```bash
113
+ ger extract-url "build-summary-report" | tail -1 # Latest Jenkins URL
114
+ ger extract-url "jenkins" --json | jq -r '.urls[-1]'
115
+ ```
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
+ ### Groups
126
+
127
+ ```bash
128
+ ger groups # List groups
129
+ ger groups --pattern "^team-.*" # Filter by regex
130
+ ger groups-show <group-id> # Group details
131
+ ger groups-members <group-id> # List members
132
+ ```
133
+
134
+ ### Utilities
135
+
136
+ ```bash
137
+ ger status # Check connection
138
+ ger open <change-id> # Open in browser
139
+ ger install-hook # Install commit-msg hook
140
+ ger projects # List projects
141
+ ```
142
+
143
+ ## Output Formats
144
+
145
+ Most commands support `--xml` for structured LLM output:
146
+
147
+ ```xml
148
+ <?xml version="1.0" encoding="UTF-8"?>
149
+ <comment_result>
150
+ <status>success</status>
151
+ <change_id>12345</change_id>
152
+ </comment_result>
153
+ ```
154
+
155
+ Some commands also support `--json`.
156
+
157
+ ## Gerrit Query Syntax
158
+
159
+ Common operators for `ger search`:
160
+
161
+ - `owner:USER` - Changes owned by user (use 'self' for yourself)
162
+ - `status:STATE` - open, merged, abandoned
163
+ - `project:NAME` - Filter by project
164
+ - `branch:NAME` - Filter by branch
165
+ - `reviewer:USER` - Changes where user is reviewer
166
+ - `is:wip` - Work-in-progress
167
+ - `is:submittable` - Ready to submit
168
+ - `age:TIME` - Time since update (1d, 2w, 1mon)
169
+ - `label:Code-Review+2` - Filter by vote
170
+
171
+ ## Common Workflows
172
+
173
+ ### Review a change
174
+
175
+ ```bash
176
+ ger show 12345 # View change
177
+ ger diff 12345 # See code changes
178
+ ger comment 12345 -m "LGTM" # Add comment
179
+ ger vote 12345 --code-review +2 # Approve
180
+ ```
181
+
182
+ ### Post AI review
183
+
184
+ ```bash
185
+ ger review 12345 --comment --yes
186
+ ```
187
+
188
+ ### Check build and get failures
189
+
190
+ ```bash
191
+ ger build-status --watch --exit-status
192
+ ger extract-url "build-summary-report" | tail -1 # Get Jenkins URL
193
+ ```
194
+
195
+ ### Push with reviewers
196
+
197
+ ```bash
198
+ ger push -r alice@example.com -r bob@example.com -t feature-topic
199
+ ```
200
+
201
+ ## Exit Codes
202
+
203
+ - `0` - Success
204
+ - `1` - Error (or build failure with --exit-status)
205
+ - `2` - Timeout
206
+ - `3` - API/network error
207
+
208
+ ## Configuration
209
+
210
+ Credentials stored in `~/.ger/config.json`. Run `ger setup` to configure:
211
+ - Gerrit host URL
212
+ - Username
213
+ - HTTP password (from Gerrit settings)
214
+
215
+ ## Links
216
+
217
+ - [Gerrit Query Syntax](https://gerrit-review.googlesource.com/Documentation/user-search.html)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "2.0.1",
3
+ "version": "2.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",
package/src/api/gerrit.ts CHANGED
@@ -90,26 +90,25 @@ export interface GerritApiServiceImpl {
90
90
  accountId: string,
91
91
  options?: { notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL' },
92
92
  ) => Effect.Effect<void, ApiError>
93
+ readonly getTopic: (changeId: string) => Effect.Effect<string | null, ApiError>
94
+ readonly setTopic: (changeId: string, topic: string) => Effect.Effect<string, ApiError>
95
+ readonly deleteTopic: (changeId: string) => Effect.Effect<void, ApiError>
93
96
  }
94
97
 
95
- // Export both the tag value and the type for use in Effect requirements
96
98
  export const GerritApiService: Context.Tag<GerritApiServiceImpl, GerritApiServiceImpl> =
97
99
  Context.GenericTag<GerritApiServiceImpl>('GerritApiService')
98
100
  export type GerritApiService = Context.Tag.Identifier<typeof GerritApiService>
99
101
 
100
- // Export ApiError fields interface explicitly
101
102
  export interface ApiErrorFields {
102
103
  readonly message: string
103
104
  readonly status?: number
104
105
  }
105
106
 
106
- // Define error schema (not exported, so type can be implicit)
107
107
  const ApiErrorSchema = Schema.TaggedError<ApiErrorFields>()('ApiError', {
108
108
  message: Schema.String,
109
109
  status: Schema.optional(Schema.Number),
110
110
  } as const) as unknown
111
111
 
112
- // Export the error class with explicit constructor signature for isolatedDeclarations
113
112
  export class ApiError
114
113
  extends (ApiErrorSchema as new (
115
114
  args: ApiErrorFields,
@@ -127,7 +126,7 @@ const createAuthHeader = (credentials: GerritCredentials): string => {
127
126
  const makeRequest = <T = unknown>(
128
127
  url: string,
129
128
  authHeader: string,
130
- method: 'GET' | 'POST' = 'GET',
129
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
131
130
  body?: unknown,
132
131
  schema?: Schema.Schema<T>,
133
132
  ): Effect.Effect<T, ApiError> =>
@@ -155,12 +154,7 @@ const makeRequest = <T = unknown>(
155
154
  try: () => response.text(),
156
155
  catch: () => 'Unknown error',
157
156
  }).pipe(Effect.orElseSucceed(() => 'Unknown error'))
158
- yield* Effect.fail(
159
- new ApiError({
160
- message: errorText,
161
- status: response.status,
162
- }),
163
- )
157
+ yield* Effect.fail(new ApiError({ message: errorText, status: response.status }))
164
158
  }
165
159
 
166
160
  const text = yield* Effect.tryPromise({
@@ -168,13 +162,8 @@ const makeRequest = <T = unknown>(
168
162
  catch: () => new ApiError({ message: 'Failed to read response data' }),
169
163
  })
170
164
 
171
- // Gerrit returns JSON with )]}' prefix for security
172
165
  const cleanJson = text.replace(/^\)\]\}'\n?/, '')
173
-
174
- if (!cleanJson.trim()) {
175
- // Empty response - return empty object for endpoints that expect void
176
- return {} as T
177
- }
166
+ if (!cleanJson.trim()) return {} as T
178
167
 
179
168
  const parsed = yield* Effect.try({
180
169
  try: () => JSON.parse(cleanJson),
@@ -186,8 +175,6 @@ const makeRequest = <T = unknown>(
186
175
  Effect.mapError(() => new ApiError({ message: 'Invalid response format from server' })),
187
176
  )
188
177
  }
189
-
190
- // When no schema is provided, the caller expects void or doesn't care about the response
191
178
  return parsed
192
179
  })
193
180
 
@@ -201,16 +188,13 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
201
188
  const credentials = yield* configService.getCredentials.pipe(
202
189
  Effect.mapError(() => new ApiError({ message: 'Failed to get credentials' })),
203
190
  )
204
- // Ensure host doesn't have trailing slash
205
- const normalizedCredentials = {
206
- ...credentials,
207
- host: credentials.host.replace(/\/$/, ''),
191
+ const normalizedCredentials = { ...credentials, host: credentials.host.replace(/\/$/, '') }
192
+ return {
193
+ credentials: normalizedCredentials,
194
+ authHeader: createAuthHeader(normalizedCredentials),
208
195
  }
209
- const authHeader = createAuthHeader(normalizedCredentials)
210
- return { credentials: normalizedCredentials, authHeader }
211
196
  })
212
197
 
213
- // Helper to normalize and validate change identifier
214
198
  const normalizeAndValidate = (changeId: string): Effect.Effect<string, ApiError> =>
215
199
  Effect.try({
216
200
  try: () => normalizeChangeIdentifier(changeId),
@@ -231,9 +215,7 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
231
215
  const listChanges = (query = 'is:open') =>
232
216
  Effect.gen(function* () {
233
217
  const { credentials, authHeader } = yield* getCredentialsAndAuth
234
- const encodedQuery = encodeURIComponent(query)
235
- // Add additional options to get detailed information
236
- const url = `${credentials.host}/a/changes/?q=${encodedQuery}&o=LABELS&o=DETAILED_LABELS&o=DETAILED_ACCOUNTS&o=SUBMITTABLE`
218
+ const url = `${credentials.host}/a/changes/?q=${encodeURIComponent(query)}&o=LABELS&o=DETAILED_LABELS&o=DETAILED_ACCOUNTS&o=SUBMITTABLE`
237
219
  return yield* makeRequest(url, authHeader, 'GET', undefined, Schema.Array(ChangeInfo))
238
220
  })
239
221
 
@@ -241,10 +223,7 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
241
223
  Effect.gen(function* () {
242
224
  const { credentials, authHeader } = yield* getCredentialsAndAuth
243
225
  let url = `${credentials.host}/a/projects/`
244
- if (options?.pattern) {
245
- url += `?p=${encodeURIComponent(options.pattern)}`
246
- }
247
- // Gerrit returns projects as a Record, need to convert to array
226
+ if (options?.pattern) url += `?p=${encodeURIComponent(options.pattern)}`
248
227
  const projectsRecord = yield* makeRequest(
249
228
  url,
250
229
  authHeader,
@@ -252,7 +231,6 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
252
231
  undefined,
253
232
  Schema.Record({ key: Schema.String, value: ProjectInfo }),
254
233
  )
255
- // Convert Record to Array and sort alphabetically by name
256
234
  return Object.values(projectsRecord).sort((a, b) => a.name.localeCompare(b.name))
257
235
  })
258
236
 
@@ -306,7 +284,6 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
306
284
  return true
307
285
  }).pipe(
308
286
  Effect.catchAll((error) => {
309
- // Log the actual error for debugging
310
287
  if (process.env.DEBUG) {
311
288
  console.error('Connection error:', error)
312
289
  }
@@ -373,15 +350,8 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
373
350
  try: () => response.text(),
374
351
  catch: () => 'Unknown error',
375
352
  }).pipe(Effect.orElseSucceed(() => 'Unknown error'))
376
-
377
- yield* Effect.fail(
378
- new ApiError({
379
- message: errorText,
380
- status: response.status,
381
- }),
382
- )
353
+ yield* Effect.fail(new ApiError({ message: errorText, status: response.status }))
383
354
  }
384
-
385
355
  const base64Content = yield* Effect.tryPromise({
386
356
  try: () => response.text(),
387
357
  catch: () => new ApiError({ message: 'Failed to read response data' }),
@@ -414,15 +384,8 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
414
384
  try: () => response.text(),
415
385
  catch: () => 'Unknown error',
416
386
  }).pipe(Effect.orElseSucceed(() => 'Unknown error'))
417
-
418
- yield* Effect.fail(
419
- new ApiError({
420
- message: errorText,
421
- status: response.status,
422
- }),
423
- )
387
+ yield* Effect.fail(new ApiError({ message: errorText, status: response.status }))
424
388
  }
425
-
426
389
  const base64Patch = yield* Effect.tryPromise({
427
390
  try: () => response.text(),
428
391
  catch: () => new ApiError({ message: 'Failed to read response data' }),
@@ -546,7 +509,6 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
546
509
  const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}?o=MESSAGES`
547
510
  const response = yield* makeRequest(url, authHeader, 'GET')
548
511
 
549
- // Extract messages from the change response with runtime validation
550
512
  const changeResponse = yield* Schema.decodeUnknown(
551
513
  Schema.Struct({
552
514
  messages: Schema.optional(Schema.Array(MessageInfo)),
@@ -616,7 +578,6 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
616
578
  url += `?${params.join('&')}`
617
579
  }
618
580
 
619
- // Gerrit returns groups as a Record, need to convert to array
620
581
  const groupsRecord = yield* makeRequest(
621
582
  url,
622
583
  authHeader,
@@ -624,12 +585,9 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
624
585
  undefined,
625
586
  Schema.Record({ key: Schema.String, value: GroupInfo }),
626
587
  )
627
- // Convert Record to Array and sort alphabetically by name
628
- return Object.values(groupsRecord).sort((a, b) => {
629
- const aName = a.name || a.id
630
- const bName = b.name || b.id
631
- return aName.localeCompare(bName)
632
- })
588
+ return Object.values(groupsRecord).sort((a, b) =>
589
+ (a.name || a.id).localeCompare(b.name || b.id),
590
+ )
633
591
  })
634
592
 
635
593
  const getGroup = (groupId: string) =>
@@ -661,12 +619,54 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
661
619
  Effect.gen(function* () {
662
620
  const { credentials, authHeader } = yield* getCredentialsAndAuth
663
621
  const normalized = yield* normalizeAndValidate(changeId)
664
- // Use POST to /delete endpoint to support request body with notify option
665
622
  const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}/reviewers/${encodeURIComponent(accountId)}/delete`
666
623
  const body = options?.notify ? { notify: options.notify } : {}
667
624
  yield* makeRequest(url, authHeader, 'POST', body)
668
625
  })
669
626
 
627
+ const getTopicUrl = (host: string, changeId: string): string =>
628
+ `${host}/a/changes/${encodeURIComponent(changeId)}/topic`
629
+
630
+ const getTopic = (changeId: string) =>
631
+ Effect.gen(function* () {
632
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
633
+ const normalized = yield* normalizeAndValidate(changeId)
634
+ return yield* makeRequest(
635
+ getTopicUrl(credentials.host, normalized),
636
+ authHeader,
637
+ 'GET',
638
+ undefined,
639
+ Schema.String,
640
+ ).pipe(
641
+ Effect.map((t) => t.replace(/^"|"$/g, '') || null),
642
+ Effect.catchIf(
643
+ (e) => e instanceof ApiError && e.status === 404,
644
+ () => Effect.succeed(null),
645
+ ),
646
+ )
647
+ })
648
+
649
+ const setTopic = (changeId: string, topic: string) =>
650
+ Effect.gen(function* () {
651
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
652
+ const normalized = yield* normalizeAndValidate(changeId)
653
+ const result = yield* makeRequest(
654
+ getTopicUrl(credentials.host, normalized),
655
+ authHeader,
656
+ 'PUT',
657
+ { topic },
658
+ Schema.String,
659
+ )
660
+ return result.replace(/^"|"$/g, '')
661
+ })
662
+
663
+ const deleteTopic = (changeId: string) =>
664
+ Effect.gen(function* () {
665
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
666
+ const normalized = yield* normalizeAndValidate(changeId)
667
+ yield* makeRequest(getTopicUrl(credentials.host, normalized), authHeader, 'DELETE')
668
+ })
669
+
670
670
  return {
671
671
  getChange,
672
672
  listChanges,
@@ -691,6 +691,9 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
691
691
  getGroupDetail,
692
692
  getGroupMembers,
693
693
  removeReviewer,
694
+ getTopic,
695
+ setTopic,
696
+ deleteTopic,
694
697
  }
695
698
  }),
696
699
  )
@@ -3,6 +3,24 @@ import { Effect, pipe } from 'effect'
3
3
  import { type ApiError, GerritApiService } from '@/api/gerrit'
4
4
  import type { ChangeInfo, ReviewInput } from '@/schemas/gerrit'
5
5
 
6
+ export const COMMENT_HELP_TEXT = `
7
+ Examples:
8
+ # Post a general comment on a change
9
+ $ ger comment 12345 -m "Looks good to me!"
10
+
11
+ # Post a comment using piped input
12
+ $ echo "This is a comment from stdin!" | ger comment 12345
13
+
14
+ # Post a line-specific comment
15
+ $ ger comment 12345 --file src/main.js --line 42 -m "Consider using const here"
16
+
17
+ # Post multiple comments using batch mode
18
+ $ echo '{"message": "Review complete", "comments": [
19
+ {"file": "src/main.js", "line": 10, "message": "Good refactor"}
20
+ ]}' | ger comment 12345 --batch
21
+
22
+ Note: Line numbers refer to the NEW version of the file, not diff line numbers.`
23
+
6
24
  interface CommentOptions {
7
25
  message?: string
8
26
  xml?: boolean
@@ -0,0 +1,59 @@
1
+ import { Effect, Console } from 'effect'
2
+ import chalk from 'chalk'
3
+ import {
4
+ CommitHookService,
5
+ type CommitHookServiceImpl,
6
+ type HookInstallError,
7
+ type NotGitRepoError,
8
+ } from '@/services/commit-hook'
9
+ import { type ConfigError, type ConfigServiceImpl } from '@/services/config'
10
+
11
+ export interface InstallHookOptions {
12
+ force?: boolean
13
+ xml?: boolean
14
+ }
15
+
16
+ export type InstallHookErrors = ConfigError | HookInstallError | NotGitRepoError
17
+
18
+ export const installHookCommand = (
19
+ options: InstallHookOptions,
20
+ ): Effect.Effect<void, InstallHookErrors, CommitHookServiceImpl | ConfigServiceImpl> =>
21
+ Effect.gen(function* () {
22
+ const commitHookService = yield* CommitHookService
23
+
24
+ // Check if hook already exists using service method
25
+ const hookExists = yield* commitHookService.hasHook()
26
+
27
+ if (hookExists && !options.force) {
28
+ if (options.xml) {
29
+ yield* Console.log('<?xml version="1.0" encoding="UTF-8"?>')
30
+ yield* Console.log('<install_hook_result>')
31
+ yield* Console.log(' <status>skipped</status>')
32
+ yield* Console.log(' <message><![CDATA[commit-msg hook already installed]]></message>')
33
+ yield* Console.log(' <hint><![CDATA[Use --force to overwrite]]></hint>')
34
+ yield* Console.log('</install_hook_result>')
35
+ } else {
36
+ yield* Console.log(chalk.yellow('commit-msg hook already installed'))
37
+ yield* Console.log(chalk.dim('Use --force to overwrite'))
38
+ }
39
+ return
40
+ }
41
+
42
+ if (hookExists && options.force) {
43
+ if (!options.xml) {
44
+ yield* Console.log(chalk.yellow('Overwriting existing commit-msg hook...'))
45
+ }
46
+ }
47
+
48
+ // Install the hook (service logs progress messages in non-XML mode)
49
+ yield* commitHookService.installHook()
50
+
51
+ // Only output XML here - service already logs success message for non-XML mode
52
+ if (options.xml) {
53
+ yield* Console.log('<?xml version="1.0" encoding="UTF-8"?>')
54
+ yield* Console.log('<install_hook_result>')
55
+ yield* Console.log(' <status>success</status>')
56
+ yield* Console.log(' <message><![CDATA[commit-msg hook installed successfully]]></message>')
57
+ yield* Console.log('</install_hook_result>')
58
+ }
59
+ })