hillclimb 0.4.0 → 0.4.2

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/cli.js +123 -21
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13515,7 +13515,7 @@ var EXEC_OPTS = {
13515
13515
  maxBuffer: 50 * 1024 * 1024
13516
13516
  };
13517
13517
  var MAX_ERROR_OUTPUT_CHARS = 2e3;
13518
- var MAX_UNTRACKED_FILE_BYTES = 10 * 1024 * 1024;
13518
+ var MAX_SNAPSHOT_FILE_BYTES = 10 * 1024 * 1024;
13519
13519
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
13520
13520
  function quoteGitArg(arg) {
13521
13521
  if (/^[A-Za-z0-9_./:=@%+,-]+$/.test(arg)) return arg;
@@ -13619,7 +13619,81 @@ function captureSnapshotSha(repoRoot) {
13619
13619
  }
13620
13620
  return git(repoRoot, ["rev-parse", "HEAD"]);
13621
13621
  }
13622
- function buildUntrackedTree(repoRoot) {
13622
+ function recordOmittedSnapshotFile(omittedFiles, file, options = {}) {
13623
+ omittedFiles?.push(file);
13624
+ if (options.log === false) return;
13625
+ const action = file.tracked ? "omitting tracked" : "skipping untracked";
13626
+ appendLog(
13627
+ "warn",
13628
+ `git-traces: ${action} ${file.path} (${file.sizeBytes} bytes > ${MAX_SNAPSHOT_FILE_BYTES} limit)`
13629
+ );
13630
+ }
13631
+ function parseLsTreeLongZ(output) {
13632
+ const entries = [];
13633
+ for (const record of output.toString("utf-8").split("\0")) {
13634
+ if (!record) continue;
13635
+ const tab = record.indexOf(" ");
13636
+ if (tab === -1) continue;
13637
+ const meta = record.slice(0, tab);
13638
+ const filePath = record.slice(tab + 1);
13639
+ const [mode, type, sha, sizeRaw] = meta.trim().split(/\s+/);
13640
+ if (type !== "blob" || !mode || !sha || !sizeRaw) continue;
13641
+ const sizeBytes = Number.parseInt(sizeRaw, 10);
13642
+ if (!Number.isFinite(sizeBytes)) continue;
13643
+ entries.push({ mode, sha, sizeBytes, path: filePath });
13644
+ }
13645
+ return entries;
13646
+ }
13647
+ function listOversizedTreeFiles(repoRoot, treeSha, options = {}) {
13648
+ const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
13649
+ const oversized = [];
13650
+ for (const entry of parseLsTreeLongZ(output)) {
13651
+ if (entry.sizeBytes <= MAX_SNAPSHOT_FILE_BYTES) continue;
13652
+ const file = {
13653
+ path: entry.path,
13654
+ sizeBytes: entry.sizeBytes,
13655
+ tracked: true,
13656
+ reason: "file-over-limit",
13657
+ gitObjectSha: entry.sha
13658
+ };
13659
+ oversized.push(file);
13660
+ recordOmittedSnapshotFile(options.omittedFiles, file, {
13661
+ log: options.log
13662
+ });
13663
+ }
13664
+ return oversized;
13665
+ }
13666
+ function removePathsFromIndex(repoRoot, env, paths) {
13667
+ if (paths.length === 0) return;
13668
+ gitBuffer(repoRoot, ["update-index", "--force-remove", "-z", "--stdin"], {
13669
+ input: `${paths.join("\0")}\0`,
13670
+ env
13671
+ });
13672
+ }
13673
+ function filterOversizedFilesFromTree(repoRoot, treeSha, options = {}) {
13674
+ const oversizedFiles = listOversizedTreeFiles(repoRoot, treeSha, options);
13675
+ if (oversizedFiles.length === 0) return treeSha;
13676
+ const tmpIndex = path14.join(
13677
+ os6.tmpdir(),
13678
+ `hillclimb-filter-${Date.now()}-${process.pid}`
13679
+ );
13680
+ const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
13681
+ try {
13682
+ gitWithEnv(repoRoot, ["read-tree", treeSha], env);
13683
+ removePathsFromIndex(
13684
+ repoRoot,
13685
+ env,
13686
+ oversizedFiles.map((file) => file.path)
13687
+ );
13688
+ return gitWithEnv(repoRoot, ["write-tree"], env);
13689
+ } finally {
13690
+ try {
13691
+ fs11.unlinkSync(tmpIndex);
13692
+ } catch {
13693
+ }
13694
+ }
13695
+ }
13696
+ function buildUntrackedTree(repoRoot, omittedFiles) {
13623
13697
  const list = git(repoRoot, [
13624
13698
  "ls-files",
13625
13699
  "--others",
@@ -13632,11 +13706,13 @@ function buildUntrackedTree(repoRoot) {
13632
13706
  if (!relPath) continue;
13633
13707
  try {
13634
13708
  const stat = fs11.lstatSync(path14.join(repoRoot, relPath));
13635
- if (stat.size > MAX_UNTRACKED_FILE_BYTES) {
13636
- appendLog(
13637
- "warn",
13638
- `git-traces: skipping untracked ${relPath} (${stat.size} bytes > ${MAX_UNTRACKED_FILE_BYTES} limit)`
13639
- );
13709
+ if (stat.size > MAX_SNAPSHOT_FILE_BYTES) {
13710
+ recordOmittedSnapshotFile(omittedFiles, {
13711
+ path: relPath,
13712
+ sizeBytes: stat.size,
13713
+ tracked: false,
13714
+ reason: "file-over-limit"
13715
+ });
13640
13716
  continue;
13641
13717
  }
13642
13718
  kept.push(relPath);
@@ -13662,19 +13738,25 @@ function buildUntrackedTree(repoRoot) {
13662
13738
  }
13663
13739
  }
13664
13740
  }
13665
- function buildSnapshotTree(repoRoot, stashSha) {
13741
+ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
13666
13742
  const trackedTree = git(repoRoot, ["rev-parse", `${stashSha}^{tree}`]);
13667
- const untrackedTree = buildUntrackedTree(repoRoot);
13668
- if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA) {
13669
- return trackedTree;
13670
- }
13743
+ const filteredTrackedTree = filterOversizedFilesFromTree(
13744
+ repoRoot,
13745
+ trackedTree,
13746
+ {
13747
+ omittedFiles: options.omittedFiles
13748
+ }
13749
+ );
13750
+ const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
13751
+ if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
13752
+ return filteredTrackedTree;
13671
13753
  const tmpIndex = path14.join(
13672
13754
  os6.tmpdir(),
13673
13755
  `hillclimb-index-${Date.now()}-${process.pid}`
13674
13756
  );
13675
13757
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
13676
13758
  try {
13677
- gitWithEnv(repoRoot, ["read-tree", trackedTree], env);
13759
+ gitWithEnv(repoRoot, ["read-tree", filteredTrackedTree], env);
13678
13760
  const untrackedList = gitBuffer(
13679
13761
  repoRoot,
13680
13762
  ["ls-tree", "-r", untrackedTree],
@@ -13727,11 +13809,22 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
13727
13809
  }
13728
13810
  function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha) {
13729
13811
  if (fromTreeSha === toTreeSha) return null;
13812
+ const filteredFromTreeSha = filterOversizedFilesFromTree(
13813
+ repoRoot,
13814
+ fromTreeSha,
13815
+ {
13816
+ log: false
13817
+ }
13818
+ );
13819
+ const filteredToTreeSha = filterOversizedFilesFromTree(repoRoot, toTreeSha, {
13820
+ log: false
13821
+ });
13822
+ if (filteredFromTreeSha === filteredToTreeSha) return null;
13730
13823
  const diff = gitBuffer(repoRoot, [
13731
13824
  "diff",
13732
13825
  "--binary",
13733
- fromTreeSha,
13734
- toTreeSha
13826
+ filteredFromTreeSha,
13827
+ filteredToTreeSha
13735
13828
  ]);
13736
13829
  if (diff.length === 0) return null;
13737
13830
  return Buffer.from(gzipSync(diff));
@@ -13778,7 +13871,7 @@ function parseDirtyFilesFromStatus(status) {
13778
13871
  return pathPart;
13779
13872
  }).filter(Boolean);
13780
13873
  }
13781
- function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt) {
13874
+ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt, omittedFiles = []) {
13782
13875
  const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
13783
13876
  const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
13784
13877
  const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
@@ -13802,7 +13895,8 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
13802
13895
  branch,
13803
13896
  remoteUrl,
13804
13897
  isDirty: dirtyFiles.length > 0,
13805
- dirtyFiles
13898
+ dirtyFiles,
13899
+ ...omittedFiles.length > 0 ? { omittedFiles } : {}
13806
13900
  },
13807
13901
  author: { name: authorName, email: authorEmail },
13808
13902
  hostname: os6.hostname(),
@@ -14246,6 +14340,10 @@ function freezeEpochBaseline(params) {
14246
14340
  sessionId,
14247
14341
  epoch
14248
14342
  );
14343
+ const omittedFiles = [];
14344
+ const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
14345
+ omittedFiles
14346
+ });
14249
14347
  const baselineMetadata = buildBaselineMetadata(
14250
14348
  repoRoot,
14251
14349
  sessionId,
@@ -14255,9 +14353,9 @@ function freezeEpochBaseline(params) {
14255
14353
  epoch,
14256
14354
  prevHeadSha,
14257
14355
  transitionKind,
14258
- startedAt
14356
+ startedAt,
14357
+ omittedFiles
14259
14358
  );
14260
- const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
14261
14359
  pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha);
14262
14360
  return { baselineSha, baselineTreeSha, baselineMetadata, headSha };
14263
14361
  } catch (err) {
@@ -14303,6 +14401,10 @@ function buildEpochBaselineArtifacts(params) {
14303
14401
  } = params;
14304
14402
  const prefix = epochPrefix(epoch);
14305
14403
  try {
14404
+ const omittedFiles = [];
14405
+ const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
14406
+ omittedFiles
14407
+ });
14306
14408
  const metadata = buildBaselineMetadata(
14307
14409
  repoRoot,
14308
14410
  sessionId,
@@ -14312,9 +14414,9 @@ function buildEpochBaselineArtifacts(params) {
14312
14414
  epoch,
14313
14415
  prevHeadSha,
14314
14416
  transitionKind,
14315
- startedAt
14417
+ startedAt,
14418
+ omittedFiles
14316
14419
  );
14317
- const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha);
14318
14420
  return buildFrozenEpochBaselineArtifacts({
14319
14421
  repoRoot,
14320
14422
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",