@brimveyn/aimux 1.5.4 → 1.6.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/README.md +14 -2
- package/package.json +5 -3
- package/src/app-runtime/side-effects.ts +41 -6
- package/src/app.tsx +17 -5
- package/src/config.ts +29 -4
- package/src/diff-parser/clean-last-newline.ts +5 -0
- package/src/diff-parser/constants.ts +13 -0
- package/src/diff-parser/index.ts +12 -0
- package/src/diff-parser/parse-line-type.ts +29 -0
- package/src/diff-parser/parse-patch-files.ts +369 -0
- package/src/diff-parser/types.ts +75 -0
- package/src/git/git-diff.ts +24 -12
- package/src/git/git-poller.ts +11 -3
- package/src/git/git-status.ts +98 -17
- package/src/input/modes/bridge.ts +8 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +4 -1
- package/src/pty/terminal-snapshot.ts +5 -5
- package/src/state/git-tree.ts +220 -0
- package/src/state/reducers/git-mode-state.ts +276 -36
- package/src/state/reducers/git-panel-state.ts +35 -4
- package/src/state/reducers/modal-state.ts +43 -10
- package/src/state/store.ts +5 -1
- package/src/state/types.ts +46 -6
- package/src/state/workspace-save.ts +2 -0
- package/src/ui/components/create-session-modal.tsx +25 -8
- package/src/ui/components/diff-renderer/build-rows.ts +349 -0
- package/src/ui/components/diff-renderer/filetype.ts +29 -0
- package/src/ui/components/diff-renderer/fold-strip.tsx +86 -0
- package/src/ui/components/diff-renderer/highlight.ts +48 -0
- package/src/ui/components/diff-renderer/index.ts +1 -0
- package/src/ui/components/diff-renderer/pierre-diff.tsx +171 -0
- package/src/ui/components/diff-renderer/split-view.tsx +211 -0
- package/src/ui/components/diff-renderer/stacked-view.tsx +168 -0
- package/src/ui/components/git-commit-modal.tsx +16 -3
- package/src/ui/components/git-pane-widget.tsx +6 -1
- package/src/ui/components/git-panel.tsx +215 -86
- package/src/ui/components/git-view.tsx +103 -90
- package/src/ui/components/help-modal.tsx +45 -12
- package/src/ui/components/input-field.tsx +4 -3
- package/src/ui/components/list-item.tsx +11 -2
- package/src/ui/components/modal-filter-bar.tsx +3 -2
- package/src/ui/components/modal-keybinds-overlay.tsx +4 -3
- package/src/ui/components/modal-shell.tsx +5 -4
- package/src/ui/components/new-tab-modal.tsx +17 -4
- package/src/ui/components/pending-chord-overlay.tsx +5 -4
- package/src/ui/components/session-bar.tsx +13 -6
- package/src/ui/components/session-picker-modal.tsx +28 -10
- package/src/ui/components/sidebar.tsx +26 -11
- package/src/ui/components/snippet-editor-modal.tsx +18 -3
- package/src/ui/components/snippet-picker-modal.tsx +13 -4
- package/src/ui/components/split-layout.tsx +3 -2
- package/src/ui/components/status-bar.tsx +15 -10
- package/src/ui/components/surface.tsx +6 -6
- package/src/ui/components/tab-item.tsx +28 -17
- package/src/ui/components/terminal-pane.tsx +28 -16
- package/src/ui/components/theme-picker-modal.tsx +113 -17
- package/src/ui/components/update-available-modal.tsx +13 -2
- package/src/ui/filter-themes.ts +15 -0
- package/src/ui/keymap-context.ts +10 -2
- package/src/ui/root.tsx +29 -7
- package/src/ui/shiki.ts +49 -0
- package/src/ui/status-bar-model.ts +32 -4
- package/src/ui/theme-store.ts +36 -0
- package/src/ui/theme.ts +4 -17
- package/src/ui/themes.ts +23 -277
- package/src/ui/syntax.ts +0 -102
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// Vendored from @pierre/diffs v1.1.15 — parser only. See AIMUX-14.
|
|
2
|
+
/* eslint-disable eqeqeq, no-console, typescript/strict-boolean-expressions, no-else-return, unicorn/no-array-for-each */
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ChangeContent,
|
|
6
|
+
ContextContent,
|
|
7
|
+
FileContents,
|
|
8
|
+
FileDiffMetadata,
|
|
9
|
+
HunkContent,
|
|
10
|
+
ParsedPatch,
|
|
11
|
+
} from './types'
|
|
12
|
+
|
|
13
|
+
import { cleanLastNewline } from './clean-last-newline'
|
|
14
|
+
import {
|
|
15
|
+
ALTERNATE_FILE_NAMES_GIT,
|
|
16
|
+
COMMIT_METADATA_SPLIT,
|
|
17
|
+
FILE_CONTEXT_BLOB,
|
|
18
|
+
FILENAME_HEADER_REGEX,
|
|
19
|
+
FILENAME_HEADER_REGEX_GIT,
|
|
20
|
+
GIT_DIFF_FILE_BREAK_REGEX,
|
|
21
|
+
HUNK_HEADER,
|
|
22
|
+
INDEX_LINE_METADATA,
|
|
23
|
+
SPLIT_WITH_NEWLINES,
|
|
24
|
+
UNIFIED_DIFF_FILE_BREAK_REGEX,
|
|
25
|
+
} from './constants'
|
|
26
|
+
import { parseLineType } from './parse-line-type'
|
|
27
|
+
|
|
28
|
+
interface ProcessFileOptions {
|
|
29
|
+
cacheKey?: string
|
|
30
|
+
isGitDiff?: boolean
|
|
31
|
+
oldFile?: FileContents
|
|
32
|
+
newFile?: FileContents
|
|
33
|
+
throwOnError?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createContentGroup(
|
|
37
|
+
type: 'change' | 'context',
|
|
38
|
+
deletionLineIndex: number,
|
|
39
|
+
additionLineIndex: number
|
|
40
|
+
): HunkContent {
|
|
41
|
+
if (type === 'change') {
|
|
42
|
+
return { additionLineIndex, additions: 0, deletionLineIndex, deletions: 0, type: 'change' }
|
|
43
|
+
}
|
|
44
|
+
return { additionLineIndex, deletionLineIndex, lines: 0, type: 'context' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function processFile(
|
|
48
|
+
fileDiffString: string,
|
|
49
|
+
{
|
|
50
|
+
cacheKey,
|
|
51
|
+
isGitDiff = GIT_DIFF_FILE_BREAK_REGEX.test(fileDiffString),
|
|
52
|
+
newFile,
|
|
53
|
+
oldFile,
|
|
54
|
+
throwOnError = false,
|
|
55
|
+
}: ProcessFileOptions = {}
|
|
56
|
+
): FileDiffMetadata | undefined {
|
|
57
|
+
let lastHunkEnd = 0
|
|
58
|
+
const hunks = fileDiffString.split(FILE_CONTEXT_BLOB)
|
|
59
|
+
let currentFile: FileDiffMetadata | undefined
|
|
60
|
+
const isPartial = oldFile == null || newFile == null
|
|
61
|
+
let deletionLineIndex = 0
|
|
62
|
+
let additionLineIndex = 0
|
|
63
|
+
for (const hunk of hunks) {
|
|
64
|
+
const lines = hunk.split(SPLIT_WITH_NEWLINES)
|
|
65
|
+
const firstLine = lines.shift()
|
|
66
|
+
if (firstLine == null) {
|
|
67
|
+
if (throwOnError) throw Error('parsePatchContent: invalid hunk')
|
|
68
|
+
else console.error('parsePatchContent: invalid hunk', hunk)
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
const fileHeaderMatch = firstLine.match(HUNK_HEADER)
|
|
72
|
+
let additionLines = 0
|
|
73
|
+
let deletionLines = 0
|
|
74
|
+
if (fileHeaderMatch == null || currentFile == null) {
|
|
75
|
+
if (currentFile != null) {
|
|
76
|
+
if (throwOnError) throw Error('parsePatchContent: Invalid hunk')
|
|
77
|
+
else console.error('parsePatchContent: Invalid hunk', hunk)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
currentFile = {
|
|
81
|
+
additionLines:
|
|
82
|
+
!isPartial && oldFile != null && newFile != null
|
|
83
|
+
? newFile.contents.split(SPLIT_WITH_NEWLINES)
|
|
84
|
+
: [],
|
|
85
|
+
cacheKey,
|
|
86
|
+
deletionLines:
|
|
87
|
+
!isPartial && oldFile != null && newFile != null
|
|
88
|
+
? oldFile.contents.split(SPLIT_WITH_NEWLINES)
|
|
89
|
+
: [],
|
|
90
|
+
hunks: [],
|
|
91
|
+
isPartial,
|
|
92
|
+
name: '',
|
|
93
|
+
splitLineCount: 0,
|
|
94
|
+
type: 'change',
|
|
95
|
+
unifiedLineCount: 0,
|
|
96
|
+
}
|
|
97
|
+
if (currentFile.additionLines.length === 1 && newFile?.contents === '') {
|
|
98
|
+
currentFile.additionLines.length = 0
|
|
99
|
+
}
|
|
100
|
+
if (currentFile.deletionLines.length === 1 && oldFile?.contents === '') {
|
|
101
|
+
currentFile.deletionLines.length = 0
|
|
102
|
+
}
|
|
103
|
+
lines.unshift(firstLine)
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const filenameMatch = line.match(
|
|
106
|
+
isGitDiff ? FILENAME_HEADER_REGEX_GIT : FILENAME_HEADER_REGEX
|
|
107
|
+
)
|
|
108
|
+
if (line.startsWith('diff --git')) {
|
|
109
|
+
const match = line.trim().match(ALTERNATE_FILE_NAMES_GIT)
|
|
110
|
+
if (match) {
|
|
111
|
+
const prevName = match[1] ?? match[2]
|
|
112
|
+
const name = match[3] ?? match[4]
|
|
113
|
+
if (name != null) {
|
|
114
|
+
currentFile.name = name.trim()
|
|
115
|
+
if (prevName != null && prevName !== name) currentFile.prevName = prevName.trim()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} else if (filenameMatch != null) {
|
|
119
|
+
const [, type, fileName] = filenameMatch
|
|
120
|
+
if (fileName != null && fileName !== '/dev/null') {
|
|
121
|
+
if (type === '---') {
|
|
122
|
+
currentFile.prevName = fileName.trim()
|
|
123
|
+
currentFile.name = fileName.trim()
|
|
124
|
+
} else if (type === '+++') {
|
|
125
|
+
currentFile.name = fileName.trim()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else if (isGitDiff) {
|
|
129
|
+
if (line.startsWith('new mode ')) currentFile.mode = line.replace('new mode', '').trim()
|
|
130
|
+
if (line.startsWith('old mode ')) {
|
|
131
|
+
currentFile.prevMode = line.replace('old mode', '').trim()
|
|
132
|
+
}
|
|
133
|
+
if (line.startsWith('new file mode')) {
|
|
134
|
+
currentFile.type = 'new'
|
|
135
|
+
currentFile.mode = line.replace('new file mode', '').trim()
|
|
136
|
+
}
|
|
137
|
+
if (line.startsWith('deleted file mode')) {
|
|
138
|
+
currentFile.type = 'deleted'
|
|
139
|
+
currentFile.mode = line.replace('deleted file mode', '').trim()
|
|
140
|
+
}
|
|
141
|
+
if (line.startsWith('similarity index')) {
|
|
142
|
+
if (line.startsWith('similarity index 100%')) currentFile.type = 'rename-pure'
|
|
143
|
+
else currentFile.type = 'rename-changed'
|
|
144
|
+
}
|
|
145
|
+
if (line.startsWith('index ')) {
|
|
146
|
+
const [, prevObjectId, newObjectId, mode] = line.trim().match(INDEX_LINE_METADATA) ?? []
|
|
147
|
+
if (prevObjectId != null) currentFile.prevObjectId = prevObjectId
|
|
148
|
+
if (newObjectId != null) currentFile.newObjectId = newObjectId
|
|
149
|
+
if (mode != null) currentFile.mode = mode
|
|
150
|
+
}
|
|
151
|
+
if (line.startsWith('rename from ')) {
|
|
152
|
+
currentFile.prevName = line.replace('rename from ', '').trim()
|
|
153
|
+
}
|
|
154
|
+
if (line.startsWith('rename to ')) {
|
|
155
|
+
currentFile.name = line.replace('rename to ', '').trim()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
let currentContent: HunkContent | undefined
|
|
162
|
+
let lastLineType: 'addition' | 'deletion' | 'context' | undefined
|
|
163
|
+
while (
|
|
164
|
+
lines.length > 0 &&
|
|
165
|
+
(lines[lines.length - 1] === '\n' ||
|
|
166
|
+
lines[lines.length - 1] === '\r' ||
|
|
167
|
+
lines[lines.length - 1] === '\r\n' ||
|
|
168
|
+
lines[lines.length - 1] === '')
|
|
169
|
+
) {
|
|
170
|
+
lines.pop()
|
|
171
|
+
}
|
|
172
|
+
const additionStart = parseInt(fileHeaderMatch[3] ?? '')
|
|
173
|
+
const deletionStart = parseInt(fileHeaderMatch[1] ?? '')
|
|
174
|
+
deletionLineIndex = isPartial ? deletionLineIndex : deletionStart - 1
|
|
175
|
+
additionLineIndex = isPartial ? additionLineIndex : additionStart - 1
|
|
176
|
+
const hunkData = {
|
|
177
|
+
additionCount: parseInt(fileHeaderMatch[4] ?? '1'),
|
|
178
|
+
additionLineIndex,
|
|
179
|
+
additionLines,
|
|
180
|
+
additionStart,
|
|
181
|
+
collapsedBefore: 0,
|
|
182
|
+
deletionCount: parseInt(fileHeaderMatch[2] ?? '1'),
|
|
183
|
+
deletionLineIndex,
|
|
184
|
+
deletionLines,
|
|
185
|
+
deletionStart,
|
|
186
|
+
hunkContent: [] as HunkContent[],
|
|
187
|
+
hunkContext: fileHeaderMatch[5],
|
|
188
|
+
hunkSpecs: firstLine,
|
|
189
|
+
noEOFCRAdditions: false,
|
|
190
|
+
noEOFCRDeletions: false,
|
|
191
|
+
splitLineCount: 0,
|
|
192
|
+
splitLineStart: 0,
|
|
193
|
+
unifiedLineCount: 0,
|
|
194
|
+
unifiedLineStart: 0,
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
isNaN(hunkData.additionCount) ||
|
|
198
|
+
isNaN(hunkData.deletionCount) ||
|
|
199
|
+
isNaN(hunkData.additionStart) ||
|
|
200
|
+
isNaN(hunkData.deletionStart)
|
|
201
|
+
) {
|
|
202
|
+
if (throwOnError) throw Error('parsePatchContent: invalid hunk metadata')
|
|
203
|
+
else console.error('parsePatchContent: invalid hunk metadata', hunkData)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
for (const rawLine of lines) {
|
|
207
|
+
const parsedLine = parseLineType(rawLine)
|
|
208
|
+
if (parsedLine == null) {
|
|
209
|
+
console.error('processFile: invalid rawLine:', rawLine)
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
const { line, type } = parsedLine
|
|
213
|
+
if (type === 'addition') {
|
|
214
|
+
if (currentContent == null || currentContent.type !== 'change') {
|
|
215
|
+
currentContent = createContentGroup('change', deletionLineIndex, additionLineIndex)
|
|
216
|
+
hunkData.hunkContent.push(currentContent)
|
|
217
|
+
}
|
|
218
|
+
additionLineIndex++
|
|
219
|
+
if (isPartial) currentFile.additionLines.push(line)
|
|
220
|
+
;(currentContent as ChangeContent).additions++
|
|
221
|
+
additionLines++
|
|
222
|
+
lastLineType = 'addition'
|
|
223
|
+
} else if (type === 'deletion') {
|
|
224
|
+
if (currentContent == null || currentContent.type !== 'change') {
|
|
225
|
+
currentContent = createContentGroup('change', deletionLineIndex, additionLineIndex)
|
|
226
|
+
hunkData.hunkContent.push(currentContent)
|
|
227
|
+
}
|
|
228
|
+
deletionLineIndex++
|
|
229
|
+
if (isPartial) currentFile.deletionLines.push(line)
|
|
230
|
+
;(currentContent as ChangeContent).deletions++
|
|
231
|
+
deletionLines++
|
|
232
|
+
lastLineType = 'deletion'
|
|
233
|
+
} else if (type === 'context') {
|
|
234
|
+
if (currentContent == null || currentContent.type !== 'context') {
|
|
235
|
+
currentContent = createContentGroup('context', deletionLineIndex, additionLineIndex)
|
|
236
|
+
hunkData.hunkContent.push(currentContent)
|
|
237
|
+
}
|
|
238
|
+
additionLineIndex++
|
|
239
|
+
deletionLineIndex++
|
|
240
|
+
if (isPartial) {
|
|
241
|
+
currentFile.deletionLines.push(line)
|
|
242
|
+
currentFile.additionLines.push(line)
|
|
243
|
+
}
|
|
244
|
+
;(currentContent as ContextContent).lines++
|
|
245
|
+
lastLineType = 'context'
|
|
246
|
+
} else if (type === 'metadata' && currentContent != null) {
|
|
247
|
+
if (currentContent.type === 'context') {
|
|
248
|
+
hunkData.noEOFCRAdditions = true
|
|
249
|
+
hunkData.noEOFCRDeletions = true
|
|
250
|
+
} else if (lastLineType === 'deletion') hunkData.noEOFCRDeletions = true
|
|
251
|
+
else if (lastLineType === 'addition') hunkData.noEOFCRAdditions = true
|
|
252
|
+
if (isPartial && (lastLineType === 'addition' || lastLineType === 'context')) {
|
|
253
|
+
const lastIndex = currentFile.additionLines.length - 1
|
|
254
|
+
const last = currentFile.additionLines[lastIndex]
|
|
255
|
+
if (lastIndex >= 0 && last != null) {
|
|
256
|
+
currentFile.additionLines[lastIndex] = cleanLastNewline(last)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (isPartial && (lastLineType === 'deletion' || lastLineType === 'context')) {
|
|
260
|
+
const lastIndex = currentFile.deletionLines.length - 1
|
|
261
|
+
const last = currentFile.deletionLines[lastIndex]
|
|
262
|
+
if (lastIndex >= 0 && last != null) {
|
|
263
|
+
currentFile.deletionLines[lastIndex] = cleanLastNewline(last)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
hunkData.additionLines = additionLines
|
|
269
|
+
hunkData.deletionLines = deletionLines
|
|
270
|
+
hunkData.collapsedBefore = Math.max(hunkData.additionStart - 1 - lastHunkEnd, 0)
|
|
271
|
+
currentFile.hunks.push(hunkData)
|
|
272
|
+
lastHunkEnd = hunkData.additionStart + hunkData.additionCount - 1
|
|
273
|
+
for (const content of hunkData.hunkContent) {
|
|
274
|
+
if (content.type === 'context') {
|
|
275
|
+
hunkData.splitLineCount += content.lines
|
|
276
|
+
hunkData.unifiedLineCount += content.lines
|
|
277
|
+
} else {
|
|
278
|
+
hunkData.splitLineCount += Math.max(content.additions, content.deletions)
|
|
279
|
+
hunkData.unifiedLineCount += content.deletions + content.additions
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
hunkData.splitLineStart = currentFile.splitLineCount + hunkData.collapsedBefore
|
|
283
|
+
hunkData.unifiedLineStart = currentFile.unifiedLineCount + hunkData.collapsedBefore
|
|
284
|
+
currentFile.splitLineCount += hunkData.collapsedBefore + hunkData.splitLineCount
|
|
285
|
+
currentFile.unifiedLineCount += hunkData.collapsedBefore + hunkData.unifiedLineCount
|
|
286
|
+
}
|
|
287
|
+
if (currentFile == null) return
|
|
288
|
+
const lastHunk = currentFile.hunks[currentFile.hunks.length - 1]
|
|
289
|
+
if (
|
|
290
|
+
lastHunk != null &&
|
|
291
|
+
!isPartial &&
|
|
292
|
+
currentFile.additionLines.length > 0 &&
|
|
293
|
+
currentFile.deletionLines.length > 0
|
|
294
|
+
) {
|
|
295
|
+
const lastHunkEnd$1 = lastHunk.additionStart + lastHunk.additionCount - 1
|
|
296
|
+
const totalFileLines = currentFile.additionLines.length
|
|
297
|
+
const collapsedAfter = Math.max(totalFileLines - lastHunkEnd$1, 0)
|
|
298
|
+
currentFile.splitLineCount += collapsedAfter
|
|
299
|
+
currentFile.unifiedLineCount += collapsedAfter
|
|
300
|
+
}
|
|
301
|
+
if (!isGitDiff) {
|
|
302
|
+
if (currentFile.prevName != null && currentFile.name !== currentFile.prevName) {
|
|
303
|
+
if (currentFile.hunks.length > 0) currentFile.type = 'rename-changed'
|
|
304
|
+
else currentFile.type = 'rename-pure'
|
|
305
|
+
} else if (newFile != null && newFile.contents === '') currentFile.type = 'deleted'
|
|
306
|
+
else if (oldFile != null && oldFile.contents === '') currentFile.type = 'new'
|
|
307
|
+
}
|
|
308
|
+
if (currentFile.type !== 'rename-pure' && currentFile.type !== 'rename-changed') {
|
|
309
|
+
currentFile.prevName = undefined
|
|
310
|
+
}
|
|
311
|
+
return currentFile
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function processPatch(
|
|
315
|
+
data: string,
|
|
316
|
+
cacheKeyPrefix?: string,
|
|
317
|
+
throwOnError = false
|
|
318
|
+
): ParsedPatch {
|
|
319
|
+
const isGitDiff = GIT_DIFF_FILE_BREAK_REGEX.test(data)
|
|
320
|
+
const rawFiles = data.split(isGitDiff ? GIT_DIFF_FILE_BREAK_REGEX : UNIFIED_DIFF_FILE_BREAK_REGEX)
|
|
321
|
+
let patchMetadata: string | undefined
|
|
322
|
+
const files: FileDiffMetadata[] = []
|
|
323
|
+
for (const fileOrPatchMetadata of rawFiles) {
|
|
324
|
+
if (isGitDiff && !GIT_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
|
|
325
|
+
if (patchMetadata == null) patchMetadata = fileOrPatchMetadata
|
|
326
|
+
else if (throwOnError) throw Error('parsePatchContent: unknown file blob')
|
|
327
|
+
else console.error('parsePatchContent: unknown file blob:', fileOrPatchMetadata)
|
|
328
|
+
continue
|
|
329
|
+
} else if (!isGitDiff && !UNIFIED_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
|
|
330
|
+
if (patchMetadata == null) patchMetadata = fileOrPatchMetadata
|
|
331
|
+
else if (throwOnError) throw Error('parsePatchContent: unknown file blob')
|
|
332
|
+
else console.error('parsePatchContent: unknown file blob:', fileOrPatchMetadata)
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
const currentFile = processFile(fileOrPatchMetadata, {
|
|
336
|
+
cacheKey: cacheKeyPrefix != null ? `${cacheKeyPrefix}-${files.length}` : undefined,
|
|
337
|
+
isGitDiff,
|
|
338
|
+
throwOnError,
|
|
339
|
+
})
|
|
340
|
+
if (currentFile != null) files.push(currentFile)
|
|
341
|
+
}
|
|
342
|
+
return { files, patchMetadata }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Parses a patch file string into an array of parsed patches.
|
|
347
|
+
*/
|
|
348
|
+
export function parsePatchFiles(
|
|
349
|
+
data: string,
|
|
350
|
+
cacheKeyPrefix?: string,
|
|
351
|
+
throwOnError = false
|
|
352
|
+
): ParsedPatch[] {
|
|
353
|
+
const patches: ParsedPatch[] = []
|
|
354
|
+
for (const patch of data.split(COMMIT_METADATA_SPLIT)) {
|
|
355
|
+
try {
|
|
356
|
+
patches.push(
|
|
357
|
+
processPatch(
|
|
358
|
+
patch,
|
|
359
|
+
cacheKeyPrefix != null ? `${cacheKeyPrefix}-${patches.length}` : undefined,
|
|
360
|
+
throwOnError
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (throwOnError) throw error
|
|
365
|
+
else console.error(error)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return patches
|
|
369
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Vendored from @pierre/diffs v1.1.15 — parser only. See AIMUX-14.
|
|
2
|
+
// Parser-only subset of the library's types. Renderer/shiki/hast types removed.
|
|
3
|
+
|
|
4
|
+
export type ChangeTypes = 'change' | 'rename-pure' | 'rename-changed' | 'new' | 'deleted'
|
|
5
|
+
|
|
6
|
+
export type HunkLineType = 'context' | 'expanded' | 'addition' | 'deletion' | 'metadata'
|
|
7
|
+
|
|
8
|
+
export interface ContextContent {
|
|
9
|
+
type: 'context'
|
|
10
|
+
lines: number
|
|
11
|
+
additionLineIndex: number
|
|
12
|
+
deletionLineIndex: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ChangeContent {
|
|
16
|
+
type: 'change'
|
|
17
|
+
deletions: number
|
|
18
|
+
deletionLineIndex: number
|
|
19
|
+
additions: number
|
|
20
|
+
additionLineIndex: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type HunkContent = ContextContent | ChangeContent
|
|
24
|
+
|
|
25
|
+
export interface Hunk {
|
|
26
|
+
collapsedBefore: number
|
|
27
|
+
additionStart: number
|
|
28
|
+
additionCount: number
|
|
29
|
+
additionLines: number
|
|
30
|
+
additionLineIndex: number
|
|
31
|
+
deletionStart: number
|
|
32
|
+
deletionCount: number
|
|
33
|
+
deletionLines: number
|
|
34
|
+
deletionLineIndex: number
|
|
35
|
+
hunkContent: HunkContent[]
|
|
36
|
+
hunkContext?: string
|
|
37
|
+
hunkSpecs?: string
|
|
38
|
+
splitLineStart: number
|
|
39
|
+
splitLineCount: number
|
|
40
|
+
unifiedLineStart: number
|
|
41
|
+
unifiedLineCount: number
|
|
42
|
+
noEOFCRDeletions: boolean
|
|
43
|
+
noEOFCRAdditions: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FileDiffMetadata {
|
|
47
|
+
name: string
|
|
48
|
+
prevName?: string
|
|
49
|
+
lang?: string
|
|
50
|
+
newObjectId?: string
|
|
51
|
+
prevObjectId?: string
|
|
52
|
+
mode?: string
|
|
53
|
+
prevMode?: string
|
|
54
|
+
type: ChangeTypes
|
|
55
|
+
hunks: Hunk[]
|
|
56
|
+
splitLineCount: number
|
|
57
|
+
unifiedLineCount: number
|
|
58
|
+
isPartial: boolean
|
|
59
|
+
deletionLines: string[]
|
|
60
|
+
additionLines: string[]
|
|
61
|
+
cacheKey?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ParsedPatch {
|
|
65
|
+
patchMetadata?: string
|
|
66
|
+
files: FileDiffMetadata[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FileContents {
|
|
70
|
+
name: string
|
|
71
|
+
contents: string
|
|
72
|
+
lang?: string
|
|
73
|
+
header?: string
|
|
74
|
+
cacheKey?: string
|
|
75
|
+
}
|
package/src/git/git-diff.ts
CHANGED
|
@@ -6,12 +6,14 @@ function resolveStatus(entry: GitFileEntry): { status: DiffFileStatus; oldPath?:
|
|
|
6
6
|
if (entry.renamedFrom) return { oldPath: entry.renamedFrom, status: 'renamed' }
|
|
7
7
|
if (entry.section === 'untracked' || entry.status === '?') return { status: 'new' }
|
|
8
8
|
if (entry.status === 'D') return { status: 'deleted' }
|
|
9
|
-
if (entry.status === 'A' && entry.section === 'staged'
|
|
9
|
+
if (entry.status === 'A' && (entry.section === 'staged' || entry.section === 'historical')) {
|
|
10
|
+
return { status: 'new' }
|
|
11
|
+
}
|
|
10
12
|
return { status: 'modified' }
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
async function isBinary(cwd: string, path: string): Promise<boolean> {
|
|
14
|
-
const result = await $`git -C ${cwd} diff
|
|
15
|
+
async function isBinary(cwd: string, ref: string, path: string): Promise<boolean> {
|
|
16
|
+
const result = await $`git -C ${cwd} diff ${ref} --numstat -- ${path}`.quiet().nothrow()
|
|
15
17
|
if (result.exitCode !== 0) return false
|
|
16
18
|
const text = result.text().trim()
|
|
17
19
|
if (!text) return false
|
|
@@ -19,8 +21,8 @@ async function isBinary(cwd: string, path: string): Promise<boolean> {
|
|
|
19
21
|
return first.startsWith('-\t-\t')
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
async function readHeadSize(cwd: string, path: string): Promise<number> {
|
|
23
|
-
const result = await $`git -C ${cwd} show
|
|
24
|
+
async function readHeadSize(cwd: string, ref: string, path: string): Promise<number> {
|
|
25
|
+
const result = await $`git -C ${cwd} show ${ref}:${path}`.quiet().nothrow()
|
|
24
26
|
if (result.exitCode !== 0) return 0
|
|
25
27
|
return result.text().length
|
|
26
28
|
}
|
|
@@ -33,9 +35,14 @@ async function readWorkingSize(cwd: string, path: string): Promise<number> {
|
|
|
33
35
|
return 0
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
async function rawUnifiedDiff(
|
|
38
|
+
async function rawUnifiedDiff(
|
|
39
|
+
cwd: string,
|
|
40
|
+
ref: string,
|
|
41
|
+
path: string,
|
|
42
|
+
status: DiffFileStatus
|
|
43
|
+
): Promise<string> {
|
|
37
44
|
if (status === 'new') {
|
|
38
|
-
const result = await $`git -C ${cwd} diff
|
|
45
|
+
const result = await $`git -C ${cwd} diff ${ref} --no-color --no-textconv -- ${path}`
|
|
39
46
|
.quiet()
|
|
40
47
|
.nothrow()
|
|
41
48
|
if (result.exitCode === 0 && result.text().length > 0) return result.text()
|
|
@@ -47,19 +54,24 @@ async function rawUnifiedDiff(cwd: string, path: string, status: DiffFileStatus)
|
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
const result =
|
|
50
|
-
await $`git -C ${cwd} diff
|
|
57
|
+
await $`git -C ${cwd} diff ${ref} --unified=99999 --no-color --no-textconv -- ${path}`
|
|
51
58
|
.quiet()
|
|
52
59
|
.nothrow()
|
|
53
60
|
if (result.exitCode !== 0) return ''
|
|
54
61
|
return result.text()
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
export async function fetchDiff(
|
|
64
|
+
export async function fetchDiff(
|
|
65
|
+
cwd: string,
|
|
66
|
+
file: GitFileEntry,
|
|
67
|
+
headOffset: number = 0
|
|
68
|
+
): Promise<DiffData> {
|
|
58
69
|
const { oldPath, status } = resolveStatus(file)
|
|
70
|
+
const ref = headOffset > 0 ? `HEAD~${headOffset}` : 'HEAD'
|
|
59
71
|
|
|
60
|
-
if (await isBinary(cwd, file.path)) {
|
|
72
|
+
if (await isBinary(cwd, ref, file.path)) {
|
|
61
73
|
const [binarySizeBefore, binarySizeAfter] = await Promise.all([
|
|
62
|
-
readHeadSize(cwd, file.path),
|
|
74
|
+
readHeadSize(cwd, ref, file.path),
|
|
63
75
|
readWorkingSize(cwd, file.path),
|
|
64
76
|
])
|
|
65
77
|
return {
|
|
@@ -71,7 +83,7 @@ export async function fetchDiff(cwd: string, file: GitFileEntry): Promise<DiffDa
|
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
const rawDiff = await rawUnifiedDiff(cwd, file.path, status)
|
|
86
|
+
const rawDiff = await rawUnifiedDiff(cwd, ref, file.path, status)
|
|
75
87
|
|
|
76
88
|
const data: DiffData = {
|
|
77
89
|
path: file.path,
|
package/src/git/git-poller.ts
CHANGED
|
@@ -9,9 +9,10 @@ const MAX_INTERVAL_MS = 30_000
|
|
|
9
9
|
interface Options {
|
|
10
10
|
enabled: boolean
|
|
11
11
|
projectPath: string | undefined
|
|
12
|
+
headOffset: number
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export function useGitPanelPolling({ enabled, projectPath }: Options): void {
|
|
15
|
+
export function useGitPanelPolling({ enabled, headOffset, projectPath }: Options): void {
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
if (!enabled || !projectPath) return undefined
|
|
17
18
|
|
|
@@ -27,11 +28,18 @@ export function useGitPanelPolling({ enabled, projectPath }: Options): void {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const tick = async () => {
|
|
30
|
-
const result = await collectGitStatus(projectPath)
|
|
31
|
+
const result = await collectGitStatus(projectPath, { headOffset })
|
|
31
32
|
if (cancelled) return
|
|
32
33
|
if (result.kind === 'ok') {
|
|
33
34
|
dispatchGlobal({ payload: result.payload, type: 'git-refresh-success' })
|
|
34
35
|
delay = BASE_INTERVAL_MS
|
|
36
|
+
} else if (result.kind === 'out-of-range') {
|
|
37
|
+
dispatchGlobal({ offset: result.maxOffset, type: 'git-mode-set-head-offset' })
|
|
38
|
+
dispatchGlobal({
|
|
39
|
+
message: `no older commit — clamped to HEAD~${result.maxOffset}`,
|
|
40
|
+
type: 'git-mode-set-message',
|
|
41
|
+
})
|
|
42
|
+
delay = BASE_INTERVAL_MS
|
|
35
43
|
} else {
|
|
36
44
|
dispatchGlobal({ kind: result.error, type: 'git-refresh-error' })
|
|
37
45
|
delay = Math.min(delay * 2, MAX_INTERVAL_MS)
|
|
@@ -45,5 +53,5 @@ export function useGitPanelPolling({ enabled, projectPath }: Options): void {
|
|
|
45
53
|
cancelled = true
|
|
46
54
|
if (timer) clearTimeout(timer)
|
|
47
55
|
}
|
|
48
|
-
}, [enabled, projectPath])
|
|
56
|
+
}, [enabled, projectPath, headOffset])
|
|
49
57
|
}
|