@aaronshaf/ger 3.0.2 → 4.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/package.json +1 -1
- package/skills/gerrit-workflow/SKILL.md +228 -141
- package/skills/gerrit-workflow/examples.md +133 -426
- package/skills/gerrit-workflow/reference.md +470 -408
- package/src/api/gerrit-types.ts +121 -0
- package/src/api/gerrit.ts +55 -96
- package/src/cli/commands/analyze.ts +284 -0
- package/src/cli/commands/cherry.ts +268 -0
- package/src/cli/commands/failures.ts +74 -0
- package/src/cli/commands/list.ts +239 -0
- package/src/cli/commands/rebase.ts +5 -1
- package/src/cli/commands/retrigger.ts +91 -0
- package/src/cli/commands/setup.ts +19 -6
- package/src/cli/commands/tree-cleanup.ts +139 -0
- package/src/cli/commands/tree-rebase.ts +168 -0
- package/src/cli/commands/tree-setup.ts +202 -0
- package/src/cli/commands/trees.ts +107 -0
- package/src/cli/commands/update.ts +73 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/register-analytics-commands.ts +90 -0
- package/src/cli/register-commands.ts +56 -40
- package/src/cli/register-list-commands.ts +105 -0
- package/src/cli/register-tree-commands.ts +128 -0
- package/src/schemas/config.ts +3 -0
- package/src/services/config.ts +15 -0
- package/tests/analyze.test.ts +197 -0
- package/tests/cherry.test.ts +208 -0
- package/tests/failures.test.ts +212 -0
- package/tests/helpers/config-mock.ts +4 -0
- package/tests/list.test.ts +220 -0
- package/tests/retrigger.test.ts +159 -0
- package/tests/tree.test.ts +517 -0
- package/tests/update.test.ts +86 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test'
|
|
2
|
+
import { HttpResponse, http } from 'msw'
|
|
3
|
+
import { setupServer } from 'msw/node'
|
|
4
|
+
import { Effect, Layer } from 'effect'
|
|
5
|
+
import { listCommand } from '@/cli/commands/list'
|
|
6
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
7
|
+
import { ConfigService } from '@/services/config'
|
|
8
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
9
|
+
import type { ChangeInfo } from '@/schemas/gerrit'
|
|
10
|
+
|
|
11
|
+
const makeChange = (overrides: Partial<ChangeInfo> = {}): ChangeInfo => ({
|
|
12
|
+
id: 'project~main~I123',
|
|
13
|
+
_number: 12345,
|
|
14
|
+
project: 'my-project',
|
|
15
|
+
branch: 'main',
|
|
16
|
+
change_id: 'I123',
|
|
17
|
+
subject: 'Fix the important thing',
|
|
18
|
+
status: 'NEW',
|
|
19
|
+
created: '2025-01-15 10:00:00.000000000',
|
|
20
|
+
updated: '2025-01-15 12:00:00.000000000',
|
|
21
|
+
owner: { _account_id: 1, name: 'Alice', email: 'alice@x.com' },
|
|
22
|
+
...overrides,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const mockChanges: ChangeInfo[] = [
|
|
26
|
+
makeChange({ _number: 1, subject: 'First change' }),
|
|
27
|
+
makeChange({
|
|
28
|
+
_number: 2,
|
|
29
|
+
subject: 'Second change with Code-Review',
|
|
30
|
+
labels: { 'Code-Review': { approved: { _account_id: 1 }, value: 2 } },
|
|
31
|
+
}),
|
|
32
|
+
makeChange({
|
|
33
|
+
_number: 3,
|
|
34
|
+
subject: 'Third change rejected',
|
|
35
|
+
labels: { 'Code-Review': { rejected: { _account_id: 2 }, value: -2 } },
|
|
36
|
+
}),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const server = setupServer(
|
|
40
|
+
http.get('*/a/accounts/self', () =>
|
|
41
|
+
HttpResponse.json({ _account_id: 1, name: 'Alice', email: 'alice@x.com' }),
|
|
42
|
+
),
|
|
43
|
+
http.get('*/a/changes/', () => HttpResponse.json(mockChanges)),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
|
|
47
|
+
afterAll(() => server.close())
|
|
48
|
+
afterEach(() => server.resetHandlers())
|
|
49
|
+
|
|
50
|
+
const mockConfig = createMockConfigService()
|
|
51
|
+
|
|
52
|
+
describe('list command', () => {
|
|
53
|
+
test('renders table with header and rows', async () => {
|
|
54
|
+
const logs: string[] = []
|
|
55
|
+
const origLog = console.log
|
|
56
|
+
console.log = (...args: unknown[]) => logs.push(String(args[0]))
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await Effect.runPromise(
|
|
60
|
+
listCommand({}).pipe(
|
|
61
|
+
Effect.provide(GerritApiServiceLive),
|
|
62
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
} finally {
|
|
66
|
+
console.log = origLog
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const output = logs.join('\n')
|
|
70
|
+
expect(output).toContain('Change')
|
|
71
|
+
expect(output).toContain('Subject')
|
|
72
|
+
expect(output).toContain('CR')
|
|
73
|
+
expect(output).toContain('Verified')
|
|
74
|
+
expect(output).toContain('1')
|
|
75
|
+
expect(output).toContain('First change')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('outputs JSON', async () => {
|
|
79
|
+
const logs: string[] = []
|
|
80
|
+
const origLog = console.log
|
|
81
|
+
console.log = (...args: unknown[]) => logs.push(String(args[0]))
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await Effect.runPromise(
|
|
85
|
+
listCommand({ json: true }).pipe(
|
|
86
|
+
Effect.provide(GerritApiServiceLive),
|
|
87
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
} finally {
|
|
91
|
+
console.log = origLog
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsed = JSON.parse(logs.join('')) as { status: string; count: number }
|
|
95
|
+
expect(parsed.status).toBe('success')
|
|
96
|
+
expect(parsed.count).toBe(3)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('outputs XML', async () => {
|
|
100
|
+
const logs: string[] = []
|
|
101
|
+
const origLog = console.log
|
|
102
|
+
console.log = (...args: unknown[]) => logs.push(String(args[0]))
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await Effect.runPromise(
|
|
106
|
+
listCommand({ xml: true }).pipe(
|
|
107
|
+
Effect.provide(GerritApiServiceLive),
|
|
108
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
} finally {
|
|
112
|
+
console.log = origLog
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const output = logs.join('\n')
|
|
116
|
+
expect(output).toContain('<changes count="3">')
|
|
117
|
+
expect(output).toContain('<number>1</number>')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('--detailed shows per-change info', async () => {
|
|
121
|
+
const logs: string[] = []
|
|
122
|
+
const origLog = console.log
|
|
123
|
+
console.log = (...args: unknown[]) => logs.push(String(args[0]))
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await Effect.runPromise(
|
|
127
|
+
listCommand({ detailed: true }).pipe(
|
|
128
|
+
Effect.provide(GerritApiServiceLive),
|
|
129
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
} finally {
|
|
133
|
+
console.log = origLog
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const output = logs.join('\n')
|
|
137
|
+
expect(output).toContain('Change:')
|
|
138
|
+
expect(output).toContain('Subject:')
|
|
139
|
+
expect(output).toContain('Project:')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('--limit caps results', async () => {
|
|
143
|
+
const logs: string[] = []
|
|
144
|
+
const origLog = console.log
|
|
145
|
+
console.log = (...args: unknown[]) => logs.push(String(args[0]))
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await Effect.runPromise(
|
|
149
|
+
listCommand({ limit: 1, json: true }).pipe(
|
|
150
|
+
Effect.provide(GerritApiServiceLive),
|
|
151
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
} finally {
|
|
155
|
+
console.log = origLog
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const parsed = JSON.parse(logs.join('')) as { count: number }
|
|
159
|
+
expect(parsed.count).toBe(1)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('--status passes query to API', async () => {
|
|
163
|
+
let capturedUrl = ''
|
|
164
|
+
server.use(
|
|
165
|
+
http.get('*/a/changes/', ({ request }) => {
|
|
166
|
+
capturedUrl = decodeURIComponent(request.url)
|
|
167
|
+
return HttpResponse.json([])
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
await Effect.runPromise(
|
|
172
|
+
listCommand({ status: 'merged' }).pipe(
|
|
173
|
+
Effect.provide(GerritApiServiceLive),
|
|
174
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expect(capturedUrl).toContain('status:merged')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('--reviewer uses reviewer query', async () => {
|
|
182
|
+
let capturedUrl = ''
|
|
183
|
+
server.use(
|
|
184
|
+
http.get('*/a/changes/', ({ request }) => {
|
|
185
|
+
capturedUrl = decodeURIComponent(request.url)
|
|
186
|
+
return HttpResponse.json([])
|
|
187
|
+
}),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
await Effect.runPromise(
|
|
191
|
+
listCommand({ reviewer: true }).pipe(
|
|
192
|
+
Effect.provide(GerritApiServiceLive),
|
|
193
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
expect(capturedUrl).toContain('reviewer:')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('shows empty message when no changes', async () => {
|
|
201
|
+
server.use(http.get('*/a/changes/', () => HttpResponse.json([])))
|
|
202
|
+
|
|
203
|
+
const logs: string[] = []
|
|
204
|
+
const origLog = console.log
|
|
205
|
+
console.log = (...args: unknown[]) => logs.push(String(args[0]))
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await Effect.runPromise(
|
|
209
|
+
listCommand({}).pipe(
|
|
210
|
+
Effect.provide(GerritApiServiceLive),
|
|
211
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
} finally {
|
|
215
|
+
console.log = origLog
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
expect(logs.join('\n')).toContain('No changes found')
|
|
219
|
+
})
|
|
220
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
import { Effect, Layer } from 'effect'
|
|
3
|
+
import { HttpResponse, http } from 'msw'
|
|
4
|
+
import { setupServer } from 'msw/node'
|
|
5
|
+
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
6
|
+
import { ConfigService } from '@/services/config'
|
|
7
|
+
import { retriggerCommand } from '@/cli/commands/retrigger'
|
|
8
|
+
import { createMockConfigService } from './helpers/config-mock'
|
|
9
|
+
|
|
10
|
+
// Mock @inquirer/prompts so tests don't block on stdin
|
|
11
|
+
const mockInput = mock(async () => 'trigger-build')
|
|
12
|
+
mock.module('@inquirer/prompts', () => ({ input: mockInput }))
|
|
13
|
+
|
|
14
|
+
const server = setupServer(
|
|
15
|
+
http.get('*/a/accounts/self', () =>
|
|
16
|
+
HttpResponse.json({ _account_id: 1, name: 'User', email: 'u@example.com' }),
|
|
17
|
+
),
|
|
18
|
+
http.post('*/a/changes/:changeId/revisions/current/review', () => HttpResponse.json({})),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
|
|
22
|
+
afterAll(() => server.close())
|
|
23
|
+
|
|
24
|
+
describe('retrigger command', () => {
|
|
25
|
+
let mockConsoleLog: ReturnType<typeof mock>
|
|
26
|
+
let mockConsoleError: ReturnType<typeof mock>
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
mockConsoleLog = mock()
|
|
30
|
+
mockConsoleError = mock()
|
|
31
|
+
console.log = mockConsoleLog
|
|
32
|
+
console.error = mockConsoleError
|
|
33
|
+
mockInput.mockReset()
|
|
34
|
+
mockInput.mockResolvedValue('trigger-build')
|
|
35
|
+
server.resetHandlers()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
server.resetHandlers()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('posts the retrigger comment when change-id is explicit and comment is configured', async () => {
|
|
43
|
+
const mockConfig = createMockConfigService(undefined, undefined, '__TRIGGER__')
|
|
44
|
+
|
|
45
|
+
await Effect.runPromise(
|
|
46
|
+
retriggerCommand('12345', {}).pipe(
|
|
47
|
+
Effect.provide(GerritApiServiceLive),
|
|
48
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Should print success
|
|
53
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('✓'))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('posts to the given change-id', async () => {
|
|
57
|
+
const mockConfig = createMockConfigService(undefined, undefined, '__TRIGGER__')
|
|
58
|
+
|
|
59
|
+
let postedChangeId = ''
|
|
60
|
+
server.use(
|
|
61
|
+
http.post('*/a/changes/:changeId/revisions/current/review', ({ params }) => {
|
|
62
|
+
postedChangeId = params.changeId as string
|
|
63
|
+
return HttpResponse.json({})
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await Effect.runPromise(
|
|
68
|
+
retriggerCommand('67890', {}).pipe(
|
|
69
|
+
Effect.provide(GerritApiServiceLive),
|
|
70
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expect(postedChangeId).toBe('67890')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('prompts for retrigger comment when not configured, then saves it', async () => {
|
|
78
|
+
let savedComment = ''
|
|
79
|
+
const mockConfig: ReturnType<typeof createMockConfigService> = {
|
|
80
|
+
...createMockConfigService(),
|
|
81
|
+
getRetriggerComment: Effect.succeed(undefined),
|
|
82
|
+
saveRetriggerComment: (comment: string) => {
|
|
83
|
+
savedComment = comment
|
|
84
|
+
return Effect.succeed(undefined as void)
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
mockInput.mockResolvedValue('my-trigger-comment')
|
|
89
|
+
|
|
90
|
+
await Effect.runPromise(
|
|
91
|
+
retriggerCommand('12345', {}).pipe(
|
|
92
|
+
Effect.provide(GerritApiServiceLive),
|
|
93
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
expect(mockInput).toHaveBeenCalled()
|
|
98
|
+
expect(savedComment).toBe('my-trigger-comment')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('throws when prompted comment is empty', async () => {
|
|
102
|
+
const mockConfig: ReturnType<typeof createMockConfigService> = {
|
|
103
|
+
...createMockConfigService(),
|
|
104
|
+
getRetriggerComment: Effect.succeed(undefined),
|
|
105
|
+
saveRetriggerComment: () => Effect.succeed(undefined as void),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
mockInput.mockResolvedValue(' ')
|
|
109
|
+
|
|
110
|
+
let threw = false
|
|
111
|
+
try {
|
|
112
|
+
await Effect.runPromise(
|
|
113
|
+
retriggerCommand('12345', {}).pipe(
|
|
114
|
+
Effect.provide(GerritApiServiceLive),
|
|
115
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
} catch (e) {
|
|
119
|
+
threw = true
|
|
120
|
+
expect(String(e)).toContain('cannot be empty')
|
|
121
|
+
}
|
|
122
|
+
expect(threw).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('outputs JSON on success', async () => {
|
|
126
|
+
const mockConfig = createMockConfigService(undefined, undefined, '__TRIGGER__')
|
|
127
|
+
|
|
128
|
+
const logs: string[] = []
|
|
129
|
+
console.log = (msg: string) => logs.push(msg)
|
|
130
|
+
|
|
131
|
+
await Effect.runPromise(
|
|
132
|
+
retriggerCommand('12345', { json: true }).pipe(
|
|
133
|
+
Effect.provide(GerritApiServiceLive),
|
|
134
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const parsed = JSON.parse(logs[0]) as { status: string; change_id: string }
|
|
139
|
+
expect(parsed.status).toBe('success')
|
|
140
|
+
expect(parsed.change_id).toBe('12345')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('outputs XML on success', async () => {
|
|
144
|
+
const mockConfig = createMockConfigService(undefined, undefined, '__TRIGGER__')
|
|
145
|
+
|
|
146
|
+
const logs: string[] = []
|
|
147
|
+
console.log = (msg: string) => logs.push(msg)
|
|
148
|
+
|
|
149
|
+
await Effect.runPromise(
|
|
150
|
+
retriggerCommand('12345', { xml: true }).pipe(
|
|
151
|
+
Effect.provide(GerritApiServiceLive),
|
|
152
|
+
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
expect(logs.join('\n')).toContain('<retrigger>')
|
|
157
|
+
expect(logs.join('\n')).toContain('<status>success</status>')
|
|
158
|
+
})
|
|
159
|
+
})
|