hillclimb 0.8.8 → 0.8.10

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.
Files changed (2) hide show
  1. package/dist/main.js +154 -68
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -14711,8 +14711,11 @@ function createGitError(args, err, timeoutMs = GIT_COMMAND_TIMEOUT_MS) {
14711
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);
14712
14712
  const wrapped = new Error(message);
14713
14713
  wrapped.isGitTimeout = isTimeout;
14714
+ wrapped.gitExitStatus = failure.code || failure.signal ? void 0 : failure.status;
14714
14715
  const stderr = outputToString(failure.stderr);
14715
14716
  wrapped.gitStderr = stderr ? truncateOutput(stderr) : void 0;
14717
+ const stdout = outputToString(failure.stdout);
14718
+ wrapped.gitStdout = stdout ? truncateOutput(stdout) : void 0;
14716
14719
  wrapped.isGitMissingObject = !isTimeout && failure.code !== "ENOBUFS" && matchesMissingObject(stderr);
14717
14720
  return wrapped;
14718
14721
  }
@@ -14751,25 +14754,64 @@ function isGitRepo(dir) {
14751
14754
  return false;
14752
14755
  }
14753
14756
  }
14754
- function captureWorkingCommitSha(repoRoot) {
14755
- try {
14756
- const sha = timeCaptureStage(
14757
- repoRoot,
14758
- "stash-create",
14759
- () => git(repoRoot, ["stash", "create"])
14760
- );
14761
- if (sha) return sha;
14762
- } catch (err) {
14763
- if (isGitTimeoutError(err)) throw err;
14757
+ function stashRetryReason(failure) {
14758
+ const status = failure.gitExitStatus;
14759
+ if (status !== 1 && status !== 128) return null;
14760
+ if (/Unable to create [^\n]*index\.lock[^\n]*: File exists/.test(
14761
+ failure.gitStderr ?? ""
14762
+ )) {
14763
+ return "index-lock-contention";
14764
14764
  }
14765
- try {
14766
- return git(repoRoot, ["rev-parse", "HEAD"]);
14767
- } catch (err) {
14768
- if (isGitTimeoutError(err)) throw err;
14769
- const tree = git(repoRoot, ["write-tree"]);
14770
- return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
14765
+ if (status === 1 && !failure.gitStderr && !failure.gitStdout) {
14766
+ return "empty-stash-failure";
14767
+ }
14768
+ return null;
14769
+ }
14770
+ function captureWorkingCommitSha(repoRoot) {
14771
+ for (let attempt = 1; ; attempt++) {
14772
+ let sha;
14773
+ try {
14774
+ sha = timeCaptureStage(
14775
+ repoRoot,
14776
+ "stash-create",
14777
+ () => (
14778
+ // Keep the two expected stderr classifications independent of locale.
14779
+ gitWithEnv(repoRoot, ["stash", "create"], {
14780
+ ...process.env,
14781
+ LC_ALL: "C"
14782
+ })
14783
+ )
14784
+ );
14785
+ } catch (err) {
14786
+ const failure = err;
14787
+ const stderr = failure.gitStderr ?? "";
14788
+ if (failure.gitExitStatus === 1 && stderr.includes("You do not have the initial commit yet") && hasUnbornHead(repoRoot)) {
14789
+ appendLog(
14790
+ "info",
14791
+ `git-traces: stash create unavailable (repo=${repoRoot}, reason=unborn-head); capturing index tree`
14792
+ );
14793
+ const tree = git(repoRoot, ["write-tree"]);
14794
+ return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
14795
+ }
14796
+ const retryReason = stashRetryReason(failure);
14797
+ if (!retryReason || attempt >= 5) throw err;
14798
+ appendLog(
14799
+ "warn",
14800
+ `git-traces: retrying stash create after ${retryReason === "index-lock-contention" ? "index lock contention" : "empty Git failure (possible index contention)"} (repo=${repoRoot}, attempt=${attempt}, maxAttempts=5, reason=${retryReason}): ${failure.message}`
14801
+ );
14802
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
14803
+ continue;
14804
+ }
14805
+ return sha || git(repoRoot, ["rev-parse", "--verify", "HEAD"]);
14771
14806
  }
14772
14807
  }
14808
+ function hasUnbornHead(repoRoot) {
14809
+ const ref = git(repoRoot, ["symbolic-ref", "--quiet", "HEAD"]);
14810
+ const args = ["show-ref", "--verify", "--quiet", ref];
14811
+ const result = spawnSync("git", args, { cwd: repoRoot, ...EXEC_OPTS });
14812
+ if (result.error) throw createGitError(args, result.error);
14813
+ return result.status === 1;
14814
+ }
14773
14815
  function captureBaselineSha(repoRoot) {
14774
14816
  return captureWorkingCommitSha(repoRoot);
14775
14817
  }
@@ -15040,39 +15082,53 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
15040
15082
  }
15041
15083
  function buildUntrackedTree(repoRoot, options = {}) {
15042
15084
  const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
15043
- const list = git(repoRoot, [
15044
- "ls-files",
15045
- "--others",
15046
- "--exclude-standard",
15047
- "-z"
15048
- ]);
15049
- if (!list) return null;
15085
+ const list = timeCaptureStage(
15086
+ repoRoot,
15087
+ "untracked-list",
15088
+ () => git(repoRoot, ["ls-files", "--others", "--exclude-standard", "-z"])
15089
+ );
15050
15090
  const kept = [];
15051
- for (const relPath of list.split("\0")) {
15052
- if (!relPath) continue;
15053
- try {
15054
- const absPath = path14.join(repoRoot, relPath);
15055
- const stat = fs14.lstatSync(absPath);
15056
- const reason = classifyOmission(
15057
- relPath,
15058
- stat.size,
15059
- () => readWorkingFileHead(absPath),
15060
- limits,
15061
- false
15062
- );
15063
- if (reason) {
15064
- options.omittedFiles?.push({
15065
- path: relPath,
15066
- sizeBytes: stat.size,
15067
- tracked: false,
15068
- reason
15069
- });
15070
- continue;
15091
+ let candidates = 0;
15092
+ let keptBytes = 0;
15093
+ let omitted = 0;
15094
+ let omittedBytes = 0;
15095
+ let skipped = 0;
15096
+ timeCaptureStage(repoRoot, "untracked-filter", () => {
15097
+ for (const relPath of list.split("\0")) {
15098
+ if (!relPath) continue;
15099
+ candidates++;
15100
+ try {
15101
+ const absPath = path14.join(repoRoot, relPath);
15102
+ const stat = fs14.lstatSync(absPath);
15103
+ const reason = classifyOmission(
15104
+ relPath,
15105
+ stat.size,
15106
+ () => readWorkingFileHead(absPath),
15107
+ limits,
15108
+ false
15109
+ );
15110
+ if (reason) {
15111
+ omitted++;
15112
+ omittedBytes += stat.size;
15113
+ options.omittedFiles?.push({
15114
+ path: relPath,
15115
+ sizeBytes: stat.size,
15116
+ tracked: false,
15117
+ reason
15118
+ });
15119
+ continue;
15120
+ }
15121
+ kept.push(relPath);
15122
+ keptBytes += stat.size;
15123
+ } catch {
15124
+ skipped++;
15071
15125
  }
15072
- kept.push(relPath);
15073
- } catch {
15074
15126
  }
15075
- }
15127
+ });
15128
+ appendLog(
15129
+ "info",
15130
+ `git-traces: untracked inventory (repo=${repoRoot}, candidates=${candidates}, kept=${kept.length}, keptBytes=${keptBytes}, omitted=${omitted}, omittedBytes=${omittedBytes}, skipped=${skipped})`
15131
+ );
15076
15132
  if (kept.length === 0) return null;
15077
15133
  const tmpIndex = path14.join(
15078
15134
  os7.tmpdir(),
@@ -15080,11 +15136,19 @@ function buildUntrackedTree(repoRoot, options = {}) {
15080
15136
  );
15081
15137
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
15082
15138
  try {
15083
- gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
15084
- input: `${kept.join("\0")}\0`,
15085
- env
15086
- });
15087
- return gitWithEnv(repoRoot, ["write-tree"], env);
15139
+ timeCaptureStage(
15140
+ repoRoot,
15141
+ "untracked-hash-and-index",
15142
+ () => gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
15143
+ input: `${kept.join("\0")}\0`,
15144
+ env
15145
+ })
15146
+ );
15147
+ return timeCaptureStage(
15148
+ repoRoot,
15149
+ "untracked-write-tree",
15150
+ () => gitWithEnv(repoRoot, ["write-tree"], env)
15151
+ );
15088
15152
  } finally {
15089
15153
  try {
15090
15154
  fs14.unlinkSync(tmpIndex);
@@ -15139,26 +15203,48 @@ function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
15139
15203
  );
15140
15204
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
15141
15205
  try {
15142
- gitWithEnv(repoRoot, ["read-tree", filteredTrackedTree], env);
15143
- const untrackedList = gitBuffer(
15206
+ timeCaptureStage(
15144
15207
  repoRoot,
15145
- ["ls-tree", "-r", untrackedTree],
15146
- {
15147
- env
15148
- }
15149
- ).toString("utf-8");
15150
- const indexInfo = untrackedList.split("\n").filter(Boolean).map((line) => {
15151
- const [meta, filePath] = line.split(" ");
15152
- const [mode, , sha] = meta.split(/\s+/);
15153
- return `${mode} ${sha} ${filePath}`;
15154
- }).join("\n");
15208
+ "assembly-read-tree",
15209
+ () => gitWithEnv(repoRoot, ["read-tree", filteredTrackedTree], env)
15210
+ );
15211
+ const untrackedList = timeCaptureStage(
15212
+ repoRoot,
15213
+ "assembly-list-untracked",
15214
+ () => gitBuffer(repoRoot, ["ls-tree", "-r", untrackedTree], { env }).toString(
15215
+ "utf-8"
15216
+ )
15217
+ );
15218
+ let overlayEntries = 0;
15219
+ const indexInfo = timeCaptureStage(
15220
+ repoRoot,
15221
+ "assembly-prepare-index",
15222
+ () => untrackedList.split("\n").filter(Boolean).map((line) => {
15223
+ const [meta, filePath] = line.split(" ");
15224
+ const [mode, , sha] = meta.split(/\s+/);
15225
+ overlayEntries++;
15226
+ return `${mode} ${sha} ${filePath}`;
15227
+ }).join("\n")
15228
+ );
15229
+ appendLog(
15230
+ "info",
15231
+ `git-traces: assembly inventory (repo=${repoRoot}, untrackedEntries=${overlayEntries})`
15232
+ );
15155
15233
  if (indexInfo) {
15156
- gitBuffer(repoRoot, ["update-index", "--index-info"], {
15157
- input: indexInfo,
15158
- env
15159
- });
15234
+ timeCaptureStage(
15235
+ repoRoot,
15236
+ "assembly-update-index",
15237
+ () => gitBuffer(repoRoot, ["update-index", "--index-info"], {
15238
+ input: indexInfo,
15239
+ env
15240
+ })
15241
+ );
15160
15242
  }
15161
- return gitWithEnv(repoRoot, ["write-tree"], env);
15243
+ return timeCaptureStage(
15244
+ repoRoot,
15245
+ "assembly-write-tree",
15246
+ () => gitWithEnv(repoRoot, ["write-tree"], env)
15247
+ );
15162
15248
  } finally {
15163
15249
  try {
15164
15250
  fs14.unlinkSync(tmpIndex);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",