@aaronshaf/ger 3.0.1 → 4.0.0

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.
Files changed (36) hide show
  1. package/docs/prd/commands.md +59 -1
  2. package/package.json +1 -1
  3. package/src/api/gerrit-types.ts +121 -0
  4. package/src/api/gerrit.ts +68 -102
  5. package/src/cli/commands/analyze.ts +284 -0
  6. package/src/cli/commands/cherry.ts +268 -0
  7. package/src/cli/commands/failures.ts +74 -0
  8. package/src/cli/commands/files.ts +86 -0
  9. package/src/cli/commands/list.ts +239 -0
  10. package/src/cli/commands/rebase.ts +5 -1
  11. package/src/cli/commands/retrigger.ts +91 -0
  12. package/src/cli/commands/reviewers.ts +95 -0
  13. package/src/cli/commands/setup.ts +19 -6
  14. package/src/cli/commands/tree-cleanup.ts +139 -0
  15. package/src/cli/commands/tree-rebase.ts +168 -0
  16. package/src/cli/commands/tree-setup.ts +202 -0
  17. package/src/cli/commands/trees.ts +107 -0
  18. package/src/cli/commands/update.ts +73 -0
  19. package/src/cli/register-analytics-commands.ts +90 -0
  20. package/src/cli/register-commands.ts +96 -40
  21. package/src/cli/register-list-commands.ts +105 -0
  22. package/src/cli/register-tree-commands.ts +128 -0
  23. package/src/schemas/config.ts +3 -0
  24. package/src/schemas/gerrit.ts +2 -0
  25. package/src/schemas/reviewer.ts +16 -0
  26. package/src/services/config.ts +15 -0
  27. package/tests/analyze.test.ts +197 -0
  28. package/tests/cherry.test.ts +208 -0
  29. package/tests/failures.test.ts +212 -0
  30. package/tests/files.test.ts +223 -0
  31. package/tests/helpers/config-mock.ts +4 -0
  32. package/tests/list.test.ts +220 -0
  33. package/tests/retrigger.test.ts +159 -0
  34. package/tests/reviewers.test.ts +259 -0
  35. package/tests/tree.test.ts +517 -0
  36. package/tests/update.test.ts +86 -0
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
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",
@@ -0,0 +1,121 @@
1
+ import { Schema } from '@effect/schema'
2
+ import type { Effect } from 'effect'
3
+ import type {
4
+ ChangeInfo,
5
+ CommentInfo,
6
+ MessageInfo,
7
+ DiffOptions,
8
+ FileDiffContent,
9
+ FileInfo,
10
+ ProjectInfo,
11
+ ReviewInput,
12
+ ReviewerInput,
13
+ ReviewerResult,
14
+ RevisionInfo,
15
+ SubmitInfo,
16
+ GroupInfo,
17
+ GroupDetailInfo,
18
+ AccountInfo,
19
+ } from '@/schemas/gerrit'
20
+ import type { ReviewerListItem } from '@/schemas/reviewer'
21
+
22
+ export interface ApiErrorFields {
23
+ readonly message: string
24
+ readonly status?: number
25
+ }
26
+
27
+ const ApiErrorSchema = Schema.TaggedError<ApiErrorFields>()('ApiError', {
28
+ message: Schema.String,
29
+ status: Schema.optional(Schema.Number),
30
+ } as const) as unknown
31
+
32
+ export class ApiError
33
+ extends (ApiErrorSchema as new (
34
+ args: ApiErrorFields,
35
+ ) => ApiErrorFields & Error & { readonly _tag: 'ApiError' })
36
+ implements Error
37
+ {
38
+ readonly name = 'ApiError'
39
+ }
40
+
41
+ export interface GerritApiServiceImpl {
42
+ readonly getChange: (changeId: string) => Effect.Effect<ChangeInfo, ApiError>
43
+ readonly listChanges: (query?: string) => Effect.Effect<readonly ChangeInfo[], ApiError>
44
+ readonly listProjects: (options?: {
45
+ pattern?: string
46
+ }) => Effect.Effect<readonly ProjectInfo[], ApiError>
47
+ readonly postReview: (changeId: string, review: ReviewInput) => Effect.Effect<void, ApiError>
48
+ readonly abandonChange: (changeId: string, message?: string) => Effect.Effect<void, ApiError>
49
+ readonly restoreChange: (
50
+ changeId: string,
51
+ message?: string,
52
+ ) => Effect.Effect<ChangeInfo, ApiError>
53
+ readonly rebaseChange: (
54
+ changeId: string,
55
+ options?: { base?: string; allowConflicts?: boolean },
56
+ ) => Effect.Effect<ChangeInfo, ApiError>
57
+ readonly submitChange: (changeId: string) => Effect.Effect<SubmitInfo, ApiError>
58
+ readonly testConnection: Effect.Effect<boolean, ApiError>
59
+ readonly getRevision: (
60
+ changeId: string,
61
+ revisionId?: string,
62
+ ) => Effect.Effect<RevisionInfo, ApiError>
63
+ readonly getFiles: (
64
+ changeId: string,
65
+ revisionId?: string,
66
+ ) => Effect.Effect<Record<string, FileInfo>, ApiError>
67
+ readonly getFileDiff: (
68
+ changeId: string,
69
+ filePath: string,
70
+ revisionId?: string,
71
+ base?: string,
72
+ ) => Effect.Effect<FileDiffContent, ApiError>
73
+ readonly getFileContent: (
74
+ changeId: string,
75
+ filePath: string,
76
+ revisionId?: string,
77
+ ) => Effect.Effect<string, ApiError>
78
+ readonly getPatch: (changeId: string, revisionId?: string) => Effect.Effect<string, ApiError>
79
+ readonly getDiff: (
80
+ changeId: string,
81
+ options?: DiffOptions,
82
+ ) => Effect.Effect<string | string[] | Record<string, unknown> | FileDiffContent, ApiError>
83
+ readonly getComments: (
84
+ changeId: string,
85
+ revisionId?: string,
86
+ ) => Effect.Effect<Record<string, readonly CommentInfo[]>, ApiError>
87
+ readonly getMessages: (changeId: string) => Effect.Effect<readonly MessageInfo[], ApiError>
88
+ readonly addReviewer: (
89
+ changeId: string,
90
+ reviewer: string,
91
+ options?: { state?: 'REVIEWER' | 'CC'; notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL' },
92
+ ) => Effect.Effect<ReviewerResult, ApiError>
93
+ readonly listGroups: (options?: {
94
+ owned?: boolean
95
+ project?: string
96
+ user?: string
97
+ pattern?: string
98
+ limit?: number
99
+ skip?: number
100
+ }) => Effect.Effect<readonly GroupInfo[], ApiError>
101
+ readonly getGroup: (groupId: string) => Effect.Effect<GroupInfo, ApiError>
102
+ readonly getGroupDetail: (groupId: string) => Effect.Effect<GroupDetailInfo, ApiError>
103
+ readonly getGroupMembers: (groupId: string) => Effect.Effect<readonly AccountInfo[], ApiError>
104
+ readonly getReviewers: (changeId: string) => Effect.Effect<readonly ReviewerListItem[], ApiError>
105
+ readonly removeReviewer: (
106
+ changeId: string,
107
+ accountId: string,
108
+ options?: { notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL' },
109
+ ) => Effect.Effect<void, ApiError>
110
+ readonly getTopic: (changeId: string) => Effect.Effect<string | null, ApiError>
111
+ readonly setTopic: (changeId: string, topic: string) => Effect.Effect<string, ApiError>
112
+ readonly deleteTopic: (changeId: string) => Effect.Effect<void, ApiError>
113
+ readonly setReady: (changeId: string, message?: string) => Effect.Effect<void, ApiError>
114
+ readonly setWip: (changeId: string, message?: string) => Effect.Effect<void, ApiError>
115
+ readonly fetchMergedChanges: (options: {
116
+ after: string
117
+ before?: string
118
+ repo?: string
119
+ maxResults?: number
120
+ }) => Effect.Effect<readonly ChangeInfo[], ApiError>
121
+ }
package/src/api/gerrit.ts CHANGED
@@ -18,109 +18,20 @@ 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'
22
23
  import { convertToUnifiedDiff } from '@/utils/diff-formatters'
23
24
  import { ConfigService } from '@/services/config'
24
25
  import { normalizeChangeIdentifier } from '@/utils/change-id'
25
26
 
26
- export interface GerritApiServiceImpl {
27
- readonly getChange: (changeId: string) => Effect.Effect<ChangeInfo, ApiError>
28
- readonly listChanges: (query?: string) => Effect.Effect<readonly ChangeInfo[], ApiError>
29
- readonly listProjects: (options?: {
30
- pattern?: string
31
- }) => Effect.Effect<readonly ProjectInfo[], ApiError>
32
- readonly postReview: (changeId: string, review: ReviewInput) => Effect.Effect<void, ApiError>
33
- readonly abandonChange: (changeId: string, message?: string) => Effect.Effect<void, ApiError>
34
- readonly restoreChange: (
35
- changeId: string,
36
- message?: string,
37
- ) => Effect.Effect<ChangeInfo, ApiError>
38
- readonly rebaseChange: (
39
- changeId: string,
40
- options?: { base?: string },
41
- ) => Effect.Effect<ChangeInfo, ApiError>
42
- readonly submitChange: (changeId: string) => Effect.Effect<SubmitInfo, ApiError>
43
- readonly testConnection: Effect.Effect<boolean, ApiError>
44
- readonly getRevision: (
45
- changeId: string,
46
- revisionId?: string,
47
- ) => Effect.Effect<RevisionInfo, ApiError>
48
- readonly getFiles: (
49
- changeId: string,
50
- revisionId?: string,
51
- ) => Effect.Effect<Record<string, FileInfo>, ApiError>
52
- readonly getFileDiff: (
53
- changeId: string,
54
- filePath: string,
55
- revisionId?: string,
56
- base?: string,
57
- ) => Effect.Effect<FileDiffContent, ApiError>
58
- readonly getFileContent: (
59
- changeId: string,
60
- filePath: string,
61
- revisionId?: string,
62
- ) => Effect.Effect<string, ApiError>
63
- readonly getPatch: (changeId: string, revisionId?: string) => Effect.Effect<string, ApiError>
64
- readonly getDiff: (
65
- changeId: string,
66
- options?: DiffOptions,
67
- ) => Effect.Effect<string | string[] | Record<string, unknown> | FileDiffContent, ApiError>
68
- readonly getComments: (
69
- changeId: string,
70
- revisionId?: string,
71
- ) => Effect.Effect<Record<string, readonly CommentInfo[]>, ApiError>
72
- readonly getMessages: (changeId: string) => Effect.Effect<readonly MessageInfo[], ApiError>
73
- readonly addReviewer: (
74
- changeId: string,
75
- reviewer: string,
76
- options?: { state?: 'REVIEWER' | 'CC'; notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL' },
77
- ) => Effect.Effect<ReviewerResult, ApiError>
78
- readonly listGroups: (options?: {
79
- owned?: boolean
80
- project?: string
81
- user?: string
82
- pattern?: string
83
- limit?: number
84
- skip?: number
85
- }) => Effect.Effect<readonly GroupInfo[], ApiError>
86
- readonly getGroup: (groupId: string) => Effect.Effect<GroupInfo, ApiError>
87
- readonly getGroupDetail: (groupId: string) => Effect.Effect<GroupDetailInfo, ApiError>
88
- readonly getGroupMembers: (groupId: string) => Effect.Effect<readonly AccountInfo[], ApiError>
89
- readonly removeReviewer: (
90
- changeId: string,
91
- accountId: string,
92
- options?: { notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL' },
93
- ) => Effect.Effect<void, ApiError>
94
- readonly getTopic: (changeId: string) => Effect.Effect<string | null, ApiError>
95
- readonly setTopic: (changeId: string, topic: string) => Effect.Effect<string, ApiError>
96
- readonly deleteTopic: (changeId: string) => Effect.Effect<void, ApiError>
97
- readonly setReady: (changeId: string, message?: string) => Effect.Effect<void, ApiError>
98
- readonly setWip: (changeId: string, message?: string) => Effect.Effect<void, ApiError>
99
- }
27
+ export type { GerritApiServiceImpl, ApiErrorFields } from './gerrit-types'
28
+ export { ApiError } from './gerrit-types'
29
+ import { ApiError, type GerritApiServiceImpl } from './gerrit-types'
100
30
 
101
31
  export const GerritApiService: Context.Tag<GerritApiServiceImpl, GerritApiServiceImpl> =
102
32
  Context.GenericTag<GerritApiServiceImpl>('GerritApiService')
103
33
  export type GerritApiService = Context.Tag.Identifier<typeof GerritApiService>
104
34
 
105
- export interface ApiErrorFields {
106
- readonly message: string
107
- readonly status?: number
108
- }
109
-
110
- const ApiErrorSchema = Schema.TaggedError<ApiErrorFields>()('ApiError', {
111
- message: Schema.String,
112
- status: Schema.optional(Schema.Number),
113
- } as const) as unknown
114
-
115
- export class ApiError
116
- extends (ApiErrorSchema as new (
117
- args: ApiErrorFields,
118
- ) => ApiErrorFields & Error & { readonly _tag: 'ApiError' })
119
- implements Error
120
- {
121
- readonly name = 'ApiError'
122
- }
123
-
124
35
  const createAuthHeader = (credentials: GerritCredentials): string => {
125
36
  const auth = btoa(`${credentials.username}:${credentials.password}`)
126
37
  return `Basic ${auth}`
@@ -227,13 +138,8 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
227
138
  const { credentials, authHeader } = yield* getCredentialsAndAuth
228
139
  let url = `${credentials.host}/a/projects/`
229
140
  if (options?.pattern) url += `?p=${encodeURIComponent(options.pattern)}`
230
- const projectsRecord = yield* makeRequest(
231
- url,
232
- authHeader,
233
- 'GET',
234
- undefined,
235
- Schema.Record({ key: Schema.String, value: ProjectInfo }),
236
- )
141
+ const schema = Schema.Record({ key: Schema.String, value: ProjectInfo })
142
+ const projectsRecord = yield* makeRequest(url, authHeader, 'GET', undefined, schema)
237
143
  return Object.values(projectsRecord).sort((a, b) => a.name.localeCompare(b.name))
238
144
  })
239
145
 
@@ -263,12 +169,17 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
263
169
  return yield* makeRequest(url, authHeader, 'POST', body, ChangeInfo)
264
170
  })
265
171
 
266
- const rebaseChange = (changeId: string, options?: { base?: string }) =>
172
+ const rebaseChange = (
173
+ changeId: string,
174
+ options?: { base?: string; allowConflicts?: boolean },
175
+ ) =>
267
176
  Effect.gen(function* () {
268
177
  const { credentials, authHeader } = yield* getCredentialsAndAuth
269
178
  const normalized = yield* normalizeAndValidate(changeId)
270
179
  const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}/revisions/current/rebase`
271
- const body = options?.base ? { base: options.base } : {}
180
+ const body: Record<string, string | boolean> = {}
181
+ if (options?.base) body['base'] = options.base
182
+ if (options?.allowConflicts) body['allow_conflicts'] = true
272
183
  return yield* makeRequest(url, authHeader, 'POST', body, ChangeInfo)
273
184
  })
274
185
 
@@ -581,6 +492,15 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
581
492
  return yield* makeRequest(url, authHeader, 'GET', undefined, Schema.Array(AccountInfo))
582
493
  })
583
494
 
495
+ const getReviewers = (changeId: string) =>
496
+ Effect.gen(function* () {
497
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
498
+ const normalized = yield* normalizeAndValidate(changeId)
499
+ const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}/reviewers`
500
+ const schema = Schema.Array(ReviewerListItem)
501
+ return yield* makeRequest(url, authHeader, 'GET', undefined, schema)
502
+ })
503
+
584
504
  const removeReviewer = (
585
505
  changeId: string,
586
506
  accountId: string,
@@ -655,6 +575,50 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
655
575
  yield* makeRequest(url, authHeader, 'POST', body)
656
576
  })
