@aaronshaf/ger 1.2.10 → 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.
- package/.ast-grep/rules/no-as-casting.yml +13 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +83 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/dependency-update.yml +84 -0
- package/.github/workflows/release.yml +166 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/security.yml +96 -0
- package/.husky/pre-commit +16 -0
- package/.husky/pre-push +25 -0
- package/.lintstagedrc.json +6 -0
- package/.tool-versions +1 -0
- package/CLAUDE.md +105 -0
- package/DEVELOPMENT.md +361 -0
- package/EXAMPLES.md +457 -0
- package/README.md +831 -16
- package/bin/ger +3 -18
- package/biome.json +36 -0
- package/bun.lock +678 -0
- package/bunfig.toml +8 -0
- package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
- package/docs/adr/0002-use-bun-runtime.md +64 -0
- package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
- package/docs/adr/0004-use-commander-for-cli.md +76 -0
- package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
- package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
- package/docs/adr/0007-git-hooks-for-quality.md +94 -0
- package/docs/adr/0008-no-as-typecasting.md +83 -0
- package/docs/adr/0009-file-size-limits.md +82 -0
- package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
- package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
- package/docs/adr/0012-build-status-message-parsing.md +94 -0
- package/docs/adr/0013-git-subprocess-integration.md +98 -0
- package/docs/adr/0014-group-management-support.md +95 -0
- package/docs/adr/0015-batch-comment-processing.md +111 -0
- package/docs/adr/0016-flexible-change-identifiers.md +94 -0
- package/docs/adr/0017-git-worktree-support.md +102 -0
- package/docs/adr/0018-auto-install-commit-hook.md +103 -0
- package/docs/adr/0019-sdk-package-exports.md +95 -0
- package/docs/adr/0020-code-coverage-enforcement.md +105 -0
- package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
- package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
- package/docs/adr/README.md +30 -0
- package/docs/prd/README.md +12 -0
- package/docs/prd/architecture.md +325 -0
- package/docs/prd/commands.md +425 -0
- package/docs/prd/data-model.md +349 -0
- package/docs/prd/overview.md +124 -0
- package/index.ts +219 -0
- package/oxlint.json +24 -0
- package/package.json +82 -15
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/skills/gerrit-workflow/SKILL.md +247 -0
- package/skills/gerrit-workflow/examples.md +572 -0
- package/skills/gerrit-workflow/reference.md +728 -0
- package/src/api/gerrit.ts +696 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/add-reviewer.ts +156 -0
- package/src/cli/commands/build-status.ts +282 -0
- package/src/cli/commands/checkout.ts +422 -0
- package/src/cli/commands/comment.ts +460 -0
- package/src/cli/commands/comments.ts +85 -0
- package/src/cli/commands/diff.ts +71 -0
- package/src/cli/commands/extract-url.ts +266 -0
- package/src/cli/commands/groups-members.ts +104 -0
- package/src/cli/commands/groups-show.ts +169 -0
- package/src/cli/commands/groups.ts +137 -0
- package/src/cli/commands/incoming.ts +226 -0
- package/src/cli/commands/init.ts +164 -0
- package/src/cli/commands/mine.ts +115 -0
- package/src/cli/commands/open.ts +57 -0
- package/src/cli/commands/projects.ts +68 -0
- package/src/cli/commands/push.ts +430 -0
- package/src/cli/commands/rebase.ts +52 -0
- package/src/cli/commands/remove-reviewer.ts +123 -0
- package/src/cli/commands/restore.ts +50 -0
- package/src/cli/commands/review.ts +486 -0
- package/src/cli/commands/search.ts +162 -0
- package/src/cli/commands/setup.ts +286 -0
- package/src/cli/commands/show.ts +491 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/submit.ts +108 -0
- package/src/cli/commands/vote.ts +119 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +53 -0
- package/src/cli/register-commands.ts +659 -0
- package/src/cli/register-group-commands.ts +88 -0
- package/src/cli/register-reviewer-commands.ts +97 -0
- package/src/prompts/default-review.md +86 -0
- package/src/prompts/system-inline-review.md +135 -0
- package/src/prompts/system-overall-review.md +206 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +84 -0
- package/src/schemas/gerrit.ts +681 -0
- package/src/services/commit-hook.ts +314 -0
- package/src/services/config.test.ts +150 -0
- package/src/services/config.ts +250 -0
- package/src/services/git-worktree.ts +342 -0
- package/src/services/review-strategy.ts +292 -0
- package/src/test-utils/mock-generator.ts +138 -0
- package/src/utils/change-id.test.ts +98 -0
- package/src/utils/change-id.ts +63 -0
- package/src/utils/comment-formatters.ts +153 -0
- package/src/utils/diff-context.ts +103 -0
- package/src/utils/diff-formatters.ts +141 -0
- package/src/utils/formatters.ts +85 -0
- package/src/utils/git-commit.test.ts +277 -0
- package/src/utils/git-commit.ts +122 -0
- package/src/utils/index.ts +55 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/review-formatters.ts +89 -0
- package/src/utils/review-prompt-builder.ts +110 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +271 -0
- package/src/utils/url-parser.ts +118 -0
- package/tests/abandon.test.ts +230 -0
- package/tests/add-reviewer.test.ts +579 -0
- package/tests/build-status-watch.test.ts +344 -0
- package/tests/build-status.test.ts +789 -0
- package/tests/change-id-formats.test.ts +268 -0
- package/tests/checkout/integration.test.ts +653 -0
- package/tests/checkout/parse-input.test.ts +55 -0
- package/tests/checkout/validation.test.ts +178 -0
- package/tests/comment-batch-advanced.test.ts +431 -0
- package/tests/comment-gerrit-api-compliance.test.ts +414 -0
- package/tests/comment.test.ts +708 -0
- package/tests/comments.test.ts +323 -0
- package/tests/config-service-simple.test.ts +100 -0
- package/tests/diff.test.ts +419 -0
- package/tests/extract-url.test.ts +517 -0
- package/tests/groups-members.test.ts +256 -0
- package/tests/groups-show.test.ts +323 -0
- package/tests/groups.test.ts +334 -0
- package/tests/helpers/build-status-test-setup.ts +83 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/init.test.ts +70 -0
- package/tests/integration/commit-hook.test.ts +246 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +285 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/projects.test.ts +259 -0
- package/tests/rebase.test.ts +271 -0
- package/tests/remove-reviewer.test.ts +357 -0
- package/tests/restore.test.ts +237 -0
- package/tests/review.test.ts +135 -0
- package/tests/search.test.ts +712 -0
- package/tests/setup.test.ts +63 -0
- package/tests/show-auto-detect.test.ts +324 -0
- package/tests/show.test.ts +813 -0
- package/tests/status.test.ts +145 -0
- package/tests/submit.test.ts +316 -0
- package/tests/unit/commands/push.test.ts +194 -0
- package/tests/unit/git-branch-detection.test.ts +82 -0
- package/tests/unit/git-worktree.test.ts +55 -0
- package/tests/unit/patterns/push-patterns.test.ts +148 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/services/commit-hook.test.ts +132 -0
- package/tests/unit/services/review-strategy.test.ts +349 -0
- package/tests/unit/test-utils/mock-generator.test.ts +154 -0
- package/tests/unit/utils/comment-formatters.test.ts +415 -0
- package/tests/unit/utils/diff-context.test.ts +171 -0
- package/tests/unit/utils/diff-formatters.test.ts +165 -0
- package/tests/unit/utils/formatters.test.ts +411 -0
- package/tests/unit/utils/message-filters.test.ts +227 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tests/vote.test.ts +317 -0
- package/tests/workspace.test.ts +295 -0
- package/tsconfig.json +36 -5
- package/src/commands/branch.ts +0 -180
- package/src/ger.ts +0 -22
- package/src/types.d.ts +0 -35
- package/src/utils.ts +0 -130
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { Console, Effect, Schema } from 'effect'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { ConfigService, type ConfigError, type ConfigServiceImpl } from '@/services/config'
|
|
5
|
+
import { GerritApiService, type ApiError, type GerritApiServiceImpl } from '@/api/gerrit'
|
|
6
|
+
import { extractChangeNumber } from '@/utils/url-parser'
|
|
7
|
+
|
|
8
|
+
/** Help text for checkout command */
|
|
9
|
+
export const CHECKOUT_HELP_TEXT = `
|
|
10
|
+
Examples:
|
|
11
|
+
# Checkout latest patchset
|
|
12
|
+
$ ger checkout 12345
|
|
13
|
+
|
|
14
|
+
# Checkout specific patchset
|
|
15
|
+
$ ger checkout 12345/3
|
|
16
|
+
|
|
17
|
+
# Checkout by Change-ID
|
|
18
|
+
$ ger checkout If5a3ae8cb5a107e187447802358417f311d0c4b1
|
|
19
|
+
|
|
20
|
+
# Checkout from URL
|
|
21
|
+
$ ger checkout https://gerrit.example.com/c/my-project/+/392385
|
|
22
|
+
|
|
23
|
+
# Detached HEAD mode (for quick review)
|
|
24
|
+
$ ger checkout 12345 --detach
|
|
25
|
+
|
|
26
|
+
# Use specific remote
|
|
27
|
+
$ ger checkout 12345 --remote upstream
|
|
28
|
+
|
|
29
|
+
Notes:
|
|
30
|
+
- Creates/updates branch named review/<change-number>
|
|
31
|
+
- Sets upstream tracking to target branch
|
|
32
|
+
- Updates existing review branch if it exists`
|
|
33
|
+
|
|
34
|
+
export interface CheckoutOptions {
|
|
35
|
+
detach?: boolean
|
|
36
|
+
remote?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Custom error for checkout-specific failures
|
|
40
|
+
export class CheckoutError extends Error {
|
|
41
|
+
readonly _tag = 'CheckoutError'
|
|
42
|
+
constructor(message: string) {
|
|
43
|
+
super(message)
|
|
44
|
+
this.name = 'CheckoutError'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class NotGitRepoError extends Error {
|
|
49
|
+
readonly _tag = 'NotGitRepoError'
|
|
50
|
+
constructor(message: string) {
|
|
51
|
+
super(message)
|
|
52
|
+
this.name = 'NotGitRepoError'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class PatchsetNotFoundError extends Error {
|
|
57
|
+
readonly _tag = 'PatchsetNotFoundError'
|
|
58
|
+
constructor(public readonly patchset: number) {
|
|
59
|
+
super(`Patchset ${patchset} not found`)
|
|
60
|
+
this.name = 'PatchsetNotFoundError'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class InvalidInputError extends Error {
|
|
65
|
+
readonly _tag = 'InvalidInputError'
|
|
66
|
+
constructor(message: string) {
|
|
67
|
+
super(message)
|
|
68
|
+
this.name = 'InvalidInputError'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type CheckoutErrors =
|
|
73
|
+
| ConfigError
|
|
74
|
+
| CheckoutError
|
|
75
|
+
| NotGitRepoError
|
|
76
|
+
| PatchsetNotFoundError
|
|
77
|
+
| ApiError
|
|
78
|
+
| InvalidInputError
|
|
79
|
+
|
|
80
|
+
// Git-safe string validation - prevents command injection
|
|
81
|
+
// Allows alphanumeric, hyphens, underscores, slashes, and dots
|
|
82
|
+
const GitSafeString = Schema.String.pipe(
|
|
83
|
+
Schema.pattern(/^[a-zA-Z0-9_\-/.]+$/),
|
|
84
|
+
Schema.annotations({ message: () => 'Invalid characters in git identifier' }),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Gerrit ref validation (refs/changes/xx/xxxxx/x)
|
|
88
|
+
const GerritRef = Schema.String.pipe(
|
|
89
|
+
Schema.pattern(/^refs\/changes\/\d{2}\/\d+\/\d+$/),
|
|
90
|
+
Schema.annotations({ message: () => 'Invalid Gerrit ref format' }),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
// Validate git-safe strings to prevent command injection
|
|
94
|
+
const validateGitSafe = (
|
|
95
|
+
value: string,
|
|
96
|
+
fieldName: string,
|
|
97
|
+
): Effect.Effect<string, InvalidInputError> =>
|
|
98
|
+
Schema.decodeUnknown(GitSafeString)(value).pipe(
|
|
99
|
+
Effect.mapError(() => {
|
|
100
|
+
// Sanitize error message to avoid exposing potentially sensitive data
|
|
101
|
+
const sanitized = value.length > 20 ? `${value.substring(0, 20)}...` : value
|
|
102
|
+
return new InvalidInputError(`${fieldName} contains invalid characters: ${sanitized}`)
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const validateGerritRef = (value: string): Effect.Effect<string, InvalidInputError> =>
|
|
107
|
+
Schema.decodeUnknown(GerritRef)(value).pipe(
|
|
108
|
+
Effect.mapError(() => {
|
|
109
|
+
// Sanitize error message to avoid exposing potentially sensitive data
|
|
110
|
+
const sanitized = value.length > 30 ? `${value.substring(0, 30)}...` : value
|
|
111
|
+
return new InvalidInputError(`Invalid Gerrit ref format: ${sanitized}`)
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
/** Parse change input to extract change ID and optional patchset */
|
|
116
|
+
interface ParsedChange {
|
|
117
|
+
changeId: string
|
|
118
|
+
patchset?: number
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const parseChangeInput = (input: string): ParsedChange => {
|
|
122
|
+
const trimmed = input.trim()
|
|
123
|
+
|
|
124
|
+
// 1. If it's a URL, extract change number and check for patchset
|
|
125
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
126
|
+
const changeId = extractChangeNumber(trimmed)
|
|
127
|
+
|
|
128
|
+
// Try to extract patchset from URL path: /c/project/+/12345/3
|
|
129
|
+
const patchsetMatch = trimmed.match(/\/(\d+)\/(\d+)(?:\/|$)/)
|
|
130
|
+
if (patchsetMatch?.[2]) {
|
|
131
|
+
return {
|
|
132
|
+
changeId: patchsetMatch[1],
|
|
133
|
+
patchset: parseInt(patchsetMatch[2], 10),
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { changeId }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 2. Check for change/patchset format: 12345/3
|
|
141
|
+
if (trimmed.includes('/') && !trimmed.startsWith('http')) {
|
|
142
|
+
const parts = trimmed.split('/')
|
|
143
|
+
if (parts.length === 2) {
|
|
144
|
+
const [changeId, patchsetStr] = parts
|
|
145
|
+
const patchset = parseInt(patchsetStr, 10)
|
|
146
|
+
|
|
147
|
+
if (!Number.isNaN(patchset) && patchset > 0) {
|
|
148
|
+
return { changeId, patchset }
|
|
149
|
+
}
|
|
150
|
+
// If patchset is invalid, just return the changeId part
|
|
151
|
+
return { changeId }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 3. Plain change number or Change-ID
|
|
156
|
+
return { changeId: trimmed }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Get git remotes
|
|
160
|
+
const getGitRemotes = (): Record<string, string> => {
|
|
161
|
+
try {
|
|
162
|
+
const output = execSync('git remote -v', { encoding: 'utf8', timeout: 5000 })
|
|
163
|
+
const remotes: Record<string, string> = {}
|
|
164
|
+
|
|
165
|
+
for (const line of output.split('\n')) {
|
|
166
|
+
const match = line.match(/^(\S+)\s+(\S+)\s+\(push\)$/)
|
|
167
|
+
if (match) {
|
|
168
|
+
remotes[match[1]] = match[2]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return remotes
|
|
173
|
+
} catch {
|
|
174
|
+
// Silently return empty object - remote detection is optional
|
|
175
|
+
return {}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Find remote matching Gerrit host
|
|
180
|
+
const findMatchingRemote = (gerritHost: string): string | null => {
|
|
181
|
+
const remotes = getGitRemotes()
|
|
182
|
+
|
|
183
|
+
// Parse gerrit host
|
|
184
|
+
const gerritUrl = new URL(gerritHost)
|
|
185
|
+
const gerritHostname = gerritUrl.hostname
|
|
186
|
+
|
|
187
|
+
// Check each remote
|
|
188
|
+
for (const [name, url] of Object.entries(remotes)) {
|
|
189
|
+
try {
|
|
190
|
+
let remoteHostname: string
|
|
191
|
+
|
|
192
|
+
if (url.startsWith('git@') || url.includes('://')) {
|
|
193
|
+
if (url.startsWith('git@')) {
|
|
194
|
+
// SSH format: git@hostname:project
|
|
195
|
+
remoteHostname = url.split('@')[1].split(':')[0]
|
|
196
|
+
} else {
|
|
197
|
+
// HTTP format
|
|
198
|
+
const remoteUrl = new URL(url)
|
|
199
|
+
remoteHostname = remoteUrl.hostname
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (remoteHostname === gerritHostname) {
|
|
203
|
+
return name
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// Ignore malformed URLs
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if we're in a git repo
|
|
215
|
+
const isInGitRepo = (): boolean => {
|
|
216
|
+
try {
|
|
217
|
+
execSync('git rev-parse --git-dir', { encoding: 'utf8', timeout: 5000 })
|
|
218
|
+
return true
|
|
219
|
+
} catch {
|
|
220
|
+
return false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Get current branch name
|
|
225
|
+
const getCurrentBranch = (): string | null => {
|
|
226
|
+
try {
|
|
227
|
+
const branch = execSync('git symbolic-ref --short HEAD', {
|
|
228
|
+
encoding: 'utf8',
|
|
229
|
+
timeout: 5000,
|
|
230
|
+
}).trim()
|
|
231
|
+
return branch || null
|
|
232
|
+
} catch {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check if a local branch exists (internal helper using Effect pattern)
|
|
238
|
+
const localBranchExists = (branchName: string): Effect.Effect<boolean, InvalidInputError> =>
|
|
239
|
+
validateGitSafe(branchName, 'branch name').pipe(
|
|
240
|
+
Effect.flatMap((validated) =>
|
|
241
|
+
Effect.sync(() => {
|
|
242
|
+
try {
|
|
243
|
+
execSync(`git rev-parse --verify ${validated}`, {
|
|
244
|
+
encoding: 'utf8',
|
|
245
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
246
|
+
timeout: 5000,
|
|
247
|
+
})
|
|
248
|
+
return true
|
|
249
|
+
} catch {
|
|
250
|
+
// Branch doesn't exist is expected, return false
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
export const checkoutCommand = (
|
|
258
|
+
changeInput: string,
|
|
259
|
+
options: CheckoutOptions,
|
|
260
|
+
): Effect.Effect<void, CheckoutErrors, ConfigServiceImpl | GerritApiServiceImpl> =>
|
|
261
|
+
Effect.gen(function* () {
|
|
262
|
+
// 1. Parse input
|
|
263
|
+
const parsed = parseChangeInput(changeInput)
|
|
264
|
+
|
|
265
|
+
// 2. Verify git repo
|
|
266
|
+
if (!isInGitRepo()) {
|
|
267
|
+
return yield* Effect.fail(new NotGitRepoError('Not in a git repository'))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 3. Get config and API service
|
|
271
|
+
const configService = yield* ConfigService
|
|
272
|
+
const apiService = yield* GerritApiService
|
|
273
|
+
const credentials = yield* configService.getCredentials
|
|
274
|
+
|
|
275
|
+
// 4. Get change details
|
|
276
|
+
const change = yield* apiService.getChange(parsed.changeId)
|
|
277
|
+
|
|
278
|
+
// 5. Get revision details - use from change if available, otherwise fetch separately
|
|
279
|
+
const revision = yield* Effect.gen(function* () {
|
|
280
|
+
// If requesting a specific patchset, always fetch it
|
|
281
|
+
if (parsed.patchset) {
|
|
282
|
+
const patchsetNum = parsed.patchset
|
|
283
|
+
if (patchsetNum === undefined) {
|
|
284
|
+
return yield* Effect.fail(
|
|
285
|
+
new InvalidInputError('Patchset number is required but was undefined'),
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
return yield* apiService
|
|
289
|
+
.getRevision(parsed.changeId, patchsetNum.toString())
|
|
290
|
+
.pipe(Effect.catchAll(() => Effect.fail(new PatchsetNotFoundError(patchsetNum))))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// For current revision, use it from change response if available
|
|
294
|
+
if (change.current_revision && change.revisions) {
|
|
295
|
+
const currentRevision = change.revisions[change.current_revision]
|
|
296
|
+
if (currentRevision) {
|
|
297
|
+
return currentRevision
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Fallback to fetching revision separately
|
|
302
|
+
return yield* apiService.getRevision(parsed.changeId, 'current')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// 6. Validate inputs before using in shell commands
|
|
306
|
+
const validatedRef = yield* validateGerritRef(revision.ref)
|
|
307
|
+
|
|
308
|
+
// 7. Find matching remote and validate
|
|
309
|
+
const rawRemote = options.remote || findMatchingRemote(credentials.host) || 'origin'
|
|
310
|
+
const remote = yield* validateGitSafe(rawRemote, 'remote')
|
|
311
|
+
|
|
312
|
+
// 8. Determine branch name and validate
|
|
313
|
+
const rawBranchName = `review/${change._number}`
|
|
314
|
+
const branchName = yield* validateGitSafe(rawBranchName, 'branch name')
|
|
315
|
+
const currentBranch = getCurrentBranch()
|
|
316
|
+
const branchExists = yield* localBranchExists(branchName)
|
|
317
|
+
|
|
318
|
+
// 9. Validate target branch for upstream tracking
|
|
319
|
+
const targetBranch = yield* validateGitSafe(change.branch, 'target branch')
|
|
320
|
+
|
|
321
|
+
// 10. Display information
|
|
322
|
+
yield* Console.log(chalk.bold('Checking out Gerrit change'))
|
|
323
|
+
yield* Console.log(` Change: ${change._number} - ${change.subject}`)
|
|
324
|
+
yield* Console.log(` Patchset: ${revision._number}`)
|
|
325
|
+
yield* Console.log(` Status: ${change.status}`)
|
|
326
|
+
yield* Console.log(` Branch: ${branchName}`)
|
|
327
|
+
yield* Console.log(` Remote: ${remote}`)
|
|
328
|
+
yield* Console.log('')
|
|
329
|
+
|
|
330
|
+
// 11. Fetch the change (using validated inputs)
|
|
331
|
+
yield* Console.log(chalk.cyan(`Fetching ${validatedRef}...`))
|
|
332
|
+
yield* Effect.try({
|
|
333
|
+
try: () => {
|
|
334
|
+
execSync(`git fetch ${remote} ${validatedRef}`, { stdio: 'inherit', timeout: 60000 })
|
|
335
|
+
},
|
|
336
|
+
catch: (e) => {
|
|
337
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
338
|
+
return new CheckoutError(`Failed to fetch change from remote: ${errorMsg}`)
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// 12. Checkout/update branch
|
|
343
|
+
if (options.detach) {
|
|
344
|
+
// Detached HEAD mode
|
|
345
|
+
yield* Effect.try({
|
|
346
|
+
try: () => {
|
|
347
|
+
execSync('git checkout FETCH_HEAD', { stdio: 'inherit', timeout: 30000 })
|
|
348
|
+
},
|
|
349
|
+
catch: (e) => {
|
|
350
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
351
|
+
return new CheckoutError(`Failed to checkout in detached HEAD mode: ${errorMsg}`)
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
yield* Console.log(chalk.green('Checked out in detached HEAD mode'))
|
|
355
|
+
} else {
|
|
356
|
+
// Named branch mode
|
|
357
|
+
if (branchExists) {
|
|
358
|
+
// Update existing branch
|
|
359
|
+
if (currentBranch !== branchName) {
|
|
360
|
+
yield* Effect.try({
|
|
361
|
+
try: () => {
|
|
362
|
+
execSync(`git checkout ${branchName}`, { stdio: 'inherit', timeout: 30000 })
|
|
363
|
+
},
|
|
364
|
+
catch: (e) => {
|
|
365
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
366
|
+
return new CheckoutError(`Failed to switch to branch: ${errorMsg}`)
|
|
367
|
+
},
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
yield* Effect.try({
|
|
371
|
+
try: () => {
|
|
372
|
+
execSync('git reset --hard FETCH_HEAD', { stdio: 'inherit', timeout: 30000 })
|
|
373
|
+
},
|
|
374
|
+
catch: (e) => {
|
|
375
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
376
|
+
return new CheckoutError(`Failed to update branch: ${errorMsg}`)
|
|
377
|
+
},
|
|
378
|
+
})
|
|
379
|
+
yield* Console.log(chalk.green(`Updated and checked out ${branchName}`))
|
|
380
|
+
} else {
|
|
381
|
+
// Create new branch
|
|
382
|
+
yield* Effect.try({
|
|
383
|
+
try: () => {
|
|
384
|
+
execSync(`git checkout -b ${branchName} FETCH_HEAD`, {
|
|
385
|
+
stdio: 'inherit',
|
|
386
|
+
timeout: 30000,
|
|
387
|
+
})
|
|
388
|
+
},
|
|
389
|
+
catch: (e) => {
|
|
390
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
391
|
+
return new CheckoutError(`Failed to create branch: ${errorMsg}`)
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
yield* Console.log(chalk.green(`Created and checked out ${branchName}`))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 13. Set upstream tracking
|
|
398
|
+
const upstreamRef = `${remote}/${targetBranch}`
|
|
399
|
+
yield* Effect.try({
|
|
400
|
+
try: () => {
|
|
401
|
+
execSync(`git branch --set-upstream-to=${upstreamRef} ${branchName}`, {
|
|
402
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
403
|
+
timeout: 10000,
|
|
404
|
+
})
|
|
405
|
+
},
|
|
406
|
+
catch: (e) => {
|
|
407
|
+
const errorMsg = e instanceof Error ? e.message : String(e)
|
|
408
|
+
return new CheckoutError(`Failed to set upstream tracking: ${errorMsg}`)
|
|
409
|
+
},
|
|
410
|
+
}).pipe(
|
|
411
|
+
Effect.flatMap(() => Console.log(`Tracking ${upstreamRef}`)),
|
|
412
|
+
Effect.catchAll(() =>
|
|
413
|
+
Console.log(chalk.yellow(`Note: Could not set upstream tracking to ${upstreamRef}`)),
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
yield* Console.log('')
|
|
419
|
+
yield* Console.log(
|
|
420
|
+
chalk.cyan(`Change URL: ${credentials.host}/c/${change.project}/+/${change._number}`),
|
|
421
|
+
)
|
|
422
|
+
})
|