tether-sync 0.2.0 → 0.6.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.
@@ -99,7 +99,7 @@ function call(endpoint, method, params = {}, timeoutMs = 10 * 60 * 1e3) {
99
99
  }
100
100
 
101
101
  // packages/client/src/version.js
102
- var VERSION = "0.2.0";
102
+ var VERSION = "0.6.0";
103
103
 
104
104
  // packages/client/src/update.js
105
105
  import fs2 from "node:fs";
@@ -154,10 +154,33 @@ function npmInstall(spec, prefix) {
154
154
  // packages/client/src/git.js
155
155
  import fs3 from "node:fs";
156
156
  import path3 from "node:path";
157
- import { execFileSync } from "node:child_process";
157
+ import { execFileSync, spawnSync as spawnSync2 } from "node:child_process";
158
158
  var GIT_MODES = ["keep", "follow"];
159
+ var gitExe;
160
+ function gitBinary() {
161
+ if (gitExe !== void 0) return gitExe;
162
+ const candidates = ["git"];
163
+ if (isWindows) {
164
+ const pf = [process.env.ProgramFiles, process.env["ProgramFiles(x86)"], process.env.LOCALAPPDATA && path3.join(process.env.LOCALAPPDATA, "Programs")].filter(Boolean);
165
+ for (const p of pf) candidates.push(path3.join(p, "Git", "cmd", "git.exe"));
166
+ const desktop = process.env.LOCALAPPDATA && path3.join(process.env.LOCALAPPDATA, "GitHubDesktop");
167
+ if (desktop && fs3.existsSync(desktop)) {
168
+ for (const app of fs3.readdirSync(desktop).filter((d) => d.startsWith("app-")).sort().reverse()) {
169
+ candidates.push(path3.join(desktop, app, "resources", "app", "git", "cmd", "git.exe"));
170
+ }
171
+ }
172
+ } else candidates.push("/usr/bin/git", "/usr/local/bin/git");
173
+ gitExe = candidates.find((c) => spawnSync2(c, ["--version"], { stdio: "ignore", windowsHide: true }).status === 0) ?? null;
174
+ return gitExe;
175
+ }
159
176
  function git(cwd, args) {
160
- return execFileSync("git", ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim();
177
+ const exe = gitBinary();
178
+ if (!exe) throw new Error("git is not installed");
179
+ return execFileSync(exe, ["-C", cwd, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true, maxBuffer: 16 * 1024 * 1024 }).replace(/\r?\n$/, "");
180
+ }
181
+ function gitOk(cwd, args) {
182
+ const exe = gitBinary();
183
+ return !!exe && spawnSync2(exe, ["-C", cwd, ...args], { stdio: "ignore", windowsHide: true }).status === 0;
161
184
  }
162
185
  function findRepo(p) {
163
186
  let dir = path3.resolve(p);
@@ -166,11 +189,23 @@ function findRepo(p) {
166
189
  if (up === dir) return null;
167
190
  dir = up;
168
191
  }
169
- try {
170
- const [root, gitDir, hooks] = git(dir, ["rev-parse", "--show-toplevel", "--absolute-git-dir", "--git-path", "hooks"]).split(/\r?\n/);
171
- return { root: path3.resolve(root), gitDir: path3.resolve(gitDir), hooksDir: path3.resolve(dir, hooks) };
172
- } catch {
173
- return null;
192
+ if (gitBinary()) {
193
+ try {
194
+ const [root, gitDir] = git(dir, ["rev-parse", "--show-toplevel", "--absolute-git-dir"]).split(/\r?\n/);
195
+ return { root: path3.resolve(root), gitDir: path3.resolve(gitDir) };
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+ for (let d = dir; ; d = path3.dirname(d)) {
201
+ const dot = path3.join(d, ".git");
202
+ const st = fs3.existsSync(dot) ? fs3.statSync(dot) : null;
203
+ if (st?.isDirectory()) return { root: d, gitDir: dot };
204
+ if (st?.isFile()) {
205
+ const m = fs3.readFileSync(dot, "utf8").match(/^gitdir:\s*(.+)$/m);
206
+ if (m) return { root: d, gitDir: path3.resolve(d, m[1].trim()) };
207
+ }
208
+ if (path3.dirname(d) === d) return null;
174
209
  }
175
210
  }
176
211
  function gitBusy(gitDir) {
@@ -183,47 +218,135 @@ function gitBusy(gitDir) {
183
218
  if (has("REVERT_HEAD")) return "revert in progress";
184
219
  return null;
185
220
  }
186
- var HOOKS = ["post-merge", "post-checkout", "post-rewrite"];
187
- var quote = (s) => `'${s.replace(/\\/g, "/").replace(/'/g, `'\\''`)}'`;
188
- var begin = (home) => `# >>> tether (${home.replace(/\\/g, "/")}) >>>`;
189
- var end = (home) => `# <<< tether (${home.replace(/\\/g, "/")}) <<<`;
190
- function installHooks(hooksDir, { home, node, cli }) {
191
- fs3.mkdirSync(hooksDir, { recursive: true });
192
- for (const hook of HOOKS) {
193
- const file = path3.join(hooksDir, hook);
194
- let text = fs3.existsSync(file) ? fs3.readFileSync(file, "utf8") : "#!/bin/sh\n";
195
- text = stripBlock(text, home);
196
- const block = [
197
- begin(home),
198
- "# Lets Tether pick up files git changed (follow mode). Remove with `tether git <path> --mode keep`.",
199
- `TETHER_HOME=${quote(home)} ${quote(node)} ${quote(cli)} git-sync --hook ${hook} --repo "$(pwd)" >/dev/null 2>&1 || true`,
200
- end(home)
201
- ].join("\n");
202
- fs3.writeFileSync(file, `${text.replace(/\s*$/, "")}
203
-
204
- ${block}
205
- `);
206
- try {
207
- fs3.chmodSync(file, 493);
208
- } catch {
221
+ var rel = (root, abs) => path3.relative(root, abs).split(path3.sep).join("/");
222
+ var head = (root) => {
223
+ try {
224
+ return git(root, ["rev-parse", "--verify", "-q", "HEAD"]);
225
+ } catch {
226
+ return null;
227
+ }
228
+ };
229
+ function fileBlob(root, abs) {
230
+ try {
231
+ return git(root, ["-c", "filter.tether-keep.clean=", "hash-object", `--path=${rel(root, abs)}`, "--", abs]);
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+ function blobAt(root, rev, abs) {
237
+ try {
238
+ return git(root, ["rev-parse", "--verify", "-q", `${rev}:${rel(root, abs)}`]);
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
243
+ function indexBlob(root, abs) {
244
+ try {
245
+ const line = git(root, ["ls-files", "-s", "--", rel(root, abs)]);
246
+ return line ? line.split(/\s+/)[1] : null;
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+ function changeFromGit(root, abs) {
252
+ const blob = fileBlob(root, abs);
253
+ if (!blob) return { fromGit: false };
254
+ const h = head(root);
255
+ if (h && blobAt(root, "HEAD", abs) === blob) return { fromGit: true, commit: h };
256
+ if (indexBlob(root, abs) === blob) return { fromGit: true, commit: null };
257
+ return { fromGit: false };
258
+ }
259
+ var isTracked = (root, abs) => gitOk(root, ["ls-files", "--error-unmatch", "--", rel(root, abs)]);
260
+ var FILTER = "tether-keep";
261
+ var CLEAN = `sh -c 'git cat-file blob ":$1" 2>/dev/null || cat' -- %f`;
262
+ function attributesFile(root) {
263
+ return path3.resolve(root, git(root, ["rev-parse", "--git-path", "info/attributes"]));
264
+ }
265
+ function attrLine(relPath) {
266
+ const pattern = /[\s"\\]/.test(relPath) ? `"/${relPath.replace(/["\\]/g, "\\$&")}"` : `/${relPath.replace(/[*?[\]!]/g, "\\$&")}`;
267
+ return `${pattern} filter=${FILTER}`;
268
+ }
269
+ var readLines = (file) => fs3.existsSync(file) ? fs3.readFileSync(file, "utf8").split(/\r?\n/).filter((l) => l !== "") : [];
270
+ function setKeep(root, abs, on) {
271
+ if (!gitBinary()) return false;
272
+ try {
273
+ const file = attributesFile(root);
274
+ const line = attrLine(rel(root, abs));
275
+ const lines = readLines(file);
276
+ const has = lines.includes(line);
277
+ if (on) {
278
+ git(root, ["config", `filter.${FILTER}.clean`, CLEAN]);
279
+ git(root, ["config", `filter.${FILTER}.required`, "false"]);
280
+ if (!has) {
281
+ fs3.mkdirSync(path3.dirname(file), { recursive: true });
282
+ fs3.writeFileSync(file, [...lines, line].join("\n") + "\n");
283
+ }
284
+ restage(root, abs);
285
+ } else if (has) {
286
+ const rest = lines.filter((l) => l !== line);
287
+ fs3.writeFileSync(file, rest.length ? rest.join("\n") + "\n" : "");
288
+ }
289
+ return true;
290
+ } catch {
291
+ return false;
292
+ }
293
+ }
294
+ function isKept(root, abs) {
295
+ try {
296
+ return readLines(attributesFile(root)).includes(attrLine(rel(root, abs)));
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+ function restage(root, abs) {
302
+ if (isTracked(root, abs)) gitOk(root, ["add", "--", rel(root, abs)]);
303
+ }
304
+ var isAncestor = (root, commit) => !!commit && gitOk(root, ["merge-base", "--is-ancestor", commit, "HEAD"]);
305
+ function changesBetween(root, from, to) {
306
+ try {
307
+ const out = git(root, ["-c", "core.quotePath=false", "log", "--reverse", "--first-parent", "--format=%x01", "--name-status", "-M", `${from}..${to}`]);
308
+ const renames = /* @__PURE__ */ new Map();
309
+ const added = /* @__PURE__ */ new Set();
310
+ for (const line of out.split(/\r?\n/)) {
311
+ const parts = line.split(" ");
312
+ if (parts.length < 2) continue;
313
+ const status = parts[0][0];
314
+ if (status === "R") {
315
+ const [, oldP, newP] = parts;
316
+ const origin = [...renames.entries()].find(([, cur]) => cur === oldP)?.[0] ?? oldP;
317
+ if (added.delete(oldP)) added.add(newP);
318
+ else renames.set(origin, newP);
319
+ } else if (status === "A") added.add(parts[1]);
320
+ else if (status === "D") {
321
+ added.delete(parts[1]);
322
+ for (const [o, cur] of renames) if (cur === parts[1]) renames.delete(o);
323
+ }
209
324
  }
325
+ const abs = (p) => path3.join(root, ...p.split("/"));
326
+ return [
327
+ ...[...renames].map(([o, n]) => ({ status: "R", from: abs(o), to: abs(n) })),
328
+ ...[...added].map((p) => ({ status: "A", from: abs(p), to: abs(p) }))
329
+ ];
330
+ } catch {
331
+ return [];
210
332
  }
211
333
  }
334
+ var HOOKS = ["post-merge", "post-checkout", "post-rewrite"];
335
+ var begin = (home) => `# >>> tether (${home.replace(/\\/g, "/")}) >>>`;
336
+ var end = (home) => `# <<< tether (${home.replace(/\\/g, "/")}) <<<`;
212
337
  function removeHooks(hooksDir, home) {
213
338
  for (const hook of HOOKS) {
214
339
  const file = path3.join(hooksDir, hook);
215
340
  if (!fs3.existsSync(file)) continue;
216
- const text = stripBlock(fs3.readFileSync(file, "utf8"), home);
341
+ let text = fs3.readFileSync(file, "utf8");
342
+ const b = text.indexOf(begin(home));
343
+ if (b < 0) continue;
344
+ const e = text.indexOf(end(home), b);
345
+ text = (text.slice(0, b) + (e < 0 ? "" : text.slice(e + end(home).length))).replace(/\n{3,}/g, "\n\n");
217
346
  if (/^(#!.*\n)?\s*$/.test(text)) fs3.rmSync(file);
218
347
  else fs3.writeFileSync(file, text.replace(/\s*$/, "\n"));
219
348
  }
220
349
  }
221
- function stripBlock(text, home) {
222
- const b = text.indexOf(begin(home));
223
- if (b < 0) return text;
224
- const e = text.indexOf(end(home), b);
225
- return (text.slice(0, b) + (e < 0 ? "" : text.slice(e + end(home).length))).replace(/\n{3,}/g, "\n\n");
226
- }
227
350
 
228
351
  export {
229
352
  isWindows,
@@ -241,8 +364,19 @@ export {
241
364
  prefixOf,
242
365
  npmInstall,
243
366
  GIT_MODES,
367
+ gitBinary,
244
368
  findRepo,
245
369
  gitBusy,
246
- installHooks,
370
+ head,
371
+ fileBlob,
372
+ blobAt,
373
+ indexBlob,
374
+ changeFromGit,
375
+ isTracked,
376
+ setKeep,
377
+ isKept,
378
+ restage,
379
+ isAncestor,
380
+ changesBetween,
247
381
  removeHooks
248
382
  };