657
577
 
578
+ const fetchMergedChanges = (options: {
579
+ after: string
580
+ before?: string
581
+ repo?: string
582
+ maxResults?: number
583
+ }) =>
584
+ Effect.gen(function* () {
585
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
586
+ const limit = options.maxResults ?? 500
587
+ const pageSize = Math.min(limit, 500)
588
+ const allChanges: ChangeInfo[] = []
589
+ let start = 0
590
+ let hasMore = true
591
+
592
+ while (hasMore) {
593
+ let q = `status:merged after:${options.after}`
594
+ if (options.before) q += ` before:${options.before}`
595
+ if (options.repo) q += ` project:${options.repo}`
596
+ const url = `${credentials.host}/a/changes/?q=${encodeURIComponent(q)}&o=DETAILED_ACCOUNTS&n=${pageSize}&S=${start}`
597
+ const page = yield* makeRequest(
598
+ url,
599
+ authHeader,
600
+ 'GET',
601
+ undefined,
602
+ Schema.Array(ChangeInfo),
603
+ )
604
+ allChanges.push(...page)
605
+ const remaining = limit - allChanges.length
606
+ if (page.length < pageSize || remaining <= 0) {
607
+ hasMore = false
608
+ } else {
609
+ start += pageSize
610
+ }
611
+ }
612
+
613
+ if (allChanges.length >= limit) {
614
+ console.warn(
615
+ `Warning: results capped at ${limit}. Use --start-date to narrow the date range.`,
616
+ )
617
+ }
618
+
619
+ return allChanges as readonly ChangeInfo[]
620
+ })
621
+
658
622
  return {
659
623
  getChange,
660
624
  listChanges,
@@ -674,6 +638,7 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
674
638
  getComments,
675
639
  getMessages,
676
640
  addReviewer,
641
+ getReviewers,
677
642
  listGroups,
678
643
  getGroup,
679
644
  getGroupDetail,
@@ -684,6 +649,7 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
684
649
  deleteTopic,
685
650
  setReady,
686
651
  setWip,
652
+ fetchMergedChanges,
687
653
  }
688
654
  }),
689
655
  )