@aaronshaf/ger 1.2.11 → 2.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 (180) hide show
  1. package/.ast-grep/rules/no-as-casting.yml +13 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/.github/workflows/ci-simple.yml +53 -0
  4. package/.github/workflows/ci.yml +171 -0
  5. package/.github/workflows/claude-code-review.yml +83 -0
  6. package/.github/workflows/claude.yml +50 -0
  7. package/.github/workflows/dependency-update.yml +84 -0
  8. package/.github/workflows/release.yml +166 -0
  9. package/.github/workflows/security-scan.yml +113 -0
  10. package/.github/workflows/security.yml +96 -0
  11. package/.husky/pre-commit +16 -0
  12. package/.husky/pre-push +25 -0
  13. package/.lintstagedrc.json +6 -0
  14. package/.tool-versions +1 -0
  15. package/CLAUDE.md +105 -0
  16. package/DEVELOPMENT.md +361 -0
  17. package/EXAMPLES.md +457 -0
  18. package/README.md +831 -16
  19. package/bin/ger +3 -18
  20. package/biome.json +36 -0
  21. package/bun.lock +678 -0
  22. package/bunfig.toml +8 -0
  23. package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
  24. package/docs/adr/0002-use-bun-runtime.md +64 -0
  25. package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
  26. package/docs/adr/0004-use-commander-for-cli.md +76 -0
  27. package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
  28. package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
  29. package/docs/adr/0007-git-hooks-for-quality.md +94 -0
  30. package/docs/adr/0008-no-as-typecasting.md +83 -0
  31. package/docs/adr/0009-file-size-limits.md +82 -0
  32. package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
  33. package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
  34. package/docs/adr/0012-build-status-message-parsing.md +94 -0
  35. package/docs/adr/0013-git-subprocess-integration.md +98 -0
  36. package/docs/adr/0014-group-management-support.md +95 -0
  37. package/docs/adr/0015-batch-comment-processing.md +111 -0
  38. package/docs/adr/0016-flexible-change-identifiers.md +94 -0
  39. package/docs/adr/0017-git-worktree-support.md +102 -0
  40. package/docs/adr/0018-auto-install-commit-hook.md +103 -0
  41. package/docs/adr/0019-sdk-package-exports.md +95 -0
  42. package/docs/adr/0020-code-coverage-enforcement.md +105 -0
  43. package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
  44. package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
  45. package/docs/adr/README.md +30 -0
  46. package/docs/prd/README.md +12 -0
  47. package/docs/prd/architecture.md +325 -0
  48. package/docs/prd/commands.md +425 -0
  49. package/docs/prd/data-model.md +349 -0
  50. package/docs/prd/overview.md +124 -0
  51. package/index.ts +219 -0
  52. package/oxlint.json +24 -0
  53. package/package.json +82 -15
  54. package/scripts/check-coverage.ts +69 -0
  55. package/scripts/check-file-size.ts +38 -0
  56. package/scripts/fix-test-mocks.ts +55 -0
  57. package/skills/gerrit-workflow/SKILL.md +247 -0
  58. package/skills/gerrit-workflow/examples.md +572 -0
  59. package/skills/gerrit-workflow/reference.md +728 -0
  60. package/src/api/gerrit.ts +696 -0
  61. package/src/cli/commands/abandon.ts +65 -0
  62. package/src/cli/commands/add-reviewer.ts +156 -0
  63. package/src/cli/commands/build-status.ts +282 -0
  64. package/src/cli/commands/checkout.ts +422 -0
  65. package/src/cli/commands/comment.ts +460 -0
  66. package/src/cli/commands/comments.ts +85 -0
  67. package/src/cli/commands/diff.ts +71 -0
  68. package/src/cli/commands/extract-url.ts +266 -0
  69. package/src/cli/commands/groups-members.ts +104 -0
  70. package/src/cli/commands/groups-show.ts +169 -0
  71. package/src/cli/commands/groups.ts +137 -0
  72. package/src/cli/commands/incoming.ts +226 -0
  73. package/src/cli/commands/init.ts +164 -0
  74. package/src/cli/commands/mine.ts +115 -0
  75. package/src/cli/commands/open.ts +57 -0
  76. package/src/cli/commands/projects.ts +68 -0
  77. package/src/cli/commands/push.ts +430 -0
  78. package/src/cli/commands/rebase.ts +52 -0
  79. package/src/cli/commands/remove-reviewer.ts +123 -0
  80. package/src/cli/commands/restore.ts +50 -0
  81. package/src/cli/commands/review.ts +486 -0
  82. package/src/cli/commands/search.ts +162 -0
  83. package/src/cli/commands/setup.ts +286 -0
  84. package/src/cli/commands/show.ts +491 -0
  85. package/src/cli/commands/status.ts +35 -0
  86. package/src/cli/commands/submit.ts +108 -0
  87. package/src/cli/commands/vote.ts +119 -0
  88. package/src/cli/commands/workspace.ts +200 -0
  89. package/src/cli/index.ts +53 -0
  90. package/src/cli/register-commands.ts +659 -0
  91. package/src/cli/register-group-commands.ts +88 -0
  92. package/src/cli/register-reviewer-commands.ts +97 -0
  93. package/src/prompts/default-review.md +86 -0
  94. package/src/prompts/system-inline-review.md +135 -0
  95. package/src/prompts/system-overall-review.md +206 -0
  96. package/src/schemas/config.test.ts +245 -0
  97. package/src/schemas/config.ts +84 -0
  98. package/src/schemas/gerrit.ts +681 -0
  99. package/src/services/commit-hook.ts +314 -0
  100. package/src/services/config.test.ts +150 -0
  101. package/src/services/config.ts +250 -0
  102. package/src/services/git-worktree.ts +342 -0
  103. package/src/services/review-strategy.ts +292 -0
  104. package/src/test-utils/mock-generator.ts +138 -0
  105. package/src/utils/change-id.test.ts +98 -0
  106. package/src/utils/change-id.ts +63 -0
  107. package/src/utils/comment-formatters.ts +153 -0
  108. package/src/utils/diff-context.ts +103 -0
  109. package/src/utils/diff-formatters.ts +141 -0
  110. package/src/utils/formatters.ts +85 -0
  111. package/src/utils/git-commit.test.ts +277 -0
  112. package/src/utils/git-commit.ts +122 -0
  113. package/src/utils/index.ts +55 -0
  114. package/src/utils/message-filters.ts +26 -0
  115. package/src/utils/review-formatters.ts +89 -0
  116. package/src/utils/review-prompt-builder.ts +110 -0
  117. package/src/utils/shell-safety.ts +117 -0
  118. package/src/utils/status-indicators.ts +100 -0
  119. package/src/utils/url-parser.test.ts +271 -0
  120. package/src/utils/url-parser.ts +118 -0
  121. package/tests/abandon.test.ts +230 -0
  122. package/tests/add-reviewer.test.ts +579 -0
  123. package/tests/build-status-watch.test.ts +344 -0
  124. package/tests/build-status.test.ts +789 -0
  125. package/tests/change-id-formats.test.ts +268 -0
  126. package/tests/checkout/integration.test.ts +653 -0
  127. package/tests/checkout/parse-input.test.ts +55 -0
  128. package/tests/checkout/validation.test.ts +178 -0
  129. package/tests/comment-batch-advanced.test.ts +431 -0
  130. package/tests/comment-gerrit-api-compliance.test.ts +414 -0
  131. package/tests/comment.test.ts +708 -0
  132. package/tests/comments.test.ts +323 -0
  133. package/tests/config-service-simple.test.ts +100 -0
  134. package/tests/diff.test.ts +419 -0
  135. package/tests/extract-url.test.ts +517 -0
  136. package/tests/groups-members.test.ts +256 -0
  137. package/tests/groups-show.test.ts +323 -0
  138. package/tests/groups.test.ts +334 -0
  139. package/tests/helpers/build-status-test-setup.ts +83 -0
  140. package/tests/helpers/config-mock.ts +27 -0
  141. package/tests/incoming.test.ts +357 -0
  142. package/tests/init.test.ts +70 -0
  143. package/tests/integration/commit-hook.test.ts +246 -0
  144. package/tests/interactive-incoming.test.ts +173 -0
  145. package/tests/mine.test.ts +285 -0
  146. package/tests/mocks/msw-handlers.ts +80 -0
  147. package/tests/open.test.ts +233 -0
  148. package/tests/projects.test.ts +259 -0
  149. package/tests/rebase.test.ts +271 -0
  150. package/tests/remove-reviewer.test.ts +357 -0
  151. package/tests/restore.test.ts +237 -0
  152. package/tests/review.test.ts +135 -0
  153. package/tests/search.test.ts +712 -0
  154. package/tests/setup.test.ts +63 -0
  155. package/tests/show-auto-detect.test.ts +324 -0
  156. package/tests/show.test.ts +813 -0
  157. package/tests/status.test.ts +145 -0
  158. package/tests/submit.test.ts +316 -0
  159. package/tests/unit/commands/push.test.ts +194 -0
  160. package/tests/unit/git-branch-detection.test.ts +82 -0
  161. package/tests/unit/git-worktree.test.ts +55 -0
  162. package/tests/unit/patterns/push-patterns.test.ts +148 -0
  163. package/tests/unit/schemas/gerrit.test.ts +85 -0
  164. package/tests/unit/services/commit-hook.test.ts +132 -0
  165. package/tests/unit/services/review-strategy.test.ts +349 -0
  166. package/tests/unit/test-utils/mock-generator.test.ts +154 -0
  167. package/tests/unit/utils/comment-formatters.test.ts +415 -0
  168. package/tests/unit/utils/diff-context.test.ts +171 -0
  169. package/tests/unit/utils/diff-formatters.test.ts +165 -0
  170. package/tests/unit/utils/formatters.test.ts +411 -0
  171. package/tests/unit/utils/message-filters.test.ts +227 -0
  172. package/tests/unit/utils/shell-safety.test.ts +230 -0
  173. package/tests/unit/utils/status-indicators.test.ts +137 -0
  174. package/tests/vote.test.ts +317 -0
  175. package/tests/workspace.test.ts +295 -0
  176. package/tsconfig.json +36 -5
  177. package/src/commands/branch.ts +0 -196
  178. package/src/ger.ts +0 -22
  179. package/src/types.d.ts +0 -35
  180. package/src/utils.ts +0 -130
