critique 0.1.109 → 0.1.116
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 +2 -1
- package/src/ansi-html.ts +167 -107
- package/src/balance-delimiters.test.ts +54 -0
- package/src/balance-delimiters.ts +4 -2
- package/src/cli.tsx +51 -14
- package/src/components/diff-view.test.tsx +93 -6
- package/src/components/diff-view.tsx +63 -4
- package/src/diff-utils.test.ts +93 -1
- package/src/diff-utils.ts +70 -8
- package/src/filter-submodule.e2e.test.ts +139 -0
- package/src/web-utils.test.ts +152 -0
- package/src/web-utils.tsx +142 -6
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "critique",
|
|
3
3
|
"module": "src/diff.tsx",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.116",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
8
8
|
"bin": "./src/cli.tsx",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"marked": "^17.0.1",
|
|
46
46
|
"picocolors": "^1.1.1",
|
|
47
47
|
"react": "^19.2.0",
|
|
48
|
+
"string-dedent": "^3.0.2",
|
|
48
49
|
"strip-ansi": "^7.1.2",
|
|
49
50
|
"supports-color": "^10.2.2",
|
|
50
51
|
"zustand": "^5.0.8"
|
package/src/ansi-html.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { TextAttributes, rgbToHex, type RGBA } from "@opentuah/core"
|
|
6
6
|
import type { CapturedFrame, CapturedLine, CapturedSpan } from "@opentuah/core"
|
|
7
|
+
import dedent from "string-dedent"
|
|
8
|
+
|
|
9
|
+
// Alias for syntax highlighting in editors (tagged template behaves identically)
|
|
10
|
+
const html = dedent
|
|
7
11
|
|
|
8
12
|
export interface ToHtmlOptions {
|
|
9
13
|
/** Background color for the container */
|
|
@@ -20,6 +24,14 @@ export interface ToHtmlOptions {
|
|
|
20
24
|
title?: string
|
|
21
25
|
/** OG image URL for social media previews */
|
|
22
26
|
ogImageUrl?: string
|
|
27
|
+
/** Custom line renderer - wraps or replaces the default <div class="line"> output per line.
|
|
28
|
+
* Generic hook: receives the default HTML, the captured line data, and the 0-based line index.
|
|
29
|
+
* Return a replacement HTML string. If not provided, the default <div class="line"> is used. */
|
|
30
|
+
renderLine?: (defaultHtml: string, line: CapturedLine, lineIndex: number) => string
|
|
31
|
+
/** Extra CSS injected into the document style block */
|
|
32
|
+
extraCss?: string
|
|
33
|
+
/** Extra JS injected as a separate script block before </body> */
|
|
34
|
+
extraJs?: string
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
/**
|
|
@@ -128,11 +140,14 @@ export function frameToHtml(frame: CapturedFrame, options: ToHtmlOptions = {}):
|
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
// Render each line as a div
|
|
131
|
-
const htmlLines = lines.map((line) => {
|
|
143
|
+
const htmlLines = lines.map((line, lineIndex) => {
|
|
132
144
|
const content = lineToHtml(line)
|
|
133
145
|
// Use a div for each line to ensure proper line breaks
|
|
134
146
|
// Empty lines get a span with nbsp for consistent flex behavior
|
|
135
|
-
|
|
147
|
+
const defaultHtml = `<div class="line">${content || "<span> </span>"}</div>`
|
|
148
|
+
return options.renderLine
|
|
149
|
+
? options.renderLine(defaultHtml, line, lineIndex)
|
|
150
|
+
: defaultHtml
|
|
136
151
|
})
|
|
137
152
|
|
|
138
153
|
return htmlLines.join("\n")
|
|
@@ -152,115 +167,160 @@ export function frameToHtmlDocument(frame: CapturedFrame, options: ToHtmlOptions
|
|
|
152
167
|
} = options
|
|
153
168
|
|
|
154
169
|
const cols = frame.cols
|
|
155
|
-
|
|
156
170
|
const content = frameToHtml(frame, options)
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<meta property="og:
|
|
161
|
-
<meta property="og:
|
|
162
|
-
<meta property="og:image" content="
|
|
163
|
-
<meta property="og:image:
|
|
164
|
-
<meta
|
|
165
|
-
<meta name="twitter:
|
|
166
|
-
<meta name="twitter:
|
|
167
|
-
|
|
172
|
+
const ogTags = options.ogImageUrl ? '\n' + html`
|
|
173
|
+
<meta property="og:title" content="${escapeHtml(title)}">
|
|
174
|
+
<meta property="og:type" content="website">
|
|
175
|
+
<meta property="og:image" content="${escapeHtml(options.ogImageUrl)}">
|
|
176
|
+
<meta property="og:image:width" content="1200">
|
|
177
|
+
<meta property="og:image:height" content="630">
|
|
178
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
179
|
+
<meta name="twitter:title" content="${escapeHtml(title)}">
|
|
180
|
+
<meta name="twitter:image" content="${escapeHtml(options.ogImageUrl)}">
|
|
181
|
+
` : ''
|
|
168
182
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<link rel="icon" href="/favicon-dark.png" media="(prefers-color-scheme: dark)">
|
|
175
|
-
<link rel="icon" href="/favicon-light.png" media="(prefers-color-scheme: light)">
|
|
176
|
-
<link rel="icon" href="/favicon-dark.png">${ogTags}
|
|
177
|
-
<style>
|
|
178
|
-
@font-face {
|
|
179
|
-
font-family: 'JetBrains Mono Nerd';
|
|
180
|
-
src: url('https://critique.work/jetbrains-mono-nerd.woff2') format('woff2');
|
|
181
|
-
font-weight: normal;
|
|
182
|
-
font-style: normal;
|
|
183
|
-
font-display: swap;
|
|
184
|
-
}
|
|
185
|
-
</style>
|
|
186
|
-
<title>${escapeHtml(title)}</title>
|
|
187
|
-
<style>
|
|
188
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
189
|
-
html {
|
|
190
|
-
-webkit-text-size-adjust: 100%;
|
|
191
|
-
text-size-adjust: 100%;
|
|
192
|
-
}
|
|
193
|
-
html, body {
|
|
194
|
-
min-height: 100%;
|
|
195
|
-
background-color: ${backgroundColor};
|
|
196
|
-
color: ${textColor};
|
|
197
|
-
font-family: ${fontFamily};
|
|
198
|
-
/*
|
|
199
|
-
* Font size scales to fit ${cols} columns within viewport.
|
|
200
|
-
* Formula: (viewport - padding) / (cols * char-ratio)
|
|
201
|
-
*
|
|
202
|
-
* The 0.6 char-ratio is the approximate width of 1ch relative to font-size
|
|
203
|
-
* in monospace fonts. Most monospace fonts (JetBrains Mono, Fira Code,
|
|
204
|
-
* Monaco, Consolas) have a ch/font-size ratio between 0.55-0.6.
|
|
205
|
-
* We use 0.6 as a safe upper bound to prevent overflow.
|
|
206
|
-
*/
|
|
207
|
-
font-size: clamp(4px, calc((100vw - 32px) / (${cols} * 0.6)), 14px);
|
|
208
|
-
line-height: 1.7;
|
|
209
|
-
}
|
|
210
|
-
body {
|
|
211
|
-
padding: 16px;
|
|
212
|
-
overflow-x: clip;
|
|
213
|
-
overflow-y: auto;
|
|
214
|
-
max-width: 100vw;
|
|
215
|
-
}
|
|
216
|
-
#content {
|
|
217
|
-
width: fit-content;
|
|
218
|
-
margin: 0 auto;
|
|
219
|
-
}
|
|
220
|
-
.line {
|
|
221
|
-
white-space: pre;
|
|
222
|
-
display: block;
|
|
223
|
-
content-visibility: auto;
|
|
224
|
-
contain-intrinsic-block-size: auto round(down, 1.7em, 1px);
|
|
225
|
-
background-color: ${backgroundColor};
|
|
226
|
-
transform: translateZ(0);
|
|
227
|
-
backface-visibility: hidden;
|
|
228
|
-
}
|
|
229
|
-
.line span {
|
|
230
|
-
white-space: pre;
|
|
231
|
-
display: inline-block;
|
|
232
|
-
line-height: 1.7;
|
|
233
|
-
vertical-align: top;
|
|
234
|
-
}
|
|
235
|
-
/* Disable content-visibility on iOS Safari where it can cause rendering issues */
|
|
236
|
-
@supports (-webkit-touch-callout: none) {
|
|
237
|
-
.line {
|
|
238
|
-
content-visibility: visible;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
${options.autoTheme ? `@media (prefers-color-scheme: light) {
|
|
242
|
-
html {
|
|
243
|
-
filter: invert(1) hue-rotate(180deg);
|
|
244
|
-
}
|
|
245
|
-
}` : ''}\nhtml{scrollbar-width:thin;scrollbar-color:#6b7280 #2d3748;}@media(prefers-color-scheme:light){html{scrollbar-color:#a0aec0 #edf2f7;}}::-webkit-scrollbar{width:12px;}::-webkit-scrollbar-track{background:#2d3748;}::-webkit-scrollbar-thumb{background:#6b7280;border-radius:6px;}::-webkit-scrollbar-thumb:hover{background:#a0aec0;}@media(prefers-color-scheme:light){::-webkit-scrollbar-track{background:#edf2f7;}::-webkit-scrollbar-thumb{background:#a0aec0;}::-webkit-scrollbar-thumb:hover{background:#cbd5e1;}}::-webkit-scrollbar {\n width: 12px;\n}\n::-webkit-scrollbar-track {\n background: #2d3748;\n}\n::-webkit-scrollbar-thumb {\n background: #6b7280;\n border-radius: 6px;\n}\n::-webkit-scrollbar-thumb:hover {\n background: #a0aec0;\n}\n@media (prefers-color-scheme: light) {\n ::-webkit-scrollbar-track {\n background: #edf2f7;\n }\n ::-webkit-scrollbar-thumb {\n background: #a0aec0;\n }\n ::-webkit-scrollbar-thumb:hover {\n background: #cbd5e1;\n }\n}\n</style>\n</head>\n<body>
|
|
246
|
-
<div id="content">
|
|
247
|
-
${content}
|
|
248
|
-
</div>
|
|
249
|
-
<script>
|
|
250
|
-
// Redirect mobile devices to ?v=mobile for optimized view
|
|
251
|
-
(function() {
|
|
252
|
-
const params = new URLSearchParams(window.location.search);
|
|
253
|
-
if (!params.has('v')) {
|
|
254
|
-
const isMobile = /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|Opera M(obi|ini)|Windows Phone|webOS/i.test(navigator.userAgent);
|
|
255
|
-
if (isMobile) {
|
|
256
|
-
params.set('v', 'mobile');
|
|
257
|
-
window.location.replace(window.location.pathname + '?' + params.toString());
|
|
183
|
+
const autoThemeCss = options.autoTheme ? '\n' + html`
|
|
184
|
+
@media (prefers-color-scheme: light) {
|
|
185
|
+
html {
|
|
186
|
+
filter: invert(1) hue-rotate(180deg);
|
|
187
|
+
}
|
|
258
188
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
</
|
|
263
|
-
|
|
189
|
+
` : ''
|
|
190
|
+
|
|
191
|
+
const extraJsBlock = options.extraJs
|
|
192
|
+
? `\n<script>\n${options.extraJs}\n</script>`
|
|
193
|
+
: ''
|
|
194
|
+
|
|
195
|
+
return html`
|
|
196
|
+
<!DOCTYPE html>
|
|
197
|
+
<html>
|
|
198
|
+
<head>
|
|
199
|
+
<meta charset="utf-8">
|
|
200
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
201
|
+
<link rel="icon" href="/favicon-dark.png" media="(prefers-color-scheme: dark)">
|
|
202
|
+
<link rel="icon" href="/favicon-light.png" media="(prefers-color-scheme: light)">
|
|
203
|
+
<link rel="icon" href="/favicon-dark.png">${ogTags}
|
|
204
|
+
<style>
|
|
205
|
+
@font-face {
|
|
206
|
+
font-family: 'JetBrains Mono Nerd';
|
|
207
|
+
src: url('https://critique.work/jetbrains-mono-nerd.woff2') format('woff2');
|
|
208
|
+
font-weight: normal;
|
|
209
|
+
font-style: normal;
|
|
210
|
+
font-display: swap;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
213
|
+
<title>${escapeHtml(title)}</title>
|
|
214
|
+
<style>
|
|
215
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
216
|
+
html {
|
|
217
|
+
-webkit-text-size-adjust: 100%;
|
|
218
|
+
text-size-adjust: 100%;
|
|
219
|
+
}
|
|
220
|
+
html, body {
|
|
221
|
+
min-height: 100%;
|
|
222
|
+
background-color: ${backgroundColor};
|
|
223
|
+
color: ${textColor};
|
|
224
|
+
font-family: ${fontFamily};
|
|
225
|
+
/*
|
|
226
|
+
* Font size scales to fit ${cols} columns within viewport.
|
|
227
|
+
* Formula: (viewport - padding) / (cols * char-ratio)
|
|
228
|
+
*
|
|
229
|
+
* The 0.6 char-ratio is the approximate width of 1ch relative to font-size
|
|
230
|
+
* in monospace fonts. Most monospace fonts (JetBrains Mono, Fira Code,
|
|
231
|
+
* Monaco, Consolas) have a ch/font-size ratio between 0.55-0.6.
|
|
232
|
+
* We use 0.6 as a safe upper bound to prevent overflow.
|
|
233
|
+
*/
|
|
234
|
+
font-size: clamp(4px, calc((100vw - 32px) / (${cols} * 0.6)), 14px);
|
|
235
|
+
line-height: 1.7;
|
|
236
|
+
}
|
|
237
|
+
body {
|
|
238
|
+
padding: 16px;
|
|
239
|
+
overflow-x: clip;
|
|
240
|
+
overflow-y: auto;
|
|
241
|
+
max-width: 100vw;
|
|
242
|
+
}
|
|
243
|
+
#content {
|
|
244
|
+
width: fit-content;
|
|
245
|
+
margin: 0 auto;
|
|
246
|
+
}
|
|
247
|
+
.line {
|
|
248
|
+
white-space: pre;
|
|
249
|
+
display: block;
|
|
250
|
+
content-visibility: auto;
|
|
251
|
+
contain-intrinsic-block-size: auto round(down, 1.7em, 1px);
|
|
252
|
+
background-color: ${backgroundColor};
|
|
253
|
+
transform: translateZ(0);
|
|
254
|
+
backface-visibility: hidden;
|
|
255
|
+
}
|
|
256
|
+
.line span {
|
|
257
|
+
white-space: pre;
|
|
258
|
+
display: inline-block;
|
|
259
|
+
line-height: 1.7;
|
|
260
|
+
vertical-align: top;
|
|
261
|
+
}
|
|
262
|
+
/* Disable content-visibility on iOS Safari where it can cause rendering issues */
|
|
263
|
+
@supports (-webkit-touch-callout: none) {
|
|
264
|
+
.line {
|
|
265
|
+
content-visibility: visible;
|
|
266
|
+
}
|
|
267
|
+
}${autoThemeCss}
|
|
268
|
+
html {
|
|
269
|
+
scrollbar-width: thin;
|
|
270
|
+
scrollbar-color: #6b7280 #2d3748;
|
|
271
|
+
}
|
|
272
|
+
@media (prefers-color-scheme: light) {
|
|
273
|
+
html {
|
|
274
|
+
scrollbar-color: #a0aec0 #edf2f7;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
::-webkit-scrollbar {
|
|
278
|
+
width: 12px;
|
|
279
|
+
}
|
|
280
|
+
::-webkit-scrollbar-track {
|
|
281
|
+
background: #2d3748;
|
|
282
|
+
}
|
|
283
|
+
::-webkit-scrollbar-thumb {
|
|
284
|
+
background: #6b7280;
|
|
285
|
+
border-radius: 6px;
|
|
286
|
+
}
|
|
287
|
+
::-webkit-scrollbar-thumb:hover {
|
|
288
|
+
background: #a0aec0;
|
|
289
|
+
}
|
|
290
|
+
@media (prefers-color-scheme: light) {
|
|
291
|
+
::-webkit-scrollbar-track {
|
|
292
|
+
background: #edf2f7;
|
|
293
|
+
}
|
|
294
|
+
::-webkit-scrollbar-thumb {
|
|
295
|
+
background: #a0aec0;
|
|
296
|
+
}
|
|
297
|
+
::-webkit-scrollbar-thumb:hover {
|
|
298
|
+
background: #cbd5e1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
${options.extraCss || ''}
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
<div id="content">
|
|
306
|
+
${content}
|
|
307
|
+
</div>
|
|
308
|
+
<script>
|
|
309
|
+
// Redirect mobile devices to ?v=mobile for optimized view
|
|
310
|
+
(function() {
|
|
311
|
+
const params = new URLSearchParams(window.location.search);
|
|
312
|
+
if (!params.has('v')) {
|
|
313
|
+
const isMobile = /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|Opera M(obi|ini)|Windows Phone|webOS/i.test(navigator.userAgent);
|
|
314
|
+
if (isMobile) {
|
|
315
|
+
params.set('v', 'mobile');
|
|
316
|
+
window.location.replace(window.location.pathname + '?' + params.toString() + window.location.hash);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
})();
|
|
320
|
+
</script>${extraJsBlock}
|
|
321
|
+
</body>
|
|
322
|
+
</html>
|
|
323
|
+
`
|
|
264
324
|
}
|
|
265
325
|
|
|
266
326
|
export type { CapturedFrame, CapturedLine, CapturedSpan }
|
|
@@ -99,6 +99,20 @@ describe("countDelimiter", () => {
|
|
|
99
99
|
expect(countDelimiter("x = 'hello'\ny = 'world'", "'''")).toBe(0)
|
|
100
100
|
})
|
|
101
101
|
})
|
|
102
|
+
|
|
103
|
+
describe("triple backticks (Markdown)", () => {
|
|
104
|
+
it("counts fenced code block markers", () => {
|
|
105
|
+
expect(countDelimiter("```ts\nconst x = 1\n```", "```")).toBe(2)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("counts single fence marker (unclosed code block)", () => {
|
|
109
|
+
expect(countDelimiter("still inside fence\n```", "```")).toBe(1)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("returns 0 for plain markdown without fences", () => {
|
|
113
|
+
expect(countDelimiter("# Title\n\nSome text with `inline` code", "```")).toBe(0)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
102
116
|
})
|
|
103
117
|
|
|
104
118
|
// ============================================================================
|
|
@@ -334,6 +348,46 @@ describe("balanceDelimiters", () => {
|
|
|
334
348
|
})
|
|
335
349
|
})
|
|
336
350
|
|
|
351
|
+
describe("markdown", () => {
|
|
352
|
+
const mdPatch = (hunkLines: string[]) => [
|
|
353
|
+
"--- file.md",
|
|
354
|
+
"+++ file.md",
|
|
355
|
+
"@@ -10,4 +10,4 @@",
|
|
356
|
+
...hunkLines,
|
|
357
|
+
].join("\n")
|
|
358
|
+
|
|
359
|
+
it("returns patch unchanged when code fences are balanced", () => {
|
|
360
|
+
const patch = mdPatch([
|
|
361
|
+
" ```ts",
|
|
362
|
+
" const x = 1",
|
|
363
|
+
" ```",
|
|
364
|
+
"+New paragraph",
|
|
365
|
+
])
|
|
366
|
+
expect(balanceDelimiters(patch, "markdown")).toBe(patch)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it("prepends balancing code fence when count is odd", () => {
|
|
370
|
+
const patch = mdPatch([
|
|
371
|
+
" inside fenced block",
|
|
372
|
+
" ```",
|
|
373
|
+
"-old line",
|
|
374
|
+
"+new line",
|
|
375
|
+
])
|
|
376
|
+
const result = balanceDelimiters(patch, "markdown")
|
|
377
|
+
const lines = result.split("\n")
|
|
378
|
+
expect(lines[3]).toBe(" ```inside fenced block")
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it("does not modify when only inline code backticks are present", () => {
|
|
382
|
+
const patch = mdPatch([
|
|
383
|
+
" This has `inline` code",
|
|
384
|
+
"-old",
|
|
385
|
+
"+new",
|
|
386
|
+
])
|
|
387
|
+
expect(balanceDelimiters(patch, "markdown")).toBe(patch)
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
337
391
|
describe("scala", () => {
|
|
338
392
|
const scalaPatch = (hunkLines: string[]) => [
|
|
339
393
|
"--- file.scala",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Delimiter balancing for syntax highlighting in diff hunks.
|
|
2
2
|
//
|
|
3
3
|
// When a diff hunk starts inside a paired delimiter (template literal,
|
|
4
|
-
// triple-quoted string, etc.), tree-sitter sees an
|
|
5
|
-
// delimiter and misparses everything after the first
|
|
4
|
+
// triple-quoted string, fenced code block, etc.), tree-sitter sees an
|
|
5
|
+
// odd number of that delimiter and misparses everything after the first
|
|
6
|
+
// occurrence.
|
|
6
7
|
//
|
|
7
8
|
// Two-pass fix:
|
|
8
9
|
// 1. Tokenizer: count delimiter occurrences in each hunk's content,
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
const LANGUAGE_DELIMITERS: Record<string, string[]> = {
|
|
28
29
|
typescript: ["`"],
|
|
29
30
|
python: ['"""', "'''"],
|
|
31
|
+
markdown: ["```"],
|
|
30
32
|
go: ["`"],
|
|
31
33
|
scala: ['"""'],
|
|
32
34
|
swift: ['"""'],
|
package/src/cli.tsx
CHANGED
|
@@ -43,6 +43,7 @@ import { logger } from "./logger.ts";
|
|
|
43
43
|
import { saveStoredLicenseKey } from "./license.ts";
|
|
44
44
|
import {
|
|
45
45
|
buildGitCommand,
|
|
46
|
+
filterParsedFilesByPatterns,
|
|
46
47
|
getFileName,
|
|
47
48
|
getFileStatus,
|
|
48
49
|
getOldFileName,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
parseGitDiffFiles,
|
|
55
56
|
getDirtySubmodulePaths,
|
|
56
57
|
buildSubmoduleDiffCommand,
|
|
58
|
+
getFilterPatterns,
|
|
57
59
|
IGNORED_FILES,
|
|
58
60
|
type ParsedFile,
|
|
59
61
|
type GitCommandOptions,
|
|
@@ -136,7 +138,9 @@ async function runReviewMode(
|
|
|
136
138
|
if (reviewOptions?.isDefaultMode) {
|
|
137
139
|
const dirtySubmodules = getDirtySubmodulePaths();
|
|
138
140
|
if (dirtySubmodules.length > 0) {
|
|
139
|
-
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules,
|
|
141
|
+
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
|
|
142
|
+
context: reviewOptions.diffOptions?.context,
|
|
143
|
+
});
|
|
140
144
|
try {
|
|
141
145
|
const { stdout: subDiff } = await execAsync(subCmd, { encoding: "utf-8" });
|
|
142
146
|
if (subDiff.trim()) {
|
|
@@ -146,6 +150,8 @@ async function runReviewMode(
|
|
|
146
150
|
// Submodule diff failed — skip
|
|
147
151
|
}
|
|
148
152
|
}
|
|
153
|
+
|
|
154
|
+
fullDiff = await filterCombinedDiffByPatterns(fullDiff, reviewOptions.diffOptions || {});
|
|
149
155
|
}
|
|
150
156
|
const gitDiffResult = fullDiff;
|
|
151
157
|
|
|
@@ -1404,6 +1410,22 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
|
|
|
1404
1410
|
|
|
1405
1411
|
const execAsync = promisify(exec);
|
|
1406
1412
|
|
|
1413
|
+
async function filterCombinedDiffByPatterns(
|
|
1414
|
+
diffContent: string,
|
|
1415
|
+
options: Pick<GitCommandOptions, "filter" | "positionalFilters">,
|
|
1416
|
+
): Promise<string> {
|
|
1417
|
+
if (!diffContent.trim()) return diffContent;
|
|
1418
|
+
if (getFilterPatterns(options).length === 0) return diffContent;
|
|
1419
|
+
|
|
1420
|
+
const { parsePatch, formatPatch } = await import("diff");
|
|
1421
|
+
const parsedFiles = parseGitDiffFiles(stripSubmoduleHeaders(diffContent), parsePatch);
|
|
1422
|
+
const filteredFiles = filterParsedFilesByPatterns(parsedFiles, options);
|
|
1423
|
+
|
|
1424
|
+
if (filteredFiles.length === 0) return "";
|
|
1425
|
+
|
|
1426
|
+
return filteredFiles.map((file) => formatPatch(file)).join("\n");
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1407
1429
|
function formatPreviewExpiry(expiresInDays?: number | null): string {
|
|
1408
1430
|
if (expiresInDays === null) {
|
|
1409
1431
|
return "(never expires)";
|
|
@@ -1822,9 +1844,7 @@ cli
|
|
|
1822
1844
|
if (!options.staged) {
|
|
1823
1845
|
const dirtySubmodules = getDirtySubmodulePaths();
|
|
1824
1846
|
if (dirtySubmodules.length > 0) {
|
|
1825
|
-
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
|
|
1826
|
-
filter: options.filter,
|
|
1827
|
-
});
|
|
1847
|
+
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {});
|
|
1828
1848
|
try {
|
|
1829
1849
|
const { stdout: subDiff } = await execAsync(subCmd, { encoding: "utf-8" });
|
|
1830
1850
|
if (subDiff.trim()) {
|
|
@@ -1834,6 +1854,11 @@ cli
|
|
|
1834
1854
|
// Submodule diff failed — skip
|
|
1835
1855
|
}
|
|
1836
1856
|
}
|
|
1857
|
+
|
|
1858
|
+
fullHunksDiff = await filterCombinedDiffByPatterns(fullHunksDiff, {
|
|
1859
|
+
filter: options.filter,
|
|
1860
|
+
positionalFilters: options['--'],
|
|
1861
|
+
});
|
|
1837
1862
|
}
|
|
1838
1863
|
|
|
1839
1864
|
if (!fullHunksDiff.trim()) {
|
|
@@ -1920,9 +1945,7 @@ cli
|
|
|
1920
1945
|
let fullHunksDiff = gitDiff;
|
|
1921
1946
|
const dirtySubmodules = getDirtySubmodulePaths();
|
|
1922
1947
|
if (dirtySubmodules.length > 0) {
|
|
1923
|
-
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
|
|
1924
|
-
filter: filename,
|
|
1925
|
-
});
|
|
1948
|
+
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {});
|
|
1926
1949
|
try {
|
|
1927
1950
|
const { stdout: subDiff } = await execAsync(subCmd, {
|
|
1928
1951
|
encoding: "utf-8",
|
|
@@ -1935,6 +1958,10 @@ cli
|
|
|
1935
1958
|
}
|
|
1936
1959
|
}
|
|
1937
1960
|
|
|
1961
|
+
fullHunksDiff = await filterCombinedDiffByPatterns(fullHunksDiff, {
|
|
1962
|
+
filter: filename,
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1938
1965
|
if (!fullHunksDiff.trim()) {
|
|
1939
1966
|
console.error(`No unstaged changes in file: ${filename}`);
|
|
1940
1967
|
process.exit(1);
|
|
@@ -2050,8 +2077,6 @@ cli
|
|
|
2050
2077
|
if (dirtySubmodules.length > 0) {
|
|
2051
2078
|
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
|
|
2052
2079
|
context: options.context,
|
|
2053
|
-
filter: options.filter,
|
|
2054
|
-
positionalFilters: options['--'],
|
|
2055
2080
|
});
|
|
2056
2081
|
try {
|
|
2057
2082
|
const { stdout: subDiff } = await execAsync(subCmd, {
|
|
@@ -2064,6 +2089,11 @@ cli
|
|
|
2064
2089
|
// Submodule diff failed (e.g. submodule not initialized) — skip
|
|
2065
2090
|
}
|
|
2066
2091
|
}
|
|
2092
|
+
|
|
2093
|
+
diffContent = await filterCombinedDiffByPatterns(diffContent, {
|
|
2094
|
+
filter: options.filter,
|
|
2095
|
+
positionalFilters: options['--'],
|
|
2096
|
+
});
|
|
2067
2097
|
}
|
|
2068
2098
|
}
|
|
2069
2099
|
|
|
@@ -2190,8 +2220,6 @@ cli
|
|
|
2190
2220
|
if (dirtySubmodules.length > 0) {
|
|
2191
2221
|
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
|
|
2192
2222
|
context: options.context,
|
|
2193
|
-
filter: options.filter,
|
|
2194
|
-
positionalFilters: options['--'],
|
|
2195
2223
|
});
|
|
2196
2224
|
try {
|
|
2197
2225
|
const { stdout: subDiff } = await execAsync(subCmd, {
|
|
@@ -2212,7 +2240,13 @@ cli
|
|
|
2212
2240
|
}
|
|
2213
2241
|
|
|
2214
2242
|
const files = parseGitDiffFiles(stripSubmoduleHeaders(fullDiff), parsePatch);
|
|
2215
|
-
const
|
|
2243
|
+
const filteredFiles = isDefaultMode
|
|
2244
|
+
? filterParsedFilesByPatterns(files, {
|
|
2245
|
+
filter: options.filter,
|
|
2246
|
+
positionalFilters: options['--'],
|
|
2247
|
+
})
|
|
2248
|
+
: files;
|
|
2249
|
+
const processedFiles = processFiles(filteredFiles, formatPatch);
|
|
2216
2250
|
setParsedFiles(processedFiles);
|
|
2217
2251
|
} catch (error) {
|
|
2218
2252
|
setParsedFiles([]);
|
|
@@ -2680,8 +2714,6 @@ cli
|
|
|
2680
2714
|
if (dirtySubmodules.length > 0) {
|
|
2681
2715
|
const subCmd = buildSubmoduleDiffCommand(dirtySubmodules, {
|
|
2682
2716
|
context: options.context,
|
|
2683
|
-
filter: options.filter,
|
|
2684
|
-
positionalFilters: options['--'],
|
|
2685
2717
|
});
|
|
2686
2718
|
try {
|
|
2687
2719
|
const { stdout: subDiff } = await execAsync(subCmd, { encoding: "utf-8" });
|
|
@@ -2692,6 +2724,11 @@ cli
|
|
|
2692
2724
|
// Submodule diff failed — skip
|
|
2693
2725
|
}
|
|
2694
2726
|
}
|
|
2727
|
+
|
|
2728
|
+
fullWebDiff = await filterCombinedDiffByPatterns(fullWebDiff, {
|
|
2729
|
+
filter: options.filter,
|
|
2730
|
+
positionalFilters: options['--'],
|
|
2731
|
+
});
|
|
2695
2732
|
}
|
|
2696
2733
|
|
|
2697
2734
|
if (!fullWebDiff.trim()) {
|