@muyichengshayu/promptx 0.1.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/README.md +80 -0
- package/apps/server/src/appPaths.js +102 -0
- package/apps/server/src/codex.js +585 -0
- package/apps/server/src/codexRunRuntime.js +212 -0
- package/apps/server/src/codexRuns.js +525 -0
- package/apps/server/src/codexSessions.js +149 -0
- package/apps/server/src/db.js +389 -0
- package/apps/server/src/gitDiff.js +1425 -0
- package/apps/server/src/index.js +909 -0
- package/apps/server/src/pdf.js +383 -0
- package/apps/server/src/repository.js +484 -0
- package/apps/server/src/sseHub.js +69 -0
- package/apps/server/src/upload.js +22 -0
- package/apps/server/src/workspaceFiles.js +662 -0
- package/apps/web/dist/assets/CodexSessionManagerDialog-c35LrKjV.js +6 -0
- package/apps/web/dist/assets/TaskDiffReviewDialog-BYcla0q4.js +12 -0
- package/apps/web/dist/assets/WorkbenchSettingsDialog-C0uQRStP.js +1 -0
- package/apps/web/dist/assets/WorkbenchView-Cp_qHNdX.js +216 -0
- package/apps/web/dist/assets/index-D9ui_gwj.js +25 -0
- package/apps/web/dist/assets/index-DDNrspNi.css +1 -0
- package/apps/web/dist/index.html +16 -0
- package/bin/promptx.js +60 -0
- package/package.json +58 -0
- package/packages/shared/src/index.js +121 -0
- package/scripts/doctor.mjs +251 -0
- package/scripts/service.mjs +308 -0
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { all, get, run, transaction } from './db.js'
|
|
7
|
+
import { getPromptxCodexSessionById } from './codexSessions.js'
|
|
8
|
+
import { getTaskBySlug } from './repository.js'
|
|
9
|
+
|
|
10
|
+
const MAX_SNAPSHOT_TEXT_BYTES = 220_000
|
|
11
|
+
const MAX_PATCH_TEXT_BYTES = 260_000
|
|
12
|
+
const DIFF_REVIEW_CACHE_TTL_MS = 4000
|
|
13
|
+
const DIFF_REVIEW_CACHE_MAX_ENTRIES = 80
|
|
14
|
+
const FILE_DIFF_CACHE_TTL_MS = 8000
|
|
15
|
+
const FILE_DIFF_CACHE_MAX_ENTRIES = 400
|
|
16
|
+
|
|
17
|
+
const diffReviewCache = new Map()
|
|
18
|
+
const fileDiffCache = new Map()
|
|
19
|
+
const gitDiffCacheMetrics = {
|
|
20
|
+
reviewHits: 0,
|
|
21
|
+
reviewMisses: 0,
|
|
22
|
+
fileHits: 0,
|
|
23
|
+
fileMisses: 0,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isGitDiffDebugEnabled(channel = 'all') {
|
|
27
|
+
const rawValue = String(process.env.PROMPTX_GIT_DIFF_DEBUG || '').trim().toLowerCase()
|
|
28
|
+
if (!rawValue) {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (rawValue === '1' || rawValue === 'true' || rawValue === 'all' || rawValue === '*') {
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const enabledChannels = rawValue
|
|
37
|
+
.split(/[,\s]+/)
|
|
38
|
+
.map((item) => item.trim())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
|
|
41
|
+
return enabledChannels.includes(channel) || enabledChannels.includes('all')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function logGitDiffDebug(channel = 'all', action = '', meta = {}) {
|
|
45
|
+
if (!isGitDiffDebugEnabled(channel)) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const normalizedMeta = Object.entries(meta)
|
|
50
|
+
.filter(([, value]) => value !== '' && value !== null && typeof value !== 'undefined')
|
|
51
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
52
|
+
.join(' ')
|
|
53
|
+
|
|
54
|
+
console.info(`[promptx][git-diff:${channel}] ${action}${normalizedMeta ? ` ${normalizedMeta}` : ''}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runGit(repoRoot = '', args = [], options = {}) {
|
|
58
|
+
const result = spawnSync('git', ['-C', repoRoot, ...args], {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
61
|
+
...options,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
status: typeof result.status === 'number' ? result.status : 1,
|
|
66
|
+
stdout: String(result.stdout || ''),
|
|
67
|
+
stderr: String(result.stderr || ''),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runGitBuffer(repoRoot = '', args = [], options = {}) {
|
|
72
|
+
const result = spawnSync('git', ['-C', repoRoot, ...args], {
|
|
73
|
+
encoding: 'buffer',
|
|
74
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
75
|
+
...options,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
status: typeof result.status === 'number' ? result.status : 1,
|
|
80
|
+
stdout: Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout || ''),
|
|
81
|
+
stderr: Buffer.isBuffer(result.stderr) ? result.stderr : Buffer.from(result.stderr || ''),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function splitNullText(value = '') {
|
|
86
|
+
return String(value || '').split('\0').filter(Boolean)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createHash(value) {
|
|
90
|
+
return crypto.createHash('sha1').update(value).digest('hex')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getCachedValue(cache, key, ttlMs, metricKey = '', options = {}) {
|
|
94
|
+
const {
|
|
95
|
+
channel = 'all',
|
|
96
|
+
cacheName = 'cache',
|
|
97
|
+
debugMeta = {},
|
|
98
|
+
} = options
|
|
99
|
+
const entry = cache.get(key)
|
|
100
|
+
if (!entry) {
|
|
101
|
+
if (metricKey) {
|
|
102
|
+
gitDiffCacheMetrics[metricKey] += 1
|
|
103
|
+
}
|
|
104
|
+
logGitDiffDebug(channel, 'miss', {
|
|
105
|
+
cache: cacheName,
|
|
106
|
+
...debugMeta,
|
|
107
|
+
})
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (Date.now() - entry.createdAt > ttlMs) {
|
|
112
|
+
cache.delete(key)
|
|
113
|
+
if (metricKey) {
|
|
114
|
+
gitDiffCacheMetrics[metricKey] += 1
|
|
115
|
+
}
|
|
116
|
+
logGitDiffDebug(channel, 'stale', {
|
|
117
|
+
cache: cacheName,
|
|
118
|
+
...debugMeta,
|
|
119
|
+
})
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cache.delete(key)
|
|
124
|
+
cache.set(key, entry)
|
|
125
|
+
logGitDiffDebug(channel, 'hit', {
|
|
126
|
+
cache: cacheName,
|
|
127
|
+
...debugMeta,
|
|
128
|
+
})
|
|
129
|
+
return entry.value
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function setCachedValue(cache, key, value, maxEntries = 0, options = {}) {
|
|
133
|
+
const {
|
|
134
|
+
channel = 'all',
|
|
135
|
+
cacheName = 'cache',
|
|
136
|
+
debugMeta = {},
|
|
137
|
+
} = options
|
|
138
|
+
cache.delete(key)
|
|
139
|
+
cache.set(key, {
|
|
140
|
+
value,
|
|
141
|
+
createdAt: Date.now(),
|
|
142
|
+
})
|
|
143
|
+
logGitDiffDebug(channel, 'store', {
|
|
144
|
+
cache: cacheName,
|
|
145
|
+
size: cache.size,
|
|
146
|
+
...debugMeta,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
while (maxEntries > 0 && cache.size > maxEntries) {
|
|
150
|
+
const oldestKey = cache.keys().next().value
|
|
151
|
+
if (typeof oldestKey === 'undefined') {
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
cache.delete(oldestKey)
|
|
155
|
+
logGitDiffDebug(channel, 'evict', {
|
|
156
|
+
cache: cacheName,
|
|
157
|
+
size: cache.size,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeDiffStatus(value = '') {
|
|
163
|
+
const status = String(value || '').trim().charAt(0).toUpperCase()
|
|
164
|
+
if (status === 'A' || status === 'D') {
|
|
165
|
+
return status
|
|
166
|
+
}
|
|
167
|
+
return 'M'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseTrackedDiffEntries(output = '') {
|
|
171
|
+
const parts = splitNullText(output)
|
|
172
|
+
const entries = new Map()
|
|
173
|
+
|
|
174
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
175
|
+
const rawStatus = String(parts[index] || '').trim()
|
|
176
|
+
if (!rawStatus) {
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
const rawPath = String(parts[index + 1] || '').trim()
|
|
180
|
+
if (!rawPath) {
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let nextPath = rawPath
|
|
185
|
+
if (rawStatus.startsWith('R') || rawStatus.startsWith('C')) {
|
|
186
|
+
nextPath = String(parts[index + 2] || rawPath).trim() || rawPath
|
|
187
|
+
index += 2
|
|
188
|
+
} else {
|
|
189
|
+
index += 1
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
entries.set(nextPath, {
|
|
193
|
+
path: nextPath,
|
|
194
|
+
status: normalizeDiffStatus(rawStatus),
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return entries
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveGitRepoRoot(cwd = '') {
|
|
202
|
+
const targetCwd = String(cwd || '').trim()
|
|
203
|
+
if (!targetCwd) {
|
|
204
|
+
return ''
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const result = runGit(targetCwd, ['rev-parse', '--show-toplevel'])
|
|
208
|
+
if (result.status !== 0) {
|
|
209
|
+
return ''
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result.stdout.trim()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveGitHeadOid(repoRoot = '') {
|
|
216
|
+
const result = runGit(repoRoot, ['rev-parse', '--verify', 'HEAD'])
|
|
217
|
+
if (result.status !== 0) {
|
|
218
|
+
return ''
|
|
219
|
+
}
|
|
220
|
+
return result.stdout.trim()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resolveGitBranchLabel(repoRoot = '') {
|
|
224
|
+
const branchResult = runGit(repoRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
|
|
225
|
+
const branchName = branchResult.stdout.trim()
|
|
226
|
+
if (branchResult.status === 0 && branchName) {
|
|
227
|
+
return branchName
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const headShort = runGit(repoRoot, ['rev-parse', '--short', 'HEAD']).stdout.trim()
|
|
231
|
+
if (headShort) {
|
|
232
|
+
return `detached@${headShort}`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return ''
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveWorkspaceStatusSignature(repoRoot = '') {
|
|
239
|
+
if (!repoRoot) {
|
|
240
|
+
return ''
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const result = runGitBuffer(repoRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all'])
|
|
244
|
+
if (result.status !== 0) {
|
|
245
|
+
return ''
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return createHash(result.stdout)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveShortOid(value = '') {
|
|
252
|
+
const text = String(value || '').trim()
|
|
253
|
+
return text ? text.slice(0, 7) : ''
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function commitExists(repoRoot = '', oid = '') {
|
|
257
|
+
const normalizedOid = String(oid || '').trim()
|
|
258
|
+
if (!repoRoot || !normalizedOid) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return runGit(repoRoot, ['cat-file', '-e', `${normalizedOid}^{commit}`]).status === 0
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isAncestorCommit(repoRoot = '', ancestorOid = '', descendantOid = '') {
|
|
266
|
+
const normalizedAncestorOid = String(ancestorOid || '').trim()
|
|
267
|
+
const normalizedDescendantOid = String(descendantOid || '').trim()
|
|
268
|
+
if (!repoRoot || !normalizedAncestorOid || !normalizedDescendantOid) {
|
|
269
|
+
return false
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return runGit(repoRoot, ['merge-base', '--is-ancestor', normalizedAncestorOid, normalizedDescendantOid]).status === 0
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function listGitChangeEntries(repoRoot = '') {
|
|
276
|
+
const entries = new Map()
|
|
277
|
+
const headOid = resolveGitHeadOid(repoRoot)
|
|
278
|
+
|
|
279
|
+
if (headOid) {
|
|
280
|
+
const trackedResult = runGit(repoRoot, ['diff', '--name-status', '-z', 'HEAD', '--'])
|
|
281
|
+
parseTrackedDiffEntries(trackedResult.stdout).forEach((entry, filePath) => {
|
|
282
|
+
entries.set(filePath, entry)
|
|
283
|
+
})
|
|
284
|
+
} else {
|
|
285
|
+
splitNullText(runGit(repoRoot, ['ls-files', '-z']).stdout).forEach((filePath) => {
|
|
286
|
+
entries.set(filePath, {
|
|
287
|
+
path: filePath,
|
|
288
|
+
status: 'A',
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
splitNullText(runGit(repoRoot, ['ls-files', '--others', '--exclude-standard', '-z']).stdout).forEach((filePath) => {
|
|
294
|
+
if (!entries.has(filePath)) {
|
|
295
|
+
entries.set(filePath, {
|
|
296
|
+
path: filePath,
|
|
297
|
+
status: 'A',
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
headOid,
|
|
304
|
+
entries,
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readFileState(repoRoot = '', filePath = '') {
|
|
309
|
+
const absolutePath = path.resolve(repoRoot, filePath)
|
|
310
|
+
if (!absolutePath.startsWith(path.resolve(repoRoot))) {
|
|
311
|
+
return {
|
|
312
|
+
exists: false,
|
|
313
|
+
isBinary: false,
|
|
314
|
+
tooLarge: false,
|
|
315
|
+
size: 0,
|
|
316
|
+
hash: '',
|
|
317
|
+
text: '',
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const stats = fs.statSync(absolutePath)
|
|
323
|
+
if (!stats.isFile()) {
|
|
324
|
+
return {
|
|
325
|
+
exists: false,
|
|
326
|
+
isBinary: false,
|
|
327
|
+
tooLarge: false,
|
|
328
|
+
size: 0,
|
|
329
|
+
hash: '',
|
|
330
|
+
text: '',
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const buffer = fs.readFileSync(absolutePath)
|
|
335
|
+
const isBinary = buffer.includes(0)
|
|
336
|
+
const tooLarge = !isBinary && buffer.length > MAX_SNAPSHOT_TEXT_BYTES
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
exists: true,
|
|
340
|
+
isBinary,
|
|
341
|
+
tooLarge,
|
|
342
|
+
size: buffer.length,
|
|
343
|
+
hash: createHash(buffer),
|
|
344
|
+
text: !isBinary && !tooLarge ? buffer.toString('utf8') : '',
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
return {
|
|
348
|
+
exists: false,
|
|
349
|
+
isBinary: false,
|
|
350
|
+
tooLarge: false,
|
|
351
|
+
size: 0,
|
|
352
|
+
hash: '',
|
|
353
|
+
text: '',
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function areFileStatesEqual(left, right) {
|
|
359
|
+
const previous = left || null
|
|
360
|
+
const next = right || null
|
|
361
|
+
|
|
362
|
+
if (!previous && !next) {
|
|
363
|
+
return true
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!previous || !next) {
|
|
367
|
+
return false
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
Boolean(previous.exists) === Boolean(next.exists)
|
|
372
|
+
&& Boolean(previous.isBinary) === Boolean(next.isBinary)
|
|
373
|
+
&& Boolean(previous.tooLarge) === Boolean(next.tooLarge)
|
|
374
|
+
&& String(previous.hash || '') === String(next.hash || '')
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function captureDirtySnapshots(repoRoot = '') {
|
|
379
|
+
const { headOid, entries } = listGitChangeEntries(repoRoot)
|
|
380
|
+
const snapshots = new Map()
|
|
381
|
+
|
|
382
|
+
entries.forEach((entry, filePath) => {
|
|
383
|
+
snapshots.set(filePath, readFileState(repoRoot, filePath))
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
headOid,
|
|
388
|
+
snapshots,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function readHeadFileState(repoRoot = '', headOid = '', filePath = '') {
|
|
393
|
+
const normalizedHeadOid = String(headOid || '').trim()
|
|
394
|
+
const normalizedPath = String(filePath || '').trim()
|
|
395
|
+
if (!repoRoot || !normalizedHeadOid || !normalizedPath) {
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = runGitBuffer(repoRoot, ['show', `${normalizedHeadOid}:${normalizedPath}`])
|
|
400
|
+
if (result.status !== 0) {
|
|
401
|
+
return null
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const buffer = result.stdout
|
|
405
|
+
const isBinary = buffer.includes(0)
|
|
406
|
+
const tooLarge = !isBinary && buffer.length > MAX_SNAPSHOT_TEXT_BYTES
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
exists: true,
|
|
410
|
+
isBinary,
|
|
411
|
+
tooLarge,
|
|
412
|
+
size: buffer.length,
|
|
413
|
+
hash: createHash(buffer),
|
|
414
|
+
text: !isBinary && !tooLarge ? buffer.toString('utf8') : '',
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function createBaselineStateResolver(repoRoot = '', baseline = null) {
|
|
419
|
+
const cache = new Map()
|
|
420
|
+
|
|
421
|
+
return (filePath = '') => {
|
|
422
|
+
const normalizedPath = String(filePath || '').trim()
|
|
423
|
+
if (!normalizedPath) {
|
|
424
|
+
return null
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (baseline?.entries?.has(normalizedPath)) {
|
|
428
|
+
return baseline.entries.get(normalizedPath) || null
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (cache.has(normalizedPath)) {
|
|
432
|
+
return cache.get(normalizedPath)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const state = readHeadFileState(repoRoot, baseline?.headOid, normalizedPath)
|
|
436
|
+
cache.set(normalizedPath, state)
|
|
437
|
+
return state
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function listCommittedChangeEntries(repoRoot = '', fromHeadOid = '', toHeadOid = '') {
|
|
442
|
+
const normalizedFromHeadOid = String(fromHeadOid || '').trim()
|
|
443
|
+
const normalizedToHeadOid = String(toHeadOid || '').trim()
|
|
444
|
+
|
|
445
|
+
if (!repoRoot || !normalizedFromHeadOid || !normalizedToHeadOid || normalizedFromHeadOid === normalizedToHeadOid) {
|
|
446
|
+
return new Map()
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const result = runGit(repoRoot, ['diff', '--name-status', '-z', `${normalizedFromHeadOid}..${normalizedToHeadOid}`, '--'])
|
|
450
|
+
return parseTrackedDiffEntries(result.stdout)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function serializeState(value = {}) {
|
|
454
|
+
return JSON.stringify({
|
|
455
|
+
exists: Boolean(value.exists),
|
|
456
|
+
isBinary: Boolean(value.isBinary),
|
|
457
|
+
tooLarge: Boolean(value.tooLarge),
|
|
458
|
+
size: Math.max(0, Number(value.size) || 0),
|
|
459
|
+
hash: String(value.hash || ''),
|
|
460
|
+
text: String(value.text || ''),
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function parseState(value = '{}') {
|
|
465
|
+
try {
|
|
466
|
+
const payload = JSON.parse(value || '{}')
|
|
467
|
+
return {
|
|
468
|
+
exists: Boolean(payload.exists),
|
|
469
|
+
isBinary: Boolean(payload.isBinary),
|
|
470
|
+
tooLarge: Boolean(payload.tooLarge),
|
|
471
|
+
size: Math.max(0, Number(payload.size) || 0),
|
|
472
|
+
hash: String(payload.hash || ''),
|
|
473
|
+
text: String(payload.text || ''),
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
return {
|
|
477
|
+
exists: false,
|
|
478
|
+
isBinary: false,
|
|
479
|
+
tooLarge: false,
|
|
480
|
+
size: 0,
|
|
481
|
+
hash: '',
|
|
482
|
+
text: '',
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function loadTaskBaseline(taskSlug = '') {
|
|
488
|
+
const row = get(
|
|
489
|
+
`SELECT task_slug, repo_root, head_oid, branch_label, created_at, updated_at
|
|
490
|
+
FROM task_git_baselines
|
|
491
|
+
WHERE task_slug = ?`,
|
|
492
|
+
[String(taskSlug || '').trim()]
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if (!row) {
|
|
496
|
+
return null
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const entries = new Map()
|
|
500
|
+
all(
|
|
501
|
+
`SELECT path, state_json
|
|
502
|
+
FROM task_git_baseline_entries
|
|
503
|
+
WHERE task_slug = ?
|
|
504
|
+
ORDER BY path ASC`,
|
|
505
|
+
[row.task_slug]
|
|
506
|
+
).forEach((entry) => {
|
|
507
|
+
entries.set(String(entry.path || '').trim(), parseState(entry.state_json))
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
taskSlug: row.task_slug,
|
|
512
|
+
repoRoot: String(row.repo_root || ''),
|
|
513
|
+
headOid: String(row.head_oid || ''),
|
|
514
|
+
branchLabel: String(row.branch_label || ''),
|
|
515
|
+
createdAt: row.created_at,
|
|
516
|
+
updatedAt: row.updated_at,
|
|
517
|
+
entries,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function loadRunBaseline(runId = '') {
|
|
522
|
+
const row = get(
|
|
523
|
+
`SELECT run_id, repo_root, head_oid, branch_label, created_at
|
|
524
|
+
FROM run_git_baselines
|
|
525
|
+
WHERE run_id = ?`,
|
|
526
|
+
[String(runId || '').trim()]
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
if (!row) {
|
|
530
|
+
return null
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const entries = new Map()
|
|
534
|
+
all(
|
|
535
|
+
`SELECT path, state_json
|
|
536
|
+
FROM run_git_baseline_entries
|
|
537
|
+
WHERE run_id = ?
|
|
538
|
+
ORDER BY path ASC`,
|
|
539
|
+
[row.run_id]
|
|
540
|
+
).forEach((entry) => {
|
|
541
|
+
entries.set(String(entry.path || '').trim(), parseState(entry.state_json))
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
runId: row.run_id,
|
|
546
|
+
repoRoot: String(row.repo_root || ''),
|
|
547
|
+
headOid: String(row.head_oid || ''),
|
|
548
|
+
branchLabel: String(row.branch_label || ''),
|
|
549
|
+
createdAt: row.created_at,
|
|
550
|
+
entries,
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function loadRunFinalSnapshot(runId = '') {
|
|
555
|
+
const row = get(
|
|
556
|
+
`SELECT run_id, repo_root, head_oid, branch_label, created_at
|
|
557
|
+
FROM run_git_final_snapshots
|
|
558
|
+
WHERE run_id = ?`,
|
|
559
|
+
[String(runId || '').trim()]
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if (!row) {
|
|
563
|
+
return null
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const entries = new Map()
|
|
567
|
+
all(
|
|
568
|
+
`SELECT path, state_json
|
|
569
|
+
FROM run_git_final_snapshot_entries
|
|
570
|
+
WHERE run_id = ?
|
|
571
|
+
ORDER BY path ASC`,
|
|
572
|
+
[row.run_id]
|
|
573
|
+
).forEach((entry) => {
|
|
574
|
+
entries.set(String(entry.path || '').trim(), parseState(entry.state_json))
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
runId: row.run_id,
|
|
579
|
+
repoRoot: String(row.repo_root || ''),
|
|
580
|
+
headOid: String(row.head_oid || ''),
|
|
581
|
+
branchLabel: String(row.branch_label || ''),
|
|
582
|
+
createdAt: row.created_at,
|
|
583
|
+
entries,
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function saveTaskBaseline(taskSlug = '', repoRoot = '', headOid = '', branchLabel = '', entries = new Map()) {
|
|
588
|
+
const now = new Date().toISOString()
|
|
589
|
+
const normalizedTaskSlug = String(taskSlug || '').trim()
|
|
590
|
+
|
|
591
|
+
transaction(() => {
|
|
592
|
+
run('DELETE FROM task_git_baseline_entries WHERE task_slug = ?', [normalizedTaskSlug])
|
|
593
|
+
run('DELETE FROM task_git_baselines WHERE task_slug = ?', [normalizedTaskSlug])
|
|
594
|
+
run(
|
|
595
|
+
`INSERT INTO task_git_baselines (task_slug, repo_root, head_oid, branch_label, created_at, updated_at)
|
|
596
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
597
|
+
[normalizedTaskSlug, repoRoot, headOid, branchLabel, now, now]
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
entries.forEach((state, filePath) => {
|
|
601
|
+
run(
|
|
602
|
+
`INSERT INTO task_git_baseline_entries (task_slug, path, state_json)
|
|
603
|
+
VALUES (?, ?, ?)`,
|
|
604
|
+
[normalizedTaskSlug, filePath, serializeState(state)]
|
|
605
|
+
)
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
return loadTaskBaseline(normalizedTaskSlug)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function saveRunBaseline(runId = '', repoRoot = '', headOid = '', branchLabel = '', entries = new Map()) {
|
|
613
|
+
const now = new Date().toISOString()
|
|
614
|
+
const normalizedRunId = String(runId || '').trim()
|
|
615
|
+
|
|
616
|
+
transaction(() => {
|
|
617
|
+
run('DELETE FROM run_git_baseline_entries WHERE run_id = ?', [normalizedRunId])
|
|
618
|
+
run('DELETE FROM run_git_baselines WHERE run_id = ?', [normalizedRunId])
|
|
619
|
+
run(
|
|
620
|
+
`INSERT INTO run_git_baselines (run_id, repo_root, head_oid, branch_label, created_at)
|
|
621
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
622
|
+
[normalizedRunId, repoRoot, headOid, branchLabel, now]
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
entries.forEach((state, filePath) => {
|
|
626
|
+
run(
|
|
627
|
+
`INSERT INTO run_git_baseline_entries (run_id, path, state_json)
|
|
628
|
+
VALUES (?, ?, ?)`,
|
|
629
|
+
[normalizedRunId, filePath, serializeState(state)]
|
|
630
|
+
)
|
|
631
|
+
})
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
return loadRunBaseline(normalizedRunId)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function saveRunFinalSnapshot(runId = '', repoRoot = '', headOid = '', branchLabel = '', entries = new Map()) {
|
|
638
|
+
const now = new Date().toISOString()
|
|
639
|
+
const normalizedRunId = String(runId || '').trim()
|
|
640
|
+
|
|
641
|
+
transaction(() => {
|
|
642
|
+
run('DELETE FROM run_git_final_snapshot_entries WHERE run_id = ?', [normalizedRunId])
|
|
643
|
+
run('DELETE FROM run_git_final_snapshots WHERE run_id = ?', [normalizedRunId])
|
|
644
|
+
run(
|
|
645
|
+
`INSERT INTO run_git_final_snapshots (run_id, repo_root, head_oid, branch_label, created_at)
|
|
646
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
647
|
+
[normalizedRunId, repoRoot, headOid, branchLabel, now]
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
entries.forEach((state, filePath) => {
|
|
651
|
+
run(
|
|
652
|
+
`INSERT INTO run_git_final_snapshot_entries (run_id, path, state_json)
|
|
653
|
+
VALUES (?, ?, ?)`,
|
|
654
|
+
[normalizedRunId, filePath, serializeState(state)]
|
|
655
|
+
)
|
|
656
|
+
})
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
return loadRunFinalSnapshot(normalizedRunId)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function resolveTaskRepoRoot(taskSlug = '') {
|
|
663
|
+
const task = getTaskBySlug(taskSlug)
|
|
664
|
+
if (!task || task.expired) {
|
|
665
|
+
return ''
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const sessionId = String(task.codexSessionId || '').trim()
|
|
669
|
+
if (!sessionId) {
|
|
670
|
+
return ''
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const session = getPromptxCodexSessionById(sessionId)
|
|
674
|
+
if (!session) {
|
|
675
|
+
return ''
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return resolveGitRepoRoot(session.cwd)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function getRunTaskSlug(runId = '') {
|
|
682
|
+
const row = get(
|
|
683
|
+
`SELECT task_slug
|
|
684
|
+
FROM codex_runs
|
|
685
|
+
WHERE id = ?`,
|
|
686
|
+
[String(runId || '').trim()]
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
return String(row?.task_slug || '').trim()
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function captureTaskGitBaseline(taskSlug = '', cwd = '') {
|
|
693
|
+
const normalizedTaskSlug = String(taskSlug || '').trim()
|
|
694
|
+
if (!normalizedTaskSlug) {
|
|
695
|
+
return null
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const repoRoot = resolveGitRepoRoot(cwd)
|
|
699
|
+
if (!repoRoot) {
|
|
700
|
+
return null
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const existing = loadTaskBaseline(normalizedTaskSlug)
|
|
704
|
+
if (existing?.repoRoot === repoRoot) {
|
|
705
|
+
return existing
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const { headOid, snapshots } = captureDirtySnapshots(repoRoot)
|
|
709
|
+
return saveTaskBaseline(normalizedTaskSlug, repoRoot, headOid, resolveGitBranchLabel(repoRoot), snapshots)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function captureRunGitBaseline(runId = '', cwd = '') {
|
|
713
|
+
const normalizedRunId = String(runId || '').trim()
|
|
714
|
+
if (!normalizedRunId) {
|
|
715
|
+
return null
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const repoRoot = resolveGitRepoRoot(cwd)
|
|
719
|
+
if (!repoRoot) {
|
|
720
|
+
return null
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const { headOid, snapshots } = captureDirtySnapshots(repoRoot)
|
|
724
|
+
return saveRunBaseline(normalizedRunId, repoRoot, headOid, resolveGitBranchLabel(repoRoot), snapshots)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export function captureRunGitFinalSnapshot(runId = '', cwd = '') {
|
|
728
|
+
const normalizedRunId = String(runId || '').trim()
|
|
729
|
+
if (!normalizedRunId) {
|
|
730
|
+
return null
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const existing = loadRunFinalSnapshot(normalizedRunId)
|
|
734
|
+
if (existing) {
|
|
735
|
+
return existing
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const baseline = loadRunBaseline(normalizedRunId)
|
|
739
|
+
const repoRoot = resolveGitRepoRoot(cwd) || resolveGitRepoRoot(baseline?.repoRoot || '')
|
|
740
|
+
if (!repoRoot) {
|
|
741
|
+
return null
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const { headOid, snapshots } = captureDirtySnapshots(repoRoot)
|
|
745
|
+
return saveRunFinalSnapshot(normalizedRunId, repoRoot, headOid, resolveGitBranchLabel(repoRoot), snapshots)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function parsePatchStats(patch = '') {
|
|
749
|
+
let additions = 0
|
|
750
|
+
let deletions = 0
|
|
751
|
+
|
|
752
|
+
String(patch || '').split('\n').forEach((line) => {
|
|
753
|
+
if (!line || line.startsWith('+++') || line.startsWith('---')) {
|
|
754
|
+
return
|
|
755
|
+
}
|
|
756
|
+
if (line.startsWith('+')) {
|
|
757
|
+
additions += 1
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
if (line.startsWith('-')) {
|
|
761
|
+
deletions += 1
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
return { additions, deletions }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function parseNumstat(output = '') {
|
|
769
|
+
const line = String(output || '')
|
|
770
|
+
.split('\n')
|
|
771
|
+
.map((entry) => entry.trim())
|
|
772
|
+
.filter(Boolean)[0] || ''
|
|
773
|
+
|
|
774
|
+
if (!line) {
|
|
775
|
+
return {
|
|
776
|
+
additions: 0,
|
|
777
|
+
deletions: 0,
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const [rawAdditions, rawDeletions] = line.split('\t')
|
|
782
|
+
return {
|
|
783
|
+
additions: rawAdditions === '-' ? 0 : Math.max(0, Number(rawAdditions) || 0),
|
|
784
|
+
deletions: rawDeletions === '-' ? 0 : Math.max(0, Number(rawDeletions) || 0),
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function buildDiffPayloadForFile(filePath = '', previousState = null, nextState = null, options = {}) {
|
|
789
|
+
const includePatch = Boolean(options.includePatch)
|
|
790
|
+
const includeStats = includePatch || Boolean(options.includeStats)
|
|
791
|
+
const cacheKey = JSON.stringify([
|
|
792
|
+
String(filePath || '').trim(),
|
|
793
|
+
includePatch,
|
|
794
|
+
includeStats,
|
|
795
|
+
Boolean(previousState?.exists),
|
|
796
|
+
Boolean(previousState?.isBinary),
|
|
797
|
+
Boolean(previousState?.tooLarge),
|
|
798
|
+
String(previousState?.hash || ''),
|
|
799
|
+
Boolean(nextState?.exists),
|
|
800
|
+
Boolean(nextState?.isBinary),
|
|
801
|
+
Boolean(nextState?.tooLarge),
|
|
802
|
+
String(nextState?.hash || ''),
|
|
803
|
+
])
|
|
804
|
+
const cachedPayload = getCachedValue(fileDiffCache, cacheKey, FILE_DIFF_CACHE_TTL_MS, 'fileMisses', {
|
|
805
|
+
channel: 'file',
|
|
806
|
+
cacheName: 'file-diff',
|
|
807
|
+
debugMeta: {
|
|
808
|
+
path: String(filePath || '').trim(),
|
|
809
|
+
includePatch,
|
|
810
|
+
includeStats,
|
|
811
|
+
},
|
|
812
|
+
})
|
|
813
|
+
if (cachedPayload) {
|
|
814
|
+
gitDiffCacheMetrics.fileHits += 1
|
|
815
|
+
return cachedPayload
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if ((previousState?.isBinary || nextState?.isBinary)) {
|
|
819
|
+
const payload = {
|
|
820
|
+
binary: true,
|
|
821
|
+
tooLarge: false,
|
|
822
|
+
patch: '',
|
|
823
|
+
patchLoaded: true,
|
|
824
|
+
additions: 0,
|
|
825
|
+
deletions: 0,
|
|
826
|
+
statsLoaded: true,
|
|
827
|
+
message: '二进制文件暂不支持在线 diff 预览。',
|
|
828
|
+
}
|
|
829
|
+
setCachedValue(fileDiffCache, cacheKey, payload, FILE_DIFF_CACHE_MAX_ENTRIES, {
|
|
830
|
+
channel: 'file',
|
|
831
|
+
cacheName: 'file-diff',
|
|
832
|
+
debugMeta: {
|
|
833
|
+
path: String(filePath || '').trim(),
|
|
834
|
+
includePatch,
|
|
835
|
+
includeStats,
|
|
836
|
+
},
|
|
837
|
+
})
|
|
838
|
+
return payload
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (previousState?.tooLarge || nextState?.tooLarge) {
|
|
842
|
+
const payload = {
|
|
843
|
+
binary: false,
|
|
844
|
+
tooLarge: true,
|
|
845
|
+
patch: '',
|
|
846
|
+
patchLoaded: true,
|
|
847
|
+
additions: 0,
|
|
848
|
+
deletions: 0,
|
|
849
|
+
statsLoaded: true,
|
|
850
|
+
message: '文件内容较大,暂不展示具体 diff。',
|
|
851
|
+
}
|
|
852
|
+
setCachedValue(fileDiffCache, cacheKey, payload, FILE_DIFF_CACHE_MAX_ENTRIES, {
|
|
853
|
+
channel: 'file',
|
|
854
|
+
cacheName: 'file-diff',
|
|
855
|
+
debugMeta: {
|
|
856
|
+
path: String(filePath || '').trim(),
|
|
857
|
+
includePatch,
|
|
858
|
+
includeStats,
|
|
859
|
+
},
|
|
860
|
+
})
|
|
861
|
+
return payload
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (!includeStats) {
|
|
865
|
+
const payload = {
|
|
866
|
+
binary: false,
|
|
867
|
+
tooLarge: false,
|
|
868
|
+
patch: '',
|
|
869
|
+
patchLoaded: false,
|
|
870
|
+
additions: null,
|
|
871
|
+
deletions: null,
|
|
872
|
+
statsLoaded: false,
|
|
873
|
+
message: '',
|
|
874
|
+
}
|
|
875
|
+
setCachedValue(fileDiffCache, cacheKey, payload, FILE_DIFF_CACHE_MAX_ENTRIES, {
|
|
876
|
+
channel: 'file',
|
|
877
|
+
cacheName: 'file-diff',
|
|
878
|
+
debugMeta: {
|
|
879
|
+
path: String(filePath || '').trim(),
|
|
880
|
+
includePatch,
|
|
881
|
+
includeStats,
|
|
882
|
+
},
|
|
883
|
+
})
|
|
884
|
+
return payload
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'promptx-git-diff-'))
|
|
888
|
+
const previousPath = path.join(tempDir, 'before')
|
|
889
|
+
const nextPath = path.join(tempDir, 'after')
|
|
890
|
+
const nullPath = process.platform === 'win32'
|
|
891
|
+
? path.join(tempDir, 'null')
|
|
892
|
+
: '/dev/null'
|
|
893
|
+
|
|
894
|
+
try {
|
|
895
|
+
if (process.platform === 'win32') {
|
|
896
|
+
fs.writeFileSync(nullPath, '', 'utf8')
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (previousState?.exists) {
|
|
900
|
+
fs.writeFileSync(previousPath, previousState.text || '', 'utf8')
|
|
901
|
+
}
|
|
902
|
+
if (nextState?.exists) {
|
|
903
|
+
fs.writeFileSync(nextPath, nextState.text || '', 'utf8')
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const statsResult = runGit(tempDir, [
|
|
907
|
+
'diff',
|
|
908
|
+
'--no-index',
|
|
909
|
+
'--numstat',
|
|
910
|
+
previousState?.exists ? previousPath : nullPath,
|
|
911
|
+
nextState?.exists ? nextPath : nullPath,
|
|
912
|
+
])
|
|
913
|
+
const numstat = parseNumstat(statsResult.stdout)
|
|
914
|
+
|
|
915
|
+
if (!includePatch) {
|
|
916
|
+
const payload = {
|
|
917
|
+
binary: false,
|
|
918
|
+
tooLarge: false,
|
|
919
|
+
patch: '',
|
|
920
|
+
patchLoaded: false,
|
|
921
|
+
additions: numstat.additions,
|
|
922
|
+
deletions: numstat.deletions,
|
|
923
|
+
statsLoaded: true,
|
|
924
|
+
message: '',
|
|
925
|
+
}
|
|
926
|
+
setCachedValue(fileDiffCache, cacheKey, payload, FILE_DIFF_CACHE_MAX_ENTRIES, {
|
|
927
|
+
channel: 'file',
|
|
928
|
+
cacheName: 'file-diff',
|
|
929
|
+
debugMeta: {
|
|
930
|
+
path: String(filePath || '').trim(),
|
|
931
|
+
includePatch,
|
|
932
|
+
includeStats,
|
|
933
|
+
},
|
|
934
|
+
})
|
|
935
|
+
return payload
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const result = runGit(tempDir, [
|
|
939
|
+
'diff',
|
|
940
|
+
'--no-index',
|
|
941
|
+
'--no-color',
|
|
942
|
+
'--unified=3',
|
|
943
|
+
previousState?.exists ? previousPath : nullPath,
|
|
944
|
+
nextState?.exists ? nextPath : nullPath,
|
|
945
|
+
])
|
|
946
|
+
|
|
947
|
+
let patch = String(result.stdout || '').trim()
|
|
948
|
+
if (patch) {
|
|
949
|
+
patch = patch
|
|
950
|
+
.replace(/^diff --git .*$|^diff --git[^\n]*$/m, `diff --git ${previousState?.exists ? `a/${filePath}` : '/dev/null'} ${nextState?.exists ? `b/${filePath}` : '/dev/null'}`)
|
|
951
|
+
.replace(/^--- .*$/m, previousState?.exists ? `--- a/${filePath}` : '--- /dev/null')
|
|
952
|
+
.replace(/^\+\+\+ .*$/m, nextState?.exists ? `+++ b/${filePath}` : '+++ /dev/null')
|
|
953
|
+
}
|
|
954
|
+
const stats = parsePatchStats(patch)
|
|
955
|
+
|
|
956
|
+
if (!patch) {
|
|
957
|
+
const payload = {
|
|
958
|
+
binary: false,
|
|
959
|
+
tooLarge: false,
|
|
960
|
+
patch: '',
|
|
961
|
+
patchLoaded: true,
|
|
962
|
+
additions: stats.additions,
|
|
963
|
+
deletions: stats.deletions,
|
|
964
|
+
statsLoaded: true,
|
|
965
|
+
message: '',
|
|
966
|
+
}
|
|
967
|
+
setCachedValue(fileDiffCache, cacheKey, payload, FILE_DIFF_CACHE_MAX_ENTRIES, {
|
|
968
|
+
channel: 'file',
|
|
969
|
+
cacheName: 'file-diff',
|
|
970
|
+
debugMeta: {
|
|
971
|
+
path: String(filePath || '').trim(),
|
|
972
|
+
includePatch,
|
|
973
|
+
includeStats,
|
|
974
|
+
},
|
|
975
|
+
})
|
|
976
|
+
return payload
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (patch.length > MAX_PATCH_TEXT_BYTES) {
|
|
980
|
+
const payload = {
|
|
981
|
+
binary: false,
|
|
982
|
+
tooLarge: true,
|
|
983
|
+
patch: '',
|
|
984
|
+
patchLoaded: true,
|
|
985
|
+
additions: stats.additions,
|
|
986
|
+
deletions: stats.deletions,
|
|
987
|
+
statsLoaded: true,
|
|
988
|
+
message: 'diff 内容较长,暂不在页面内完整展示。',
|
|
989
|
+
}
|
|
990
|
+
setCachedValue(fileDiffCache, cacheKey, payload, FILE_DIFF_CACHE_MAX_ENTRIES, {
|
|
991
|
+
channel: 'file',
|
|
992
|
+
cacheName: 'file-diff',
|
|
993
|
+
debugMeta: {
|
|
994
|
+
path: String(filePath || '').trim(),
|
|
995
|
+
includePatch,
|
|
996
|
+
includeStats,
|
|
997
|
+
},
|
|
998
|
+
})
|
|
999
|
+
return payload
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const payload = {
|
|
1003
|
+
binary: false,
|
|
1004
|
+
tooLarge: false,
|
|
1005
|
+
patch,
|
|
1006
|
+
patchLoaded: true,
|
|
1007
|
+
additions: stats.additions,
|
|
1008
|
+
deletions: stats.deletions,
|
|
1009
|
+
statsLoaded: true,
|
|
1010
|
+
message: '',
|
|
1011
|
+
}
|
|
1012
|
+
setCachedValue(fileDiffCache, cacheKey, payload, FILE_DIFF_CACHE_MAX_ENTRIES, {
|
|
1013
|
+
channel: 'file',
|
|
1014
|
+
cacheName: 'file-diff',
|
|
1015
|
+
debugMeta: {
|
|
1016
|
+
path: String(filePath || '').trim(),
|
|
1017
|
+
includePatch,
|
|
1018
|
+
includeStats,
|
|
1019
|
+
},
|
|
1020
|
+
})
|
|
1021
|
+
return payload
|
|
1022
|
+
} finally {
|
|
1023
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function deriveFileStatus(previousState, nextState) {
|
|
1028
|
+
if (!previousState?.exists && nextState?.exists) {
|
|
1029
|
+
return 'A'
|
|
1030
|
+
}
|
|
1031
|
+
if (previousState?.exists && !nextState?.exists) {
|
|
1032
|
+
return 'D'
|
|
1033
|
+
}
|
|
1034
|
+
return 'M'
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function sortDiffFiles(items = []) {
|
|
1038
|
+
const weightMap = {
|
|
1039
|
+
A: 0,
|
|
1040
|
+
M: 1,
|
|
1041
|
+
D: 2,
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return [...items].sort((left, right) => {
|
|
1045
|
+
const statusDiff = (weightMap[left.status] ?? 9) - (weightMap[right.status] ?? 9)
|
|
1046
|
+
if (statusDiff) {
|
|
1047
|
+
return statusDiff
|
|
1048
|
+
}
|
|
1049
|
+
return String(left.path || '').localeCompare(String(right.path || ''), 'zh-CN')
|
|
1050
|
+
})
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function createDiffFileEntry(filePath = '', previousState = null, nextState = null, options = {}) {
|
|
1054
|
+
if (areFileStatesEqual(previousState, nextState)) {
|
|
1055
|
+
return null
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const patchPayload = buildDiffPayloadForFile(filePath, previousState, nextState, options)
|
|
1059
|
+
return {
|
|
1060
|
+
path: filePath,
|
|
1061
|
+
status: deriveFileStatus(previousState, nextState),
|
|
1062
|
+
additions: patchPayload.additions,
|
|
1063
|
+
deletions: patchPayload.deletions,
|
|
1064
|
+
statsLoaded: patchPayload.statsLoaded,
|
|
1065
|
+
binary: patchPayload.binary,
|
|
1066
|
+
tooLarge: patchPayload.tooLarge,
|
|
1067
|
+
patch: patchPayload.patch,
|
|
1068
|
+
patchLoaded: patchPayload.patchLoaded,
|
|
1069
|
+
message: patchPayload.message,
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function createUnsupportedResult(reason = '', repoRoot = '', branch = '') {
|
|
1074
|
+
return {
|
|
1075
|
+
supported: false,
|
|
1076
|
+
reason,
|
|
1077
|
+
repoRoot,
|
|
1078
|
+
branch,
|
|
1079
|
+
baseline: null,
|
|
1080
|
+
warnings: [],
|
|
1081
|
+
summary: {
|
|
1082
|
+
fileCount: 0,
|
|
1083
|
+
additions: 0,
|
|
1084
|
+
deletions: 0,
|
|
1085
|
+
},
|
|
1086
|
+
files: [],
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
export function getWorkspaceGitDiffReviewByCwd(cwd = '', options = {}) {
|
|
1091
|
+
const repoRoot = resolveGitRepoRoot(cwd)
|
|
1092
|
+
if (!repoRoot) {
|
|
1093
|
+
return createUnsupportedResult('当前工作目录不是 Git 仓库,暂不支持代码变更审查。')
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const branch = resolveGitBranchLabel(repoRoot)
|
|
1097
|
+
const workspaceStatusSignature = resolveWorkspaceStatusSignature(repoRoot)
|
|
1098
|
+
const currentHeadOid = resolveGitHeadOid(repoRoot)
|
|
1099
|
+
const targetFilePath = String(options.filePath || '').trim()
|
|
1100
|
+
const includeFiles = targetFilePath || options.includeFiles !== false
|
|
1101
|
+
const includeStats = targetFilePath || options.includeStats !== false
|
|
1102
|
+
const cacheKey = JSON.stringify([
|
|
1103
|
+
'workspace',
|
|
1104
|
+
repoRoot,
|
|
1105
|
+
branch,
|
|
1106
|
+
currentHeadOid,
|
|
1107
|
+
workspaceStatusSignature,
|
|
1108
|
+
targetFilePath,
|
|
1109
|
+
includeFiles,
|
|
1110
|
+
includeStats,
|
|
1111
|
+
])
|
|
1112
|
+
const cachedReview = getCachedValue(diffReviewCache, cacheKey, DIFF_REVIEW_CACHE_TTL_MS, 'reviewMisses', {
|
|
1113
|
+
channel: 'review',
|
|
1114
|
+
cacheName: 'diff-review',
|
|
1115
|
+
debugMeta: {
|
|
1116
|
+
scope: 'workspace',
|
|
1117
|
+
repo: path.basename(repoRoot),
|
|
1118
|
+
filePath: targetFilePath,
|
|
1119
|
+
includeFiles,
|
|
1120
|
+
includeStats,
|
|
1121
|
+
},
|
|
1122
|
+
})
|
|
1123
|
+
if (cachedReview) {
|
|
1124
|
+
gitDiffCacheMetrics.reviewHits += 1
|
|
1125
|
+
return cachedReview
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const { headOid, entries: workingTreeEntries } = listGitChangeEntries(repoRoot)
|
|
1129
|
+
const baselineStateForPath = createBaselineStateResolver(repoRoot, {
|
|
1130
|
+
headOid,
|
|
1131
|
+
entries: new Map(),
|
|
1132
|
+
})
|
|
1133
|
+
const files = []
|
|
1134
|
+
let additions = 0
|
|
1135
|
+
let deletions = 0
|
|
1136
|
+
let fileCount = 0
|
|
1137
|
+
const candidatePaths = targetFilePath ? [targetFilePath] : [...workingTreeEntries.keys()]
|
|
1138
|
+
|
|
1139
|
+
candidatePaths.forEach((filePath) => {
|
|
1140
|
+
const previousState = baselineStateForPath(filePath)
|
|
1141
|
+
const nextState = readFileState(repoRoot, filePath)
|
|
1142
|
+
const diffEntry = createDiffFileEntry(filePath, previousState, nextState, {
|
|
1143
|
+
includePatch: Boolean(targetFilePath),
|
|
1144
|
+
includeStats,
|
|
1145
|
+
})
|
|
1146
|
+
if (!diffEntry) {
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
fileCount += 1
|
|
1151
|
+
additions += Math.max(0, Number(diffEntry.additions) || 0)
|
|
1152
|
+
deletions += Math.max(0, Number(diffEntry.deletions) || 0)
|
|
1153
|
+
if (includeFiles) {
|
|
1154
|
+
files.push(diffEntry)
|
|
1155
|
+
}
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
const payload = {
|
|
1159
|
+
supported: true,
|
|
1160
|
+
scope: 'workspace',
|
|
1161
|
+
runId: '',
|
|
1162
|
+
repoRoot,
|
|
1163
|
+
branch,
|
|
1164
|
+
baseline: null,
|
|
1165
|
+
warnings: [],
|
|
1166
|
+
baselineCreatedAt: '',
|
|
1167
|
+
summary: {
|
|
1168
|
+
fileCount,
|
|
1169
|
+
additions: includeStats ? additions : null,
|
|
1170
|
+
deletions: includeStats ? deletions : null,
|
|
1171
|
+
statsComplete: includeStats,
|
|
1172
|
+
},
|
|
1173
|
+
files: includeFiles ? sortDiffFiles(files) : [],
|
|
1174
|
+
}
|
|
1175
|
+
setCachedValue(diffReviewCache, cacheKey, payload, DIFF_REVIEW_CACHE_MAX_ENTRIES, {
|
|
1176
|
+
channel: 'review',
|
|
1177
|
+
cacheName: 'diff-review',
|
|
1178
|
+
debugMeta: {
|
|
1179
|
+
scope: 'workspace',
|
|
1180
|
+
repo: path.basename(repoRoot),
|
|
1181
|
+
fileCount,
|
|
1182
|
+
includeFiles,
|
|
1183
|
+
includeStats,
|
|
1184
|
+
},
|
|
1185
|
+
})
|
|
1186
|
+
return payload
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
export function getTaskGitDiffReview(taskSlug = '', options = {}) {
|
|
1190
|
+
const normalizedTaskSlug = String(taskSlug || '').trim()
|
|
1191
|
+
if (!normalizedTaskSlug) {
|
|
1192
|
+
return createUnsupportedResult('任务不存在。')
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const rawScope = String(options.scope || 'workspace').trim()
|
|
1196
|
+
const scope = rawScope === 'run'
|
|
1197
|
+
? 'run'
|
|
1198
|
+
: rawScope === 'task'
|
|
1199
|
+
? 'task'
|
|
1200
|
+
: 'workspace'
|
|
1201
|
+
const runId = String(options.runId || '').trim()
|
|
1202
|
+
const targetFilePath = String(options.filePath || '').trim()
|
|
1203
|
+
const includeFiles = targetFilePath || options.includeFiles !== false
|
|
1204
|
+
const includeStats = targetFilePath || options.includeStats !== false
|
|
1205
|
+
|
|
1206
|
+
if (scope === 'workspace') {
|
|
1207
|
+
return getWorkspaceGitDiffReviewByCwd(resolveTaskRepoRoot(normalizedTaskSlug), {
|
|
1208
|
+
filePath: targetFilePath,
|
|
1209
|
+
})
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
let baseline = null
|
|
1213
|
+
let comparisonSnapshot = null
|
|
1214
|
+
if (scope === 'run') {
|
|
1215
|
+
if (!runId) {
|
|
1216
|
+
return createUnsupportedResult('请选择一轮执行后再查看本轮代码变更。')
|
|
1217
|
+
}
|
|
1218
|
+
if (getRunTaskSlug(runId) !== normalizedTaskSlug) {
|
|
1219
|
+
return createUnsupportedResult('没有找到对应的执行记录。')
|
|
1220
|
+
}
|
|
1221
|
+
baseline = loadRunBaseline(runId)
|
|
1222
|
+
comparisonSnapshot = loadRunFinalSnapshot(runId)
|
|
1223
|
+
} else {
|
|
1224
|
+
baseline = loadTaskBaseline(normalizedTaskSlug)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (!baseline) {
|
|
1228
|
+
const fallbackRepoRoot = resolveTaskRepoRoot(normalizedTaskSlug)
|
|
1229
|
+
if (!fallbackRepoRoot) {
|
|
1230
|
+
return createUnsupportedResult('当前工作目录不是 Git 仓库,暂不支持代码变更审查。')
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return createUnsupportedResult(
|
|
1234
|
+
scope === 'run'
|
|
1235
|
+
? '这轮执行还没有建立代码变更基线,暂时无法查看本轮 diff。'
|
|
1236
|
+
: '当前任务还没有建立代码变更基线,请先让 Codex 执行一轮。',
|
|
1237
|
+
fallbackRepoRoot,
|
|
1238
|
+
resolveGitBranchLabel(fallbackRepoRoot)
|
|
1239
|
+
)
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (scope === 'run' && !comparisonSnapshot) {
|
|
1243
|
+
const fallbackRepoRoot = resolveGitRepoRoot(baseline.repoRoot) || resolveTaskRepoRoot(normalizedTaskSlug)
|
|
1244
|
+
return createUnsupportedResult(
|
|
1245
|
+
'这轮执行缺少结束快照,暂时无法准确还原本轮代码变更。',
|
|
1246
|
+
fallbackRepoRoot,
|
|
1247
|
+
fallbackRepoRoot ? resolveGitBranchLabel(fallbackRepoRoot) : ''
|
|
1248
|
+
)
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const repoRoot = resolveGitRepoRoot(baseline.repoRoot)
|
|
1252
|
+
if (!repoRoot) {
|
|
1253
|
+
return createUnsupportedResult('原工作目录已不是有效的 Git 仓库,暂时无法读取代码变更。', baseline.repoRoot)
|
|
1254
|
+
}
|
|
1255
|
+
const currentBranchLabel = resolveGitBranchLabel(repoRoot)
|
|
1256
|
+
const branch = scope === 'run' && comparisonSnapshot?.branchLabel
|
|
1257
|
+
? String(comparisonSnapshot.branchLabel || '')
|
|
1258
|
+
: currentBranchLabel
|
|
1259
|
+
const currentHeadOid = scope === 'run' && comparisonSnapshot
|
|
1260
|
+
? String(comparisonSnapshot.headOid || '')
|
|
1261
|
+
: resolveGitHeadOid(repoRoot)
|
|
1262
|
+
const workspaceStatusSignature = scope === 'run' && comparisonSnapshot
|
|
1263
|
+
? ''
|
|
1264
|
+
: resolveWorkspaceStatusSignature(repoRoot)
|
|
1265
|
+
const warnings = []
|
|
1266
|
+
|
|
1267
|
+
if (baseline.headOid) {
|
|
1268
|
+
if (!commitExists(repoRoot, baseline.headOid)) {
|
|
1269
|
+
return createUnsupportedResult(
|
|
1270
|
+
'基线对应的 commit 已不存在,仓库可能被 reset、rebase 或切换到无关历史,暂时无法准确读取该范围的代码变更。',
|
|
1271
|
+
repoRoot,
|
|
1272
|
+
branch
|
|
1273
|
+
)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (baseline.branchLabel && branch && baseline.branchLabel !== branch) {
|
|
1277
|
+
warnings.push(`当前分支已从 ${baseline.branchLabel} 切换到 ${branch}`)
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if (currentHeadOid && baseline.headOid !== currentHeadOid && !isAncestorCommit(repoRoot, baseline.headOid, currentHeadOid)) {
|
|
1281
|
+
warnings.push('当前 HEAD 已不在基线 commit 的后续历史中,仓库可能经历了 reset、rebase 或切分支')
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const cacheKey = JSON.stringify([
|
|
1286
|
+
scope,
|
|
1287
|
+
normalizedTaskSlug,
|
|
1288
|
+
runId,
|
|
1289
|
+
repoRoot,
|
|
1290
|
+
branch,
|
|
1291
|
+
currentHeadOid,
|
|
1292
|
+
workspaceStatusSignature,
|
|
1293
|
+
baseline.createdAt,
|
|
1294
|
+
baseline.headOid,
|
|
1295
|
+
baseline.branchLabel,
|
|
1296
|
+
baseline.entries.size,
|
|
1297
|
+
comparisonSnapshot?.createdAt || '',
|
|
1298
|
+
comparisonSnapshot?.headOid || '',
|
|
1299
|
+
comparisonSnapshot?.branchLabel || '',
|
|
1300
|
+
comparisonSnapshot?.entries?.size || 0,
|
|
1301
|
+
targetFilePath,
|
|
1302
|
+
includeFiles,
|
|
1303
|
+
includeStats,
|
|
1304
|
+
])
|
|
1305
|
+
const cachedReview = getCachedValue(diffReviewCache, cacheKey, DIFF_REVIEW_CACHE_TTL_MS, 'reviewMisses', {
|
|
1306
|
+
channel: 'review',
|
|
1307
|
+
cacheName: 'diff-review',
|
|
1308
|
+
debugMeta: {
|
|
1309
|
+
scope,
|
|
1310
|
+
task: normalizedTaskSlug,
|
|
1311
|
+
runId,
|
|
1312
|
+
repo: path.basename(repoRoot),
|
|
1313
|
+
filePath: targetFilePath,
|
|
1314
|
+
includeFiles,
|
|
1315
|
+
includeStats,
|
|
1316
|
+
},
|
|
1317
|
+
})
|
|
1318
|
+
if (cachedReview) {
|
|
1319
|
+
gitDiffCacheMetrics.reviewHits += 1
|
|
1320
|
+
return cachedReview
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const { entries: workingTreeEntries } = listGitChangeEntries(repoRoot)
|
|
1324
|
+
const baselineStateForPath = createBaselineStateResolver(repoRoot, baseline)
|
|
1325
|
+
const nextStateForPath = scope === 'run' && comparisonSnapshot
|
|
1326
|
+
? createBaselineStateResolver(repoRoot, comparisonSnapshot)
|
|
1327
|
+
: (filePath) => readFileState(repoRoot, filePath)
|
|
1328
|
+
const files = []
|
|
1329
|
+
let additions = 0
|
|
1330
|
+
let deletions = 0
|
|
1331
|
+
let fileCount = 0
|
|
1332
|
+
|
|
1333
|
+
const candidatePaths = targetFilePath
|
|
1334
|
+
? [targetFilePath]
|
|
1335
|
+
: new Set([
|
|
1336
|
+
...baseline.entries.keys(),
|
|
1337
|
+
...listCommittedChangeEntries(repoRoot, baseline.headOid, currentHeadOid).keys(),
|
|
1338
|
+
...(comparisonSnapshot ? comparisonSnapshot.entries.keys() : workingTreeEntries.keys()),
|
|
1339
|
+
])
|
|
1340
|
+
|
|
1341
|
+
candidatePaths.forEach((filePath) => {
|
|
1342
|
+
const previousState = baselineStateForPath(filePath)
|
|
1343
|
+
const nextState = nextStateForPath(filePath)
|
|
1344
|
+
const diffEntry = createDiffFileEntry(filePath, previousState, nextState, {
|
|
1345
|
+
includePatch: Boolean(targetFilePath),
|
|
1346
|
+
includeStats,
|
|
1347
|
+
})
|
|
1348
|
+
if (!diffEntry) {
|
|
1349
|
+
return
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
fileCount += 1
|
|
1353
|
+
additions += Math.max(0, Number(diffEntry.additions) || 0)
|
|
1354
|
+
deletions += Math.max(0, Number(diffEntry.deletions) || 0)
|
|
1355
|
+
if (includeFiles) {
|
|
1356
|
+
files.push(diffEntry)
|
|
1357
|
+
}
|
|
1358
|
+
})
|
|
1359
|
+
|
|
1360
|
+
const payload = {
|
|
1361
|
+
supported: true,
|
|
1362
|
+
scope,
|
|
1363
|
+
runId: scope === 'run' ? runId : '',
|
|
1364
|
+
repoRoot,
|
|
1365
|
+
branch,
|
|
1366
|
+
baseline: {
|
|
1367
|
+
createdAt: baseline.createdAt,
|
|
1368
|
+
headOid: baseline.headOid,
|
|
1369
|
+
headShort: resolveShortOid(baseline.headOid),
|
|
1370
|
+
branch: baseline.branchLabel || '',
|
|
1371
|
+
currentHeadOid,
|
|
1372
|
+
currentHeadShort: resolveShortOid(currentHeadOid),
|
|
1373
|
+
},
|
|
1374
|
+
warnings,
|
|
1375
|
+
baselineCreatedAt: baseline.createdAt,
|
|
1376
|
+
summary: {
|
|
1377
|
+
fileCount,
|
|
1378
|
+
additions: includeStats ? additions : null,
|
|
1379
|
+
deletions: includeStats ? deletions : null,
|
|
1380
|
+
statsComplete: includeStats,
|
|
1381
|
+
},
|
|
1382
|
+
files: includeFiles ? sortDiffFiles(files) : [],
|
|
1383
|
+
}
|
|
1384
|
+
setCachedValue(diffReviewCache, cacheKey, payload, DIFF_REVIEW_CACHE_MAX_ENTRIES, {
|
|
1385
|
+
channel: 'review',
|
|
1386
|
+
cacheName: 'diff-review',
|
|
1387
|
+
debugMeta: {
|
|
1388
|
+
scope,
|
|
1389
|
+
task: normalizedTaskSlug,
|
|
1390
|
+
runId,
|
|
1391
|
+
repo: path.basename(repoRoot),
|
|
1392
|
+
fileCount,
|
|
1393
|
+
includeFiles,
|
|
1394
|
+
includeStats,
|
|
1395
|
+
},
|
|
1396
|
+
})
|
|
1397
|
+
return payload
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
export function __resetGitDiffCachesForTest() {
|
|
1401
|
+
diffReviewCache.clear()
|
|
1402
|
+
fileDiffCache.clear()
|
|
1403
|
+
gitDiffCacheMetrics.reviewHits = 0
|
|
1404
|
+
gitDiffCacheMetrics.reviewMisses = 0
|
|
1405
|
+
gitDiffCacheMetrics.fileHits = 0
|
|
1406
|
+
gitDiffCacheMetrics.fileMisses = 0
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
export function __getGitDiffCacheMetricsForTest() {
|
|
1410
|
+
return {
|
|
1411
|
+
reviewHits: gitDiffCacheMetrics.reviewHits,
|
|
1412
|
+
reviewMisses: gitDiffCacheMetrics.reviewMisses,
|
|
1413
|
+
fileHits: gitDiffCacheMetrics.fileHits,
|
|
1414
|
+
fileMisses: gitDiffCacheMetrics.fileMisses,
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
export function getGitDiffCacheDebugSnapshot() {
|
|
1419
|
+
return {
|
|
1420
|
+
debugEnabled: isGitDiffDebugEnabled(),
|
|
1421
|
+
reviewCacheSize: diffReviewCache.size,
|
|
1422
|
+
fileCacheSize: fileDiffCache.size,
|
|
1423
|
+
metrics: __getGitDiffCacheMetricsForTest(),
|
|
1424
|
+
}
|
|
1425
|
+
}
|