git-jev-stage 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/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +10 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/git-jev-stage.js +2945 -0
- package/dist/index.d.ts +361 -0
- package/dist/index.js +1851 -0
- package/package.json +69 -0
- package/skills/git-jev-stage/SKILL.md +34 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1851 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var JevCoreError = class extends Error {
|
|
3
|
+
};
|
|
4
|
+
var ProviderError = class extends JevCoreError {
|
|
5
|
+
code = "provider";
|
|
6
|
+
status;
|
|
7
|
+
requestId;
|
|
8
|
+
retryable;
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = "ProviderError";
|
|
12
|
+
this.status = options?.status;
|
|
13
|
+
this.requestId = options?.requestId;
|
|
14
|
+
this.retryable = options?.retryable ?? false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var RequestTooLargeError = class extends JevCoreError {
|
|
18
|
+
code = "request-too-large";
|
|
19
|
+
estimatedTokens;
|
|
20
|
+
tokenCeiling;
|
|
21
|
+
constructor(estimatedTokens, tokenCeiling, options) {
|
|
22
|
+
super(
|
|
23
|
+
`the request needs about ${estimatedTokens} tokens, over the ceiling of ${tokenCeiling}`,
|
|
24
|
+
options
|
|
25
|
+
);
|
|
26
|
+
this.name = "RequestTooLargeError";
|
|
27
|
+
this.estimatedTokens = estimatedTokens;
|
|
28
|
+
this.tokenCeiling = tokenCeiling;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var ProviderConfigError = class extends JevCoreError {
|
|
32
|
+
code = "provider-config";
|
|
33
|
+
constructor(message, options) {
|
|
34
|
+
super(message, options);
|
|
35
|
+
this.name = "ProviderConfigError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/core/fakeProvider.ts
|
|
40
|
+
import { readFileSync } from "node:fs";
|
|
41
|
+
|
|
42
|
+
// src/core/jevClient.ts
|
|
43
|
+
import process2 from "node:process";
|
|
44
|
+
import {
|
|
45
|
+
APIConnectionError,
|
|
46
|
+
APIError,
|
|
47
|
+
choice,
|
|
48
|
+
TypeSafeClient
|
|
49
|
+
} from "@typesafe-ai/sdk";
|
|
50
|
+
var DEFAULT_MODEL = "jev-latest";
|
|
51
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
52
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
53
|
+
var TypeSafeJevProvider = class {
|
|
54
|
+
model;
|
|
55
|
+
#client;
|
|
56
|
+
constructor(options = {}) {
|
|
57
|
+
const apiKey = options.apiKey?.trim() ?? "";
|
|
58
|
+
if (apiKey.length === 0) {
|
|
59
|
+
throw new ProviderConfigError("TYPESAFE_API_KEY is not set");
|
|
60
|
+
}
|
|
61
|
+
const model = options.model?.trim() ?? "";
|
|
62
|
+
this.model = model.length > 0 ? model : DEFAULT_MODEL;
|
|
63
|
+
const config = {
|
|
64
|
+
apiKey,
|
|
65
|
+
logLevel: "off",
|
|
66
|
+
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
67
|
+
retry: { maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES }
|
|
68
|
+
};
|
|
69
|
+
const baseURL = options.baseURL?.trim() ?? "";
|
|
70
|
+
if (baseURL.length > 0) {
|
|
71
|
+
config.baseURL = baseURL;
|
|
72
|
+
}
|
|
73
|
+
if (options.fetch !== void 0) {
|
|
74
|
+
config.fetch = options.fetch;
|
|
75
|
+
}
|
|
76
|
+
this.#client = createClient(config);
|
|
77
|
+
}
|
|
78
|
+
async classify(request, options) {
|
|
79
|
+
const specs = Object.entries(request.questions);
|
|
80
|
+
if (specs.length === 0) {
|
|
81
|
+
return { outcomes: {}, usage: { inputTokens: 0, outputTokens: 0 }, model: this.model };
|
|
82
|
+
}
|
|
83
|
+
const questions = {};
|
|
84
|
+
for (const [id, spec] of specs) {
|
|
85
|
+
questions[id] = choice(spec.instructions, { ...spec.options });
|
|
86
|
+
}
|
|
87
|
+
const requestOptions = options?.signal === void 0 ? {} : { signal: options.signal };
|
|
88
|
+
let payload;
|
|
89
|
+
let requestId;
|
|
90
|
+
try {
|
|
91
|
+
const response = await this.#client.systemOne(
|
|
92
|
+
{ state: toStatePayload(request.state), questions, model: this.model },
|
|
93
|
+
requestOptions
|
|
94
|
+
).withResponse();
|
|
95
|
+
payload = response.data;
|
|
96
|
+
requestId = response.requestId;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw toProviderError(error);
|
|
99
|
+
}
|
|
100
|
+
const answers = readAnswers(payload);
|
|
101
|
+
const outcomes = {};
|
|
102
|
+
for (const [id, spec] of specs) {
|
|
103
|
+
const raw = answers[id];
|
|
104
|
+
outcomes[id] = raw === void 0 ? { kind: "missing" } : validateChoiceAnswer(raw, Object.keys(spec.options));
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
outcomes,
|
|
108
|
+
usage: readUsage(payload),
|
|
109
|
+
model: readModel(payload) ?? this.model,
|
|
110
|
+
...requestId === void 0 ? {} : { requestId }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function createProviderFromEnv(env = process2.env) {
|
|
115
|
+
const apiKey = env.TYPESAFE_API_KEY?.trim() ?? "";
|
|
116
|
+
if (apiKey.length === 0) {
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
const baseURL = env.TYPESAFE_BASE_URL?.trim() ?? "";
|
|
120
|
+
return new TypeSafeJevProvider({ apiKey, ...baseURL.length > 0 ? { baseURL } : {} });
|
|
121
|
+
}
|
|
122
|
+
function validateChoiceAnswer(raw, options) {
|
|
123
|
+
const labels = options;
|
|
124
|
+
if (!isRecord(raw)) {
|
|
125
|
+
return { kind: "invalid", reason: "answer is not an object" };
|
|
126
|
+
}
|
|
127
|
+
if (raw.type !== "choice") {
|
|
128
|
+
return { kind: "invalid", reason: "answer type is not choice" };
|
|
129
|
+
}
|
|
130
|
+
const selected = raw.choice;
|
|
131
|
+
if (typeof selected !== "string" || !labels.includes(selected)) {
|
|
132
|
+
return { kind: "invalid", reason: "choice is not one of the options" };
|
|
133
|
+
}
|
|
134
|
+
const confidence = raw.confidence;
|
|
135
|
+
if (!isProbability(confidence)) {
|
|
136
|
+
return { kind: "invalid", reason: "confidence is not a number between 0 and 1" };
|
|
137
|
+
}
|
|
138
|
+
const rawProbabilities = raw.probabilities;
|
|
139
|
+
if (!isRecord(rawProbabilities)) {
|
|
140
|
+
return { kind: "invalid", reason: "probabilities is not an object" };
|
|
141
|
+
}
|
|
142
|
+
const probabilities = {};
|
|
143
|
+
for (const [label, probability] of Object.entries(rawProbabilities)) {
|
|
144
|
+
if (!labels.includes(label)) {
|
|
145
|
+
return { kind: "invalid", reason: "probabilities has a label that is not an option" };
|
|
146
|
+
}
|
|
147
|
+
if (!isProbability(probability)) {
|
|
148
|
+
return {
|
|
149
|
+
kind: "invalid",
|
|
150
|
+
reason: "probabilities has a value that is not a number between 0 and 1"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
probabilities[label] = probability;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
kind: "answer",
|
|
157
|
+
answer: {
|
|
158
|
+
choice: selected,
|
|
159
|
+
confidence,
|
|
160
|
+
probabilities
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function createClient(config) {
|
|
165
|
+
try {
|
|
166
|
+
return new TypeSafeClient(config);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new ProviderConfigError("the TypeSafe client rejected its configuration", {
|
|
169
|
+
cause: error
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function toProviderError(error) {
|
|
174
|
+
if (error instanceof APIError) {
|
|
175
|
+
return new ProviderError(`TypeSafe request failed (${error.status})`, {
|
|
176
|
+
status: error.status,
|
|
177
|
+
requestId: error.requestId,
|
|
178
|
+
retryable: error.status === 429 || error.status >= 500,
|
|
179
|
+
cause: error
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (error instanceof APIConnectionError) {
|
|
183
|
+
return new ProviderError("TypeSafe request failed (network)", {
|
|
184
|
+
retryable: true,
|
|
185
|
+
cause: error
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return new ProviderError("TypeSafe request failed (network)", {
|
|
189
|
+
retryable: false,
|
|
190
|
+
cause: error
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function toStatePayload(state) {
|
|
194
|
+
if (typeof state === "number" || typeof state === "boolean") {
|
|
195
|
+
return String(state);
|
|
196
|
+
}
|
|
197
|
+
return state;
|
|
198
|
+
}
|
|
199
|
+
function readAnswers(payload) {
|
|
200
|
+
if (!isRecord(payload)) {
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
const answers = payload.answers;
|
|
204
|
+
return isRecord(answers) ? answers : {};
|
|
205
|
+
}
|
|
206
|
+
function readUsage(payload) {
|
|
207
|
+
if (!isRecord(payload)) {
|
|
208
|
+
return { inputTokens: 0, outputTokens: 0 };
|
|
209
|
+
}
|
|
210
|
+
const usage = payload.usage;
|
|
211
|
+
if (!isRecord(usage)) {
|
|
212
|
+
return { inputTokens: 0, outputTokens: 0 };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
inputTokens: readCount(usage.input_tokens),
|
|
216
|
+
outputTokens: readCount(usage.output_tokens)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function readCount(value) {
|
|
220
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
221
|
+
}
|
|
222
|
+
function readModel(payload) {
|
|
223
|
+
if (!isRecord(payload)) {
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
const model = payload.model;
|
|
227
|
+
return typeof model === "string" && model.length > 0 ? model : void 0;
|
|
228
|
+
}
|
|
229
|
+
function isProbability(value) {
|
|
230
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
231
|
+
}
|
|
232
|
+
function isRecord(value) {
|
|
233
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/core/fakeProvider.ts
|
|
237
|
+
var FakeProvider = class _FakeProvider {
|
|
238
|
+
requests = [];
|
|
239
|
+
#script;
|
|
240
|
+
constructor(script = {}) {
|
|
241
|
+
this.#script = script;
|
|
242
|
+
}
|
|
243
|
+
async classify(request, options) {
|
|
244
|
+
const recorded = widenRequest(request);
|
|
245
|
+
this.requests.push(recorded);
|
|
246
|
+
this.#script.onRequest?.(recorded);
|
|
247
|
+
const delayMs = this.#script.delayMs ?? 0;
|
|
248
|
+
if (delayMs > 0) {
|
|
249
|
+
await delay(delayMs);
|
|
250
|
+
}
|
|
251
|
+
options?.signal?.throwIfAborted();
|
|
252
|
+
const scriptedError = this.#script.error;
|
|
253
|
+
if (scriptedError !== void 0) {
|
|
254
|
+
throw scriptedError;
|
|
255
|
+
}
|
|
256
|
+
const outcomes = {};
|
|
257
|
+
for (const [id, spec] of Object.entries(request.questions)) {
|
|
258
|
+
const scripted = this.#script.answers?.[id] ?? this.#script.defaultAnswer;
|
|
259
|
+
outcomes[id] = scripted === void 0 ? { kind: "missing" } : validateChoiceAnswer(toPayload(scripted), Object.keys(spec.options));
|
|
260
|
+
}
|
|
261
|
+
return { outcomes, usage: { inputTokens: 0, outputTokens: 0 }, model: "fake" };
|
|
262
|
+
}
|
|
263
|
+
static fromJsonFile(path) {
|
|
264
|
+
let parsed;
|
|
265
|
+
try {
|
|
266
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw new ProviderConfigError(`the fake provider script at ${path} could not be read`, {
|
|
269
|
+
cause: error
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (!isRecord2(parsed)) {
|
|
273
|
+
throw new ProviderConfigError(`the fake provider script at ${path} is not an object`);
|
|
274
|
+
}
|
|
275
|
+
const script = {};
|
|
276
|
+
const answers = parsed.answers;
|
|
277
|
+
if (answers !== void 0) {
|
|
278
|
+
if (!isRecord2(answers)) {
|
|
279
|
+
throw new ProviderConfigError(`answers in ${path} is not an object`);
|
|
280
|
+
}
|
|
281
|
+
const scripted = {};
|
|
282
|
+
for (const [id, value] of Object.entries(answers)) {
|
|
283
|
+
scripted[id] = toScriptedAnswer(value);
|
|
284
|
+
}
|
|
285
|
+
script.answers = scripted;
|
|
286
|
+
}
|
|
287
|
+
const defaultAnswer = parsed.defaultAnswer;
|
|
288
|
+
if (defaultAnswer !== void 0) {
|
|
289
|
+
const answer = isRecord2(defaultAnswer) ? toChoiceAnswer(defaultAnswer) : void 0;
|
|
290
|
+
if (answer === void 0) {
|
|
291
|
+
throw new ProviderConfigError(`defaultAnswer in ${path} is not a choice answer`);
|
|
292
|
+
}
|
|
293
|
+
script.defaultAnswer = answer;
|
|
294
|
+
}
|
|
295
|
+
const errorMessage = parsed.errorMessage;
|
|
296
|
+
if (errorMessage !== void 0) {
|
|
297
|
+
if (typeof errorMessage !== "string" || errorMessage.length === 0) {
|
|
298
|
+
throw new ProviderConfigError(`errorMessage in ${path} is not a non-empty string`);
|
|
299
|
+
}
|
|
300
|
+
script.error = new ProviderError(errorMessage, { retryable: false });
|
|
301
|
+
}
|
|
302
|
+
return new _FakeProvider(script);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
function toPayload(scripted) {
|
|
306
|
+
if ("raw" in scripted) {
|
|
307
|
+
return scripted.raw;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
type: "choice",
|
|
311
|
+
choice: scripted.choice,
|
|
312
|
+
confidence: scripted.confidence,
|
|
313
|
+
probabilities: scripted.probabilities
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function toScriptedAnswer(value) {
|
|
317
|
+
if (!isRecord2(value)) {
|
|
318
|
+
return { raw: value };
|
|
319
|
+
}
|
|
320
|
+
if ("raw" in value) {
|
|
321
|
+
return { raw: value.raw };
|
|
322
|
+
}
|
|
323
|
+
return toChoiceAnswer(value) ?? { raw: value };
|
|
324
|
+
}
|
|
325
|
+
function toChoiceAnswer(value) {
|
|
326
|
+
const selected = value.choice;
|
|
327
|
+
const confidence = value.confidence;
|
|
328
|
+
const rawProbabilities = value.probabilities;
|
|
329
|
+
if (typeof selected !== "string" || typeof confidence !== "number") {
|
|
330
|
+
return void 0;
|
|
331
|
+
}
|
|
332
|
+
if (!isRecord2(rawProbabilities)) {
|
|
333
|
+
return void 0;
|
|
334
|
+
}
|
|
335
|
+
const probabilities = {};
|
|
336
|
+
for (const [label, probability] of Object.entries(rawProbabilities)) {
|
|
337
|
+
if (typeof probability !== "number") {
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
probabilities[label] = probability;
|
|
341
|
+
}
|
|
342
|
+
return { choice: selected, confidence, probabilities };
|
|
343
|
+
}
|
|
344
|
+
function widenRequest(request) {
|
|
345
|
+
const questions = {};
|
|
346
|
+
for (const [id, spec] of Object.entries(request.questions)) {
|
|
347
|
+
questions[id] = { instructions: spec.instructions, options: { ...spec.options } };
|
|
348
|
+
}
|
|
349
|
+
return { state: request.state, questions };
|
|
350
|
+
}
|
|
351
|
+
function delay(ms) {
|
|
352
|
+
return new Promise((resolve2) => {
|
|
353
|
+
setTimeout(resolve2, ms);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function isRecord2(value) {
|
|
357
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/core/tokens.ts
|
|
361
|
+
var CHARS_PER_TOKEN = 4;
|
|
362
|
+
var DEFAULT_TOKEN_CEILING = 25e3;
|
|
363
|
+
var DEFAULT_PER_QUESTION_TOKENS = 40;
|
|
364
|
+
function estimateTokens(text) {
|
|
365
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
366
|
+
}
|
|
367
|
+
function estimateJsonTokens(value) {
|
|
368
|
+
return estimateTokens(JSON.stringify(value));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/core/windowing.ts
|
|
372
|
+
var DEFAULT_CONCURRENCY = 4;
|
|
373
|
+
function buildWindows(items, options) {
|
|
374
|
+
const ceiling = options.tokenCeiling ?? DEFAULT_TOKEN_CEILING;
|
|
375
|
+
const perQuestionTokens = options.perQuestionTokens ?? DEFAULT_PER_QUESTION_TOKENS;
|
|
376
|
+
const withContext = options.neighborContext ?? true;
|
|
377
|
+
const shared = options.sharedTokens;
|
|
378
|
+
const entries = items.map((item, index) => ({
|
|
379
|
+
index,
|
|
380
|
+
id: item.id,
|
|
381
|
+
tokens: estimateTokens(item.text)
|
|
382
|
+
}));
|
|
383
|
+
const neighbors = buildNeighborIndex(items, entries);
|
|
384
|
+
const packed = [];
|
|
385
|
+
let current = [];
|
|
386
|
+
let currentCost = shared;
|
|
387
|
+
for (const entry of entries) {
|
|
388
|
+
const askCost = entry.tokens + perQuestionTokens;
|
|
389
|
+
if (shared + askCost > ceiling) {
|
|
390
|
+
if (current.length > 0) {
|
|
391
|
+
packed.push(current);
|
|
392
|
+
current = [];
|
|
393
|
+
currentCost = shared;
|
|
394
|
+
}
|
|
395
|
+
packed.push([entry]);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (current.length > 0 && currentCost + askCost > ceiling) {
|
|
399
|
+
packed.push(current);
|
|
400
|
+
current = [];
|
|
401
|
+
currentCost = shared;
|
|
402
|
+
}
|
|
403
|
+
current.push(entry);
|
|
404
|
+
currentCost += askCost;
|
|
405
|
+
}
|
|
406
|
+
if (current.length > 0) {
|
|
407
|
+
packed.push(current);
|
|
408
|
+
}
|
|
409
|
+
return packed.map((asks) => {
|
|
410
|
+
const asked = new Set(asks.map((entry) => entry.index));
|
|
411
|
+
const context = [];
|
|
412
|
+
let cost = shared;
|
|
413
|
+
for (const entry of asks) {
|
|
414
|
+
cost += entry.tokens + perQuestionTokens;
|
|
415
|
+
}
|
|
416
|
+
if (withContext) {
|
|
417
|
+
const taken = /* @__PURE__ */ new Set();
|
|
418
|
+
for (const entry of asks) {
|
|
419
|
+
for (const candidate of neighbors.get(entry.index) ?? []) {
|
|
420
|
+
if (asked.has(candidate.index) || taken.has(candidate.index)) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (cost + candidate.tokens > ceiling) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
taken.add(candidate.index);
|
|
427
|
+
context.push(candidate);
|
|
428
|
+
cost += candidate.tokens;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
askIds: asks.map((entry) => entry.id),
|
|
434
|
+
contextIds: context.map((entry) => entry.id),
|
|
435
|
+
estimatedTokens: cost
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async function runWindows(windows, fn, options = {}) {
|
|
440
|
+
options.signal?.throwIfAborted();
|
|
441
|
+
if (windows.length === 0) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
const concurrency = Math.max(1, Math.trunc(options.concurrency ?? DEFAULT_CONCURRENCY));
|
|
445
|
+
const results = new Array(windows.length);
|
|
446
|
+
const controller = new AbortController();
|
|
447
|
+
const external = options.signal;
|
|
448
|
+
const forwardAbort = () => {
|
|
449
|
+
controller.abort(external?.reason);
|
|
450
|
+
};
|
|
451
|
+
external?.addEventListener("abort", forwardAbort, { once: true });
|
|
452
|
+
let cursor = 0;
|
|
453
|
+
let failed = false;
|
|
454
|
+
let failure;
|
|
455
|
+
const worker = async () => {
|
|
456
|
+
while (!failed) {
|
|
457
|
+
const index = cursor;
|
|
458
|
+
cursor += 1;
|
|
459
|
+
const window = windows[index];
|
|
460
|
+
if (window === void 0) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
results[index] = await fn(window, index, controller.signal);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (!failed) {
|
|
467
|
+
failed = true;
|
|
468
|
+
failure = error;
|
|
469
|
+
controller.abort();
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
try {
|
|
476
|
+
await Promise.all(
|
|
477
|
+
Array.from({ length: Math.min(concurrency, windows.length) }, () => worker())
|
|
478
|
+
);
|
|
479
|
+
} finally {
|
|
480
|
+
external?.removeEventListener("abort", forwardAbort);
|
|
481
|
+
}
|
|
482
|
+
if (failed) {
|
|
483
|
+
throw failure;
|
|
484
|
+
}
|
|
485
|
+
return results;
|
|
486
|
+
}
|
|
487
|
+
function buildNeighborIndex(items, entries) {
|
|
488
|
+
const groups = /* @__PURE__ */ new Map();
|
|
489
|
+
items.forEach((item, index) => {
|
|
490
|
+
const entry = entries[index];
|
|
491
|
+
if (entry === void 0) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const bucket = groups.get(item.group);
|
|
495
|
+
if (bucket === void 0) {
|
|
496
|
+
groups.set(item.group, [entry]);
|
|
497
|
+
} else {
|
|
498
|
+
bucket.push(entry);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
const neighbors = /* @__PURE__ */ new Map();
|
|
502
|
+
for (const bucket of groups.values()) {
|
|
503
|
+
const ordered = [...bucket].sort((left, right) => {
|
|
504
|
+
const leftItem = items[left.index];
|
|
505
|
+
const rightItem = items[right.index];
|
|
506
|
+
if (leftItem === void 0 || rightItem === void 0) {
|
|
507
|
+
return left.index - right.index;
|
|
508
|
+
}
|
|
509
|
+
return leftItem.ordinal - rightItem.ordinal || left.index - right.index;
|
|
510
|
+
});
|
|
511
|
+
ordered.forEach((entry, position) => {
|
|
512
|
+
const candidates = [];
|
|
513
|
+
const previous = ordered[position - 1];
|
|
514
|
+
const next = ordered[position + 1];
|
|
515
|
+
if (previous !== void 0) {
|
|
516
|
+
candidates.push(previous);
|
|
517
|
+
}
|
|
518
|
+
if (next !== void 0) {
|
|
519
|
+
candidates.push(next);
|
|
520
|
+
}
|
|
521
|
+
neighbors.set(entry.index, candidates);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return neighbors;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/errors.ts
|
|
528
|
+
var JevStageError = class extends Error {
|
|
529
|
+
exitCode = 1;
|
|
530
|
+
};
|
|
531
|
+
var UsageError = class extends JevStageError {
|
|
532
|
+
code = "usage";
|
|
533
|
+
exitCode = 2;
|
|
534
|
+
constructor(message, options) {
|
|
535
|
+
super(message, options);
|
|
536
|
+
this.name = "UsageError";
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
var NotARepositoryError = class extends JevStageError {
|
|
540
|
+
code = "not-a-repository";
|
|
541
|
+
constructor(message = "not a git repository", options) {
|
|
542
|
+
super(message, options);
|
|
543
|
+
this.name = "NotARepositoryError";
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
var UnbornRepositoryError = class extends JevStageError {
|
|
547
|
+
code = "unborn-repository";
|
|
548
|
+
constructor(message = "the repository has no commits yet", options) {
|
|
549
|
+
super(message, options);
|
|
550
|
+
this.name = "UnbornRepositoryError";
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
var UnsupportedGitVersionError = class extends JevStageError {
|
|
554
|
+
code = "unsupported-git-version";
|
|
555
|
+
found;
|
|
556
|
+
minimum;
|
|
557
|
+
constructor(found, minimum, options) {
|
|
558
|
+
super(`git ${found} is too old; ${minimum} or newer is required`, options);
|
|
559
|
+
this.name = "UnsupportedGitVersionError";
|
|
560
|
+
this.found = found;
|
|
561
|
+
this.minimum = minimum;
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
var UnmergedEntriesError = class extends JevStageError {
|
|
565
|
+
code = "unmerged-entries";
|
|
566
|
+
paths;
|
|
567
|
+
constructor(paths, options) {
|
|
568
|
+
super(`resolve the unmerged paths first: ${paths.join(", ")}`, options);
|
|
569
|
+
this.name = "UnmergedEntriesError";
|
|
570
|
+
this.paths = [...paths];
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var UnsupportedEntryError = class extends JevStageError {
|
|
574
|
+
code = "unsupported-entry";
|
|
575
|
+
kind;
|
|
576
|
+
paths;
|
|
577
|
+
constructor(kind, paths, options) {
|
|
578
|
+
super(`${kind} entries cannot be staged by hunk: ${paths.join(", ")}`, options);
|
|
579
|
+
this.name = "UnsupportedEntryError";
|
|
580
|
+
this.kind = kind;
|
|
581
|
+
this.paths = [...paths];
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
var DiffParseError = class extends JevStageError {
|
|
585
|
+
code = "diff-parse";
|
|
586
|
+
constructor(message, options) {
|
|
587
|
+
super(message, options);
|
|
588
|
+
this.name = "DiffParseError";
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
var UnknownHunkError = class extends JevStageError {
|
|
592
|
+
code = "unknown-hunk";
|
|
593
|
+
ids;
|
|
594
|
+
constructor(ids, options) {
|
|
595
|
+
super(`no hunk matches the selected ids: ${ids.join(", ")}`, options);
|
|
596
|
+
this.name = "UnknownHunkError";
|
|
597
|
+
this.ids = [...ids];
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
var PatchApplyError = class extends JevStageError {
|
|
601
|
+
code = "patch-apply";
|
|
602
|
+
stderr;
|
|
603
|
+
constructor(stderr, options) {
|
|
604
|
+
const detail = stderr.trim();
|
|
605
|
+
super(detail.length > 0 ? `git apply failed: ${detail}` : "git apply failed", options);
|
|
606
|
+
this.name = "PatchApplyError";
|
|
607
|
+
this.stderr = detail;
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
var STALE_SNAPSHOT_MESSAGES = {
|
|
611
|
+
head: "HEAD moved while the plan was being reviewed",
|
|
612
|
+
index: "the index changed while the plan was being reviewed",
|
|
613
|
+
diff: "the working tree changed while the plan was being reviewed"
|
|
614
|
+
};
|
|
615
|
+
var StaleSnapshotError = class extends JevStageError {
|
|
616
|
+
code = "stale-snapshot";
|
|
617
|
+
exitCode = 3;
|
|
618
|
+
which;
|
|
619
|
+
constructor(which, options) {
|
|
620
|
+
super(`${STALE_SNAPSHOT_MESSAGES[which]}; nothing was staged`, options);
|
|
621
|
+
this.name = "StaleSnapshotError";
|
|
622
|
+
this.which = which;
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
var IndexLockedError = class extends JevStageError {
|
|
626
|
+
code = "index-locked";
|
|
627
|
+
exitCode = 3;
|
|
628
|
+
lockPath;
|
|
629
|
+
constructor(lockPath, options) {
|
|
630
|
+
super(`another git process holds ${lockPath}`, options);
|
|
631
|
+
this.name = "IndexLockedError";
|
|
632
|
+
this.lockPath = lockPath;
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
var MissingApiKeyError = class extends JevStageError {
|
|
636
|
+
code = "missing-api-key";
|
|
637
|
+
constructor(message = "TYPESAFE_API_KEY is not set", options) {
|
|
638
|
+
super(message, options);
|
|
639
|
+
this.name = "MissingApiKeyError";
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
var GitCommandError = class extends JevStageError {
|
|
643
|
+
code = "git-command";
|
|
644
|
+
argv;
|
|
645
|
+
gitExitCode;
|
|
646
|
+
stderr;
|
|
647
|
+
constructor(argv, exitCode, stderr, options) {
|
|
648
|
+
const detail = stderr.trim();
|
|
649
|
+
const command = `git ${argv.join(" ")}`;
|
|
650
|
+
super(
|
|
651
|
+
detail.length > 0 ? `${command} exited ${exitCode}: ${detail}` : `${command} exited ${exitCode}`,
|
|
652
|
+
options
|
|
653
|
+
);
|
|
654
|
+
this.name = "GitCommandError";
|
|
655
|
+
this.argv = [...argv];
|
|
656
|
+
this.gitExitCode = exitCode;
|
|
657
|
+
this.stderr = detail;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// src/git/applySelection.ts
|
|
662
|
+
import { randomBytes } from "node:crypto";
|
|
663
|
+
import {
|
|
664
|
+
closeSync,
|
|
665
|
+
constants,
|
|
666
|
+
copyFileSync,
|
|
667
|
+
fsyncSync,
|
|
668
|
+
openSync,
|
|
669
|
+
readdirSync,
|
|
670
|
+
readFileSync as readFileSync3,
|
|
671
|
+
renameSync,
|
|
672
|
+
rmSync,
|
|
673
|
+
writeSync
|
|
674
|
+
} from "node:fs";
|
|
675
|
+
import { basename, dirname, join } from "node:path";
|
|
676
|
+
|
|
677
|
+
// src/git/composePatch.ts
|
|
678
|
+
function composePatch(files, selectedIds) {
|
|
679
|
+
assertKnownIds(files, selectedIds);
|
|
680
|
+
const parts = [];
|
|
681
|
+
let total2 = 0;
|
|
682
|
+
for (const file of files) {
|
|
683
|
+
const selected = selectHunks(file, selectedIds);
|
|
684
|
+
if (selected.length === 0) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
parts.push(file.headerBytes);
|
|
688
|
+
total2 += file.headerBytes.length;
|
|
689
|
+
for (const hunk of selected) {
|
|
690
|
+
parts.push(hunk.bytes);
|
|
691
|
+
total2 += hunk.bytes.length;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return Buffer.concat(parts, total2);
|
|
695
|
+
}
|
|
696
|
+
function summarizeSelection(files, selectedIds) {
|
|
697
|
+
assertKnownIds(files, selectedIds);
|
|
698
|
+
const perFile = [];
|
|
699
|
+
let hunks = 0;
|
|
700
|
+
let added = 0;
|
|
701
|
+
let removed = 0;
|
|
702
|
+
for (const file of files) {
|
|
703
|
+
const selected = selectHunks(file, selectedIds);
|
|
704
|
+
if (selected.length === 0) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const fileAdded = total(selected, (hunk) => hunk.added);
|
|
708
|
+
const fileRemoved = total(selected, (hunk) => hunk.removed);
|
|
709
|
+
perFile.push({
|
|
710
|
+
path: file.path,
|
|
711
|
+
hunks: selected.length,
|
|
712
|
+
added: fileAdded,
|
|
713
|
+
removed: fileRemoved
|
|
714
|
+
});
|
|
715
|
+
hunks += selected.length;
|
|
716
|
+
added += fileAdded;
|
|
717
|
+
removed += fileRemoved;
|
|
718
|
+
}
|
|
719
|
+
return { files: perFile.length, hunks, added, removed, perFile };
|
|
720
|
+
}
|
|
721
|
+
function selectHunks(file, selectedIds) {
|
|
722
|
+
return file.hunks.filter((hunk) => selectedIds.has(hunk.id));
|
|
723
|
+
}
|
|
724
|
+
function assertKnownIds(files, selectedIds) {
|
|
725
|
+
const known = /* @__PURE__ */ new Set();
|
|
726
|
+
for (const file of files) {
|
|
727
|
+
for (const hunk of file.hunks) {
|
|
728
|
+
known.add(hunk.id);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const unknown = [...selectedIds].filter((id) => !known.has(id)).sort();
|
|
732
|
+
if (unknown.length > 0) {
|
|
733
|
+
throw new UnknownHunkError(unknown);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function total(hunks, pick) {
|
|
737
|
+
return hunks.reduce((sum, hunk) => sum + pick(hunk), 0);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/git/hash.ts
|
|
741
|
+
import { createHash } from "node:crypto";
|
|
742
|
+
var HUNK_ID_LENGTH = 16;
|
|
743
|
+
var SEPARATOR = Buffer.from([0]);
|
|
744
|
+
function hashBytes(bytes) {
|
|
745
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
746
|
+
}
|
|
747
|
+
function hashHunkId(path, bytes) {
|
|
748
|
+
return createHash("sha256").update(Buffer.from(path, "utf8")).update(SEPARATOR).update(bytes).digest("hex").slice(0, HUNK_ID_LENGTH);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/git/runGit.ts
|
|
752
|
+
import { spawn } from "node:child_process";
|
|
753
|
+
var DEFAULT_MAX_BUFFER = 512 * 1024 * 1024;
|
|
754
|
+
var NO_EXIT_CODE = -1;
|
|
755
|
+
function createGitRunner(gitBinary = "git") {
|
|
756
|
+
return {
|
|
757
|
+
run(args, options) {
|
|
758
|
+
return spawnGit(gitBinary, [...args], options);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
async function runGitOrThrow(git, args, options) {
|
|
763
|
+
const result = await git.run(args, options);
|
|
764
|
+
if (result.exitCode !== 0) {
|
|
765
|
+
throw new GitCommandError(args, result.exitCode, result.stderr);
|
|
766
|
+
}
|
|
767
|
+
return result;
|
|
768
|
+
}
|
|
769
|
+
function spawnGit(gitBinary, args, options) {
|
|
770
|
+
const maxBuffer = options?.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
771
|
+
const extraEnv = options?.env;
|
|
772
|
+
const input = options?.stdin;
|
|
773
|
+
return new Promise((resolve2, reject) => {
|
|
774
|
+
const child = spawn(gitBinary, args, {
|
|
775
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
776
|
+
env: extraEnv === void 0 ? process.env : { ...process.env, ...extraEnv },
|
|
777
|
+
windowsHide: true
|
|
778
|
+
});
|
|
779
|
+
const stdoutChunks = [];
|
|
780
|
+
const stderrChunks = [];
|
|
781
|
+
let stdoutLength = 0;
|
|
782
|
+
let settled = false;
|
|
783
|
+
const fail = (error) => {
|
|
784
|
+
if (settled) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
settled = true;
|
|
788
|
+
child.kill("SIGKILL");
|
|
789
|
+
reject(error);
|
|
790
|
+
};
|
|
791
|
+
child.on("error", (error) => {
|
|
792
|
+
fail(new GitCommandError(args, NO_EXIT_CODE, error.message, { cause: error }));
|
|
793
|
+
});
|
|
794
|
+
child.stdout.on("data", (chunk) => {
|
|
795
|
+
stdoutLength += chunk.length;
|
|
796
|
+
if (stdoutLength > maxBuffer) {
|
|
797
|
+
fail(new GitCommandError(args, NO_EXIT_CODE, `stdout exceeded ${maxBuffer} bytes`));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
stdoutChunks.push(chunk);
|
|
801
|
+
});
|
|
802
|
+
child.stderr.on("data", (chunk) => {
|
|
803
|
+
stderrChunks.push(chunk);
|
|
804
|
+
});
|
|
805
|
+
child.stdin.on("error", (error) => {
|
|
806
|
+
if (error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED") {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
fail(new GitCommandError(args, NO_EXIT_CODE, error.message, { cause: error }));
|
|
810
|
+
});
|
|
811
|
+
if (input === void 0) {
|
|
812
|
+
child.stdin.end();
|
|
813
|
+
} else {
|
|
814
|
+
child.stdin.end(input);
|
|
815
|
+
}
|
|
816
|
+
child.on("close", (code) => {
|
|
817
|
+
if (settled) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
settled = true;
|
|
821
|
+
resolve2({
|
|
822
|
+
stdout: Buffer.concat(stdoutChunks, stdoutLength),
|
|
823
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
824
|
+
exitCode: code ?? NO_EXIT_CODE
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/git/snapshot.ts
|
|
831
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
832
|
+
import { resolve } from "node:path";
|
|
833
|
+
|
|
834
|
+
// src/git/parseDiff.ts
|
|
835
|
+
var LF = 10;
|
|
836
|
+
var CR = 13;
|
|
837
|
+
var SPACE = 32;
|
|
838
|
+
var QUOTE = 34;
|
|
839
|
+
var PLUS = 43;
|
|
840
|
+
var MINUS = 45;
|
|
841
|
+
var BACKSLASH = 92;
|
|
842
|
+
var FILE_HEADER = "diff --git ";
|
|
843
|
+
var HUNK_HEADER = "@@ ";
|
|
844
|
+
var OLD_FILE = "--- ";
|
|
845
|
+
var NEW_FILE = "+++ ";
|
|
846
|
+
var NEW_FILE_MODE = "new file mode ";
|
|
847
|
+
var DELETED_FILE_MODE = "deleted file mode ";
|
|
848
|
+
var OLD_MODE = "old mode ";
|
|
849
|
+
var NEW_MODE = "new mode ";
|
|
850
|
+
var INDEX_LINE = "index ";
|
|
851
|
+
var BINARY_PATCH = "GIT binary patch";
|
|
852
|
+
var BINARY_FILES = "Binary files ";
|
|
853
|
+
var BINARY_FILES_SUFFIX = " differ";
|
|
854
|
+
var DEV_NULL = "/dev/null";
|
|
855
|
+
var SYMLINK_MODE = "120000";
|
|
856
|
+
var SUBMODULE_MODE = "160000";
|
|
857
|
+
var SIDE_PREFIXES = ["a/", "b/"];
|
|
858
|
+
var DIFF_GIT_PATHS_OVERHEAD = "a/ b/".length;
|
|
859
|
+
var UNSUPPORTED_KIND_ORDER = ["binary", "symlink", "submodule"];
|
|
860
|
+
var SIMPLE_ESCAPES = /* @__PURE__ */ new Map([
|
|
861
|
+
[34, 34],
|
|
862
|
+
[92, 92],
|
|
863
|
+
[97, 7],
|
|
864
|
+
[98, 8],
|
|
865
|
+
[102, 12],
|
|
866
|
+
[110, 10],
|
|
867
|
+
[114, 13],
|
|
868
|
+
[116, 9],
|
|
869
|
+
[118, 11]
|
|
870
|
+
]);
|
|
871
|
+
function parseUnifiedDiff(diff) {
|
|
872
|
+
const parsed = parseUnifiedDiffDetailed(diff);
|
|
873
|
+
const error = buildUnsupportedEntryError(parsed.unsupported);
|
|
874
|
+
if (error !== void 0) {
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
return parsed.files;
|
|
878
|
+
}
|
|
879
|
+
function parseUnifiedDiffDetailed(diff) {
|
|
880
|
+
if (diff.length === 0) {
|
|
881
|
+
return { files: [], unsupported: [] };
|
|
882
|
+
}
|
|
883
|
+
const lines = splitLines(diff);
|
|
884
|
+
const sectionStarts = findSectionStarts(diff, lines);
|
|
885
|
+
if (sectionStarts[0] !== 0) {
|
|
886
|
+
throw new DiffParseError("the diff does not start with a 'diff --git' header");
|
|
887
|
+
}
|
|
888
|
+
const files = [];
|
|
889
|
+
const unsupported = [];
|
|
890
|
+
for (let index = 0; index < sectionStarts.length; index += 1) {
|
|
891
|
+
const startLine = sectionStarts[index];
|
|
892
|
+
if (startLine === void 0) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const endLine = sectionStarts[index + 1] ?? lines.length;
|
|
896
|
+
const file = parseSection(diff, lines, startLine, endLine);
|
|
897
|
+
files.push(file.file);
|
|
898
|
+
if (file.unsupported !== void 0) {
|
|
899
|
+
unsupported.push({ path: file.file.path, kind: file.unsupported });
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
assertByteExact(diff, files);
|
|
903
|
+
return { files, unsupported };
|
|
904
|
+
}
|
|
905
|
+
function buildUnsupportedEntryError(entries) {
|
|
906
|
+
for (const kind of UNSUPPORTED_KIND_ORDER) {
|
|
907
|
+
const paths = entries.filter((entry) => entry.kind === kind).map((entry) => entry.path);
|
|
908
|
+
if (paths.length > 0) {
|
|
909
|
+
return new UnsupportedEntryError(kind, paths);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return void 0;
|
|
913
|
+
}
|
|
914
|
+
function parseSection(diff, lines, startLine, endLine) {
|
|
915
|
+
const headerLine = lines[startLine];
|
|
916
|
+
const lastLine = lines[endLine - 1];
|
|
917
|
+
if (headerLine === void 0 || lastLine === void 0) {
|
|
918
|
+
throw new DiffParseError("the diff ends inside a file section");
|
|
919
|
+
}
|
|
920
|
+
let firstHunkLine = endLine;
|
|
921
|
+
for (let index = startLine + 1; index < endLine; index += 1) {
|
|
922
|
+
const line = lines[index];
|
|
923
|
+
if (line !== void 0 && lineStartsWith(diff, line, HUNK_HEADER)) {
|
|
924
|
+
firstHunkLine = index;
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const meta = readFileMeta(diff, lines, startLine, firstHunkLine);
|
|
929
|
+
const path = resolvePath(diff, headerLine, meta);
|
|
930
|
+
const hunkStarts = findHunkStarts(diff, lines, firstHunkLine, endLine);
|
|
931
|
+
const hunks = buildHunks(diff, lines, hunkStarts, endLine, path);
|
|
932
|
+
const headerEnd = firstHunkLine < endLine ? lines[firstHunkLine]?.start ?? lastLine.end : lastLine.end;
|
|
933
|
+
return {
|
|
934
|
+
file: {
|
|
935
|
+
path,
|
|
936
|
+
kind: resolveKind(meta, hunks.length),
|
|
937
|
+
headerBytes: diff.subarray(headerLine.start, headerEnd),
|
|
938
|
+
hunks
|
|
939
|
+
},
|
|
940
|
+
unsupported: resolveUnsupportedKind(meta)
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function readFileMeta(diff, lines, startLine, headerEndLine) {
|
|
944
|
+
const meta = {
|
|
945
|
+
newPath: void 0,
|
|
946
|
+
oldPath: void 0,
|
|
947
|
+
isNewFile: false,
|
|
948
|
+
isDeletedFile: false,
|
|
949
|
+
hasOldMode: false,
|
|
950
|
+
hasNewMode: false,
|
|
951
|
+
binary: false,
|
|
952
|
+
symlink: false,
|
|
953
|
+
submodule: false
|
|
954
|
+
};
|
|
955
|
+
for (let index = startLine + 1; index < headerEndLine; index += 1) {
|
|
956
|
+
const line = lines[index];
|
|
957
|
+
if (line === void 0) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (lineStartsWith(diff, line, NEW_FILE)) {
|
|
961
|
+
meta.newPath = pathFromSideLine(diff, line);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
if (lineStartsWith(diff, line, OLD_FILE)) {
|
|
965
|
+
meta.oldPath = pathFromSideLine(diff, line);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const text = decodeLine(diff, line);
|
|
969
|
+
if (text === BINARY_PATCH) {
|
|
970
|
+
meta.binary = true;
|
|
971
|
+
} else if (text.startsWith(BINARY_FILES) && text.endsWith(BINARY_FILES_SUFFIX)) {
|
|
972
|
+
meta.binary = true;
|
|
973
|
+
} else if (text.startsWith(NEW_FILE_MODE)) {
|
|
974
|
+
meta.isNewFile = true;
|
|
975
|
+
applyMode(meta, text.slice(NEW_FILE_MODE.length));
|
|
976
|
+
} else if (text.startsWith(DELETED_FILE_MODE)) {
|
|
977
|
+
meta.isDeletedFile = true;
|
|
978
|
+
applyMode(meta, text.slice(DELETED_FILE_MODE.length));
|
|
979
|
+
} else if (text.startsWith(OLD_MODE)) {
|
|
980
|
+
meta.hasOldMode = true;
|
|
981
|
+
applyMode(meta, text.slice(OLD_MODE.length));
|
|
982
|
+
} else if (text.startsWith(NEW_MODE)) {
|
|
983
|
+
meta.hasNewMode = true;
|
|
984
|
+
applyMode(meta, text.slice(NEW_MODE.length));
|
|
985
|
+
} else if (text.startsWith(INDEX_LINE)) {
|
|
986
|
+
applyMode(meta, text.split(" ")[2] ?? "");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return meta;
|
|
990
|
+
}
|
|
991
|
+
function applyMode(meta, mode) {
|
|
992
|
+
const value = mode.trim();
|
|
993
|
+
if (value === SYMLINK_MODE) {
|
|
994
|
+
meta.symlink = true;
|
|
995
|
+
} else if (value === SUBMODULE_MODE) {
|
|
996
|
+
meta.submodule = true;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function resolveKind(meta, hunkCount) {
|
|
1000
|
+
if (meta.isNewFile) {
|
|
1001
|
+
return "added";
|
|
1002
|
+
}
|
|
1003
|
+
if (meta.isDeletedFile) {
|
|
1004
|
+
return "deleted";
|
|
1005
|
+
}
|
|
1006
|
+
if (hunkCount === 0 && meta.hasOldMode && meta.hasNewMode && !meta.binary) {
|
|
1007
|
+
return "mode-only";
|
|
1008
|
+
}
|
|
1009
|
+
return "modified";
|
|
1010
|
+
}
|
|
1011
|
+
function resolveUnsupportedKind(meta) {
|
|
1012
|
+
if (meta.submodule) {
|
|
1013
|
+
return "submodule";
|
|
1014
|
+
}
|
|
1015
|
+
if (meta.symlink) {
|
|
1016
|
+
return "symlink";
|
|
1017
|
+
}
|
|
1018
|
+
if (meta.binary) {
|
|
1019
|
+
return "binary";
|
|
1020
|
+
}
|
|
1021
|
+
return void 0;
|
|
1022
|
+
}
|
|
1023
|
+
function resolvePath(diff, headerLine, meta) {
|
|
1024
|
+
if (meta.newPath !== void 0 && meta.newPath !== DEV_NULL) {
|
|
1025
|
+
return meta.newPath;
|
|
1026
|
+
}
|
|
1027
|
+
if (meta.oldPath !== void 0 && meta.oldPath !== DEV_NULL) {
|
|
1028
|
+
return meta.oldPath;
|
|
1029
|
+
}
|
|
1030
|
+
return pathFromFileHeaderLine(diff, headerLine);
|
|
1031
|
+
}
|
|
1032
|
+
function findSectionStarts(diff, lines) {
|
|
1033
|
+
const starts = [];
|
|
1034
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1035
|
+
const line = lines[index];
|
|
1036
|
+
if (line !== void 0 && lineStartsWith(diff, line, FILE_HEADER)) {
|
|
1037
|
+
starts.push(index);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return starts;
|
|
1041
|
+
}
|
|
1042
|
+
function findHunkStarts(diff, lines, from, to) {
|
|
1043
|
+
const starts = [];
|
|
1044
|
+
for (let index = from; index < to; index += 1) {
|
|
1045
|
+
const line = lines[index];
|
|
1046
|
+
if (line !== void 0 && lineStartsWith(diff, line, HUNK_HEADER)) {
|
|
1047
|
+
starts.push(index);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return starts;
|
|
1051
|
+
}
|
|
1052
|
+
function buildHunks(diff, lines, hunkStarts, sectionEndLine, path) {
|
|
1053
|
+
const hunks = [];
|
|
1054
|
+
for (let ordinal = 0; ordinal < hunkStarts.length; ordinal += 1) {
|
|
1055
|
+
const startLine = hunkStarts[ordinal];
|
|
1056
|
+
if (startLine === void 0) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const endLine = hunkStarts[ordinal + 1] ?? sectionEndLine;
|
|
1060
|
+
const first = lines[startLine];
|
|
1061
|
+
const last = lines[endLine - 1];
|
|
1062
|
+
if (first === void 0 || last === void 0) {
|
|
1063
|
+
throw new DiffParseError("the diff ends inside a hunk");
|
|
1064
|
+
}
|
|
1065
|
+
const bytes = diff.subarray(first.start, last.end);
|
|
1066
|
+
let added = 0;
|
|
1067
|
+
let removed = 0;
|
|
1068
|
+
for (let index = startLine + 1; index < endLine; index += 1) {
|
|
1069
|
+
const line = lines[index];
|
|
1070
|
+
if (line === void 0) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const marker = diff[line.start];
|
|
1074
|
+
if (marker === PLUS) {
|
|
1075
|
+
added += 1;
|
|
1076
|
+
} else if (marker === MINUS) {
|
|
1077
|
+
removed += 1;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
hunks.push({
|
|
1081
|
+
id: hashHunkId(path, bytes),
|
|
1082
|
+
path,
|
|
1083
|
+
ordinal,
|
|
1084
|
+
header: decodeLine(diff, first),
|
|
1085
|
+
bytes,
|
|
1086
|
+
text: bytes.toString("utf8"),
|
|
1087
|
+
added,
|
|
1088
|
+
removed
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
return hunks;
|
|
1092
|
+
}
|
|
1093
|
+
function assertByteExact(diff, files) {
|
|
1094
|
+
const parts = [];
|
|
1095
|
+
let total2 = 0;
|
|
1096
|
+
for (const file of files) {
|
|
1097
|
+
parts.push(file.headerBytes);
|
|
1098
|
+
total2 += file.headerBytes.length;
|
|
1099
|
+
for (const hunk of file.hunks) {
|
|
1100
|
+
parts.push(hunk.bytes);
|
|
1101
|
+
total2 += hunk.bytes.length;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
if (total2 !== diff.length) {
|
|
1105
|
+
throw new DiffParseError(`the parsed diff covers ${total2} of ${diff.length} bytes`);
|
|
1106
|
+
}
|
|
1107
|
+
if (!Buffer.concat(parts, total2).equals(diff)) {
|
|
1108
|
+
throw new DiffParseError("the parsed diff does not reassemble to the input bytes");
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function splitLines(diff) {
|
|
1112
|
+
const lines = [];
|
|
1113
|
+
let start = 0;
|
|
1114
|
+
while (start < diff.length) {
|
|
1115
|
+
const newline = diff.indexOf(LF, start);
|
|
1116
|
+
if (newline === -1) {
|
|
1117
|
+
lines.push({ start, end: diff.length });
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
lines.push({ start, end: newline + 1 });
|
|
1121
|
+
start = newline + 1;
|
|
1122
|
+
}
|
|
1123
|
+
return lines;
|
|
1124
|
+
}
|
|
1125
|
+
function lineStartsWith(diff, line, prefix) {
|
|
1126
|
+
if (line.end - line.start < prefix.length) {
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
return diff.toString("latin1", line.start, line.start + prefix.length) === prefix;
|
|
1130
|
+
}
|
|
1131
|
+
function contentEnd(diff, line) {
|
|
1132
|
+
let end = line.end;
|
|
1133
|
+
if (end > line.start && diff[end - 1] === LF) {
|
|
1134
|
+
end -= 1;
|
|
1135
|
+
}
|
|
1136
|
+
if (end > line.start && diff[end - 1] === CR) {
|
|
1137
|
+
end -= 1;
|
|
1138
|
+
}
|
|
1139
|
+
return end;
|
|
1140
|
+
}
|
|
1141
|
+
function decodeLine(diff, line) {
|
|
1142
|
+
return diff.toString("utf8", line.start, contentEnd(diff, line));
|
|
1143
|
+
}
|
|
1144
|
+
function pathFromSideLine(diff, line) {
|
|
1145
|
+
const raw = diff.subarray(line.start + OLD_FILE.length, contentEnd(diff, line));
|
|
1146
|
+
if (raw[0] === QUOTE) {
|
|
1147
|
+
return stripSidePrefix(unquoteCStyle(raw));
|
|
1148
|
+
}
|
|
1149
|
+
let value = raw.toString("utf8");
|
|
1150
|
+
if (value.includes(" ") && value.endsWith(" ")) {
|
|
1151
|
+
value = value.slice(0, -1);
|
|
1152
|
+
}
|
|
1153
|
+
return stripSidePrefix(value);
|
|
1154
|
+
}
|
|
1155
|
+
function pathFromFileHeaderLine(diff, line) {
|
|
1156
|
+
const raw = diff.subarray(line.start + FILE_HEADER.length, contentEnd(diff, line));
|
|
1157
|
+
if (raw[0] === QUOTE) {
|
|
1158
|
+
const secondStart = quotedTokenEnd(raw, 0) + 1;
|
|
1159
|
+
if (raw[secondStart - 1] !== SPACE || raw[secondStart] !== QUOTE) {
|
|
1160
|
+
throw new DiffParseError("malformed 'diff --git' header");
|
|
1161
|
+
}
|
|
1162
|
+
return stripSidePrefix(unquoteCStyle(raw.subarray(secondStart)));
|
|
1163
|
+
}
|
|
1164
|
+
const remaining = raw.length - DIFF_GIT_PATHS_OVERHEAD;
|
|
1165
|
+
if (remaining <= 0 || remaining % 2 !== 0) {
|
|
1166
|
+
throw new DiffParseError("malformed 'diff --git' header");
|
|
1167
|
+
}
|
|
1168
|
+
return raw.subarray(raw.length - remaining / 2).toString("utf8");
|
|
1169
|
+
}
|
|
1170
|
+
function stripSidePrefix(value) {
|
|
1171
|
+
if (value === DEV_NULL) {
|
|
1172
|
+
return DEV_NULL;
|
|
1173
|
+
}
|
|
1174
|
+
for (const prefix of SIDE_PREFIXES) {
|
|
1175
|
+
if (value.startsWith(prefix)) {
|
|
1176
|
+
return value.slice(prefix.length);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return value;
|
|
1180
|
+
}
|
|
1181
|
+
function quotedTokenEnd(raw, start) {
|
|
1182
|
+
let index = start + 1;
|
|
1183
|
+
while (index < raw.length) {
|
|
1184
|
+
const byte = raw[index];
|
|
1185
|
+
if (byte === BACKSLASH) {
|
|
1186
|
+
index += 2;
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (byte === QUOTE) {
|
|
1190
|
+
return index + 1;
|
|
1191
|
+
}
|
|
1192
|
+
index += 1;
|
|
1193
|
+
}
|
|
1194
|
+
throw new DiffParseError("unterminated quoted path in the diff header");
|
|
1195
|
+
}
|
|
1196
|
+
function unquoteCStyle(raw) {
|
|
1197
|
+
const out = [];
|
|
1198
|
+
let index = 1;
|
|
1199
|
+
let closed = false;
|
|
1200
|
+
while (index < raw.length) {
|
|
1201
|
+
const byte = raw[index];
|
|
1202
|
+
if (byte === void 0) {
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
if (byte === QUOTE) {
|
|
1206
|
+
closed = true;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
if (byte !== BACKSLASH) {
|
|
1210
|
+
out.push(byte);
|
|
1211
|
+
index += 1;
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
const escaped = raw[index + 1];
|
|
1215
|
+
if (escaped === void 0) {
|
|
1216
|
+
throw new DiffParseError("truncated escape in a quoted diff path");
|
|
1217
|
+
}
|
|
1218
|
+
if (escaped >= 48 && escaped <= 55) {
|
|
1219
|
+
let value = 0;
|
|
1220
|
+
let digits = 0;
|
|
1221
|
+
while (digits < 3) {
|
|
1222
|
+
const digit = raw[index + 1 + digits];
|
|
1223
|
+
if (digit === void 0 || digit < 48 || digit > 55) {
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
value = value * 8 + (digit - 48);
|
|
1227
|
+
digits += 1;
|
|
1228
|
+
}
|
|
1229
|
+
if (value > 255) {
|
|
1230
|
+
throw new DiffParseError("octal escape out of range in a quoted diff path");
|
|
1231
|
+
}
|
|
1232
|
+
out.push(value);
|
|
1233
|
+
index += 1 + digits;
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
const mapped = SIMPLE_ESCAPES.get(escaped);
|
|
1237
|
+
if (mapped === void 0) {
|
|
1238
|
+
throw new DiffParseError("unknown escape in a quoted diff path");
|
|
1239
|
+
}
|
|
1240
|
+
out.push(mapped);
|
|
1241
|
+
index += 2;
|
|
1242
|
+
}
|
|
1243
|
+
if (!closed) {
|
|
1244
|
+
throw new DiffParseError("unterminated quoted path in the diff header");
|
|
1245
|
+
}
|
|
1246
|
+
return Buffer.from(out).toString("utf8");
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/git/snapshot.ts
|
|
1250
|
+
var DIFF_ARGS = Object.freeze([
|
|
1251
|
+
"-c",
|
|
1252
|
+
"core.quotePath=true",
|
|
1253
|
+
"-c",
|
|
1254
|
+
"diff.suppressBlankEmpty=false",
|
|
1255
|
+
"-c",
|
|
1256
|
+
"diff.noprefix=false",
|
|
1257
|
+
"-c",
|
|
1258
|
+
"diff.mnemonicPrefix=false",
|
|
1259
|
+
"diff",
|
|
1260
|
+
"--no-color",
|
|
1261
|
+
"--binary",
|
|
1262
|
+
"--full-index",
|
|
1263
|
+
"--no-ext-diff",
|
|
1264
|
+
"--no-textconv",
|
|
1265
|
+
"--no-renames",
|
|
1266
|
+
"--ignore-submodules=dirty",
|
|
1267
|
+
"--unified=6",
|
|
1268
|
+
"--src-prefix=a/",
|
|
1269
|
+
"--dst-prefix=b/",
|
|
1270
|
+
"--no-relative"
|
|
1271
|
+
]);
|
|
1272
|
+
var MIN_GIT_MAJOR = 2;
|
|
1273
|
+
var MIN_GIT_MINOR = 30;
|
|
1274
|
+
var MIN_GIT_VERSION = `${MIN_GIT_MAJOR}.${MIN_GIT_MINOR}`;
|
|
1275
|
+
var GIT_VERSION_LINE = /^git version (\d+)\.(\d+)/;
|
|
1276
|
+
async function captureSnapshot({ cwd, git }) {
|
|
1277
|
+
const gitEnv = resolveGitEnv();
|
|
1278
|
+
await assertSupportedGitVersion(git, cwd, gitEnv);
|
|
1279
|
+
const locations = await resolveLocations(git, cwd, gitEnv);
|
|
1280
|
+
const headOid = await resolveHeadOid(git, locations.workTree, gitEnv);
|
|
1281
|
+
await assertNoUnmergedEntries(git, locations.workTree, gitEnv);
|
|
1282
|
+
const diff = await runGitOrThrow(git, [...DIFF_ARGS], { cwd: locations.workTree, env: gitEnv });
|
|
1283
|
+
const diffBytes = diff.stdout;
|
|
1284
|
+
const parsed = parseUnifiedDiffDetailed(diffBytes);
|
|
1285
|
+
const unsupported = buildUnsupportedEntryError(parsed.unsupported);
|
|
1286
|
+
if (unsupported !== void 0) {
|
|
1287
|
+
throw unsupported;
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
workTree: locations.workTree,
|
|
1291
|
+
gitDir: locations.gitDir,
|
|
1292
|
+
indexPath: locations.indexPath,
|
|
1293
|
+
gitEnv,
|
|
1294
|
+
headOid,
|
|
1295
|
+
indexHash: hashBytes(readIndexBytes(locations.indexPath)),
|
|
1296
|
+
diffHash: hashBytes(diffBytes),
|
|
1297
|
+
diffBytes,
|
|
1298
|
+
files: parsed.files.filter((file) => file.hunks.length > 0),
|
|
1299
|
+
skipped: parsed.files.filter((file) => file.hunks.length === 0).map((file) => ({ path: file.path, reason: skippedReason(file.kind) }))
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function skippedReason(kind) {
|
|
1303
|
+
return kind === "mode-only" ? "mode-only" : "empty-file";
|
|
1304
|
+
}
|
|
1305
|
+
function resolveGitEnv() {
|
|
1306
|
+
const envIndex = process.env.GIT_INDEX_FILE;
|
|
1307
|
+
if (envIndex === void 0 || envIndex.length === 0) {
|
|
1308
|
+
return {};
|
|
1309
|
+
}
|
|
1310
|
+
return { GIT_INDEX_FILE: resolve(process.cwd(), envIndex) };
|
|
1311
|
+
}
|
|
1312
|
+
async function assertSupportedGitVersion(git, cwd, env) {
|
|
1313
|
+
const result = await runGitOrThrow(git, ["--version"], { cwd, env });
|
|
1314
|
+
const line = firstLine(result.stdout.toString("utf8"));
|
|
1315
|
+
const match = GIT_VERSION_LINE.exec(line);
|
|
1316
|
+
const major = Number(match?.[1]);
|
|
1317
|
+
const minor = Number(match?.[2]);
|
|
1318
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor)) {
|
|
1319
|
+
throw new UnsupportedGitVersionError(line, MIN_GIT_VERSION);
|
|
1320
|
+
}
|
|
1321
|
+
if (major < MIN_GIT_MAJOR || major === MIN_GIT_MAJOR && minor < MIN_GIT_MINOR) {
|
|
1322
|
+
throw new UnsupportedGitVersionError(`${major}.${minor}`, MIN_GIT_VERSION);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async function resolveLocations(git, cwd, env) {
|
|
1326
|
+
const result = await git.run(
|
|
1327
|
+
["rev-parse", "--show-toplevel", "--git-dir", "--git-path", "index"],
|
|
1328
|
+
{
|
|
1329
|
+
cwd,
|
|
1330
|
+
env
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1333
|
+
if (result.exitCode !== 0) {
|
|
1334
|
+
throw new NotARepositoryError();
|
|
1335
|
+
}
|
|
1336
|
+
const lines = splitOutputLines(result.stdout.toString("utf8"));
|
|
1337
|
+
const workTree = lines[0];
|
|
1338
|
+
const gitDir = lines[1];
|
|
1339
|
+
const indexFile = lines[2];
|
|
1340
|
+
if (workTree === void 0 || gitDir === void 0 || indexFile === void 0) {
|
|
1341
|
+
throw new NotARepositoryError();
|
|
1342
|
+
}
|
|
1343
|
+
return {
|
|
1344
|
+
workTree: resolve(cwd, workTree),
|
|
1345
|
+
gitDir: resolve(cwd, gitDir),
|
|
1346
|
+
indexPath: env.GIT_INDEX_FILE ?? resolve(cwd, indexFile)
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
async function resolveHeadOid(git, cwd, env) {
|
|
1350
|
+
const result = await git.run(["rev-parse", "--verify", "HEAD"], { cwd, env });
|
|
1351
|
+
const oid = result.stdout.toString("utf8").trim();
|
|
1352
|
+
if (result.exitCode !== 0 || oid.length === 0) {
|
|
1353
|
+
throw new UnbornRepositoryError();
|
|
1354
|
+
}
|
|
1355
|
+
return oid;
|
|
1356
|
+
}
|
|
1357
|
+
async function assertNoUnmergedEntries(git, cwd, env) {
|
|
1358
|
+
const result = await runGitOrThrow(git, ["ls-files", "-u", "-z"], { cwd, env });
|
|
1359
|
+
if (result.stdout.length === 0) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const paths = [];
|
|
1363
|
+
for (const record of result.stdout.toString("utf8").split("\0")) {
|
|
1364
|
+
if (record.length === 0) {
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
const tab = record.indexOf(" ");
|
|
1368
|
+
const path = tab === -1 ? record : record.slice(tab + 1);
|
|
1369
|
+
if (!paths.includes(path)) {
|
|
1370
|
+
paths.push(path);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (paths.length > 0) {
|
|
1374
|
+
throw new UnmergedEntriesError(paths);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
function readIndexBytes(indexPath) {
|
|
1378
|
+
try {
|
|
1379
|
+
return readFileSync2(indexPath);
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
if (isMissingFile(error)) {
|
|
1382
|
+
return Buffer.alloc(0);
|
|
1383
|
+
}
|
|
1384
|
+
throw error;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
function isMissingFile(error) {
|
|
1388
|
+
if (!(error instanceof Error) || !("code" in error)) {
|
|
1389
|
+
return false;
|
|
1390
|
+
}
|
|
1391
|
+
return error.code === "ENOENT" || error.code === "EISDIR";
|
|
1392
|
+
}
|
|
1393
|
+
function firstLine(output) {
|
|
1394
|
+
return stripCarriageReturn(output.split("\n")[0] ?? "").trim();
|
|
1395
|
+
}
|
|
1396
|
+
function splitOutputLines(output) {
|
|
1397
|
+
return output.split("\n").map(stripCarriageReturn).filter((line) => line.length > 0);
|
|
1398
|
+
}
|
|
1399
|
+
function stripCarriageReturn(line) {
|
|
1400
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/git/applySelection.ts
|
|
1404
|
+
var LOCK_SUFFIX = ".lock";
|
|
1405
|
+
var LOCK_MODE = 420;
|
|
1406
|
+
var TEMP_MARKER = ".jev-stage-";
|
|
1407
|
+
var TEMP_SUFFIX_BYTES = 8;
|
|
1408
|
+
var TEMP_SUFFIX = /^(\d+)-[0-9a-f]+$/;
|
|
1409
|
+
var ownedPaths = /* @__PURE__ */ new Set();
|
|
1410
|
+
var exitHookInstalled = false;
|
|
1411
|
+
async function applyPatchToIndex({
|
|
1412
|
+
snapshot,
|
|
1413
|
+
patch,
|
|
1414
|
+
git
|
|
1415
|
+
}) {
|
|
1416
|
+
if (patch.length === 0) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
sweepAbandonedTempIndexes(snapshot.indexPath);
|
|
1420
|
+
const tempPath = await createTempIndex(snapshot, git);
|
|
1421
|
+
const apply = { git, snapshot, patch, tempPath };
|
|
1422
|
+
try {
|
|
1423
|
+
await runApply(apply, ["apply", "--cached", "--check"]);
|
|
1424
|
+
await runApply(apply, ["apply", "--cached"]);
|
|
1425
|
+
await verifyTempIndex(apply);
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
discard(tempPath);
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
const lockPath = `${snapshot.indexPath}${LOCK_SUFFIX}`;
|
|
1431
|
+
const lockFd = openLock(lockPath, tempPath);
|
|
1432
|
+
let lockClosed = false;
|
|
1433
|
+
try {
|
|
1434
|
+
await assertSnapshotIsCurrent(git, snapshot);
|
|
1435
|
+
writeAll(lockFd, readFileSync3(tempPath));
|
|
1436
|
+
fsyncSync(lockFd);
|
|
1437
|
+
closeSync(lockFd);
|
|
1438
|
+
lockClosed = true;
|
|
1439
|
+
renameSync(lockPath, snapshot.indexPath);
|
|
1440
|
+
untrack(lockPath);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
if (!lockClosed) {
|
|
1443
|
+
closeQuietly(lockFd);
|
|
1444
|
+
}
|
|
1445
|
+
discard(lockPath);
|
|
1446
|
+
discard(tempPath);
|
|
1447
|
+
throw error;
|
|
1448
|
+
}
|
|
1449
|
+
discard(tempPath);
|
|
1450
|
+
}
|
|
1451
|
+
async function stageHunks(snapshot, selectedIds, git) {
|
|
1452
|
+
const patch = composePatch(snapshot.files, selectedIds);
|
|
1453
|
+
await applyPatchToIndex({ snapshot, patch, git });
|
|
1454
|
+
return patch;
|
|
1455
|
+
}
|
|
1456
|
+
function sweepAbandonedTempIndexes(indexPath) {
|
|
1457
|
+
const directory = dirname(indexPath);
|
|
1458
|
+
const prefix = `${basename(indexPath)}${TEMP_MARKER}`;
|
|
1459
|
+
let names;
|
|
1460
|
+
try {
|
|
1461
|
+
names = readdirSync(directory);
|
|
1462
|
+
} catch {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
for (const name of names) {
|
|
1466
|
+
if (!name.startsWith(prefix)) {
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
const match = TEMP_SUFFIX.exec(name.slice(prefix.length));
|
|
1470
|
+
if (match === null) {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
const pid = Number(match[1]);
|
|
1474
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || isProcessAlive(pid)) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
removeQuietly(join(directory, name));
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function isProcessAlive(pid) {
|
|
1481
|
+
try {
|
|
1482
|
+
process.kill(pid, 0);
|
|
1483
|
+
return true;
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
return !hasErrnoCode(error, "ESRCH");
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
async function createTempIndex(snapshot, git) {
|
|
1489
|
+
const suffix = randomBytes(TEMP_SUFFIX_BYTES).toString("hex");
|
|
1490
|
+
const tempPath = `${snapshot.indexPath}${TEMP_MARKER}${process.pid}-${suffix}`;
|
|
1491
|
+
track(tempPath);
|
|
1492
|
+
try {
|
|
1493
|
+
copyFileSync(snapshot.indexPath, tempPath, constants.COPYFILE_EXCL);
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
if (!hasErrnoCode(error, "ENOENT")) {
|
|
1496
|
+
discard(tempPath);
|
|
1497
|
+
throw error;
|
|
1498
|
+
}
|
|
1499
|
+
try {
|
|
1500
|
+
await runGitOrThrow(git, ["read-tree", "HEAD"], {
|
|
1501
|
+
cwd: snapshot.workTree,
|
|
1502
|
+
env: { ...snapshot.gitEnv, GIT_INDEX_FILE: tempPath }
|
|
1503
|
+
});
|
|
1504
|
+
} catch (readTreeError) {
|
|
1505
|
+
discard(tempPath);
|
|
1506
|
+
throw readTreeError;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return tempPath;
|
|
1510
|
+
}
|
|
1511
|
+
function tempIndexEnv(apply) {
|
|
1512
|
+
return { ...apply.snapshot.gitEnv, GIT_INDEX_FILE: apply.tempPath };
|
|
1513
|
+
}
|
|
1514
|
+
async function runApply(apply, args) {
|
|
1515
|
+
const result = await apply.git.run(args, {
|
|
1516
|
+
cwd: apply.snapshot.workTree,
|
|
1517
|
+
env: tempIndexEnv(apply),
|
|
1518
|
+
stdin: apply.patch
|
|
1519
|
+
});
|
|
1520
|
+
if (result.exitCode !== 0) {
|
|
1521
|
+
throw new PatchApplyError(result.stderr);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
async function verifyTempIndex(apply) {
|
|
1525
|
+
await runApply(apply, ["apply", "--cached", "--check", "-R"]);
|
|
1526
|
+
const listed = await apply.git.run(["ls-files", "-s"], {
|
|
1527
|
+
cwd: apply.snapshot.workTree,
|
|
1528
|
+
env: tempIndexEnv(apply)
|
|
1529
|
+
});
|
|
1530
|
+
if (listed.exitCode !== 0) {
|
|
1531
|
+
throw new PatchApplyError(listed.stderr);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
async function assertSnapshotIsCurrent(git, snapshot) {
|
|
1535
|
+
const head = await runGitOrThrow(git, ["rev-parse", "HEAD"], {
|
|
1536
|
+
cwd: snapshot.workTree,
|
|
1537
|
+
env: snapshot.gitEnv
|
|
1538
|
+
});
|
|
1539
|
+
if (head.stdout.toString("utf8").trim() !== snapshot.headOid) {
|
|
1540
|
+
throw new StaleSnapshotError("head");
|
|
1541
|
+
}
|
|
1542
|
+
if (hashBytes(readIndexBytes(snapshot.indexPath)) !== snapshot.indexHash) {
|
|
1543
|
+
throw new StaleSnapshotError("index");
|
|
1544
|
+
}
|
|
1545
|
+
const diff = await runGitOrThrow(git, [...DIFF_ARGS], {
|
|
1546
|
+
cwd: snapshot.workTree,
|
|
1547
|
+
env: { ...snapshot.gitEnv, GIT_INDEX_FILE: snapshot.indexPath }
|
|
1548
|
+
});
|
|
1549
|
+
if (hashBytes(diff.stdout) !== snapshot.diffHash) {
|
|
1550
|
+
throw new StaleSnapshotError("diff");
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function openLock(lockPath, tempPath) {
|
|
1554
|
+
let fd;
|
|
1555
|
+
try {
|
|
1556
|
+
fd = openSync(lockPath, "wx", LOCK_MODE);
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
discard(tempPath);
|
|
1559
|
+
if (hasErrnoCode(error, "EEXIST")) {
|
|
1560
|
+
throw new IndexLockedError(lockPath, { cause: error });
|
|
1561
|
+
}
|
|
1562
|
+
throw error;
|
|
1563
|
+
}
|
|
1564
|
+
track(lockPath);
|
|
1565
|
+
return fd;
|
|
1566
|
+
}
|
|
1567
|
+
function writeAll(fd, bytes) {
|
|
1568
|
+
let offset = 0;
|
|
1569
|
+
while (offset < bytes.length) {
|
|
1570
|
+
offset += writeSync(fd, bytes, offset, bytes.length - offset);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function track(path) {
|
|
1574
|
+
ownedPaths.add(path);
|
|
1575
|
+
if (!exitHookInstalled) {
|
|
1576
|
+
exitHookInstalled = true;
|
|
1577
|
+
process.on("exit", cleanupOwnedPaths);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function untrack(path) {
|
|
1581
|
+
ownedPaths.delete(path);
|
|
1582
|
+
}
|
|
1583
|
+
function discard(path) {
|
|
1584
|
+
untrack(path);
|
|
1585
|
+
removeQuietly(path);
|
|
1586
|
+
}
|
|
1587
|
+
function cleanupOwnedPaths() {
|
|
1588
|
+
for (const path of [...ownedPaths]) {
|
|
1589
|
+
ownedPaths.delete(path);
|
|
1590
|
+
removeQuietly(path);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
function removeQuietly(path) {
|
|
1594
|
+
try {
|
|
1595
|
+
rmSync(path, { force: true });
|
|
1596
|
+
} catch {
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
function closeQuietly(fd) {
|
|
1601
|
+
try {
|
|
1602
|
+
closeSync(fd);
|
|
1603
|
+
} catch {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function hasErrnoCode(error, code) {
|
|
1608
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/selection/policy.ts
|
|
1612
|
+
var DEFAULT_THRESHOLD = 0.6;
|
|
1613
|
+
var DECISION_LABELS = Object.freeze([
|
|
1614
|
+
"include",
|
|
1615
|
+
"exclude",
|
|
1616
|
+
"mixed"
|
|
1617
|
+
]);
|
|
1618
|
+
function decide(outcome, threshold) {
|
|
1619
|
+
if (outcome.kind === "missing") {
|
|
1620
|
+
return { decision: "mixed", source: "missing" };
|
|
1621
|
+
}
|
|
1622
|
+
if (outcome.kind === "invalid") {
|
|
1623
|
+
return { decision: "mixed", source: "invalid" };
|
|
1624
|
+
}
|
|
1625
|
+
const { choice: choice2, confidence, probabilities } = outcome.answer;
|
|
1626
|
+
if (choice2 === "mixed") {
|
|
1627
|
+
return { decision: "mixed", source: "model", confidence, probabilities };
|
|
1628
|
+
}
|
|
1629
|
+
if (confidence >= threshold) {
|
|
1630
|
+
return { decision: choice2, source: "model", confidence, probabilities };
|
|
1631
|
+
}
|
|
1632
|
+
return { decision: "mixed", source: "low-confidence", confidence, probabilities };
|
|
1633
|
+
}
|
|
1634
|
+
function validateThreshold(threshold) {
|
|
1635
|
+
if (!Number.isFinite(threshold) || threshold <= 0 || threshold > 1) {
|
|
1636
|
+
throw new UsageError(`threshold must be greater than 0 and at most 1, got ${threshold}`);
|
|
1637
|
+
}
|
|
1638
|
+
return threshold;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/selection/classify.ts
|
|
1642
|
+
var CLASSIFY_CONCURRENCY = 4;
|
|
1643
|
+
var BASE_INSTRUCTIONS = "Decide, for each hunk marked role=ask, whether its changed lines belong to the change described by intent. Context hunks are for reference only.";
|
|
1644
|
+
var EXCLUDE_INSTRUCTIONS = " Lines that match exclude never belong.";
|
|
1645
|
+
var HUNK_OPTIONS = Object.freeze({
|
|
1646
|
+
include: "every changed line in this hunk belongs to the described change",
|
|
1647
|
+
exclude: "no changed line in this hunk belongs to the described change",
|
|
1648
|
+
mixed: "some changed lines belong and some do not"
|
|
1649
|
+
});
|
|
1650
|
+
async function classifyHunks(options) {
|
|
1651
|
+
const { snapshot, intent, exclude, threshold, provider, tokenCeiling } = options;
|
|
1652
|
+
const instructions = buildInstructions(exclude);
|
|
1653
|
+
const ceiling = tokenCeiling ?? DEFAULT_TOKEN_CEILING;
|
|
1654
|
+
const sharedTokens = envelopeTokens(intent, exclude);
|
|
1655
|
+
const windows = buildWindows(buildItems(snapshot), {
|
|
1656
|
+
sharedTokens,
|
|
1657
|
+
...tokenCeiling === void 0 ? {} : { tokenCeiling }
|
|
1658
|
+
});
|
|
1659
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
1660
|
+
const sendable = [];
|
|
1661
|
+
for (const window of windows) {
|
|
1662
|
+
if (window.estimatedTokens > ceiling) {
|
|
1663
|
+
for (const hunkId of window.askIds) {
|
|
1664
|
+
decisions.set(hunkId, { hunkId, decision: "mixed", source: "too-large" });
|
|
1665
|
+
}
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
sendable.push(window);
|
|
1669
|
+
}
|
|
1670
|
+
const results = await runWindows(
|
|
1671
|
+
sendable,
|
|
1672
|
+
(window, _index, signal) => provider.classify(buildRequest(snapshot, intent, exclude, instructions, window), { signal }),
|
|
1673
|
+
{
|
|
1674
|
+
concurrency: CLASSIFY_CONCURRENCY,
|
|
1675
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1676
|
+
}
|
|
1677
|
+
);
|
|
1678
|
+
const usage = { requests: sendable.length, inputTokens: 0, outputTokens: 0 };
|
|
1679
|
+
results.forEach((result, index) => {
|
|
1680
|
+
usage.inputTokens += result.usage.inputTokens;
|
|
1681
|
+
usage.outputTokens += result.usage.outputTokens;
|
|
1682
|
+
const window = sendable[index];
|
|
1683
|
+
if (window === void 0) {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
for (const hunkId of window.askIds) {
|
|
1687
|
+
const outcome = result.outcomes[hunkId] ?? { kind: "missing" };
|
|
1688
|
+
decisions.set(hunkId, { hunkId, ...decide(outcome, threshold) });
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
return { decisions, usage };
|
|
1692
|
+
}
|
|
1693
|
+
function envelopeTokens(intent, exclude) {
|
|
1694
|
+
return estimateJsonTokens(buildState(intent, exclude, buildInstructions(exclude), []));
|
|
1695
|
+
}
|
|
1696
|
+
function buildInstructions(exclude) {
|
|
1697
|
+
return exclude === void 0 ? BASE_INSTRUCTIONS : `${BASE_INSTRUCTIONS}${EXCLUDE_INSTRUCTIONS}`;
|
|
1698
|
+
}
|
|
1699
|
+
function buildItems(snapshot) {
|
|
1700
|
+
const items = [];
|
|
1701
|
+
for (const file of snapshot.files) {
|
|
1702
|
+
for (const hunk of file.hunks) {
|
|
1703
|
+
items.push({ id: hunk.id, text: hunk.text, group: file.path, ordinal: hunk.ordinal });
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return items;
|
|
1707
|
+
}
|
|
1708
|
+
function buildRequest(snapshot, intent, exclude, instructions, window) {
|
|
1709
|
+
const roles = /* @__PURE__ */ new Map();
|
|
1710
|
+
for (const hunkId of window.contextIds) {
|
|
1711
|
+
roles.set(hunkId, "context");
|
|
1712
|
+
}
|
|
1713
|
+
for (const hunkId of window.askIds) {
|
|
1714
|
+
roles.set(hunkId, "ask");
|
|
1715
|
+
}
|
|
1716
|
+
const files = [];
|
|
1717
|
+
for (const file of snapshot.files) {
|
|
1718
|
+
const hunks = [];
|
|
1719
|
+
for (const hunk of file.hunks) {
|
|
1720
|
+
const role = roles.get(hunk.id);
|
|
1721
|
+
if (role === void 0) {
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
hunks.push({ id: hunk.id, header: hunk.header, patch: hunk.text, role });
|
|
1725
|
+
}
|
|
1726
|
+
if (hunks.length > 0) {
|
|
1727
|
+
files.push({ path: file.path, hunks });
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const questions = {};
|
|
1731
|
+
for (const hunkId of window.askIds) {
|
|
1732
|
+
questions[hunkId] = {
|
|
1733
|
+
instructions: `Does hunk ${hunkId} belong to the described change?`,
|
|
1734
|
+
options: { ...HUNK_OPTIONS }
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
return { state: buildState(intent, exclude, instructions, files), questions };
|
|
1738
|
+
}
|
|
1739
|
+
function buildState(intent, exclude, instructions, files) {
|
|
1740
|
+
return {
|
|
1741
|
+
intent,
|
|
1742
|
+
...exclude === void 0 ? {} : { exclude },
|
|
1743
|
+
instructions,
|
|
1744
|
+
files
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/library.ts
|
|
1749
|
+
async function planSelection(options) {
|
|
1750
|
+
const threshold = validateThreshold(options.threshold ?? DEFAULT_THRESHOLD);
|
|
1751
|
+
const git = options.git ?? createGitRunner();
|
|
1752
|
+
const snapshot = await captureSnapshot({ cwd: options.cwd, git });
|
|
1753
|
+
const { intent, exclude, provider } = options;
|
|
1754
|
+
const excludeField = exclude === void 0 ? {} : { exclude };
|
|
1755
|
+
if (provider === void 0) {
|
|
1756
|
+
return {
|
|
1757
|
+
intent,
|
|
1758
|
+
...excludeField,
|
|
1759
|
+
threshold,
|
|
1760
|
+
snapshot,
|
|
1761
|
+
decisions: withoutProvider(snapshot),
|
|
1762
|
+
usage: { requests: 0, inputTokens: 0, outputTokens: 0 }
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
const { decisions, usage } = await classifyHunks({
|
|
1766
|
+
snapshot,
|
|
1767
|
+
intent,
|
|
1768
|
+
...excludeField,
|
|
1769
|
+
threshold,
|
|
1770
|
+
provider
|
|
1771
|
+
});
|
|
1772
|
+
return { intent, ...excludeField, threshold, snapshot, decisions, usage };
|
|
1773
|
+
}
|
|
1774
|
+
async function applySelection(plan, options) {
|
|
1775
|
+
const hunkIds = collectHunkIds(plan.snapshot);
|
|
1776
|
+
const known = new Set(hunkIds);
|
|
1777
|
+
const unknown = [...new Set(options.includeIds)].filter((id) => !known.has(id)).sort();
|
|
1778
|
+
if (unknown.length > 0) {
|
|
1779
|
+
throw new UnknownHunkError(unknown);
|
|
1780
|
+
}
|
|
1781
|
+
const included = new Set(options.includeIds);
|
|
1782
|
+
const stagedHunkIds = hunkIds.filter((id) => included.has(id));
|
|
1783
|
+
const skippedMixedIds = hunkIds.filter(
|
|
1784
|
+
(id) => plan.decisions.get(id)?.decision === "mixed" && !included.has(id)
|
|
1785
|
+
);
|
|
1786
|
+
const git = options.git ?? createGitRunner();
|
|
1787
|
+
const patchBytes = await stageHunks(plan.snapshot, included, git);
|
|
1788
|
+
return { stagedHunkIds, skippedMixedIds, patchBytes };
|
|
1789
|
+
}
|
|
1790
|
+
function collectHunkIds(snapshot) {
|
|
1791
|
+
const ids = [];
|
|
1792
|
+
for (const file of snapshot.files) {
|
|
1793
|
+
for (const hunk of file.hunks) {
|
|
1794
|
+
ids.push(hunk.id);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return ids;
|
|
1798
|
+
}
|
|
1799
|
+
function withoutProvider(snapshot) {
|
|
1800
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
1801
|
+
for (const hunkId of collectHunkIds(snapshot)) {
|
|
1802
|
+
decisions.set(hunkId, { hunkId, decision: "mixed", source: "no-provider" });
|
|
1803
|
+
}
|
|
1804
|
+
return decisions;
|
|
1805
|
+
}
|
|
1806
|
+
export {
|
|
1807
|
+
CHARS_PER_TOKEN,
|
|
1808
|
+
DEFAULT_CONCURRENCY,
|
|
1809
|
+
DEFAULT_PER_QUESTION_TOKENS,
|
|
1810
|
+
DEFAULT_THRESHOLD,
|
|
1811
|
+
DEFAULT_TOKEN_CEILING,
|
|
1812
|
+
DIFF_ARGS,
|
|
1813
|
+
DiffParseError,
|
|
1814
|
+
FakeProvider,
|
|
1815
|
+
GitCommandError,
|
|
1816
|
+
IndexLockedError,
|
|
1817
|
+
JevCoreError,
|
|
1818
|
+
JevStageError,
|
|
1819
|
+
MissingApiKeyError,
|
|
1820
|
+
NotARepositoryError,
|
|
1821
|
+
PatchApplyError,
|
|
1822
|
+
ProviderConfigError,
|
|
1823
|
+
ProviderError,
|
|
1824
|
+
RequestTooLargeError,
|
|
1825
|
+
StaleSnapshotError,
|
|
1826
|
+
TypeSafeJevProvider,
|
|
1827
|
+
UnbornRepositoryError,
|
|
1828
|
+
UnknownHunkError,
|
|
1829
|
+
UnmergedEntriesError,
|
|
1830
|
+
UnsupportedEntryError,
|
|
1831
|
+
UnsupportedGitVersionError,
|
|
1832
|
+
UsageError,
|
|
1833
|
+
applyPatchToIndex,
|
|
1834
|
+
applySelection,
|
|
1835
|
+
buildWindows,
|
|
1836
|
+
captureSnapshot,
|
|
1837
|
+
classifyHunks,
|
|
1838
|
+
cleanupOwnedPaths,
|
|
1839
|
+
composePatch,
|
|
1840
|
+
createGitRunner,
|
|
1841
|
+
createProviderFromEnv,
|
|
1842
|
+
decide,
|
|
1843
|
+
estimateJsonTokens,
|
|
1844
|
+
estimateTokens,
|
|
1845
|
+
parseUnifiedDiff,
|
|
1846
|
+
planSelection,
|
|
1847
|
+
runWindows,
|
|
1848
|
+
stageHunks,
|
|
1849
|
+
summarizeSelection,
|
|
1850
|
+
validateChoiceAnswer
|
|
1851
|
+
};
|