@aaronshaf/ger 0.1.10 → 0.2.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/.github/workflows/claude-code-review.yml +61 -56
- package/.github/workflows/claude.yml +10 -24
- package/README.md +53 -6
- package/bun.lock +8 -8
- package/package.json +3 -3
- package/src/api/gerrit.ts +54 -16
- package/src/cli/commands/extract-url.ts +266 -0
- package/src/cli/commands/review.ts +13 -2
- package/src/cli/commands/setup.ts +1 -1
- package/src/cli/commands/show.ts +112 -18
- package/src/cli/index.ts +140 -23
- package/src/schemas/config.ts +13 -4
- package/src/services/config.test.ts +150 -0
- package/src/services/config.ts +60 -16
- package/src/services/git-worktree.ts +73 -16
- package/src/services/review-strategy.ts +40 -22
- package/src/utils/change-id.test.ts +98 -0
- package/src/utils/change-id.ts +63 -0
- package/src/utils/git-commit.test.ts +277 -0
- package/src/utils/git-commit.ts +122 -0
- package/tests/change-id-formats.test.ts +268 -0
- package/tests/extract-url.test.ts +518 -0
- package/tests/mocks/fetch-mock.ts +5 -2
- package/tests/mocks/msw-handlers.ts +3 -3
- package/tests/show-auto-detect.test.ts +306 -0
- package/tests/show.test.ts +157 -1
- package/tests/unit/git-worktree.test.ts +2 -1
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect'
|
|
2
|
+
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
3
|
+
import type { CommentInfo, MessageInfo } from '@/schemas/gerrit'
|
|
4
|
+
import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
|
|
5
|
+
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
6
|
+
|
|
7
|
+
// Schema for validating extract-url options
|
|
8
|
+
const ExtractUrlOptionsSchema: Schema.Schema<
|
|
9
|
+
{
|
|
10
|
+
readonly includeComments?: boolean
|
|
11
|
+
readonly regex?: boolean
|
|
12
|
+
readonly xml?: boolean
|
|
13
|
+
readonly json?: boolean
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
readonly includeComments?: boolean
|
|
17
|
+
readonly regex?: boolean
|
|
18
|
+
readonly xml?: boolean
|
|
19
|
+
readonly json?: boolean
|
|
20
|
+
}
|
|
21
|
+
> = Schema.Struct({
|
|
22
|
+
includeComments: Schema.optional(Schema.Boolean),
|
|
23
|
+
regex: Schema.optional(Schema.Boolean),
|
|
24
|
+
xml: Schema.optional(Schema.Boolean),
|
|
25
|
+
json: Schema.optional(Schema.Boolean),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export interface ExtractUrlOptions extends Schema.Schema.Type<typeof ExtractUrlOptionsSchema> {}
|
|
29
|
+
|
|
30
|
+
// Schema for validating pattern input
|
|
31
|
+
const PatternSchema: Schema.Schema<string, string> = Schema.String.pipe(
|
|
32
|
+
Schema.minLength(1, { message: () => 'Pattern cannot be empty' }),
|
|
33
|
+
Schema.maxLength(500, { message: () => 'Pattern is too long (max 500 characters)' }),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// URL matching regex - matches http:// and https:// URLs
|
|
37
|
+
const URL_REGEX = /https?:\/\/[^\s<>"{}|\\^`[\]]+/g
|
|
38
|
+
|
|
39
|
+
// Regex validation error class
|
|
40
|
+
export class RegexValidationError extends Error {
|
|
41
|
+
readonly _tag = 'RegexValidationError'
|
|
42
|
+
constructor(message: string) {
|
|
43
|
+
super(message)
|
|
44
|
+
this.name = 'RegexValidationError'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Safely create regex with validation and timeout protection
|
|
49
|
+
const createSafeRegex = (pattern: string): Effect.Effect<RegExp, RegexValidationError> =>
|
|
50
|
+
Effect.try({
|
|
51
|
+
try: () => {
|
|
52
|
+
// Validate regex complexity by checking for dangerous patterns
|
|
53
|
+
// These patterns check for nested quantifiers that can cause ReDoS
|
|
54
|
+
const dangerousPatterns = [
|
|
55
|
+
/\([^)]*[+*][^)]*\)[+*]/, // Nested quantifiers like (a+)+ or (a*)*
|
|
56
|
+
/\([^)]*[+*][^)]*\)[+*?]/, // Nested quantifiers with ? like (a+)+?
|
|
57
|
+
/\[[^\]]*\][+*]{2,}/, // Character class with multiple quantifiers like [a-z]++
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
for (const dangerous of dangerousPatterns) {
|
|
61
|
+
if (dangerous.test(pattern)) {
|
|
62
|
+
throw new RegexValidationError(
|
|
63
|
+
'Pattern contains potentially dangerous nested quantifiers that could cause performance issues',
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Try to create the regex - this will throw if syntax is invalid
|
|
69
|
+
return new RegExp(pattern)
|
|
70
|
+
},
|
|
71
|
+
catch: (error) => {
|
|
72
|
+
if (error instanceof RegexValidationError) {
|
|
73
|
+
return error
|
|
74
|
+
}
|
|
75
|
+
return new RegexValidationError(
|
|
76
|
+
`Invalid regular expression: ${error instanceof Error ? error.message : String(error)}`,
|
|
77
|
+
)
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const extractUrlsFromText = (
|
|
82
|
+
text: string,
|
|
83
|
+
pattern: string,
|
|
84
|
+
useRegex: boolean,
|
|
85
|
+
): Effect.Effect<readonly string[], RegexValidationError> =>
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
// First, find all URLs in the text
|
|
88
|
+
const urls = text.match(URL_REGEX) || []
|
|
89
|
+
|
|
90
|
+
// Filter URLs by pattern
|
|
91
|
+
if (useRegex) {
|
|
92
|
+
const regex = yield* createSafeRegex(pattern)
|
|
93
|
+
return urls.filter((url) => regex.test(url))
|
|
94
|
+
} else {
|
|
95
|
+
// Substring match (case-insensitive)
|
|
96
|
+
const lowerPattern = pattern.toLowerCase()
|
|
97
|
+
return urls.filter((url) => url.toLowerCase().includes(lowerPattern))
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const getCommentsAndMessages = (
|
|
102
|
+
changeId: string,
|
|
103
|
+
): Effect.Effect<
|
|
104
|
+
{ readonly comments: readonly CommentInfo[]; readonly messages: readonly MessageInfo[] },
|
|
105
|
+
ApiError,
|
|
106
|
+
GerritApiService
|
|
107
|
+
> =>
|
|
108
|
+
Effect.gen(function* () {
|
|
109
|
+
const gerritApi = yield* GerritApiService
|
|
110
|
+
|
|
111
|
+
// Get both inline comments and review messages concurrently
|
|
112
|
+
const [comments, messages] = yield* Effect.all(
|
|
113
|
+
[gerritApi.getComments(changeId), gerritApi.getMessages(changeId)],
|
|
114
|
+
{ concurrency: 'unbounded' },
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Flatten all inline comments from all files using functional patterns
|
|
118
|
+
const allComments = Object.entries(comments).flatMap(([path, fileComments]) =>
|
|
119
|
+
fileComments.map((comment) => ({
|
|
120
|
+
...comment,
|
|
121
|
+
path: path === '/COMMIT_MSG' ? 'Commit Message' : path,
|
|
122
|
+
})),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// Sort inline comments by date (ascending - oldest first)
|
|
126
|
+
const sortedComments = [...allComments].sort((a, b) => {
|
|
127
|
+
const dateA = a.updated ? new Date(a.updated).getTime() : 0
|
|
128
|
+
const dateB = b.updated ? new Date(b.updated).getTime() : 0
|
|
129
|
+
return dateA - dateB
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Sort messages by date (ascending - oldest first)
|
|
133
|
+
const sortedMessages = [...messages].sort((a, b) => {
|
|
134
|
+
const dateA = new Date(a.date).getTime()
|
|
135
|
+
const dateB = new Date(b.date).getTime()
|
|
136
|
+
return dateA - dateB
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return { comments: sortedComments, messages: sortedMessages }
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const extractUrlsFromChange = (
|
|
143
|
+
changeId: string,
|
|
144
|
+
pattern: string,
|
|
145
|
+
options: ExtractUrlOptions,
|
|
146
|
+
): Effect.Effect<readonly string[], ApiError | RegexValidationError, GerritApiService> =>
|
|
147
|
+
Effect.gen(function* () {
|
|
148
|
+
const { comments, messages } = yield* getCommentsAndMessages(changeId)
|
|
149
|
+
|
|
150
|
+
// Extract URLs from messages using functional patterns
|
|
151
|
+
const messageUrls = yield* Effect.all(
|
|
152
|
+
messages.map((message) =>
|
|
153
|
+
extractUrlsFromText(message.message, pattern, options.regex || false),
|
|
154
|
+
),
|
|
155
|
+
{ concurrency: 'unbounded' },
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Optionally extract URLs from comments
|
|
159
|
+
const commentUrls = options.includeComments
|
|
160
|
+
? yield* Effect.all(
|
|
161
|
+
comments
|
|
162
|
+
.filter((comment) => comment.message !== undefined)
|
|
163
|
+
.map((comment) =>
|
|
164
|
+
extractUrlsFromText(comment.message!, pattern, options.regex || false),
|
|
165
|
+
),
|
|
166
|
+
{ concurrency: 'unbounded' },
|
|
167
|
+
)
|
|
168
|
+
: []
|
|
169
|
+
|
|
170
|
+
// Flatten all URLs
|
|
171
|
+
return [...messageUrls.flat(), ...commentUrls.flat()]
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const formatUrlsPretty = (urls: readonly string[]): Effect.Effect<void> =>
|
|
175
|
+
Effect.sync(() => {
|
|
176
|
+
for (const url of urls) {
|
|
177
|
+
console.log(url)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const formatUrlsXml = (urls: readonly string[]): Effect.Effect<void> =>
|
|
182
|
+
Effect.sync(() => {
|
|
183
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
184
|
+
console.log(`<extract_url_result>`)
|
|
185
|
+
console.log(` <status>success</status>`)
|
|
186
|
+
console.log(` <urls>`)
|
|
187
|
+
console.log(` <count>${urls.length}</count>`)
|
|
188
|
+
for (const url of urls) {
|
|
189
|
+
console.log(` <url>${escapeXML(url)}</url>`)
|
|
190
|
+
}
|
|
191
|
+
console.log(` </urls>`)
|
|
192
|
+
console.log(`</extract_url_result>`)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const formatUrlsJson = (urls: readonly string[]): Effect.Effect<void> =>
|
|
196
|
+
Effect.sync(() => {
|
|
197
|
+
const output = {
|
|
198
|
+
status: 'success',
|
|
199
|
+
urls,
|
|
200
|
+
}
|
|
201
|
+
console.log(JSON.stringify(output, null, 2))
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
export const extractUrlCommand = (
|
|
205
|
+
pattern: string,
|
|
206
|
+
changeId: string | undefined,
|
|
207
|
+
options: ExtractUrlOptions,
|
|
208
|
+
): Effect.Effect<
|
|
209
|
+
void,
|
|
210
|
+
ApiError | Error | GitError | NoChangeIdError | RegexValidationError,
|
|
211
|
+
GerritApiService
|
|
212
|
+
> =>
|
|
213
|
+
Effect.gen(function* () {
|
|
214
|
+
// Validate inputs using Effect Schema
|
|
215
|
+
const validatedPattern = yield* Schema.decodeUnknown(PatternSchema)(pattern)
|
|
216
|
+
const validatedOptions = yield* Schema.decodeUnknown(ExtractUrlOptionsSchema)(options)
|
|
217
|
+
|
|
218
|
+
// Auto-detect Change-ID from HEAD commit if not provided
|
|
219
|
+
const resolvedChangeId = changeId || (yield* getChangeIdFromHead())
|
|
220
|
+
|
|
221
|
+
// Extract URLs
|
|
222
|
+
const urls = yield* extractUrlsFromChange(resolvedChangeId, validatedPattern, validatedOptions)
|
|
223
|
+
|
|
224
|
+
// Format output using Effect-wrapped functions
|
|
225
|
+
if (validatedOptions.json) {
|
|
226
|
+
yield* formatUrlsJson(urls)
|
|
227
|
+
} else if (validatedOptions.xml) {
|
|
228
|
+
yield* formatUrlsXml(urls)
|
|
229
|
+
} else {
|
|
230
|
+
yield* formatUrlsPretty(urls)
|
|
231
|
+
}
|
|
232
|
+
}).pipe(
|
|
233
|
+
// Regional error boundary for the entire command
|
|
234
|
+
Effect.catchAll((error) =>
|
|
235
|
+
Effect.sync(() => {
|
|
236
|
+
const errorMessage =
|
|
237
|
+
error instanceof GitError ||
|
|
238
|
+
error instanceof NoChangeIdError ||
|
|
239
|
+
error instanceof RegexValidationError ||
|
|
240
|
+
error instanceof Error
|
|
241
|
+
? error.message
|
|
242
|
+
: String(error)
|
|
243
|
+
|
|
244
|
+
if (options.json) {
|
|
245
|
+
console.log(
|
|
246
|
+
JSON.stringify(
|
|
247
|
+
{
|
|
248
|
+
status: 'error',
|
|
249
|
+
error: errorMessage,
|
|
250
|
+
},
|
|
251
|
+
null,
|
|
252
|
+
2,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
} else if (options.xml) {
|
|
256
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
257
|
+
console.log(`<extract_url_result>`)
|
|
258
|
+
console.log(` <status>error</status>`)
|
|
259
|
+
console.log(` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
|
|
260
|
+
console.log(`</extract_url_result>`)
|
|
261
|
+
} else {
|
|
262
|
+
console.error(`✗ Error: ${errorMessage}`)
|
|
263
|
+
}
|
|
264
|
+
}),
|
|
265
|
+
),
|
|
266
|
+
)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Effect, pipe, Schema, Layer } from 'effect'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ReviewStrategyService,
|
|
4
|
+
type ReviewStrategy,
|
|
5
|
+
ReviewStrategyError,
|
|
6
|
+
} from '@/services/review-strategy'
|
|
3
7
|
import { commentCommandWithInput } from './comment'
|
|
4
8
|
import { Console } from 'effect'
|
|
5
9
|
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
@@ -314,7 +318,14 @@ const promptUser = (message: string): Effect.Effect<boolean, never> =>
|
|
|
314
318
|
})
|
|
315
319
|
})
|
|
316
320
|
|
|
317
|
-
export const reviewCommand = (
|
|
321
|
+
export const reviewCommand = (
|
|
322
|
+
changeId: string,
|
|
323
|
+
options: ReviewOptions = {},
|
|
324
|
+
): Effect.Effect<
|
|
325
|
+
void,
|
|
326
|
+
Error | ReviewStrategyError,
|
|
327
|
+
GerritApiService | ReviewStrategyService | GitWorktreeService
|
|
328
|
+
> =>
|
|
318
329
|
Effect.gen(function* () {
|
|
319
330
|
const reviewStrategy = yield* ReviewStrategyService
|
|
320
331
|
const gitService = yield* GitWorktreeService
|
|
@@ -270,7 +270,7 @@ const setupEffect = (configService: ConfigServiceImpl) =>
|
|
|
270
270
|
),
|
|
271
271
|
)
|
|
272
272
|
|
|
273
|
-
export async function setup() {
|
|
273
|
+
export async function setup(): Promise<void> {
|
|
274
274
|
const program = pipe(
|
|
275
275
|
ConfigService,
|
|
276
276
|
Effect.flatMap((configService) => setupEffect(configService)),
|
package/src/cli/commands/show.ts
CHANGED
|
@@ -6,10 +6,11 @@ import { getDiffContext } from '@/utils/diff-context'
|
|
|
6
6
|
import { formatDiffPretty } from '@/utils/diff-formatters'
|
|
7
7
|
import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
|
|
8
8
|
import { formatDate } from '@/utils/formatters'
|
|
9
|
-
import {
|
|
9
|
+
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
10
10
|
|
|
11
11
|
interface ShowOptions {
|
|
12
12
|
xml?: boolean
|
|
13
|
+
json?: boolean
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
interface ChangeDetails {
|
|
@@ -86,15 +87,19 @@ const getCommentsAndMessagesForChange = (
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
// Sort inline comments by
|
|
90
|
+
// Sort inline comments by date (ascending - oldest first)
|
|
90
91
|
allComments.sort((a, b) => {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
return
|
|
92
|
+
const dateA = a.updated ? new Date(a.updated).getTime() : 0
|
|
93
|
+
const dateB = b.updated ? new Date(b.updated).getTime() : 0
|
|
94
|
+
return dateA - dateB
|
|
94
95
|
})
|
|
95
96
|
|
|
96
|
-
// Sort messages by date (
|
|
97
|
-
const sortedMessages =
|
|
97
|
+
// Sort messages by date (ascending - oldest first)
|
|
98
|
+
const sortedMessages = [...messages].sort((a, b) => {
|
|
99
|
+
const dateA = new Date(a.date).getTime()
|
|
100
|
+
const dateB = new Date(b.date).getTime()
|
|
101
|
+
return dateA - dateB
|
|
102
|
+
})
|
|
98
103
|
|
|
99
104
|
return { comments: allComments, messages: sortedMessages }
|
|
100
105
|
})
|
|
@@ -172,6 +177,74 @@ const formatShowPretty = (
|
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
|
|
180
|
+
// Helper to remove undefined values from objects
|
|
181
|
+
const removeUndefined = <T extends Record<string, any>>(obj: T): Partial<T> => {
|
|
182
|
+
return Object.fromEntries(
|
|
183
|
+
Object.entries(obj).filter(([_, value]) => value !== undefined),
|
|
184
|
+
) as Partial<T>
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const formatShowJson = (
|
|
188
|
+
changeDetails: ChangeDetails,
|
|
189
|
+
diff: string,
|
|
190
|
+
commentsWithContext: Array<{ comment: CommentInfo; context?: any }>,
|
|
191
|
+
messages: MessageInfo[],
|
|
192
|
+
): void => {
|
|
193
|
+
const output = {
|
|
194
|
+
status: 'success',
|
|
195
|
+
change: removeUndefined({
|
|
196
|
+
id: changeDetails.id,
|
|
197
|
+
number: changeDetails.number,
|
|
198
|
+
subject: changeDetails.subject,
|
|
199
|
+
status: changeDetails.status,
|
|
200
|
+
project: changeDetails.project,
|
|
201
|
+
branch: changeDetails.branch,
|
|
202
|
+
owner: removeUndefined(changeDetails.owner),
|
|
203
|
+
created: changeDetails.created,
|
|
204
|
+
updated: changeDetails.updated,
|
|
205
|
+
}),
|
|
206
|
+
diff,
|
|
207
|
+
comments: commentsWithContext.map(({ comment, context }) =>
|
|
208
|
+
removeUndefined({
|
|
209
|
+
id: comment.id,
|
|
210
|
+
path: comment.path,
|
|
211
|
+
line: comment.line,
|
|
212
|
+
range: comment.range,
|
|
213
|
+
author: comment.author
|
|
214
|
+
? removeUndefined({
|
|
215
|
+
name: comment.author.name,
|
|
216
|
+
email: comment.author.email,
|
|
217
|
+
account_id: comment.author._account_id,
|
|
218
|
+
})
|
|
219
|
+
: undefined,
|
|
220
|
+
updated: comment.updated,
|
|
221
|
+
message: comment.message,
|
|
222
|
+
unresolved: comment.unresolved,
|
|
223
|
+
in_reply_to: comment.in_reply_to,
|
|
224
|
+
context,
|
|
225
|
+
}),
|
|
226
|
+
),
|
|
227
|
+
messages: messages.map((message) =>
|
|
228
|
+
removeUndefined({
|
|
229
|
+
id: message.id,
|
|
230
|
+
author: message.author
|
|
231
|
+
? removeUndefined({
|
|
232
|
+
name: message.author.name,
|
|
233
|
+
email: message.author.email,
|
|
234
|
+
account_id: message.author._account_id,
|
|
235
|
+
})
|
|
236
|
+
: undefined,
|
|
237
|
+
date: message.date,
|
|
238
|
+
message: message.message,
|
|
239
|
+
revision: message._revision_number,
|
|
240
|
+
tag: message.tag,
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(JSON.stringify(output, null, 2))
|
|
246
|
+
}
|
|
247
|
+
|
|
175
248
|
const formatShowXml = (
|
|
176
249
|
changeDetails: ChangeDetails,
|
|
177
250
|
diff: string,
|
|
@@ -248,16 +321,19 @@ const formatShowXml = (
|
|
|
248
321
|
}
|
|
249
322
|
|
|
250
323
|
export const showCommand = (
|
|
251
|
-
changeId: string,
|
|
324
|
+
changeId: string | undefined,
|
|
252
325
|
options: ShowOptions,
|
|
253
|
-
): Effect.Effect<void, ApiError | Error, GerritApiService> =>
|
|
326
|
+
): Effect.Effect<void, ApiError | Error | GitError | NoChangeIdError, GerritApiService> =>
|
|
254
327
|
Effect.gen(function* () {
|
|
328
|
+
// Auto-detect Change-ID from HEAD commit if not provided
|
|
329
|
+
const resolvedChangeId = changeId || (yield* getChangeIdFromHead())
|
|
330
|
+
|
|
255
331
|
// Fetch all data concurrently
|
|
256
332
|
const [changeDetails, diff, commentsAndMessages] = yield* Effect.all(
|
|
257
333
|
[
|
|
258
|
-
getChangeDetails(
|
|
259
|
-
getDiffForChange(
|
|
260
|
-
getCommentsAndMessagesForChange(
|
|
334
|
+
getChangeDetails(resolvedChangeId),
|
|
335
|
+
getDiffForChange(resolvedChangeId),
|
|
336
|
+
getCommentsAndMessagesForChange(resolvedChangeId),
|
|
261
337
|
],
|
|
262
338
|
{ concurrency: 'unbounded' },
|
|
263
339
|
)
|
|
@@ -267,7 +343,7 @@ export const showCommand = (
|
|
|
267
343
|
// Get context for each comment using concurrent requests
|
|
268
344
|
const contextEffects = comments.map((comment) =>
|
|
269
345
|
comment.path && comment.line
|
|
270
|
-
? getDiffContext(
|
|
346
|
+
? getDiffContext(resolvedChangeId, comment.path, comment.line).pipe(
|
|
271
347
|
Effect.map((context) => ({ comment, context })),
|
|
272
348
|
// Graceful degradation for diff fetch failures
|
|
273
349
|
Effect.catchAll(() => Effect.succeed({ comment, context: undefined })),
|
|
@@ -281,22 +357,40 @@ export const showCommand = (
|
|
|
281
357
|
})
|
|
282
358
|
|
|
283
359
|
// Format output
|
|
284
|
-
if (options.
|
|
360
|
+
if (options.json) {
|
|
361
|
+
formatShowJson(changeDetails, diff, commentsWithContext, messages)
|
|
362
|
+
} else if (options.xml) {
|
|
285
363
|
formatShowXml(changeDetails, diff, commentsWithContext, messages)
|
|
286
364
|
} else {
|
|
287
365
|
formatShowPretty(changeDetails, diff, commentsWithContext, messages)
|
|
288
366
|
}
|
|
289
367
|
}).pipe(
|
|
290
368
|
// Regional error boundary for the entire command
|
|
291
|
-
Effect.
|
|
292
|
-
|
|
369
|
+
Effect.catchAll((error) => {
|
|
370
|
+
const errorMessage =
|
|
371
|
+
error instanceof GitError || error instanceof NoChangeIdError || error instanceof Error
|
|
372
|
+
? error.message
|
|
373
|
+
: String(error)
|
|
374
|
+
|
|
375
|
+
if (options.json) {
|
|
376
|
+
console.log(
|
|
377
|
+
JSON.stringify(
|
|
378
|
+
{
|
|
379
|
+
status: 'error',
|
|
380
|
+
error: errorMessage,
|
|
381
|
+
},
|
|
382
|
+
null,
|
|
383
|
+
2,
|
|
384
|
+
),
|
|
385
|
+
)
|
|
386
|
+
} else if (options.xml) {
|
|
293
387
|
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
294
388
|
console.log(`<show_result>`)
|
|
295
389
|
console.log(` <status>error</status>`)
|
|
296
|
-
console.log(` <error><![CDATA[${
|
|
390
|
+
console.log(` <error><![CDATA[${sanitizeCDATA(errorMessage)}]]></error>`)
|
|
297
391
|
console.log(`</show_result>`)
|
|
298
392
|
} else {
|
|
299
|
-
console.error(`✗
|
|
393
|
+
console.error(`✗ Error: ${errorMessage}`)
|
|
300
394
|
}
|
|
301
395
|
return Effect.succeed(undefined)
|
|
302
396
|
}),
|