@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 +16 -0
- package/docs/prd/commands.md +71 -0
- package/llms.txt +217 -0
- package/package.json +1 -1
- package/src/api/gerrit.ts +63 -60
- package/src/cli/commands/comment.ts +18 -0
- package/src/cli/commands/install-hook.ts +59 -0
- package/src/cli/commands/show.ts +9 -0
- package/src/cli/commands/topic.ts +108 -0
- package/src/cli/register-commands.ts +55 -33
- package/src/schemas/gerrit.ts +2 -0
- package/tests/topic.test.ts +443 -0
- package/tests/unit/commands/install-hook.test.ts +258 -0
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
|
package/docs/prd/commands.md
CHANGED
|
@@ -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
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
+
})
|