@aaronshaf/ger 2.0.2 → 2.0.3
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/docs/adr/0023-show-reviewer-list.md +42 -0
- package/docs/adr/README.md +1 -0
- package/docs/prd/commands.md +3 -1
- package/docs/prd/data-model.md +19 -8
- package/package.json +1 -1
- package/src/cli/commands/show.ts +114 -1
- package/src/schemas/gerrit.ts +24 -8
- package/tests/show.test.ts +126 -0
- package/tests/submit.test.ts +28 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ADR 0023: Surface Reviewers and CCs in `ger show`
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`ger` supports reviewer management (`add-reviewer`, `remove-reviewer`) but did not expose a reliable way to view current reviewers for a change in one command.
|
|
10
|
+
|
|
11
|
+
This created a workflow gap:
|
|
12
|
+
|
|
13
|
+
- Users could mutate reviewer state but not inspect it from the CLI.
|
|
14
|
+
- `show` already served as the canonical "single change detail" command and was the best place to surface reviewer assignments.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
Add reviewer visibility to `ger show` by:
|
|
19
|
+
|
|
20
|
+
1. Keeping `getChange` lightweight and using a `listChanges` fallback (with detailed account/label options) when reviewer state is not present in the base change response.
|
|
21
|
+
2. Extending `ChangeInfo` schema with Gerrit reviewer state maps (`REVIEWER`, `CC`, `REMOVED`).
|
|
22
|
+
3. Rendering reviewers and CCs in all `show` output formats (pretty, JSON, XML).
|
|
23
|
+
|
|
24
|
+
## Rationale
|
|
25
|
+
|
|
26
|
+
- **Single source of truth**: `show` remains the canonical command for full change context.
|
|
27
|
+
- **No new command surface**: avoids adding a narrowly scoped `list-reviewers` command.
|
|
28
|
+
- **Automation-friendly**: JSON/XML consumers can parse reviewer state without scraping text output.
|
|
29
|
+
- **Backward compatible**: reviewer fields are optional and do not break servers or older data shapes.
|
|
30
|
+
|
|
31
|
+
## Consequences
|
|
32
|
+
|
|
33
|
+
### Positive
|
|
34
|
+
|
|
35
|
+
- Users can verify reviewer assignment directly after add/remove operations.
|
|
36
|
+
- Better parity between mutation commands and read visibility.
|
|
37
|
+
- More complete machine-readable change payloads.
|
|
38
|
+
|
|
39
|
+
### Negative
|
|
40
|
+
|
|
41
|
+
- Extra `listChanges` request when reviewer data is absent from `getChange`.
|
|
42
|
+
- Additional schema/output maintenance for reviewer state rendering.
|
package/docs/adr/README.md
CHANGED
|
@@ -28,3 +28,4 @@ Records of significant architectural decisions with context and rationale. Each
|
|
|
28
28
|
| [0020](0020-code-coverage-enforcement.md) | 80% coverage threshold in pre-commit |
|
|
29
29
|
| [0021](0021-typescript-isolated-declarations.md) | Explicit return types on exports |
|
|
30
30
|
| [0022](0022-biome-oxlint-tooling.md) | Biome formatter + oxlint linter |
|
|
31
|
+
| [0023](0023-show-reviewer-list.md) | Surface reviewers and CCs in `ger show` |
|
package/docs/prd/commands.md
CHANGED
|
@@ -23,11 +23,13 @@ ger show # Auto-detect from HEAD
|
|
|
23
23
|
|
|
24
24
|
**Output includes:**
|
|
25
25
|
- Change metadata (number, project, branch, status)
|
|
26
|
-
- Owner and
|
|
26
|
+
- Owner, reviewers, and CC information
|
|
27
27
|
- Submit requirements
|
|
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).
|
|
32
|
+
|
|
31
33
|
### diff
|
|
32
34
|
|
|
33
35
|
Get change diff in various formats.
|
package/docs/prd/data-model.md
CHANGED
|
@@ -14,25 +14,36 @@ const ChangeInfo = Schema.Struct({
|
|
|
14
14
|
_number: Schema.Number, // Numeric change ID
|
|
15
15
|
project: Schema.String, // Project name
|
|
16
16
|
branch: Schema.String, // Target branch
|
|
17
|
+
change_id: Schema.String, // Gerrit Change-Id
|
|
17
18
|
topic: Schema.optional(Schema.String),
|
|
18
19
|
subject: Schema.String, // First line of commit message
|
|
19
20
|
status: Schema.Literal('NEW', 'MERGED', 'ABANDONED', 'DRAFT'),
|
|
20
|
-
created: Schema.String,
|
|
21
|
-
updated: Schema.String,
|
|
22
|
-
|
|
23
|
-
submitter: Schema.optional(AccountInfo),
|
|
24
|
-
owner: AccountInfo,
|
|
21
|
+
created: Schema.optional(Schema.String),
|
|
22
|
+
updated: Schema.optional(Schema.String),
|
|
23
|
+
owner: Schema.optional(AccountInfo),
|
|
25
24
|
current_revision: Schema.optional(Schema.String),
|
|
26
25
|
revisions: Schema.optional(Schema.Record(Schema.String, RevisionInfo)),
|
|
27
26
|
labels: Schema.optional(Schema.Record(Schema.String, LabelInfo)),
|
|
28
|
-
reviewers: Schema.optional(
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
reviewers: Schema.optional(ReviewerStateMap),
|
|
28
|
+
submittable: Schema.optional(Schema.Boolean),
|
|
29
|
+
work_in_progress: Schema.optional(Schema.Boolean),
|
|
31
30
|
insertions: Schema.optional(Schema.Number),
|
|
32
31
|
deletions: Schema.optional(Schema.Number),
|
|
33
32
|
})
|
|
34
33
|
```
|
|
35
34
|
|
|
35
|
+
### ReviewerStateMap
|
|
36
|
+
|
|
37
|
+
Reviewer assignments grouped by Gerrit reviewer state.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const ReviewerStateMap = Schema.Struct({
|
|
41
|
+
REVIEWER: Schema.optional(Schema.Array(AccountInfo)),
|
|
42
|
+
CC: Schema.optional(Schema.Array(AccountInfo)),
|
|
43
|
+
REMOVED: Schema.optional(Schema.Array(AccountInfo)),
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
36
47
|
### AccountInfo
|
|
37
48
|
|
|
38
49
|
User account information.
|
package/package.json
CHANGED
package/src/cli/commands/show.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { formatDiffPretty } from '@/utils/diff-formatters'
|
|
|
7
7
|
import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
|
|
8
8
|
import { formatDate } from '@/utils/formatters'
|
|
9
9
|
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
10
|
-
import { writeFileSync } from 'node:fs'
|
|
11
10
|
|
|
12
11
|
export const SHOW_HELP_TEXT = `
|
|
13
12
|
Examples:
|
|
@@ -34,6 +33,13 @@ interface ShowOptions {
|
|
|
34
33
|
json?: boolean
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
interface ReviewerIdentity {
|
|
37
|
+
accountId?: number
|
|
38
|
+
name?: string
|
|
39
|
+
email?: string
|
|
40
|
+
username?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
interface ChangeDetails {
|
|
38
44
|
id: string
|
|
39
45
|
number: number
|
|
@@ -49,6 +55,24 @@ interface ChangeDetails {
|
|
|
49
55
|
updated?: string
|
|
50
56
|
commitMessage: string
|
|
51
57
|
topic?: string
|
|
58
|
+
reviewers: ReviewerIdentity[]
|
|
59
|
+
ccs: ReviewerIdentity[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const formatReviewerLabel = (reviewer: ReviewerIdentity): string => {
|
|
63
|
+
const preferredIdentity = reviewer.name || reviewer.email || reviewer.username
|
|
64
|
+
if (!preferredIdentity) {
|
|
65
|
+
if (reviewer.accountId !== undefined) {
|
|
66
|
+
return `Account ${reviewer.accountId}`
|
|
67
|
+
}
|
|
68
|
+
return 'Unknown Reviewer'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (reviewer.email && reviewer.name && reviewer.name !== reviewer.email) {
|
|
72
|
+
return `${reviewer.name} <${reviewer.email}>`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return preferredIdentity
|
|
52
76
|
}
|
|
53
77
|
|
|
54
78
|
const getChangeDetails = (
|
|
@@ -58,6 +82,21 @@ const getChangeDetails = (
|
|
|
58
82
|
const gerritApi = yield* GerritApiService
|
|
59
83
|
const change = yield* gerritApi.getChange(changeId)
|
|
60
84
|
|
|
85
|
+
let reviewerMap = change.reviewers
|
|
86
|
+
const shouldFetchReviewerFallback =
|
|
87
|
+
reviewerMap === undefined ||
|
|
88
|
+
(reviewerMap.REVIEWER === undefined && reviewerMap.CC === undefined)
|
|
89
|
+
|
|
90
|
+
if (shouldFetchReviewerFallback) {
|
|
91
|
+
const detailedChanges = yield* gerritApi
|
|
92
|
+
.listChanges(`change:${change._number}`)
|
|
93
|
+
.pipe(Effect.catchAll(() => Effect.succeed([])))
|
|
94
|
+
const detailedChange =
|
|
95
|
+
detailedChanges.find((candidate) => candidate._number === change._number) ||
|
|
96
|
+
detailedChanges[0]
|
|
97
|
+
reviewerMap = detailedChange?.reviewers
|
|
98
|
+
}
|
|
99
|
+
|
|
61
100
|
return {
|
|
62
101
|
id: change.change_id,
|
|
63
102
|
number: change._number,
|
|
@@ -73,6 +112,18 @@ const getChangeDetails = (
|
|
|
73
112
|
updated: change.updated,
|
|
74
113
|
commitMessage: change.subject, // For now, using subject as commit message
|
|
75
114
|
topic: change.topic,
|
|
115
|
+
reviewers: (reviewerMap?.REVIEWER ?? []).map((reviewer) => ({
|
|
116
|
+
accountId: reviewer._account_id,
|
|
117
|
+
name: reviewer.name,
|
|
118
|
+
email: reviewer.email,
|
|
119
|
+
username: reviewer.username,
|
|
120
|
+
})),
|
|
121
|
+
ccs: (reviewerMap?.CC ?? []).map((cc) => ({
|
|
122
|
+
accountId: cc._account_id,
|
|
123
|
+
name: cc.name,
|
|
124
|
+
email: cc.email,
|
|
125
|
+
username: cc.username,
|
|
126
|
+
})),
|
|
76
127
|
}
|
|
77
128
|
})
|
|
78
129
|
|
|
@@ -154,6 +205,14 @@ const formatShowPretty = (
|
|
|
154
205
|
console.log(
|
|
155
206
|
` Updated: ${changeDetails.updated ? formatDate(changeDetails.updated) : 'Unknown'}`,
|
|
156
207
|
)
|
|
208
|
+
if (changeDetails.reviewers.length > 0) {
|
|
209
|
+
console.log(
|
|
210
|
+
` Reviewers: ${changeDetails.reviewers.map((reviewer) => formatReviewerLabel(reviewer)).join(', ')}`,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
if (changeDetails.ccs.length > 0) {
|
|
214
|
+
console.log(` CCs: ${changeDetails.ccs.map((cc) => formatReviewerLabel(cc)).join(', ')}`)
|
|
215
|
+
}
|
|
157
216
|
console.log(` Change-Id: ${changeDetails.id}`)
|
|
158
217
|
console.log()
|
|
159
218
|
|
|
@@ -227,6 +286,22 @@ const formatShowJson = async (
|
|
|
227
286
|
branch: changeDetails.branch,
|
|
228
287
|
topic: changeDetails.topic,
|
|
229
288
|
owner: removeUndefined(changeDetails.owner),
|
|
289
|
+
reviewers: changeDetails.reviewers.map((reviewer) =>
|
|
290
|
+
removeUndefined({
|
|
291
|
+
account_id: reviewer.accountId,
|
|
292
|
+
name: reviewer.name,
|
|
293
|
+
email: reviewer.email,
|
|
294
|
+
username: reviewer.username,
|
|
295
|
+
}),
|
|
296
|
+
),
|
|
297
|
+
ccs: changeDetails.ccs.map((cc) =>
|
|
298
|
+
removeUndefined({
|
|
299
|
+
account_id: cc.accountId,
|
|
300
|
+
name: cc.name,
|
|
301
|
+
email: cc.email,
|
|
302
|
+
username: cc.username,
|
|
303
|
+
}),
|
|
304
|
+
),
|
|
230
305
|
created: changeDetails.created,
|
|
231
306
|
updated: changeDetails.updated,
|
|
232
307
|
}),
|
|
@@ -318,6 +393,44 @@ const formatShowXml = async (
|
|
|
318
393
|
xmlParts.push(` <email>${escapeXML(changeDetails.owner.email)}</email>`)
|
|
319
394
|
}
|
|
320
395
|
xmlParts.push(` </owner>`)
|
|
396
|
+
xmlParts.push(` <reviewers>`)
|
|
397
|
+
xmlParts.push(` <count>${changeDetails.reviewers.length}</count>`)
|
|
398
|
+
for (const reviewer of changeDetails.reviewers) {
|
|
399
|
+
xmlParts.push(` <reviewer>`)
|
|
400
|
+
if (reviewer.accountId !== undefined) {
|
|
401
|
+
xmlParts.push(` <account_id>${reviewer.accountId}</account_id>`)
|
|
402
|
+
}
|
|
403
|
+
if (reviewer.name) {
|
|
404
|
+
xmlParts.push(` <name><![CDATA[${sanitizeCDATA(reviewer.name)}]]></name>`)
|
|
405
|
+
}
|
|
406
|
+
if (reviewer.email) {
|
|
407
|
+
xmlParts.push(` <email>${escapeXML(reviewer.email)}</email>`)
|
|
408
|
+
}
|
|
409
|
+
if (reviewer.username) {
|
|
410
|
+
xmlParts.push(` <username>${escapeXML(reviewer.username)}</username>`)
|
|
411
|
+
}
|
|
412
|
+
xmlParts.push(` </reviewer>`)
|
|
413
|
+
}
|
|
414
|
+
xmlParts.push(` </reviewers>`)
|
|
415
|
+
xmlParts.push(` <ccs>`)
|
|
416
|
+
xmlParts.push(` <count>${changeDetails.ccs.length}</count>`)
|
|
417
|
+
for (const cc of changeDetails.ccs) {
|
|
418
|
+
xmlParts.push(` <cc>`)
|
|
419
|
+
if (cc.accountId !== undefined) {
|
|
420
|
+
xmlParts.push(` <account_id>${cc.accountId}</account_id>`)
|
|
421
|
+
}
|
|
422
|
+
if (cc.name) {
|
|
423
|
+
xmlParts.push(` <name><![CDATA[${sanitizeCDATA(cc.name)}]]></name>`)
|
|
424
|
+
}
|
|
425
|
+
if (cc.email) {
|
|
426
|
+
xmlParts.push(` <email>${escapeXML(cc.email)}</email>`)
|
|
427
|
+
}
|
|
428
|
+
if (cc.username) {
|
|
429
|
+
xmlParts.push(` <username>${escapeXML(cc.username)}</username>`)
|
|
430
|
+
}
|
|
431
|
+
xmlParts.push(` </cc>`)
|
|
432
|
+
}
|
|
433
|
+
xmlParts.push(` </ccs>`)
|
|
321
434
|
xmlParts.push(` <created>${escapeXML(changeDetails.created || '')}</created>`)
|
|
322
435
|
xmlParts.push(` <updated>${escapeXML(changeDetails.updated || '')}</updated>`)
|
|
323
436
|
xmlParts.push(` </change>`)
|
package/src/schemas/gerrit.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Schema } from '@effect/schema'
|
|
2
2
|
|
|
3
|
-
// Authentication schemas
|
|
4
3
|
export const GerritCredentials: Schema.Schema<{
|
|
5
4
|
readonly host: string
|
|
6
5
|
readonly username: string
|
|
@@ -21,7 +20,6 @@ export const GerritCredentials: Schema.Schema<{
|
|
|
21
20
|
})
|
|
22
21
|
export type GerritCredentials = Schema.Schema.Type<typeof GerritCredentials>
|
|
23
22
|
|
|
24
|
-
// Forward declare RevisionInfo type for use in ChangeInfo
|
|
25
23
|
export interface RevisionInfoType {
|
|
26
24
|
readonly kind?: string
|
|
27
25
|
readonly _number: number
|
|
@@ -65,7 +63,22 @@ export interface RevisionInfoType {
|
|
|
65
63
|
>
|
|
66
64
|
}
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
type ChangeReviewerAccount = {
|
|
67
|
+
readonly _account_id?: number
|
|
68
|
+
readonly name?: string
|
|
69
|
+
readonly email?: string
|
|
70
|
+
readonly username?: string
|
|
71
|
+
}
|
|
72
|
+
const ChangeReviewerAccountInfo: Schema.Schema<ChangeReviewerAccount> = Schema.Struct({
|
|
73
|
+
_account_id: Schema.optional(Schema.Number),
|
|
74
|
+
name: Schema.optional(Schema.String),
|
|
75
|
+
email: Schema.optional(Schema.String),
|
|
76
|
+
username: Schema.optional(Schema.String),
|
|
77
|
+
})
|
|
78
|
+
type ChangeReviewerMap = Partial<
|
|
79
|
+
Record<'REVIEWER' | 'CC' | 'REMOVED', ReadonlyArray<ChangeReviewerAccount>>
|
|
80
|
+
>
|
|
81
|
+
|
|
69
82
|
export const ChangeInfo: Schema.Schema<{
|
|
70
83
|
readonly id: string
|
|
71
84
|
readonly project: string
|
|
@@ -119,6 +132,7 @@ export const ChangeInfo: Schema.Schema<{
|
|
|
119
132
|
readonly current_revision?: string
|
|
120
133
|
readonly revisions?: Record<string, RevisionInfoType>
|
|
121
134
|
readonly topic?: string
|
|
135
|
+
readonly reviewers?: ChangeReviewerMap
|
|
122
136
|
}> = Schema.Struct({
|
|
123
137
|
id: Schema.String,
|
|
124
138
|
project: Schema.String,
|
|
@@ -184,10 +198,16 @@ export const ChangeInfo: Schema.Schema<{
|
|
|
184
198
|
current_revision: Schema.optional(Schema.String),
|
|
185
199
|
revisions: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Any })),
|
|
186
200
|
topic: Schema.optional(Schema.String),
|
|
201
|
+
reviewers: Schema.optional(
|
|
202
|
+
Schema.Struct({
|
|
203
|
+
REVIEWER: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
|
|
204
|
+
CC: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
|
|
205
|
+
REMOVED: Schema.optional(Schema.Array(ChangeReviewerAccountInfo)),
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
187
208
|
})
|
|
188
209
|
export type ChangeInfo = Schema.Schema.Type<typeof ChangeInfo>
|
|
189
210
|
|
|
190
|
-
// Comment schemas
|
|
191
211
|
export const CommentInput: Schema.Schema<{
|
|
192
212
|
readonly message: string
|
|
193
213
|
readonly unresolved?: boolean
|
|
@@ -200,7 +220,6 @@ export const CommentInput: Schema.Schema<{
|
|
|
200
220
|
})
|
|
201
221
|
export type CommentInput = Schema.Schema.Type<typeof CommentInput>
|
|
202
222
|
|
|
203
|
-
// Comment info returned from API
|
|
204
223
|
export const CommentInfo: Schema.Schema<{
|
|
205
224
|
readonly id: string
|
|
206
225
|
readonly path?: string
|
|
@@ -246,7 +265,6 @@ export const CommentInfo: Schema.Schema<{
|
|
|
246
265
|
})
|
|
247
266
|
export type CommentInfo = Schema.Schema.Type<typeof CommentInfo>
|
|
248
267
|
|
|
249
|
-
// Message info for review messages
|
|
250
268
|
export const MessageInfo: Schema.Schema<{
|
|
251
269
|
readonly id: string
|
|
252
270
|
readonly message: string
|
|
@@ -319,7 +337,6 @@ export const ReviewInput: Schema.Schema<{
|
|
|
319
337
|
})
|
|
320
338
|
export type ReviewInput = Schema.Schema.Type<typeof ReviewInput>
|
|
321
339
|
|
|
322
|
-
// Project schema
|
|
323
340
|
export const ProjectInfo: Schema.Schema<{
|
|
324
341
|
readonly id: string
|
|
325
342
|
readonly name: string
|
|
@@ -333,7 +350,6 @@ export const ProjectInfo: Schema.Schema<{
|
|
|
333
350
|
})
|
|
334
351
|
export type ProjectInfo = Schema.Schema.Type<typeof ProjectInfo>
|
|
335
352
|
|
|
336
|
-
// File and diff schemas
|
|
337
353
|
export const FileInfo: Schema.Schema<{
|
|
338
354
|
readonly status?: 'A' | 'D' | 'R' | 'C' | 'M'
|
|
339
355
|
readonly lines_inserted?: number
|
package/tests/show.test.ts
CHANGED
|
@@ -95,6 +95,27 @@ describe('show command', () => {
|
|
|
95
95
|
name: 'John Doe',
|
|
96
96
|
email: 'john@example.com',
|
|
97
97
|
},
|
|
98
|
+
reviewers: {
|
|
99
|
+
REVIEWER: [
|
|
100
|
+
{
|
|
101
|
+
_account_id: 2001,
|
|
102
|
+
name: 'Jane Reviewer',
|
|
103
|
+
email: 'jane.reviewer@example.com',
|
|
104
|
+
username: 'jreviewer',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
email: 'second.reviewer@example.com',
|
|
108
|
+
username: 'sreviewer',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
CC: [
|
|
112
|
+
{
|
|
113
|
+
_account_id: 2003,
|
|
114
|
+
name: 'Team Observer',
|
|
115
|
+
email: 'observer@example.com',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
98
119
|
})
|
|
99
120
|
|
|
100
121
|
const mockDiff = `--- a/src/auth.js
|
|
@@ -194,6 +215,9 @@ describe('show command', () => {
|
|
|
194
215
|
expect(output).toContain('Branch: main')
|
|
195
216
|
expect(output).toContain('Status: NEW')
|
|
196
217
|
expect(output).toContain('Owner: John Doe')
|
|
218
|
+
expect(output).toContain('Reviewers: Jane Reviewer <jane.reviewer@example.com>')
|
|
219
|
+
expect(output).toContain('second.reviewer@example.com')
|
|
220
|
+
expect(output).toContain('CCs: Team Observer <observer@example.com>')
|
|
197
221
|
expect(output).toContain('Change-Id: I123abc456def')
|
|
198
222
|
expect(output).toContain('🔍 Diff:')
|
|
199
223
|
expect(output).toContain('💬 Inline Comments:')
|
|
@@ -234,6 +258,13 @@ describe('show command', () => {
|
|
|
234
258
|
expect(output).toContain('<owner>')
|
|
235
259
|
expect(output).toContain('<name><![CDATA[John Doe]]></name>')
|
|
236
260
|
expect(output).toContain('<email>john@example.com</email>')
|
|
261
|
+
expect(output).toContain('<reviewers>')
|
|
262
|
+
expect(output).toContain('<count>2</count>')
|
|
263
|
+
expect(output).toContain('<name><![CDATA[Jane Reviewer]]></name>')
|
|
264
|
+
expect(output).toContain('<ccs>')
|
|
265
|
+
expect(output).toContain('<count>1</count>')
|
|
266
|
+
expect(output).toContain('<name><![CDATA[Team Observer]]></name>')
|
|
267
|
+
expect(output).not.toContain('<account_id>undefined</account_id>')
|
|
237
268
|
expect(output).toContain('<diff><![CDATA[')
|
|
238
269
|
expect(output).toContain('<comments>')
|
|
239
270
|
expect(output).toContain('<count>3</count>')
|
|
@@ -482,6 +513,14 @@ describe('show command', () => {
|
|
|
482
513
|
expect(parsed.change.branch).toBe('main')
|
|
483
514
|
expect(parsed.change.owner.name).toBe('John Doe')
|
|
484
515
|
expect(parsed.change.owner.email).toBe('john@example.com')
|
|
516
|
+
expect(Array.isArray(parsed.change.reviewers)).toBe(true)
|
|
517
|
+
expect(parsed.change.reviewers.length).toBe(2)
|
|
518
|
+
expect(parsed.change.reviewers[0].name).toBe('Jane Reviewer')
|
|
519
|
+
expect(parsed.change.reviewers[1].email).toBe('second.reviewer@example.com')
|
|
520
|
+
expect(parsed.change.reviewers[1].account_id).toBeUndefined()
|
|
521
|
+
expect(Array.isArray(parsed.change.ccs)).toBe(true)
|
|
522
|
+
expect(parsed.change.ccs.length).toBe(1)
|
|
523
|
+
expect(parsed.change.ccs[0].name).toBe('Team Observer')
|
|
485
524
|
|
|
486
525
|
// Check diff is present
|
|
487
526
|
expect(parsed.diff).toContain('src/auth.js')
|
|
@@ -611,6 +650,93 @@ describe('show command', () => {
|
|
|
611
650
|
expect(parsed.messages[0].revision).toBe(2)
|
|
612
651
|
})
|
|
613
652
|
|
|
653
|
+
test('should fetch reviewers from listChanges when getChange lacks reviewer data', async () => {
|
|
654
|
+
let listChangesOptions: string[] = []
|
|
655
|
+
let listChangesQuery = ''
|
|
656
|
+
|
|
657
|
+
const changeWithoutReviewers = {
|
|
658
|
+
...mockChange,
|
|
659
|
+
reviewers: undefined,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
server.use(
|
|
663
|
+
http.get('*/a/changes/:changeId', ({ request }) => {
|
|
664
|
+
const url = new URL(request.url)
|
|
665
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
666
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: [] })}`)
|
|
667
|
+
}
|
|
668
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithoutReviewers)}`)
|
|
669
|
+
}),
|
|
670
|
+
http.get('*/a/changes/', ({ request }) => {
|
|
671
|
+
const url = new URL(request.url)
|
|
672
|
+
listChangesOptions = url.searchParams.getAll('o')
|
|
673
|
+
listChangesQuery = url.searchParams.get('q') || ''
|
|
674
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify([mockChange])}`)
|
|
675
|
+
}),
|
|
676
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
677
|
+
return HttpResponse.text(btoa(mockDiff))
|
|
678
|
+
}),
|
|
679
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
680
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
|
|
681
|
+
}),
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
685
|
+
const program = showCommand('12345', {}).pipe(
|
|
686
|
+
Effect.provide(GerritApiServiceLive),
|
|
687
|
+
Effect.provide(mockConfigLayer),
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
await Effect.runPromise(program)
|
|
691
|
+
|
|
692
|
+
expect(listChangesQuery).toBe('change:12345')
|
|
693
|
+
expect(listChangesOptions).toContain('LABELS')
|
|
694
|
+
expect(listChangesOptions).toContain('DETAILED_LABELS')
|
|
695
|
+
expect(listChangesOptions).toContain('DETAILED_ACCOUNTS')
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
test('should not fetch listChanges when reviewer data is explicitly present but empty', async () => {
|
|
699
|
+
let listChangesCalled = false
|
|
700
|
+
|
|
701
|
+
const changeWithEmptyReviewerLists = {
|
|
702
|
+
...mockChange,
|
|
703
|
+
reviewers: {
|
|
704
|
+
REVIEWER: [],
|
|
705
|
+
CC: [],
|
|
706
|
+
},
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
server.use(
|
|
710
|
+
http.get('*/a/changes/:changeId', ({ request }) => {
|
|
711
|
+
const url = new URL(request.url)
|
|
712
|
+
if (url.searchParams.get('o') === 'MESSAGES') {
|
|
713
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify({ messages: [] })}`)
|
|
714
|
+
}
|
|
715
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(changeWithEmptyReviewerLists)}`)
|
|
716
|
+
}),
|
|
717
|
+
http.get('*/a/changes/', () => {
|
|
718
|
+
listChangesCalled = true
|
|
719
|
+
return HttpResponse.text(`)]}'\n[]`)
|
|
720
|
+
}),
|
|
721
|
+
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
|
|
722
|
+
return HttpResponse.text(btoa(mockDiff))
|
|
723
|
+
}),
|
|
724
|
+
http.get('*/a/changes/:changeId/revisions/current/comments', () => {
|
|
725
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockComments)}`)
|
|
726
|
+
}),
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
const mockConfigLayer = createMockConfigLayer()
|
|
730
|
+
const program = showCommand('12345', {}).pipe(
|
|
731
|
+
Effect.provide(GerritApiServiceLive),
|
|
732
|
+
Effect.provide(mockConfigLayer),
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
await Effect.runPromise(program)
|
|
736
|
+
|
|
737
|
+
expect(listChangesCalled).toBe(false)
|
|
738
|
+
})
|
|
739
|
+
|
|
614
740
|
test('should handle large JSON output without truncation', async () => {
|
|
615
741
|
// Create a large diff to simulate output > 64KB
|
|
616
742
|
const largeDiff = '--- a/large-file.js\n+++ b/large-file.js\n' + 'x'.repeat(100000)
|
package/tests/submit.test.ts
CHANGED
|
@@ -132,6 +132,34 @@ describe('submit command', () => {
|
|
|
132
132
|
expect(output).toContain('Status: MERGED')
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
+
it('should fetch change without detailed reviewer options', async () => {
|
|
136
|
+
let requestedOptions: string[] = []
|
|
137
|
+
|
|
138
|
+
server.use(
|
|
139
|
+
http.get('*/a/changes/12345', ({ request }) => {
|
|
140
|
+
const url = new URL(request.url)
|
|
141
|
+
requestedOptions = url.searchParams.getAll('o')
|
|
142
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockSubmittableChange)}`)
|
|
143
|
+
}),
|
|
144
|
+
http.post('*/a/changes/12345/submit', () => {
|
|
145
|
+
return HttpResponse.text(`)]}'\n${JSON.stringify(mockSubmitResponse)}`)
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
150
|
+
const program = submitCommand('12345', {}).pipe(
|
|
151
|
+
Effect.provide(GerritApiServiceLive),
|
|
152
|
+
Effect.provide(mockConfigLayer),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
await Effect.runPromise(program)
|
|
156
|
+
|
|
157
|
+
expect(requestedOptions).toContain('CURRENT_REVISION')
|
|
158
|
+
expect(requestedOptions).toContain('CURRENT_COMMIT')
|
|
159
|
+
expect(requestedOptions).not.toContain('DETAILED_LABELS')
|
|
160
|
+
expect(requestedOptions).not.toContain('DETAILED_ACCOUNTS')
|
|
161
|
+
})
|
|
162
|
+
|
|
135
163
|
it('should output XML format when --xml flag is used', async () => {
|
|
136
164
|
server.use(
|
|
137
165
|
http.get('*/a/changes/12345', () => {
|