@procedures/snapshot 0.10.0 → 0.12.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.
@@ -11,9 +11,10 @@ import { tmpdir, hostname } from "node:os";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import * as tar from "tar";
13
13
  import type { ProcedureContext } from "@procedures/client";
14
+ import { isOutsideRoot, realPathIsInside, resolveRoot } from "@procedures/fs/root";
14
15
  import { assertSnapshotName, isExcluded } from "./names.js";
15
16
  import { readPart, sha256File } from "./archive-io.js";
16
- import { gitText, pnpmVersion } from "./commands.js";
17
+ import { gitCall, pnpmVersion } from "./commands.js";
17
18
  import type {
18
19
  SnapshotCreateInput,
19
20
  SnapshotCreateOutput,
@@ -49,7 +50,7 @@ export async function snapshotCreate(
49
50
  );
50
51
 
51
52
  // Get environment info
52
- const environment = await getEnvironmentInfo(ctx.signal);
53
+ const environment = await getEnvironmentInfo(ctx.signal, ctx.fsRoot);
53
54
 
54
55
  // Create metadata
55
56
  const metadata: SnapshotMetadata = {
@@ -132,10 +133,10 @@ export async function snapshotCreate(
132
133
  /**
133
134
  * Get ecosystem repository paths from manifest
134
135
  */
135
- async function getEcosystemPaths(_ctx: ProcedureContext): Promise<string[]> {
136
+ async function getEcosystemPaths(ctx: ProcedureContext): Promise<string[]> {
136
137
  // TODO: Read from ecosystem manifest
137
- // For now, return current working directory
138
- return [process.cwd()];
138
+ // For now, the root of the files under a root, else the current working directory
139
+ return [ctx.fsRoot ?? process.cwd()];
139
140
  }
140
141
 
141
142
  /**
@@ -152,8 +153,8 @@ async function getRepositoryInfo(
152
153
  >(["git", "status"], { cwd: repoPath });
153
154
 
154
155
  // The commit hash ("" for a repository with no commit) and the remote URL (none without "origin")
155
- const commit = (await gitText(["rev-parse", "HEAD"], repoPath, ctx.signal)) ?? "";
156
- const remoteUrl = await gitText(["remote", "get-url", "origin"], repoPath, ctx.signal);
156
+ const commit = (await gitCall<{ value: string }>(ctx, ["git", "revParse"], { cwd: repoPath }))?.value ?? "";
157
+ const remoteUrl = (await gitCall<{ url: string }>(ctx, ["git", "remote"], { cwd: repoPath, name: "origin" }))?.url;
157
158
 
158
159
  // Get stash count
159
160
  let stashCount = 0;
@@ -167,11 +168,13 @@ async function getRepositoryInfo(
167
168
  // No stashes
168
169
  }
169
170
 
170
- // Get package name from package.json
171
+ // Get package name from package.json. Under a root, a package.json whose real path is outside
172
+ // the root (a symlink) is not read.
171
173
  let name = basename(repoPath);
172
174
  try {
173
175
  const pkgPath = join(repoPath, "package.json");
174
- if (existsSync(pkgPath)) {
176
+ const readable = ctx.fsRoot === undefined || (await realPathIsInside(await resolveRoot(ctx.fsRoot), pkgPath));
177
+ if (readable && existsSync(pkgPath)) {
175
178
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
176
179
  name = pkg.name || name;
177
180
  }
@@ -191,6 +194,8 @@ async function getRepositoryInfo(
191
194
  behind: result.behind,
192
195
  };
193
196
  } catch (error) {
197
+ // A refusal of the root is not "no repository": the repository is there, and the root refuses it
198
+ if (isOutsideRoot(error)) throw error;
194
199
  // Not a git repo or error
195
200
  return {
196
201
  path: repoPath,
@@ -208,11 +213,11 @@ async function getRepositoryInfo(
208
213
  /**
209
214
  * Get environment info
210
215
  */
211
- async function getEnvironmentInfo(signal?: AbortSignal): Promise<SnapshotMetadata["environment"]> {
216
+ async function getEnvironmentInfo(signal?: AbortSignal, fsRoot?: string): Promise<SnapshotMetadata["environment"]> {
212
217
  return {
213
218
  os: process.platform,
214
219
  nodeVersion: process.version,
215
- pnpmVersion: await pnpmVersion(signal),
220
+ pnpmVersion: await pnpmVersion(signal, fsRoot),
216
221
  username: process.env["USER"] || process.env["USERNAME"] || "unknown",
217
222
  hostname: hostname(),
218
223
  };
@@ -259,11 +264,14 @@ async function createArchive(
259
264
  }
260
265
  // heavy preset includes everything
261
266
 
267
+ // Tar reads only the listed folders from their parent. Without `follow`, it stores a symlink as
268
+ // a link and does not read its target: under a root, the archive has only files of the root.
262
269
  await tar.create(
263
270
  {
264
271
  gzip: gzipOpts,
265
272
  file: archivePath,
266
273
  cwd,
274
+ follow: false,
267
275
  // Whole path segments, and "*.ext" for file endings. (Before, `path.includes("dist")`
268
276
  // left out src/distance.ts, and "*.log" matched nothing: deep dive DATA-3.)
269
277
  filter: (path) => !isExcluded(path, excludePatterns),
@@ -6,8 +6,9 @@
6
6
 
7
7
  import { existsSync } from "node:fs";
8
8
  import { basename } from "node:path";
9
- import { gitText } from "./commands.js";
10
9
  import type { ProcedureContext } from "@procedures/client";
10
+ import { isOutsideRoot } from "@procedures/fs/root";
11
+ import { gitCall } from "./commands.js";
11
12
  import type {
12
13
  SnapshotDiffInput,
13
14
  SnapshotDiffOutput,
@@ -72,7 +73,7 @@ export async function snapshotDiff(
72
73
 
73
74
  try {
74
75
  // Get current branch (a failure is an error of this repository: the catch below)
75
- const currentBranch = await gitText(["rev-parse", "--abbrev-ref", "HEAD"], repoPath, ctx.signal);
76
+ const currentBranch = (await gitCall<{ value: string }>(ctx, ["git", "revParse"], { cwd: repoPath, abbrevRef: true }))?.value;
76
77
  if (currentBranch === undefined) throw new Error(`git cannot read the branch of ${repoPath}`);
77
78
 
78
79
  if (currentBranch !== snapshotRepo.branch) {
@@ -83,7 +84,7 @@ export async function snapshotDiff(
83
84
  }
84
85
 
85
86
  // Get current commit
86
- const currentCommit = await gitText(["rev-parse", "HEAD"], repoPath, ctx.signal);
87
+ const currentCommit = (await gitCall<{ value: string }>(ctx, ["git", "revParse"], { cwd: repoPath }))?.value;
87
88
  if (currentCommit === undefined) throw new Error(`git cannot read the commit of ${repoPath}`);
88
89
 
89
90
  if (currentCommit !== snapshotRepo.commit) {
@@ -99,12 +100,13 @@ export async function snapshotDiff(
99
100
  if (!/^[0-9a-f]{7,64}$/i.test(snapshotRepo.commit)) {
100
101
  throw new Error(`Not a commit hash: ${snapshotRepo.commit}`);
101
102
  }
102
- const diffStat = await gitText(["diff", "--stat", `${snapshotRepo.commit}..HEAD`], repoPath, ctx.signal);
103
- if (diffStat === undefined) throw new Error(`git cannot compare ${snapshotRepo.commit} with HEAD`);
104
- // Count lines that represent files (contain |)
105
- diff.filesChanged = diffStat.split("\n").filter((l) => l.includes("|")).length;
103
+ const changes = await gitCall<{ files: unknown[] }>(ctx, ["git", "diff"], { cwd: repoPath, ref: `${snapshotRepo.commit}..HEAD`, stat: true });
104
+ if (changes === undefined) throw new Error(`git cannot compare ${snapshotRepo.commit} with HEAD`);
105
+ // One entry for each file that changed, also a binary file
106
+ diff.filesChanged = changes.files.length;
106
107
  totalFilesChanged += diff.filesChanged;
107
- } catch {
108
+ } catch (error) {
109
+ if (isOutsideRoot(error)) throw error;
108
110
  // Snapshot commit may not exist locally
109
111
  diff.filesChanged = -1;
110
112
  }
@@ -126,7 +128,9 @@ export async function snapshotDiff(
126
128
  ) {
127
129
  reposChanged++;
128
130
  }
129
- } catch {
131
+ } catch (error) {
132
+ // A refusal of the root is not "no repository": the repository is there, and the root refuses it
133
+ if (isOutsideRoot(error)) throw error;
130
134
  // Not a git repo or error
131
135
  diff.filesChanged = -1;
132
136
  reposChanged++;