hillclimb 0.8.7 → 0.8.9
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/dist/main.js +229 -90
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -14402,6 +14402,35 @@ import fs14 from "fs";
|
|
|
14402
14402
|
import os7 from "os";
|
|
14403
14403
|
import path14 from "path";
|
|
14404
14404
|
import { createGzip } from "zlib";
|
|
14405
|
+
|
|
14406
|
+
// src/git-traces/timing.ts
|
|
14407
|
+
import { performance } from "perf_hooks";
|
|
14408
|
+
function startCaptureStage(stage, context) {
|
|
14409
|
+
appendLog(
|
|
14410
|
+
"info",
|
|
14411
|
+
`git-traces: capture timing (${context}, stage=${stage}, status=started)`
|
|
14412
|
+
);
|
|
14413
|
+
const started = performance.now();
|
|
14414
|
+
return (status) => {
|
|
14415
|
+
appendLog(
|
|
14416
|
+
"info",
|
|
14417
|
+
`git-traces: capture timing (${context}, stage=${stage}, status=${status}, elapsedMs=${Math.round(performance.now() - started)})`
|
|
14418
|
+
);
|
|
14419
|
+
};
|
|
14420
|
+
}
|
|
14421
|
+
function timeCaptureStage(repoRoot, stage, run) {
|
|
14422
|
+
const finish = startCaptureStage(stage, `repo=${repoRoot}`);
|
|
14423
|
+
let status = "failed";
|
|
14424
|
+
try {
|
|
14425
|
+
const result = run();
|
|
14426
|
+
status = "ok";
|
|
14427
|
+
return result;
|
|
14428
|
+
} finally {
|
|
14429
|
+
finish(status);
|
|
14430
|
+
}
|
|
14431
|
+
}
|
|
14432
|
+
|
|
14433
|
+
// src/git-traces/git-ops.ts
|
|
14405
14434
|
var DEFAULT_GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
14406
14435
|
var FULL_TREE_SCAN_TIMEOUT_MS = 3e5;
|
|
14407
14436
|
function resolveGitCommandTimeoutMs() {
|
|
@@ -14682,6 +14711,7 @@ function createGitError(args, err, timeoutMs = GIT_COMMAND_TIMEOUT_MS) {
|
|
|
14682
14711
|
const message = isTimeout ? `${command} timed out after ${timeoutMs}ms` : failure.code === "ENOBUFS" ? `${command} output exceeded the ${formatSize(failure.maxBufferBytes ?? EXEC_OPTS.maxBuffer)} limit` : formatGitFailure(command, failure, err);
|
|
14683
14712
|
const wrapped = new Error(message);
|
|
14684
14713
|
wrapped.isGitTimeout = isTimeout;
|
|
14714
|
+
wrapped.gitExitStatus = failure.status;
|
|
14685
14715
|
const stderr = outputToString(failure.stderr);
|
|
14686
14716
|
wrapped.gitStderr = stderr ? truncateOutput(stderr) : void 0;
|
|
14687
14717
|
wrapped.isGitMissingObject = !isTimeout && failure.code !== "ENOBUFS" && matchesMissingObject(stderr);
|
|
@@ -14723,20 +14753,50 @@ function isGitRepo(dir) {
|
|
|
14723
14753
|
}
|
|
14724
14754
|
}
|
|
14725
14755
|
function captureWorkingCommitSha(repoRoot) {
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14756
|
+
for (let attempt = 1; ; attempt++) {
|
|
14757
|
+
let sha;
|
|
14758
|
+
try {
|
|
14759
|
+
sha = timeCaptureStage(
|
|
14760
|
+
repoRoot,
|
|
14761
|
+
"stash-create",
|
|
14762
|
+
() => (
|
|
14763
|
+
// Keep the two expected stderr classifications independent of locale.
|
|
14764
|
+
gitWithEnv(repoRoot, ["stash", "create"], {
|
|
14765
|
+
...process.env,
|
|
14766
|
+
LC_ALL: "C"
|
|
14767
|
+
})
|
|
14768
|
+
)
|
|
14769
|
+
);
|
|
14770
|
+
} catch (err) {
|
|
14771
|
+
const failure = err;
|
|
14772
|
+
const stderr = failure.gitStderr ?? "";
|
|
14773
|
+
if (failure.gitExitStatus === 1 && stderr.includes("You do not have the initial commit yet") && hasUnbornHead(repoRoot)) {
|
|
14774
|
+
appendLog(
|
|
14775
|
+
"info",
|
|
14776
|
+
`git-traces: stash create unavailable (repo=${repoRoot}, reason=unborn-head); capturing index tree`
|
|
14777
|
+
);
|
|
14778
|
+
const tree = git(repoRoot, ["write-tree"]);
|
|
14779
|
+
return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
|
|
14780
|
+
}
|
|
14781
|
+
if (failure.gitExitStatus !== 1 || !/Unable to create [^\n]*index\.lock[^\n]*: File exists/.test(stderr) || attempt >= 5)
|
|
14782
|
+
throw err;
|
|
14783
|
+
appendLog(
|
|
14784
|
+
"warn",
|
|
14785
|
+
`git-traces: retrying stash create after index lock contention (repo=${repoRoot}, attempt=${attempt}, maxAttempts=5): ${failure.message}`
|
|
14786
|
+
);
|
|
14787
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
14788
|
+
continue;
|
|
14789
|
+
}
|
|
14790
|
+
return sha || git(repoRoot, ["rev-parse", "--verify", "HEAD"]);
|
|
14738
14791
|
}
|
|
14739
14792
|
}
|
|
14793
|
+
function hasUnbornHead(repoRoot) {
|
|
14794
|
+
const ref = git(repoRoot, ["symbolic-ref", "--quiet", "HEAD"]);
|
|
14795
|
+
const args = ["show-ref", "--verify", "--quiet", ref];
|
|
14796
|
+
const result = spawnSync("git", args, { cwd: repoRoot, ...EXEC_OPTS });
|
|
14797
|
+
if (result.error) throw createGitError(args, result.error);
|
|
14798
|
+
return result.status === 1;
|
|
14799
|
+
}
|
|
14740
14800
|
function captureBaselineSha(repoRoot) {
|
|
14741
14801
|
return captureWorkingCommitSha(repoRoot);
|
|
14742
14802
|
}
|
|
@@ -14971,69 +15031,89 @@ function removePathsFromIndex(repoRoot, env, paths) {
|
|
|
14971
15031
|
});
|
|
14972
15032
|
}
|
|
14973
15033
|
function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
14974
|
-
const omittedFiles =
|
|
15034
|
+
const omittedFiles = timeCaptureStage(
|
|
14975
15035
|
repoRoot,
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
15036
|
+
"omission-scan",
|
|
15037
|
+
() => listOmittedTreeFiles(
|
|
15038
|
+
repoRoot,
|
|
15039
|
+
treeSha,
|
|
15040
|
+
options.limits ?? DEFAULT_SNAPSHOT_LIMITS,
|
|
15041
|
+
options.previousScan
|
|
15042
|
+
)
|
|
14979
15043
|
);
|
|
14980
15044
|
options.omittedFiles?.push(...omittedFiles);
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
14987
|
-
try {
|
|
14988
|
-
gitWithEnv(repoRoot, ["read-tree", treeSha], env);
|
|
14989
|
-
removePathsFromIndex(
|
|
14990
|
-
repoRoot,
|
|
14991
|
-
env,
|
|
14992
|
-
omittedFiles.map((file) => file.path)
|
|
15045
|
+
return timeCaptureStage(repoRoot, "tracked-tree-filter", () => {
|
|
15046
|
+
if (omittedFiles.length === 0) return treeSha;
|
|
15047
|
+
const tmpIndex = path14.join(
|
|
15048
|
+
os7.tmpdir(),
|
|
15049
|
+
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
14993
15050
|
);
|
|
14994
|
-
|
|
14995
|
-
} finally {
|
|
15051
|
+
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
14996
15052
|
try {
|
|
14997
|
-
|
|
14998
|
-
|
|
15053
|
+
gitWithEnv(repoRoot, ["read-tree", treeSha], env);
|
|
15054
|
+
removePathsFromIndex(
|
|
15055
|
+
repoRoot,
|
|
15056
|
+
env,
|
|
15057
|
+
omittedFiles.map((file) => file.path)
|
|
15058
|
+
);
|
|
15059
|
+
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
15060
|
+
} finally {
|
|
15061
|
+
try {
|
|
15062
|
+
fs14.unlinkSync(tmpIndex);
|
|
15063
|
+
} catch {
|
|
15064
|
+
}
|
|
14999
15065
|
}
|
|
15000
|
-
}
|
|
15066
|
+
});
|
|
15001
15067
|
}
|
|
15002
15068
|
function buildUntrackedTree(repoRoot, options = {}) {
|
|
15003
15069
|
const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
|
|
15004
|
-
const list =
|
|
15005
|
-
|
|
15006
|
-
"
|
|
15007
|
-
"--exclude-standard",
|
|
15008
|
-
|
|
15009
|
-
]);
|
|
15010
|
-
if (!list) return null;
|
|
15070
|
+
const list = timeCaptureStage(
|
|
15071
|
+
repoRoot,
|
|
15072
|
+
"untracked-list",
|
|
15073
|
+
() => git(repoRoot, ["ls-files", "--others", "--exclude-standard", "-z"])
|
|
15074
|
+
);
|
|
15011
15075
|
const kept = [];
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
15020
|
-
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15076
|
+
let candidates = 0;
|
|
15077
|
+
let keptBytes = 0;
|
|
15078
|
+
let omitted = 0;
|
|
15079
|
+
let omittedBytes = 0;
|
|
15080
|
+
let skipped = 0;
|
|
15081
|
+
timeCaptureStage(repoRoot, "untracked-filter", () => {
|
|
15082
|
+
for (const relPath of list.split("\0")) {
|
|
15083
|
+
if (!relPath) continue;
|
|
15084
|
+
candidates++;
|
|
15085
|
+
try {
|
|
15086
|
+
const absPath = path14.join(repoRoot, relPath);
|
|
15087
|
+
const stat = fs14.lstatSync(absPath);
|
|
15088
|
+
const reason = classifyOmission(
|
|
15089
|
+
relPath,
|
|
15090
|
+
stat.size,
|
|
15091
|
+
() => readWorkingFileHead(absPath),
|
|
15092
|
+
limits,
|
|
15093
|
+
false
|
|
15094
|
+
);
|
|
15095
|
+
if (reason) {
|
|
15096
|
+
omitted++;
|
|
15097
|
+
omittedBytes += stat.size;
|
|
15098
|
+
options.omittedFiles?.push({
|
|
15099
|
+
path: relPath,
|
|
15100
|
+
sizeBytes: stat.size,
|
|
15101
|
+
tracked: false,
|
|
15102
|
+
reason
|
|
15103
|
+
});
|
|
15104
|
+
continue;
|
|
15105
|
+
}
|
|
15106
|
+
kept.push(relPath);
|
|
15107
|
+
keptBytes += stat.size;
|
|
15108
|
+
} catch {
|
|
15109
|
+
skipped++;
|
|
15032
15110
|
}
|
|
15033
|
-
kept.push(relPath);
|
|
15034
|
-
} catch {
|
|
15035
15111
|
}
|
|
15036
|
-
}
|
|
15112
|
+
});
|
|
15113
|
+
appendLog(
|
|
15114
|
+
"info",
|
|
15115
|
+
`git-traces: untracked inventory (repo=${repoRoot}, candidates=${candidates}, kept=${kept.length}, keptBytes=${keptBytes}, omitted=${omitted}, omittedBytes=${omittedBytes}, skipped=${skipped})`
|
|
15116
|
+
);
|
|
15037
15117
|
if (kept.length === 0) return null;
|
|
15038
15118
|
const tmpIndex = path14.join(
|
|
15039
15119
|
os7.tmpdir(),
|
|
@@ -15041,11 +15121,19 @@ function buildUntrackedTree(repoRoot, options = {}) {
|
|
|
15041
15121
|
);
|
|
15042
15122
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
15043
15123
|
try {
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15124
|
+
timeCaptureStage(
|
|
15125
|
+
repoRoot,
|
|
15126
|
+
"untracked-hash-and-index",
|
|
15127
|
+
() => gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
|
|
15128
|
+
input: `${kept.join("\0")}\0`,
|
|
15129
|
+
env
|
|
15130
|
+
})
|
|
15131
|
+
);
|
|
15132
|
+
return timeCaptureStage(
|
|
15133
|
+
repoRoot,
|
|
15134
|
+
"untracked-write-tree",
|
|
15135
|
+
() => gitWithEnv(repoRoot, ["write-tree"], env)
|
|
15136
|
+
);
|
|
15049
15137
|
} finally {
|
|
15050
15138
|
try {
|
|
15051
15139
|
fs14.unlinkSync(tmpIndex);
|
|
@@ -15056,7 +15144,11 @@ function buildUntrackedTree(repoRoot, options = {}) {
|
|
|
15056
15144
|
function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
15057
15145
|
const omittedFiles = options.omittedFiles ?? [];
|
|
15058
15146
|
const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
|
|
15059
|
-
const trackedTree =
|
|
15147
|
+
const trackedTree = timeCaptureStage(
|
|
15148
|
+
repoRoot,
|
|
15149
|
+
"tracked-tree-resolve",
|
|
15150
|
+
() => git(repoRoot, ["rev-parse", `${stashSha}^{tree}`])
|
|
15151
|
+
);
|
|
15060
15152
|
const trackedOmitted = [];
|
|
15061
15153
|
const filteredTrackedTree = filterOmittedFilesFromTree(
|
|
15062
15154
|
repoRoot,
|
|
@@ -15068,12 +15160,16 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
15068
15160
|
}
|
|
15069
15161
|
);
|
|
15070
15162
|
omittedFiles.push(...trackedOmitted);
|
|
15071
|
-
const untrackedTree =
|
|
15163
|
+
const untrackedTree = timeCaptureStage(
|
|
15164
|
+
repoRoot,
|
|
15165
|
+
"untracked-capture",
|
|
15166
|
+
() => buildUntrackedTree(repoRoot, { omittedFiles, limits })
|
|
15167
|
+
);
|
|
15072
15168
|
if (options.log !== false) logOmittedSnapshotSummary(omittedFiles);
|
|
15073
|
-
const snapshotTree =
|
|
15169
|
+
const snapshotTree = timeCaptureStage(
|
|
15074
15170
|
repoRoot,
|
|
15075
|
-
|
|
15076
|
-
untrackedTree
|
|
15171
|
+
"tree-assembly",
|
|
15172
|
+
() => mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree)
|
|
15077
15173
|
);
|
|
15078
15174
|
options.scanOut?.push({
|
|
15079
15175
|
trackedTreeSha: trackedTree,
|
|
@@ -15092,26 +15188,48 @@ function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
|
|
|
15092
15188
|
);
|
|
15093
15189
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
15094
15190
|
try {
|
|
15095
|
-
|
|
15096
|
-
const untrackedList = gitBuffer(
|
|
15191
|
+
timeCaptureStage(
|
|
15097
15192
|
repoRoot,
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15193
|
+
"assembly-read-tree",
|
|
15194
|
+
() => gitWithEnv(repoRoot, ["read-tree", filteredTrackedTree], env)
|
|
15195
|
+
);
|
|
15196
|
+
const untrackedList = timeCaptureStage(
|
|
15197
|
+
repoRoot,
|
|
15198
|
+
"assembly-list-untracked",
|
|
15199
|
+
() => gitBuffer(repoRoot, ["ls-tree", "-r", untrackedTree], { env }).toString(
|
|
15200
|
+
"utf-8"
|
|
15201
|
+
)
|
|
15202
|
+
);
|
|
15203
|
+
let overlayEntries = 0;
|
|
15204
|
+
const indexInfo = timeCaptureStage(
|
|
15205
|
+
repoRoot,
|
|
15206
|
+
"assembly-prepare-index",
|
|
15207
|
+
() => untrackedList.split("\n").filter(Boolean).map((line) => {
|
|
15208
|
+
const [meta, filePath] = line.split(" ");
|
|
15209
|
+
const [mode, , sha] = meta.split(/\s+/);
|
|
15210
|
+
overlayEntries++;
|
|
15211
|
+
return `${mode} ${sha} ${filePath}`;
|
|
15212
|
+
}).join("\n")
|
|
15213
|
+
);
|
|
15214
|
+
appendLog(
|
|
15215
|
+
"info",
|
|
15216
|
+
`git-traces: assembly inventory (repo=${repoRoot}, untrackedEntries=${overlayEntries})`
|
|
15217
|
+
);
|
|
15108
15218
|
if (indexInfo) {
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
|
|
15219
|
+
timeCaptureStage(
|
|
15220
|
+
repoRoot,
|
|
15221
|
+
"assembly-update-index",
|
|
15222
|
+
() => gitBuffer(repoRoot, ["update-index", "--index-info"], {
|
|
15223
|
+
input: indexInfo,
|
|
15224
|
+
env
|
|
15225
|
+
})
|
|
15226
|
+
);
|
|
15113
15227
|
}
|
|
15114
|
-
return
|
|
15228
|
+
return timeCaptureStage(
|
|
15229
|
+
repoRoot,
|
|
15230
|
+
"assembly-write-tree",
|
|
15231
|
+
() => gitWithEnv(repoRoot, ["write-tree"], env)
|
|
15232
|
+
);
|
|
15115
15233
|
} finally {
|
|
15116
15234
|
try {
|
|
15117
15235
|
fs14.unlinkSync(tmpIndex);
|
|
@@ -15709,6 +15827,27 @@ async function deleteLegacySessionState(repoRoot, tool, sessionId) {
|
|
|
15709
15827
|
if (legacy?.sessionId === sessionId) await deleteStateFile(legacyFile);
|
|
15710
15828
|
}
|
|
15711
15829
|
async function acquireLock3(repoRoot, tool, sessionId, retries, delayMs, options = {}) {
|
|
15830
|
+
const finish = startCaptureStage(
|
|
15831
|
+
"lock-wait",
|
|
15832
|
+
`repo=${repoRoot}, tool=${tool}, session=${sessionId || "<legacy>"}`
|
|
15833
|
+
);
|
|
15834
|
+
let status = "failed";
|
|
15835
|
+
try {
|
|
15836
|
+
const result = await acquireLockWithoutTiming(
|
|
15837
|
+
repoRoot,
|
|
15838
|
+
tool,
|
|
15839
|
+
sessionId,
|
|
15840
|
+
retries,
|
|
15841
|
+
delayMs,
|
|
15842
|
+
options
|
|
15843
|
+
);
|
|
15844
|
+
status = "ok";
|
|
15845
|
+
return result;
|
|
15846
|
+
} finally {
|
|
15847
|
+
finish(status);
|
|
15848
|
+
}
|
|
15849
|
+
}
|
|
15850
|
+
async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, delayMs, options = {}) {
|
|
15712
15851
|
const lockPath = lockFileForRepo(repoRoot, tool, sessionId);
|
|
15713
15852
|
await fs15.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
15714
15853
|
const bounded = retries !== void 0 || delayMs !== void 0;
|