git-shots-cli 0.7.0 → 0.8.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.
Files changed (2) hide show
  1. package/dist/index.js +92 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ function checkAuthError(res) {
44
44
 
45
45
  // src/upload.ts
46
46
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
47
+ import { createHash } from "crypto";
47
48
  import { resolve as resolve2, basename, dirname } from "path";
48
49
  import { execSync } from "child_process";
49
50
  import { glob } from "glob";
@@ -56,6 +57,25 @@ function chunk(arr, size) {
56
57
  }
57
58
  return chunks;
58
59
  }
60
+ function computeFileHash(buffer) {
61
+ return createHash("sha256").update(buffer).digest("hex");
62
+ }
63
+ async function fetchServerHashes(config, branch) {
64
+ try {
65
+ const params = new URLSearchParams({ branch });
66
+ if (config.platform) params.set("platform", config.platform);
67
+ const url = `${config.server}/api/projects/${config.project}/hashes?${params}`;
68
+ const res = await fetch(url, {
69
+ headers: { ...authHeaders(config) }
70
+ });
71
+ checkAuthError(res);
72
+ if (!res.ok) return null;
73
+ const data = await res.json();
74
+ return data.hashes;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
59
79
  async function upload(config, options) {
60
80
  const dir = resolve2(process.cwd(), config.directory);
61
81
  if (!existsSync2(dir)) {
@@ -77,22 +97,81 @@ async function upload(config, options) {
77
97
  }
78
98
  const files = rawFiles.map((file) => {
79
99
  const dirName = dirname(file);
100
+ const fileName = basename(file);
80
101
  return {
81
- fieldName: dirName !== "." ? `${dirName}/${basename(file)}` : basename(file),
82
- fileName: basename(file),
102
+ fieldName: dirName !== "." ? `${dirName}/${fileName}` : fileName,
103
+ fileName,
104
+ slug: fileName.replace(/\.png$/, ""),
83
105
  fullPath: resolve2(dir, file)
84
106
  };
85
107
  });
86
- const localSlugs = files.map((f) => f.fileName.replace(/\.png$/, ""));
87
- const allSlugs = [.../* @__PURE__ */ new Set([...options.baseManifest ?? [], ...localSlugs])];
108
+ const allSlugs = [.../* @__PURE__ */ new Set([...options.baseManifest ?? [], ...files.map((f) => f.slug)])];
88
109
  if (options.force) {
89
110
  console.log(chalk2.yellow("Force mode: bypassing content hash dedup"));
90
111
  }
91
112
  console.log(chalk2.dim(`Found ${files.length} screenshots`));
113
+ let filesToUpload = files;
114
+ let clientDedupSkipped = 0;
115
+ if (!options.force) {
116
+ const [branchHashes, mainHashes] = await Promise.all([
117
+ fetchServerHashes(config, branch),
118
+ branch !== "main" ? fetchServerHashes(config, "main") : Promise.resolve(null)
119
+ ]);
120
+ if (branchHashes || mainHashes) {
121
+ console.log(chalk2.dim("Computing local hashes..."));
122
+ filesToUpload = files.filter((file) => {
123
+ file.buffer = readFileSync2(file.fullPath);
124
+ const localHash = computeFileHash(file.buffer);
125
+ const branchHash = branchHashes?.[file.slug];
126
+ const mainHash = mainHashes?.[file.slug];
127
+ if (branchHash && localHash === branchHash) return false;
128
+ if (mainHash && localHash === mainHash) return false;
129
+ return true;
130
+ });
131
+ clientDedupSkipped = files.length - filesToUpload.length;
132
+ if (clientDedupSkipped > 0) {
133
+ console.log(
134
+ chalk2.cyan(
135
+ `Client dedup: ${clientDedupSkipped} unchanged, ${filesToUpload.length} to upload`
136
+ )
137
+ );
138
+ }
139
+ } else {
140
+ console.log(chalk2.dim("Hash endpoint unavailable, uploading all files"));
141
+ }
142
+ }
92
143
  const url = `${config.server}/api/upload`;
93
- const batches = chunk(files, BATCH_SIZE);
144
+ if (filesToUpload.length === 0) {
145
+ console.log(chalk2.dim("All files unchanged, sending manifest only"));
146
+ const formData = new FormData();
147
+ formData.append("project", config.project);
148
+ formData.append("branch", branch);
149
+ formData.append("gitSha", sha);
150
+ if (config.platform) formData.append("platform", config.platform);
151
+ formData.append("allSlugs", JSON.stringify(allSlugs));
152
+ try {
153
+ const res = await fetch(url, {
154
+ method: "POST",
155
+ body: formData,
156
+ headers: { Origin: config.server, ...authHeaders(config) }
157
+ });
158
+ checkAuthError(res);
159
+ if (!res.ok) {
160
+ const data = await res.json();
161
+ console.error(chalk2.red(`Manifest update failed: ${JSON.stringify(data)}`));
162
+ process.exit(1);
163
+ }
164
+ } catch (err) {
165
+ console.error(chalk2.red(`Manifest request failed: ${err}`));
166
+ process.exit(1);
167
+ }
168
+ console.log(`
169
+ ${chalk2.dim(`${clientDedupSkipped} unchanged`)} (manifest updated)`);
170
+ return;
171
+ }
172
+ const batches = chunk(filesToUpload, BATCH_SIZE);
94
173
  let totalUploaded = 0;
95
- let totalSkipped = 0;
174
+ let serverSkipped = 0;
96
175
  const totalSkippedScreens = [];
97
176
  for (let i = 0; i < batches.length; i++) {
98
177
  const batch = batches[i];
@@ -107,7 +186,7 @@ async function upload(config, options) {
107
186
  formData.append("allSlugs", JSON.stringify(allSlugs));
108
187
  }
109
188
  for (const file of batch) {
110
- const buffer = readFileSync2(file.fullPath);
189
+ const buffer = file.buffer ?? readFileSync2(file.fullPath);
111
190
  const blob = new Blob([buffer], { type: "image/png" });
112
191
  formData.append(file.fieldName, blob, file.fileName);
113
192
  }
@@ -125,7 +204,7 @@ async function upload(config, options) {
125
204
  process.exit(1);
126
205
  }
127
206
  totalUploaded += data.uploaded ?? 0;
128
- totalSkipped += data.skipped ?? 0;
207
+ serverSkipped += data.skipped ?? 0;
129
208
  if (data.skippedScreens) totalSkippedScreens.push(...data.skippedScreens);
130
209
  if (batches.length > 1) {
131
210
  const parts2 = [];
@@ -140,9 +219,13 @@ async function upload(config, options) {
140
219
  }
141
220
  const parts = [];
142
221
  if (totalUploaded > 0) parts.push(chalk2.green(`${totalUploaded} uploaded`));
143
- if (totalSkipped > 0) parts.push(chalk2.dim(`${totalSkipped} unchanged`));
222
+ const totalUnchanged = clientDedupSkipped + serverSkipped;
223
+ if (totalUnchanged > 0) parts.push(chalk2.dim(`${totalUnchanged} unchanged`));
144
224
  console.log(`
145
225
  ${parts.join(", ") || chalk2.dim("nothing to do")}`);
226
+ if (clientDedupSkipped > 0) {
227
+ console.log(chalk2.dim(` (${clientDedupSkipped} skipped client-side, ${serverSkipped} server-side)`));
228
+ }
146
229
  if (totalSkippedScreens.length > 0) {
147
230
  const MAX_DISPLAY = 20;
148
231
  if (totalSkippedScreens.length <= MAX_DISPLAY) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-shots-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI for git-shots visual regression platform",
5
5
  "type": "module",
6
6
  "bin": {