@@ -0,0 +1,653 @@
1
+ import { describe, test, expect, beforeAll, afterAll, afterEach, mock, spyOn } from 'bun:test'
2
+ import { HttpResponse, http } from 'msw'
3
+ import { setupServer } from 'msw/node'
4
+ import { Effect, Layer } from 'effect'
5
+ import { checkoutCommand } from '@/cli/commands/checkout'
6
+ import { GerritApiServiceLive } from '@/api/gerrit'
7
+ import { ConfigService } from '@/services/config'
8
+ import { createMockConfigService } from '../helpers/config-mock'
9
+ import type { ChangeInfo, RevisionInfo } from '@/schemas/gerrit'
10
+ import * as childProcess from 'node:child_process'
11
+
12
+ /**
13
+ * Integration tests for checkout command
14
+ *
15
+ * Tests complete command workflows including:
16
+ * - API interactions (change details, revisions)
17
+ * - Git operations (fetch, checkout, branch management)
18
+ * - Error handling (network errors, not found, etc.)
19
+ * - Various input formats and options
20
+ */
21
+
22
+ const server = setupServer(
23
+ http.get('*/a/accounts/self', ({ request }) => {
24
+ const auth = request.headers.get('Authorization')
25
+ if (!auth || !auth.startsWith('Basic ')) {
26
+ return HttpResponse.text('Unauthorized', { status: 401 })
27
+ }
28
+ return HttpResponse.json({
29
+ _account_id: 1000,
30
+ name: 'Test User',
31
+ email: 'test@example.com',
32
+ })
33
+ }),
34
+ )
35
+
36
+ describe('Checkout Command - Integration Tests', () => {
37
+ let mockConsoleLog: ReturnType<typeof mock>
38
+ let mockConsoleError: ReturnType<typeof mock>
39
+ let mockExecSync: ReturnType<typeof spyOn>
40
+
41
+ const validChangeId = 'If5a3ae8cb5a107e187447802358417f311d0c4b1'
42
+
43
+ const mockChange: ChangeInfo = {
44
+ id: `test-project~main~${validChangeId}`,
45
+ _number: 12345,
46
+ project: 'test-project',
47
+ branch: 'main',
48
+ change_id: validChangeId,
49
+ subject: 'Test change',
50
+ status: 'NEW',
51
+ created: '2024-01-15 10:00:00.000000000',
52
+ updated: '2024-01-15 10:00:00.000000000',
53
+ }
54
+
55
+ const mockRevision: RevisionInfo = {
56
+ _number: 1,
57
+ ref: 'refs/changes/45/12345/1',
58
+ created: '2024-01-15 10:00:00.000000000',
59
+ uploader: {
60
+ _account_id: 1000,
61
+ name: 'Test User',
62
+ email: 'test@example.com',
63
+ },
64
+ }
65
+
66
+ beforeAll(() => {
67
+ server.listen({ onUnhandledRequest: 'bypass' })
68
+ mockConsoleLog = mock(() => {})
69
+ mockConsoleError = mock(() => {})
70
+ console.log = mockConsoleLog
71
+ console.error = mockConsoleError
72
+ })
73
+
74
+ afterAll(() => {
75
+ server.close()
76
+ })
77
+
78
+ afterEach(() => {
79
+ server.resetHandlers()
80
+ mockConsoleLog.mockClear()
81
+ mockConsoleError.mockClear()
82
+ mockExecSync?.mockRestore()
83
+ })
84
+
85
+ test('should handle change not found error', async () => {
86
+ server.use(
87
+ http.get('*/a/changes/:changeId', () => {
88
+ return HttpResponse.text('Not Found', { status: 404 })
89
+ }),
90
+ )
91
+
92
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
93
+
94
+ const program = checkoutCommand('99999', {}).pipe(
95
+ Effect.provide(GerritApiServiceLive),
96
+ Effect.provide(mockConfigLayer),
97
+ )
98
+
99
+ // Expect the effect to fail with an ApiError
100
+ const result = await Effect.runPromise(program.pipe(Effect.either))
101
+ expect(result._tag).toBe('Left')
102
+ })
103
+
104
+ test('should fetch change details successfully', async () => {
105
+ // Mock git operations to simulate being in a git repo
106
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
107
+ command: string,
108
+ _options?: unknown,
109
+ ) => {
110
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
111
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
112
+ if (command === 'git remote -v') {
113
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
114
+ }
115
+ if (command.startsWith('git rev-parse --verify review/12345')) {
116
+ throw new Error('branch does not exist')
117
+ }
118
+ if (command.startsWith('git fetch')) return Buffer.from('')
119
+ if (command.startsWith('git checkout -b')) return Buffer.from('')
120
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
121
+ return Buffer.from('')
122
+ }) as typeof childProcess.execSync)
123
+
124
+ server.use(
125
+ http.get('*/a/changes/12345', () => {
126
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
127
+ }),
128
+ http.get('*/a/changes/12345/revisions/current', () => {
129
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
130
+ }),
131
+ )
132
+
133
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
134
+
135
+ const program = checkoutCommand('12345', {}).pipe(
136
+ Effect.provide(GerritApiServiceLive),
137
+ Effect.provide(mockConfigLayer),
138
+ )
139
+
140
+ const result = await Effect.runPromise(program.pipe(Effect.either))
141
+
142
+ // Check that we made the API calls and got change details
143
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
144
+ expect(output).toContain('Checking out Gerrit change')
145
+ expect(output).toContain('12345')
146
+ expect(output).toContain('Test change')
147
+ expect(output).toContain('Created and checked out review/12345')
148
+
149
+ // Should succeed now with mocked git operations
150
+ expect(result._tag).toBe('Right')
151
+ })
152
+
153
+ test('should parse URL input correctly', async () => {
154
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
155
+ command: string,
156
+ _options?: unknown,
157
+ ) => {
158
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
159
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
160
+ if (command === 'git remote -v') {
161
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
162
+ }
163
+ if (command.startsWith('git rev-parse --verify review/12345')) {
164
+ throw new Error('branch does not exist')
165
+ }
166
+ return Buffer.from('')
167
+ }) as typeof childProcess.execSync)
168
+
169
+ server.use(
170
+ http.get('*/a/changes/12345', () => {
171
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
172
+ }),
173
+ http.get('*/a/changes/12345/revisions/current', () => {
174
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
175
+ }),
176
+ )
177
+
178
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
179
+
180
+ const program = checkoutCommand('https://test.gerrit.com/c/test-project/+/12345', {}).pipe(
181
+ Effect.provide(GerritApiServiceLive),
182
+ Effect.provide(mockConfigLayer),
183
+ )
184
+
185
+ const result = await Effect.runPromise(program.pipe(Effect.either))
186
+
187
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
188
+ expect(output).toContain('Checking out Gerrit change')
189
+ expect(output).toContain('12345')
190
+ expect(result._tag).toBe('Right')
191
+ })
192
+
193
+ test('should handle specific patchset request', async () => {
194
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
195
+ command: string,
196
+ _options?: unknown,
197
+ ) => {
198
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
199
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
200
+ if (command === 'git remote -v') {
201
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
202
+ }
203
+ if (command.startsWith('git rev-parse --verify review/12345')) {
204
+ throw new Error('branch does not exist')
205
+ }
206
+ if (command.startsWith('git fetch')) return Buffer.from('')
207
+ if (command.startsWith('git checkout -b')) return Buffer.from('')
208
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
209
+ return Buffer.from('')
210
+ }) as typeof childProcess.execSync)
211
+
212
+ const mockRevision2: RevisionInfo = {
213
+ _number: 2,
214
+ ref: 'refs/changes/45/12345/2',
215
+ created: '2024-01-15 11:00:00.000000000',
216
+ uploader: {
217
+ _account_id: 1000,
218
+ name: 'Test User',
219
+ email: 'test@example.com',
220
+ },
221
+ }
222
+
223
+ server.use(
224
+ http.get('*/a/changes/12345', () => {
225
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
226
+ }),
227
+ http.get('*/a/changes/12345/revisions/2', () => {
228
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision2)}`)
229
+ }),
230
+ )
231
+
232
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
233
+
234
+ const program = checkoutCommand('12345/2', {}).pipe(
235
+ Effect.provide(GerritApiServiceLive),
236
+ Effect.provide(mockConfigLayer),
237
+ )
238
+
239
+ await Effect.runPromise(program.pipe(Effect.either))
240
+
241
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
242
+ expect(output).toContain('Patchset: 2')
243
+ })
244
+
245
+ test('should handle detach mode', async () => {
246
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
247
+ command: string,
248
+ _options?: unknown,
249
+ ) => {
250
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
251
+ if (command === 'git remote -v') {
252
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
253
+ }
254
+ return Buffer.from('')
255
+ }) as typeof childProcess.execSync)
256
+
257
+ server.use(
258
+ http.get('*/a/changes/12345', () => {
259
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
260
+ }),
261
+ http.get('*/a/changes/12345/revisions/current', () => {
262
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
263
+ }),
264
+ )
265
+
266
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
267
+
268
+ const program = checkoutCommand('12345', { detach: true }).pipe(
269
+ Effect.provide(GerritApiServiceLive),
270
+ Effect.provide(mockConfigLayer),
271
+ )
272
+
273
+ const result = await Effect.runPromise(program.pipe(Effect.either))
274
+
275
+ // Verify detach mode was indicated in output
276
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
277
+ expect(output).toContain('Checking out Gerrit change')
278
+ expect(output).toContain('12345')
279
+ expect(output).toContain('detached HEAD mode')
280
+ expect(result._tag).toBe('Right')
281
+ })
282
+
283
+ test('should handle network errors gracefully', async () => {
284
+ server.use(
285
+ http.get('*/a/changes/12345', () => {
286
+ return HttpResponse.error()
287
+ }),
288
+ )
289
+
290
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
291
+
292
+ const program = checkoutCommand('12345', {}).pipe(
293
+ Effect.provide(GerritApiServiceLive),
294
+ Effect.provide(mockConfigLayer),
295
+ )
296
+
297
+ // Expect the effect to fail with a network error
298
+ const result = await Effect.runPromise(program.pipe(Effect.either))
299
+ expect(result._tag).toBe('Left')
300
+ })
301
+
302
+ test('should handle Change-ID input', async () => {
303
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
304
+ command: string,
305
+ _options?: unknown,
306
+ ) => {
307
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
308
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
309
+ if (command === 'git remote -v') {
310
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
311
+ }
312
+ if (command.startsWith('git rev-parse --verify review/12345')) {
313
+ throw new Error('branch does not exist')
314
+ }
315
+ if (command.startsWith('git fetch')) return Buffer.from('')
316
+ if (command.startsWith('git checkout -b')) return Buffer.from('')
317
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
318
+ return Buffer.from('')
319
+ }) as typeof childProcess.execSync)
320
+
321
+ server.use(
322
+ http.get(`*/a/changes/${validChangeId}`, () => {
323
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
324
+ }),
325
+ http.get(`*/a/changes/${validChangeId}/revisions/current`, () => {
326
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
327
+ }),
328
+ )
329
+
330
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
331
+
332
+ const program = checkoutCommand(validChangeId, {}).pipe(
333
+ Effect.provide(GerritApiServiceLive),
334
+ Effect.provide(mockConfigLayer),
335
+ )
336
+
337
+ await Effect.runPromise(program.pipe(Effect.either))
338
+
339
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
340
+
341
+ // Should see the change number in the output
342
+ expect(output).toContain('12345')
343
+ })
344
+
345
+ test('should display change information', async () => {
346
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
347
+ command: string,
348
+ _options?: unknown,
349
+ ) => {
350
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
351
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
352
+ if (command === 'git remote -v') {
353
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
354
+ }
355
+ if (command.startsWith('git rev-parse --verify review/12345')) {
356
+ throw new Error('branch does not exist')
357
+ }
358
+ if (command.startsWith('git fetch')) return Buffer.from('')
359
+ if (command.startsWith('git checkout -b')) return Buffer.from('')
360
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
361
+ return Buffer.from('')
362
+ }) as typeof childProcess.execSync)
363
+
364
+ server.use(
365
+ http.get('*/a/changes/12345', () => {
366
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
367
+ }),
368
+ http.get('*/a/changes/12345/revisions/current', () => {
369
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
370
+ }),
371
+ )
372
+
373
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
374
+
375
+ const program = checkoutCommand('12345', {}).pipe(
376
+ Effect.provide(GerritApiServiceLive),
377
+ Effect.provide(mockConfigLayer),
378
+ )
379
+
380
+ await Effect.runPromise(program.pipe(Effect.either))
381
+
382
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
383
+ expect(output).toContain('Test change')
384
+ expect(output).toContain('12345')
385
+ })
386
+
387
+ test('should handle abandoned change', async () => {
388
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
389
+ command: string,
390
+ _options?: unknown,
391
+ ) => {
392
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
393
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
394
+ if (command === 'git remote -v') {
395
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
396
+ }
397
+ if (command.startsWith('git rev-parse --verify review/12345')) {
398
+ throw new Error('branch does not exist')
399
+ }
400
+ if (command.startsWith('git fetch')) return Buffer.from('')
401
+ if (command.startsWith('git checkout -b')) return Buffer.from('')
402
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
403
+ return Buffer.from('')
404
+ }) as typeof childProcess.execSync)
405
+
406
+ const abandonedChange: ChangeInfo = {
407
+ ...mockChange,
408
+ status: 'ABANDONED',
409
+ }
410
+
411
+ server.use(
412
+ http.get('*/a/changes/12345', () => {
413
+ return HttpResponse.text(`)]}'\n${JSON.stringify(abandonedChange)}`)
414
+ }),
415
+ http.get('*/a/changes/12345/revisions/current', () => {
416
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
417
+ }),
418
+ )
419
+
420
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
421
+
422
+ const program = checkoutCommand('12345', {}).pipe(
423
+ Effect.provide(GerritApiServiceLive),
424
+ Effect.provide(mockConfigLayer),
425
+ )
426
+
427
+ await Effect.runPromise(program.pipe(Effect.either))
428
+
429
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
430
+ expect(output).toContain('ABANDONED')
431
+ })
432
+
433
+ test('should handle merged change', async () => {
434
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
435
+ command: string,
436
+ _options?: unknown,
437
+ ) => {
438
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
439
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
440
+ if (command === 'git remote -v') {
441
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
442
+ }
443
+ if (command.startsWith('git rev-parse --verify review/12345')) {
444
+ throw new Error('branch does not exist')
445
+ }
446
+ if (command.startsWith('git fetch')) return Buffer.from('')
447
+ if (command.startsWith('git checkout -b')) return Buffer.from('')
448
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
449
+ return Buffer.from('')
450
+ }) as typeof childProcess.execSync)
451
+
452
+ const mergedChange: ChangeInfo = {
453
+ ...mockChange,
454
+ status: 'MERGED',
455
+ }
456
+
457
+ server.use(
458
+ http.get('*/a/changes/12345', () => {
459
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mergedChange)}`)
460
+ }),
461
+ http.get('*/a/changes/12345/revisions/current', () => {
462
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
463
+ }),
464
+ )
465
+
466
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
467
+
468
+ const program = checkoutCommand('12345', {}).pipe(
469
+ Effect.provide(GerritApiServiceLive),
470
+ Effect.provide(mockConfigLayer),
471
+ )
472
+
473
+ await Effect.runPromise(program.pipe(Effect.either))
474
+
475
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
476
+ expect(output).toContain('MERGED')
477
+ })
478
+
479
+ test('should update existing branch when branch already exists', async () => {
480
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
481
+ command: string,
482
+ _options?: unknown,
483
+ ) => {
484
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
485
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
486
+ if (command === 'git remote -v') {
487
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
488
+ }
489
+ if (command.startsWith('git rev-parse --verify review/12345')) {
490
+ // Branch exists
491
+ return Buffer.from('abc123\n')
492
+ }
493
+ if (command.startsWith('git fetch')) return Buffer.from('')
494
+ if (command.startsWith('git checkout review/12345')) return Buffer.from('')
495
+ if (command.startsWith('git reset --hard FETCH_HEAD')) return Buffer.from('')
496
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
497
+ return Buffer.from('')
498
+ }) as typeof childProcess.execSync)
499
+
500
+ server.use(
501
+ http.get('*/a/changes/12345', () => {
502
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
503
+ }),
504
+ http.get('*/a/changes/12345/revisions/current', () => {
505
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
506
+ }),
507
+ )
508
+
509
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
510
+
511
+ const program = checkoutCommand('12345', {}).pipe(
512
+ Effect.provide(GerritApiServiceLive),
513
+ Effect.provide(mockConfigLayer),
514
+ )
515
+
516
+ const result = await Effect.runPromise(program.pipe(Effect.either))
517
+
518
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
519
+ expect(output).toContain('Updated and checked out review/12345')
520
+ expect(result._tag).toBe('Right')
521
+ })
522
+
523
+ test('should handle when branch exists and is currently checked out', async () => {
524
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
525
+ command: string,
526
+ _options?: unknown,
527
+ ) => {
528
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
529
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('review/12345\n')
530
+ if (command === 'git remote -v') {
531
+ return Buffer.from('origin\thttps://test.gerrit.com/repo.git\t(push)\n')
532
+ }
533
+ if (command.startsWith('git rev-parse --verify review/12345')) {
534
+ // Branch exists
535
+ return Buffer.from('abc123\n')
536
+ }
537
+ if (command.startsWith('git fetch')) return Buffer.from('')
538
+ if (command.startsWith('git reset --hard FETCH_HEAD')) return Buffer.from('')
539
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
540
+ return Buffer.from('')
541
+ }) as typeof childProcess.execSync)
542
+
543
+ server.use(
544
+ http.get('*/a/changes/12345', () => {
545
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
546
+ }),
547
+ http.get('*/a/changes/12345/revisions/current', () => {
548
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
549
+ }),
550
+ )
551
+
552
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
553
+
554
+ const program = checkoutCommand('12345', {}).pipe(
555
+ Effect.provide(GerritApiServiceLive),
556
+ Effect.provide(mockConfigLayer),
557
+ )
558
+
559
+ const result = await Effect.runPromise(program.pipe(Effect.either))
560
+
561
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
562
+ expect(output).toContain('Updated and checked out review/12345')
563
+ // Should not try to switch branches since already on it
564
+ expect(result._tag).toBe('Right')
565
+ })
566
+
567
+ test('should fallback to origin when no remote matches Gerrit host', async () => {
568
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
569
+ command: string,
570
+ _options?: unknown,
571
+ ) => {
572
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
573
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
574
+ if (command === 'git remote -v') {
575
+ // Remote with different hostname
576
+ return Buffer.from('origin\thttps://different.gerrit.com/repo.git\t(push)\n')
577
+ }
578
+ if (command.startsWith('git rev-parse --verify')) {
579
+ throw new Error('branch does not exist')
580
+ }
581
+ if (command.startsWith('git fetch')) return Buffer.from('')
582
+ if (command.startsWith('git checkout -b')) return Buffer.from('')
583
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
584
+ return Buffer.from('')
585
+ }) as typeof childProcess.execSync)
586
+
587
+ server.use(
588
+ http.get('*/a/changes/12345', () => {
589
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
590
+ }),
591
+ http.get('*/a/changes/12345/revisions/current', () => {
592
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
593
+ }),
594
+ )
595
+
596
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
597
+
598
+ const program = checkoutCommand('12345', {}).pipe(
599
+ Effect.provide(GerritApiServiceLive),
600
+ Effect.provide(mockConfigLayer),
601
+ )
602
+
603
+ const result = await Effect.runPromise(program.pipe(Effect.either))
604
+
605
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
606
+ // Should use 'origin' as fallback
607
+ expect(output).toContain('Remote: origin')
608
+ expect(result._tag).toBe('Right')
609
+ })
610
+
611
+ test('should use custom remote when specified', async () => {
612
+ mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((
613
+ command: string,
614
+ _options?: unknown,
615
+ ) => {
616
+ if (command === 'git rev-parse --git-dir') return Buffer.from('.git\n')
617
+ if (command === 'git symbolic-ref --short HEAD') return Buffer.from('main\n')
618
+ if (command === 'git remote -v') {
619
+ return Buffer.from('upstream\thttps://test.gerrit.com/repo.git\t(push)\n')
620
+ }
621
+ if (command.startsWith('git rev-parse --verify')) {
622
+ throw new Error('branch does not exist')
623
+ }
624
+ if (command.startsWith('git fetch')) return Buffer.from('')
625
+ if (command.startsWith('git checkout -b')) return Buffer.from('')
626
+ if (command.startsWith('git branch --set-upstream-to')) return Buffer.from('')
627
+ return Buffer.from('')
628
+ }) as typeof childProcess.execSync)
629
+
630
+ server.use(
631
+ http.get('*/a/changes/12345', () => {
632
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
633
+ }),
634
+ http.get('*/a/changes/12345/revisions/current', () => {
635
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockRevision)}`)
636
+ }),
637
+ )
638
+
639
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
640
+
641
+ const program = checkoutCommand('12345', { remote: 'upstream' }).pipe(
642
+ Effect.provide(GerritApiServiceLive),
643
+ Effect.provide(mockConfigLayer),
644
+ )
645
+
646
+ const result = await Effect.runPromise(program.pipe(Effect.either))
647
+
648
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
649
+ // Should use specified remote
650
+ expect(output).toContain('Remote: upstream')
651
+ expect(result._tag).toBe('Right')
652
+ })
653
+ })