@aaronshaf/ger 2.0.0 → 2.0.1
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/prd/commands.md +10 -2
- package/package.json +1 -1
- package/src/cli/commands/rebase.ts +31 -12
- package/src/cli/register-commands.ts +2 -2
- package/tests/rebase.test.ts +114 -17
package/docs/prd/commands.md
CHANGED
|
@@ -283,10 +283,18 @@ ger push --wip
|
|
|
283
283
|
Rebase a change on target branch.
|
|
284
284
|
|
|
285
285
|
```bash
|
|
286
|
-
ger rebase
|
|
287
|
-
ger rebase
|
|
286
|
+
ger rebase [change-id]
|
|
287
|
+
ger rebase 12345
|
|
288
|
+
ger rebase If5a3ae8... # Change-ID format
|
|
289
|
+
ger rebase # Auto-detect from HEAD
|
|
290
|
+
ger rebase --base <ref> # Rebase onto specific ref
|
|
288
291
|
```
|
|
289
292
|
|
|
293
|
+
| Option | Description |
|
|
294
|
+
|--------|-------------|
|
|
295
|
+
| `--base <ref>` | Base revision to rebase onto |
|
|
296
|
+
| `--xml` | Output as XML for LLM consumption |
|
|
297
|
+
|
|
290
298
|
### submit
|
|
291
299
|
|
|
292
300
|
Submit a change for merge.
|
package/package.json
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Effect } from 'effect'
|
|
2
2
|
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
3
|
+
import { GitError, NoChangeIdError, getChangeIdFromHead } from '@/utils/git-commit'
|
|
4
|
+
import { escapeXML, sanitizeCDATA } from '@/utils/shell-safety'
|
|
3
5
|
|
|
4
6
|
interface RebaseOptions {
|
|
5
7
|
base?: string
|
|
@@ -9,7 +11,7 @@ interface RebaseOptions {
|
|
|
9
11
|
/**
|
|
10
12
|
* Rebases a Gerrit change onto the target branch or specified base.
|
|
11
13
|
*
|
|
12
|
-
* @param changeId - Change number or Change-ID to rebase
|
|
14
|
+
* @param changeId - Change number or Change-ID to rebase (optional, auto-detects from HEAD if not provided)
|
|
13
15
|
* @param options - Configuration options
|
|
14
16
|
* @param options.base - Optional base revision to rebase onto (default: target branch HEAD)
|
|
15
17
|
* @param options.xml - Whether to output in XML format for LLM consumption
|
|
@@ -18,28 +20,25 @@ interface RebaseOptions {
|
|
|
18
20
|
export const rebaseCommand = (
|
|
19
21
|
changeId?: string,
|
|
20
22
|
options: RebaseOptions = {},
|
|
21
|
-
): Effect.Effect<void,
|
|
23
|
+
): Effect.Effect<void, never, GerritApiService> =>
|
|
22
24
|
Effect.gen(function* () {
|
|
23
25
|
const gerritApi = yield* GerritApiService
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
console.error(' Usage: ger rebase <change-id> [--base <ref>]')
|
|
28
|
-
return
|
|
29
|
-
}
|
|
27
|
+
// Auto-detect Change-ID from HEAD commit if not provided
|
|
28
|
+
const resolvedChangeId = changeId || (yield* getChangeIdFromHead())
|
|
30
29
|
|
|
31
30
|
// Perform the rebase - this returns the rebased change info
|
|
32
|
-
const change = yield* gerritApi.rebaseChange(
|
|
31
|
+
const change = yield* gerritApi.rebaseChange(resolvedChangeId, { base: options.base })
|
|
33
32
|
|
|
34
33
|
if (options.xml) {
|
|
35
34
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
36
35
|
console.log(`<rebase_result>`)
|
|
37
36
|
console.log(` <status>success</status>`)
|
|
38
37
|
console.log(` <change_number>${change._number}</change_number>`)
|
|
39
|
-
console.log(` <subject><![CDATA[${change.subject}]]></subject>`)
|
|
40
|
-
console.log(` <branch>${change.branch}</branch>`)
|
|
38
|
+
console.log(` <subject><![CDATA[${sanitizeCDATA(change.subject)}]]></subject>`)
|
|
39
|
+
console.log(` <branch>${escapeXML(change.branch)}</branch>`)
|
|
41
40
|
if (options.base) {
|
|
42
|
-
console.log(` <base><![CDATA[${options.base}]]></base>`)
|
|
41
|
+
console.log(` <base><![CDATA[${sanitizeCDATA(options.base)}]]></base>`)
|
|
43
42
|
}
|
|
44
43
|
console.log(`</rebase_result>`)
|
|
45
44
|
} else {
|
|
@@ -49,4 +48,24 @@ export const rebaseCommand = (
|
|
|
49
48
|
console.log(` Base: ${options.base}`)
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
|
-
})
|
|
51
|
+
}).pipe(
|
|
52
|
+
// Regional error boundary for the entire command
|
|
53
|
+
Effect.catchAll((error: ApiError | GitError | NoChangeIdError) =>
|
|
54
|
+
Effect.sync(() => {
|
|
55
|
+
const errorMessage =
|
|
56
|
+
error instanceof GitError || error instanceof NoChangeIdError || error instanceof Error
|
|
57
|
+
? error.message
|
|
58
|
+
: String(error)
|
|
59
|
+
|
|
60
|
+
if (options.xml) {
|
|
61
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
62
|
+
console.log(`<rebase_result>`)
|
|
63
|
+
console.log(` <status>error</status>`)
|
|
64
|
+
console.log(` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
|
|
65
|
+
console.log(`</rebase_result>`)
|
|
66
|
+
} else {
|
|
67
|
+
console.error(`✗ Error: ${errorMessage}`)
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
@@ -285,8 +285,8 @@ Note:
|
|
|
285
285
|
|
|
286
286
|
// rebase command
|
|
287
287
|
program
|
|
288
|
-
.command('rebase
|
|
289
|
-
.description('Rebase a change onto target branch (
|
|
288
|
+
.command('rebase [change-id]')
|
|
289
|
+
.description('Rebase a change onto target branch (auto-detects from HEAD if not provided)')
|
|
290
290
|
.option('--base <ref>', 'Base revision to rebase onto (default: target branch HEAD)')
|
|
291
291
|
.option('--xml', 'XML output for LLM consumption')
|
|
292
292
|
.action(async (changeId, options) => {
|
package/tests/rebase.test.ts
CHANGED
|
@@ -170,7 +170,7 @@ describe('rebase command', () => {
|
|
|
170
170
|
expect(output).toContain('</rebase_result>')
|
|
171
171
|
})
|
|
172
172
|
|
|
173
|
-
it('should handle not found errors gracefully', async () => {
|
|
173
|
+
it('should handle not found errors gracefully with pretty output', async () => {
|
|
174
174
|
server.use(
|
|
175
175
|
http.post('*/a/changes/99999/revisions/current/rebase', () => {
|
|
176
176
|
return HttpResponse.text('Change not found', { status: 404 })
|
|
@@ -183,25 +183,86 @@ describe('rebase command', () => {
|
|
|
183
183
|
Effect.provide(mockConfigLayer),
|
|
184
184
|
)
|
|
185
185
|
|
|
186
|
-
//
|
|
187
|
-
await
|
|
186
|
+
// Error boundary catches and outputs to console.error
|
|
187
|
+
await Effect.runPromise(program)
|
|
188
|
+
|
|
189
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
190
|
+
expect(errorOutput).toContain('Error:')
|
|
188
191
|
})
|
|
189
192
|
|
|
190
|
-
it('should
|
|
193
|
+
it('should handle not found errors with XML output when --xml flag is used', async () => {
|
|
194
|
+
server.use(
|
|
195
|
+
http.post('*/a/changes/99999/revisions/current/rebase', () => {
|
|
196
|
+
return HttpResponse.text('Change not found', { status: 404 })
|
|
197
|
+
}),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
201
|
+
const program = rebaseCommand('99999', { xml: true }).pipe(
|
|
202
|
+
Effect.provide(GerritApiServiceLive),
|
|
203
|
+
Effect.provide(mockConfigLayer),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// Error boundary catches and outputs XML error
|
|
207
|
+
await Effect.runPromise(program)
|
|
208
|
+
|
|
209
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
210
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
211
|
+
expect(output).toContain('<rebase_result>')
|
|
212
|
+
expect(output).toContain('<status>error</status>')
|
|
213
|
+
expect(output).toContain('<error><![CDATA[')
|
|
214
|
+
expect(output).toContain('</rebase_result>')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should output error to console.error when no change ID and HEAD has no Change-Id', async () => {
|
|
191
218
|
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
192
219
|
const program = rebaseCommand(undefined, {}).pipe(
|
|
193
220
|
Effect.provide(GerritApiServiceLive),
|
|
194
221
|
Effect.provide(mockConfigLayer),
|
|
195
222
|
)
|
|
196
223
|
|
|
224
|
+
// Error boundary catches NoChangeIdError and outputs to console.error
|
|
197
225
|
await Effect.runPromise(program)
|
|
198
226
|
|
|
199
227
|
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
200
|
-
expect(errorOutput).toContain('
|
|
201
|
-
expect(errorOutput).toContain('
|
|
228
|
+
expect(errorOutput).toContain('Error:')
|
|
229
|
+
expect(errorOutput).toContain('No Change-ID found in HEAD commit')
|
|
202
230
|
})
|
|
203
231
|
|
|
204
|
-
it('should
|
|
232
|
+
it('should output XML error when no change ID and HEAD has no Change-Id with --xml flag', async () => {
|
|
233
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
234
|
+
const program = rebaseCommand(undefined, { xml: true }).pipe(
|
|
235
|
+
Effect.provide(GerritApiServiceLive),
|
|
236
|
+
Effect.provide(mockConfigLayer),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
// Error boundary catches NoChangeIdError and outputs XML error
|
|
240
|
+
await Effect.runPromise(program)
|
|
241
|
+
|
|
242
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
243
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
244
|
+
expect(output).toContain('<rebase_result>')
|
|
245
|
+
expect(output).toContain('<status>error</status>')
|
|
246
|
+
expect(output).toContain('No Change-ID found in HEAD commit')
|
|
247
|
+
expect(output).toContain('</rebase_result>')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should treat empty string as missing change ID and auto-detect', async () => {
|
|
251
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
252
|
+
const program = rebaseCommand('', {}).pipe(
|
|
253
|
+
Effect.provide(GerritApiServiceLive),
|
|
254
|
+
Effect.provide(mockConfigLayer),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
// Empty string triggers auto-detection, which fails with NoChangeIdError
|
|
258
|
+
await Effect.runPromise(program)
|
|
259
|
+
|
|
260
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
261
|
+
expect(errorOutput).toContain('Error:')
|
|
262
|
+
expect(errorOutput).toContain('No Change-ID found in HEAD commit')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should handle rebase conflicts gracefully', async () => {
|
|
205
266
|
server.use(
|
|
206
267
|
http.post('*/a/changes/12345/revisions/current/rebase', () => {
|
|
207
268
|
return HttpResponse.text('Rebase conflict detected', { status: 409 })
|
|
@@ -214,11 +275,14 @@ describe('rebase command', () => {
|
|
|
214
275
|
Effect.provide(mockConfigLayer),
|
|
215
276
|
)
|
|
216
277
|
|
|
217
|
-
//
|
|
218
|
-
await
|
|
278
|
+
// Error boundary catches and outputs to console.error
|
|
279
|
+
await Effect.runPromise(program)
|
|
280
|
+
|
|
281
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
282
|
+
expect(errorOutput).toContain('Error:')
|
|
219
283
|
})
|
|
220
284
|
|
|
221
|
-
it('should handle API errors', async () => {
|
|
285
|
+
it('should handle API errors gracefully', async () => {
|
|
222
286
|
server.use(
|
|
223
287
|
http.post('*/a/changes/12345/revisions/current/rebase', () => {
|
|
224
288
|
return HttpResponse.text('Forbidden', { status: 403 })
|
|
@@ -231,8 +295,11 @@ describe('rebase command', () => {
|
|
|
231
295
|
Effect.provide(mockConfigLayer),
|
|
232
296
|
)
|
|
233
297
|
|
|
234
|
-
//
|
|
235
|
-
await
|
|
298
|
+
// Error boundary catches and outputs to console.error
|
|
299
|
+
await Effect.runPromise(program)
|
|
300
|
+
|
|
301
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
302
|
+
expect(errorOutput).toContain('Error:')
|
|
236
303
|
})
|
|
237
304
|
|
|
238
305
|
it('should handle changes that are already up to date', async () => {
|
|
@@ -248,11 +315,14 @@ describe('rebase command', () => {
|
|
|
248
315
|
Effect.provide(mockConfigLayer),
|
|
249
316
|
)
|
|
250
317
|
|
|
251
|
-
//
|
|
252
|
-
await
|
|
318
|
+
// Error boundary catches and outputs to console.error
|
|
319
|
+
await Effect.runPromise(program)
|
|
320
|
+
|
|
321
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
322
|
+
expect(errorOutput).toContain('Error:')
|
|
253
323
|
})
|
|
254
324
|
|
|
255
|
-
it('should handle network errors', async () => {
|
|
325
|
+
it('should handle network errors gracefully', async () => {
|
|
256
326
|
server.use(
|
|
257
327
|
http.post('*/a/changes/12345/revisions/current/rebase', () => {
|
|
258
328
|
return HttpResponse.error()
|
|
@@ -265,7 +335,34 @@ describe('rebase command', () => {
|
|
|
265
335
|
Effect.provide(mockConfigLayer),
|
|
266
336
|
)
|
|
267
337
|
|
|
268
|
-
//
|
|
269
|
-
await
|
|
338
|
+
// Error boundary catches and outputs to console.error
|
|
339
|
+
await Effect.runPromise(program)
|
|
340
|
+
|
|
341
|
+
const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
|
|
342
|
+
expect(errorOutput).toContain('Error:')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('should handle network errors with XML output', async () => {
|
|
346
|
+
server.use(
|
|
347
|
+
http.post('*/a/changes/12345/revisions/current/rebase', () => {
|
|
348
|
+
return HttpResponse.error()
|
|
349
|
+
}),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
|
|
353
|
+
const program = rebaseCommand('12345', { xml: true }).pipe(
|
|
354
|
+
Effect.provide(GerritApiServiceLive),
|
|
355
|
+
Effect.provide(mockConfigLayer),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
// Error boundary catches and outputs XML error
|
|
359
|
+
await Effect.runPromise(program)
|
|
360
|
+
|
|
361
|
+
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
|
|
362
|
+
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
|
|
363
|
+
expect(output).toContain('<rebase_result>')
|
|
364
|
+
expect(output).toContain('<status>error</status>')
|
|
365
|
+
expect(output).toContain('<error><![CDATA[')
|
|
366
|
+
expect(output).toContain('</rebase_result>')
|
|
270
367
|
})
|
|
271
368
|
})
|