shadow-git 0.1.0 → 1.0.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.
- package/.claude/settings.local.json +5 -1
- package/.temp/.sgit/index +0 -0
- package/.temp/.sgit/info/exclude +16 -7
- package/.temp/.sgit/objects/03/7f4e37a02573e40ab78b35dd4b442cc6110bae +0 -0
- package/.temp/.sgit/objects/2f/f0fd85dec20039eef6ff63ead905778a88843f +0 -0
- package/.temp/.sgit/objects/c3/2432f724b666f647fe21a1ce16535cd7693f74 +0 -0
- package/.temp/.sgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
- package/.temp/.sgit/objects/ee/9dd32cca3b134623d184eb4de5805704c493e3 +0 -0
- package/.temp/.sgit.json +6 -2
- package/.temp/README.md +0 -3
- package/.temp/rec1/rec2/test +0 -0
- package/.temp/temp/shouldnt-track +0 -0
- package/.temp/test/1 +0 -0
- package/CLAUDE.md +10 -2
- package/README.md +16 -29
- package/dist/cli.js +810 -119
- package/package.json +1 -1
- package/src/cli.ts +50 -19
- package/src/commands/debug/gitignores.ts +28 -0
- package/src/commands/debug/index.ts +32 -0
- package/src/commands/track/add.ts +11 -0
- package/src/commands/track/clone.ts +213 -0
- package/src/commands/track/completion.ts +50 -15
- package/src/commands/track/destroy.ts +97 -19
- package/src/commands/track/ignore.ts +55 -0
- package/src/commands/track/index.ts +23 -1
- package/src/commands/track/init.ts +5 -2
- package/src/commands/track/sync.ts +1 -1
- package/src/commands/track/unignore.ts +50 -0
- package/src/commands/track/verify.ts +93 -20
- package/src/core/errors.ts +23 -2
- package/src/core/exclude.ts +40 -43
- package/src/core/gitignore.ts +242 -0
- package/src/core/hooks.ts +19 -0
- package/src/core/manifest.ts +33 -1
- package/src/core/paths.ts +21 -3
- package/src/core/repository.ts +17 -2
- package/tests/commands/track/add.test.ts +31 -1
- package/tests/commands/track/clone.test.ts +315 -0
- package/tests/commands/track/completion.test.ts +1 -1
- package/tests/commands/track/destroy.test.ts +68 -2
- package/tests/commands/track/ignore.test.ts +109 -0
- package/tests/commands/track/init.test.ts +27 -4
- package/tests/commands/track/remove.test.ts +1 -1
- package/tests/commands/track/sync.test.ts +3 -3
- package/tests/commands/track/unignore.test.ts +72 -0
- package/tests/commands/track/verify.test.ts +132 -1
- package/tests/core/exclude.test.ts +107 -19
- package/tests/core/gitignore.test.ts +561 -0
- package/tests/core/hooks.test.ts +34 -0
- package/tests/core/paths.test.ts +24 -0
- package/tests/core/repository.test.ts +39 -5
- package/tests/helpers/guard-child.mjs +125 -0
- package/tests/helpers.ts +37 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command as Command2 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/core/repository.ts
|
|
7
7
|
import { access, lstat } from "fs/promises";
|
|
@@ -35,7 +35,14 @@ var SgitError = class extends Error {
|
|
|
35
35
|
};
|
|
36
36
|
var NotGitRepoError = class extends SgitError {
|
|
37
37
|
constructor() {
|
|
38
|
-
super("not a git repository");
|
|
38
|
+
super("not a git repository (no .git in current directory)");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var NoGitIdentityError = class extends SgitError {
|
|
42
|
+
constructor() {
|
|
43
|
+
super(
|
|
44
|
+
"git user identity not configured; set user.name and user.email with 'git config'"
|
|
45
|
+
);
|
|
39
46
|
}
|
|
40
47
|
};
|
|
41
48
|
var AlreadyInitializedError = class extends SgitError {
|
|
@@ -45,7 +52,7 @@ var AlreadyInitializedError = class extends SgitError {
|
|
|
45
52
|
};
|
|
46
53
|
var NotInitializedError = class extends SgitError {
|
|
47
54
|
constructor() {
|
|
48
|
-
super("not a shadow git repository; run 'sgit track init'
|
|
55
|
+
super("not a shadow git repository; run 'sgit track init' or 'sgit track clone <url>'");
|
|
49
56
|
}
|
|
50
57
|
};
|
|
51
58
|
var PathNotFoundError = class extends SgitError {
|
|
@@ -66,11 +73,25 @@ var PathNotTrackedError = class extends SgitError {
|
|
|
66
73
|
|
|
67
74
|
// src/core/repository.ts
|
|
68
75
|
async function findRoot() {
|
|
76
|
+
const cwd = process.cwd();
|
|
69
77
|
try {
|
|
70
|
-
|
|
78
|
+
await access(join(cwd, ".git"), constants.F_OK);
|
|
71
79
|
} catch {
|
|
72
80
|
throw new NotGitRepoError();
|
|
73
81
|
}
|
|
82
|
+
return cwd;
|
|
83
|
+
}
|
|
84
|
+
async function validateGitIdentity(root) {
|
|
85
|
+
try {
|
|
86
|
+
await execGit(["config", "user.name"], { cwd: root });
|
|
87
|
+
} catch {
|
|
88
|
+
throw new NoGitIdentityError();
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await execGit(["config", "user.email"], { cwd: root });
|
|
92
|
+
} catch {
|
|
93
|
+
throw new NoGitIdentityError();
|
|
94
|
+
}
|
|
74
95
|
}
|
|
75
96
|
async function validateShadowRepo(root) {
|
|
76
97
|
try {
|
|
@@ -142,7 +163,11 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
142
163
|
import { join as join3 } from "path";
|
|
143
164
|
async function readManifest(root) {
|
|
144
165
|
const content = await readFile(join3(root, ".sgit.json"), "utf-8");
|
|
145
|
-
|
|
166
|
+
const data = JSON.parse(content);
|
|
167
|
+
if (!Array.isArray(data.ignore)) {
|
|
168
|
+
data.ignore = [];
|
|
169
|
+
}
|
|
170
|
+
return data;
|
|
146
171
|
}
|
|
147
172
|
async function writeManifest(root, data) {
|
|
148
173
|
await writeFile(
|
|
@@ -169,81 +194,106 @@ async function removePath(root, normalizedPath) {
|
|
|
169
194
|
await writeManifest(root, manifest);
|
|
170
195
|
return true;
|
|
171
196
|
}
|
|
197
|
+
async function addIgnore(root, normalizedPath) {
|
|
198
|
+
const manifest = await readManifest(root);
|
|
199
|
+
if (manifest.ignore.includes(normalizedPath)) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
manifest.ignore.push(normalizedPath);
|
|
203
|
+
await writeManifest(root, manifest);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
async function removeIgnore(root, normalizedPath) {
|
|
207
|
+
const manifest = await readManifest(root);
|
|
208
|
+
const index = manifest.ignore.indexOf(normalizedPath);
|
|
209
|
+
if (index === -1) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
manifest.ignore.splice(index, 1);
|
|
213
|
+
await writeManifest(root, manifest);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
172
216
|
|
|
173
217
|
// src/core/exclude.ts
|
|
174
|
-
var
|
|
175
|
-
var
|
|
218
|
+
var SGIT_BLOCK_START = "# sgit-managed";
|
|
219
|
+
var SGIT_BLOCK_END = "# end: sgit-managed";
|
|
176
220
|
function generateMainExclude(existingContent, manifestPaths) {
|
|
177
221
|
const lines = existingContent.split("\n");
|
|
178
222
|
const preserved = [];
|
|
223
|
+
let insideBlock = false;
|
|
179
224
|
let skipNext = false;
|
|
180
225
|
for (const line of lines) {
|
|
181
|
-
if (line ===
|
|
182
|
-
|
|
226
|
+
if (line === SGIT_BLOCK_START || line === "# sgit-managed (do not edit below this line)") {
|
|
227
|
+
insideBlock = true;
|
|
183
228
|
continue;
|
|
184
229
|
}
|
|
185
|
-
if (line ===
|
|
186
|
-
|
|
230
|
+
if (line === SGIT_BLOCK_END) {
|
|
231
|
+
insideBlock = false;
|
|
187
232
|
continue;
|
|
188
233
|
}
|
|
189
|
-
if (
|
|
234
|
+
if (insideBlock) continue;
|
|
235
|
+
if (line === "# sgit") {
|
|
236
|
+
skipNext = true;
|
|
190
237
|
continue;
|
|
191
238
|
}
|
|
192
239
|
if (skipNext) {
|
|
193
240
|
skipNext = false;
|
|
194
241
|
continue;
|
|
195
242
|
}
|
|
243
|
+
if (line.endsWith(" # sgit")) continue;
|
|
196
244
|
preserved.push(line);
|
|
197
245
|
}
|
|
198
246
|
while (preserved.length > 0 && preserved[preserved.length - 1].trim() === "") {
|
|
199
247
|
preserved.pop();
|
|
200
248
|
}
|
|
201
|
-
const sgitEntries = [".sgit", ".sgit.json", ...manifestPaths];
|
|
202
|
-
|
|
203
|
-
const sections = [
|
|
249
|
+
const sgitEntries = [".sgit", ".sgit.json", ".gitignore-sgit.bat", ...manifestPaths];
|
|
250
|
+
return [
|
|
204
251
|
...preserved,
|
|
205
252
|
"",
|
|
206
|
-
|
|
207
|
-
...
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
function generateShadowExclude(manifestPaths) {
|
|
212
|
-
const lines = [
|
|
213
|
-
"# Exclude everything by default",
|
|
214
|
-
"*",
|
|
215
|
-
"",
|
|
216
|
-
"# .sgit.json (always tracked)",
|
|
217
|
-
"!.sgit.json"
|
|
218
|
-
];
|
|
253
|
+
SGIT_BLOCK_START,
|
|
254
|
+
...sgitEntries,
|
|
255
|
+
SGIT_BLOCK_END
|
|
256
|
+
].join("\n") + "\n";
|
|
257
|
+
}
|
|
258
|
+
function generateShadowExclude(manifestPaths, ignorePaths = []) {
|
|
219
259
|
const emitted = /* @__PURE__ */ new Set();
|
|
260
|
+
const entryLines = [];
|
|
220
261
|
for (const trackedPath of manifestPaths) {
|
|
221
|
-
const pathLines = [];
|
|
222
262
|
const ancestors = getAncestors(trackedPath);
|
|
223
263
|
for (const ancestor of ancestors) {
|
|
224
264
|
const unignore = `!${ancestor}/`;
|
|
225
265
|
const reignore = `${ancestor}/*`;
|
|
226
266
|
if (!emitted.has(unignore)) {
|
|
227
|
-
|
|
267
|
+
entryLines.push(unignore);
|
|
228
268
|
emitted.add(unignore);
|
|
229
269
|
}
|
|
230
270
|
if (!emitted.has(reignore)) {
|
|
231
|
-
|
|
271
|
+
entryLines.push(reignore);
|
|
232
272
|
emitted.add(reignore);
|
|
233
273
|
}
|
|
234
274
|
}
|
|
235
275
|
const target = `!${trackedPath}`;
|
|
236
276
|
if (!emitted.has(target)) {
|
|
237
|
-
|
|
277
|
+
entryLines.push(target);
|
|
238
278
|
emitted.add(target);
|
|
239
279
|
}
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
280
|
+
if (trackedPath.endsWith("/")) {
|
|
281
|
+
const recursive = `!${trackedPath}**`;
|
|
282
|
+
if (!emitted.has(recursive)) {
|
|
283
|
+
entryLines.push(recursive);
|
|
284
|
+
emitted.add(recursive);
|
|
285
|
+
}
|
|
244
286
|
}
|
|
245
287
|
}
|
|
246
|
-
return
|
|
288
|
+
return [
|
|
289
|
+
SGIT_BLOCK_START,
|
|
290
|
+
"*",
|
|
291
|
+
"!.sgit.json",
|
|
292
|
+
...entryLines,
|
|
293
|
+
...ignorePaths,
|
|
294
|
+
".gitignore-sgit.bat",
|
|
295
|
+
SGIT_BLOCK_END
|
|
296
|
+
].join("\n") + "\n";
|
|
247
297
|
}
|
|
248
298
|
function getAncestors(path) {
|
|
249
299
|
const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
@@ -296,6 +346,16 @@ else
|
|
|
296
346
|
echo "sgit: not found in PATH, skipping pre-commit check" >&2
|
|
297
347
|
fi
|
|
298
348
|
`;
|
|
349
|
+
var SHADOW_POST_MERGE_HOOK = `#!/bin/sh
|
|
350
|
+
if command -v sgit >/dev/null 2>&1; then
|
|
351
|
+
sgit track sync
|
|
352
|
+
fi
|
|
353
|
+
`;
|
|
354
|
+
var SHADOW_POST_CHECKOUT_HOOK = `#!/bin/sh
|
|
355
|
+
if command -v sgit >/dev/null 2>&1; then
|
|
356
|
+
sgit track sync
|
|
357
|
+
fi
|
|
358
|
+
`;
|
|
299
359
|
async function getHookDir(root) {
|
|
300
360
|
try {
|
|
301
361
|
const hookPath = await execGit(["config", "core.hooksPath"], {
|
|
@@ -337,6 +397,12 @@ async function installShadowHook(sgitDir) {
|
|
|
337
397
|
await mkdir(hooksDir, { recursive: true });
|
|
338
398
|
await writeFile2(join4(hooksDir, "pre-commit"), SHADOW_HOOK, { mode: 493 });
|
|
339
399
|
}
|
|
400
|
+
async function installShadowSyncHooks(sgitDir) {
|
|
401
|
+
const hooksDir = join4(sgitDir, "hooks");
|
|
402
|
+
await mkdir(hooksDir, { recursive: true });
|
|
403
|
+
await writeFile2(join4(hooksDir, "post-merge"), SHADOW_POST_MERGE_HOOK, { mode: 493 });
|
|
404
|
+
await writeFile2(join4(hooksDir, "post-checkout"), SHADOW_POST_CHECKOUT_HOOK, { mode: 493 });
|
|
405
|
+
}
|
|
340
406
|
async function uninstallMainHook(root) {
|
|
341
407
|
const hookDir = await getHookDir(root);
|
|
342
408
|
const preCommitPath = join4(hookDir, "pre-commit");
|
|
@@ -380,6 +446,7 @@ async function trackInit() {
|
|
|
380
446
|
const root = await findRoot();
|
|
381
447
|
await validateNotWorktree(root);
|
|
382
448
|
await validateNotSubmodule(root);
|
|
449
|
+
await validateGitIdentity(root);
|
|
383
450
|
const sgitDir = join5(root, ".sgit");
|
|
384
451
|
try {
|
|
385
452
|
await access3(sgitDir, constants3.F_OK);
|
|
@@ -409,7 +476,7 @@ async function trackInit() {
|
|
|
409
476
|
join5(sgitDir, "info", "exclude"),
|
|
410
477
|
generateShadowExclude([])
|
|
411
478
|
);
|
|
412
|
-
await writeManifest(root, { paths: [] });
|
|
479
|
+
await writeManifest(root, { paths: [], ignore: [] });
|
|
413
480
|
const mainExcludePath = join5(root, ".git", "info", "exclude");
|
|
414
481
|
let existingExclude = "";
|
|
415
482
|
try {
|
|
@@ -420,6 +487,7 @@ async function trackInit() {
|
|
|
420
487
|
await writeFile3(mainExcludePath, generateMainExclude(existingExclude, []));
|
|
421
488
|
await installMainHook(root);
|
|
422
489
|
await installShadowHook(sgitDir);
|
|
490
|
+
await installShadowSyncHooks(sgitDir);
|
|
423
491
|
await execGit(["add", "-f", ".sgit.json"], {
|
|
424
492
|
gitDir: sgitDir,
|
|
425
493
|
workTree: root
|
|
@@ -450,7 +518,7 @@ async function syncExcludes(root) {
|
|
|
450
518
|
);
|
|
451
519
|
const sgitExcludePath = join6(root, ".sgit", "info", "exclude");
|
|
452
520
|
await mkdir3(join6(root, ".sgit", "info"), { recursive: true });
|
|
453
|
-
await writeFile4(sgitExcludePath, generateShadowExclude(manifest.paths));
|
|
521
|
+
await writeFile4(sgitExcludePath, generateShadowExclude(manifest.paths, manifest.ignore));
|
|
454
522
|
for (const p of manifest.paths) {
|
|
455
523
|
const checkPath = p.endsWith("/") ? p.slice(0, -1) : p;
|
|
456
524
|
try {
|
|
@@ -494,21 +562,35 @@ async function normalizePath(inputPath, root) {
|
|
|
494
562
|
}
|
|
495
563
|
return rel;
|
|
496
564
|
}
|
|
497
|
-
function isPathCoveredByManifest(filePath, manifestPaths) {
|
|
565
|
+
function isPathCoveredByManifest(filePath, manifestPaths, ignorePaths = []) {
|
|
498
566
|
const caseInsensitive = process.platform === "darwin";
|
|
499
567
|
const normalize = caseInsensitive ? (s) => s.toLowerCase() : (s) => s;
|
|
500
568
|
const normalizedFile = normalize(filePath);
|
|
569
|
+
let covered = false;
|
|
501
570
|
for (const mPath of manifestPaths) {
|
|
502
571
|
const normalizedManifest = normalize(mPath);
|
|
503
|
-
if (normalizedManifest === normalizedFile)
|
|
572
|
+
if (normalizedManifest === normalizedFile) {
|
|
573
|
+
covered = true;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
504
576
|
if (normalizedManifest.endsWith("/") && normalizedFile.startsWith(normalizedManifest)) {
|
|
505
|
-
|
|
577
|
+
covered = true;
|
|
578
|
+
break;
|
|
506
579
|
}
|
|
507
580
|
}
|
|
508
|
-
return false;
|
|
581
|
+
if (!covered) return false;
|
|
582
|
+
for (const iPath of ignorePaths) {
|
|
583
|
+
const normalizedIgnore = normalize(iPath);
|
|
584
|
+
if (normalizedIgnore === normalizedFile) return false;
|
|
585
|
+
if (normalizedIgnore.endsWith("/") && normalizedFile.startsWith(normalizedIgnore)) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
509
590
|
}
|
|
510
591
|
|
|
511
592
|
// src/commands/track/add.ts
|
|
593
|
+
var BLOCKED_BASENAMES = /* @__PURE__ */ new Set([".gitignore", ".gitignore-sgit.bat"]);
|
|
512
594
|
async function trackAdd(paths, opts = {}) {
|
|
513
595
|
const root = await findRoot();
|
|
514
596
|
await validateShadowRepo(root);
|
|
@@ -516,6 +598,12 @@ async function trackAdd(paths, opts = {}) {
|
|
|
516
598
|
const added = [];
|
|
517
599
|
for (const inputPath of paths) {
|
|
518
600
|
const normalized = await normalizePath(inputPath, root);
|
|
601
|
+
const basename = normalized.split("/").pop() ?? "";
|
|
602
|
+
if (BLOCKED_BASENAMES.has(basename)) {
|
|
603
|
+
throw new SgitError(
|
|
604
|
+
`cannot track ${normalized}: .gitignore files are managed by sgit and cannot be tracked directly`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
519
607
|
const checkPath = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
520
608
|
try {
|
|
521
609
|
await access5(join7(root, checkPath), constants5.F_OK);
|
|
@@ -614,41 +702,252 @@ async function trackList() {
|
|
|
614
702
|
}
|
|
615
703
|
|
|
616
704
|
// src/commands/track/verify.ts
|
|
617
|
-
import { join as
|
|
705
|
+
import { join as join10 } from "path";
|
|
706
|
+
|
|
707
|
+
// src/core/gitignore.ts
|
|
708
|
+
import {
|
|
709
|
+
readdirSync,
|
|
710
|
+
renameSync,
|
|
711
|
+
existsSync,
|
|
712
|
+
readFileSync,
|
|
713
|
+
writeFileSync,
|
|
714
|
+
unlinkSync
|
|
715
|
+
} from "fs";
|
|
716
|
+
import { join as join9, relative as relative3, sep as sep3 } from "path";
|
|
717
|
+
var DISABLED_NAME = ".gitignore-sgit.bat";
|
|
718
|
+
var LOCK_FILE = ".gitignore-guard";
|
|
719
|
+
function enableGitignores(root, manifestPaths) {
|
|
720
|
+
const disabled = findInTrackedDirs(root, manifestPaths, DISABLED_NAME);
|
|
721
|
+
const enabled = [];
|
|
722
|
+
for (const relPath of disabled) {
|
|
723
|
+
const absPath = join9(root, relPath);
|
|
724
|
+
const restoredPath = absPath.substring(0, absPath.length - DISABLED_NAME.length) + ".gitignore";
|
|
725
|
+
renameSync(absPath, restoredPath);
|
|
726
|
+
enabled.push(relPath);
|
|
727
|
+
}
|
|
728
|
+
return enabled;
|
|
729
|
+
}
|
|
730
|
+
function findStaleGitignores(root, manifestPaths) {
|
|
731
|
+
return findInTrackedDirs(root, manifestPaths, DISABLED_NAME);
|
|
732
|
+
}
|
|
733
|
+
function isGuardActive(root) {
|
|
734
|
+
const lockPath = join9(root, ".sgit", LOCK_FILE);
|
|
735
|
+
try {
|
|
736
|
+
const pid = parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
737
|
+
if (isNaN(pid)) return false;
|
|
738
|
+
process.kill(pid, 0);
|
|
739
|
+
return true;
|
|
740
|
+
} catch {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function setupGitignoreGuard(root, manifestPaths) {
|
|
745
|
+
const lockPath = join9(root, ".sgit", LOCK_FILE);
|
|
746
|
+
if (isGuardActive(root)) {
|
|
747
|
+
return () => {
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const stale = findInTrackedDirs(root, manifestPaths, DISABLED_NAME);
|
|
751
|
+
if (stale.length > 0) {
|
|
752
|
+
warn(
|
|
753
|
+
"found stale .gitignore-sgit.bat files from a previous crash; restoring"
|
|
754
|
+
);
|
|
755
|
+
enableGitignores(root, manifestPaths);
|
|
756
|
+
}
|
|
757
|
+
cleanStaleLock(lockPath);
|
|
758
|
+
const gitignores = findInTrackedDirs(root, manifestPaths, ".gitignore");
|
|
759
|
+
if (gitignores.length === 0) {
|
|
760
|
+
return () => {
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
for (const relPath of gitignores) {
|
|
764
|
+
const absPath = join9(root, relPath);
|
|
765
|
+
const dir = absPath.substring(0, absPath.length - ".gitignore".length);
|
|
766
|
+
renameSync(absPath, dir + DISABLED_NAME);
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
writeFileSync(lockPath, String(process.pid));
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
let cleaned = false;
|
|
773
|
+
const cleanup = () => {
|
|
774
|
+
if (cleaned) return;
|
|
775
|
+
cleaned = true;
|
|
776
|
+
try {
|
|
777
|
+
enableGitignores(root, manifestPaths);
|
|
778
|
+
} catch {
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
unlinkSync(lockPath);
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
const onSigint = () => {
|
|
786
|
+
cleanup();
|
|
787
|
+
process.exit(130);
|
|
788
|
+
};
|
|
789
|
+
const onSigterm = () => {
|
|
790
|
+
cleanup();
|
|
791
|
+
process.exit(143);
|
|
792
|
+
};
|
|
793
|
+
process.on("SIGINT", onSigint);
|
|
794
|
+
process.on("SIGTERM", onSigterm);
|
|
795
|
+
process.on("exit", cleanup);
|
|
796
|
+
return () => {
|
|
797
|
+
process.removeListener("SIGINT", onSigint);
|
|
798
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
799
|
+
process.removeListener("exit", cleanup);
|
|
800
|
+
cleanup();
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function findInTrackedDirs(root, manifestPaths, targetName) {
|
|
804
|
+
const found = /* @__PURE__ */ new Set();
|
|
805
|
+
for (const mPath of manifestPaths) {
|
|
806
|
+
for (const ancestor of getAncestorDirs(mPath)) {
|
|
807
|
+
const dirAbs = ancestor === "" ? root : join9(root, ancestor);
|
|
808
|
+
const targetPath = join9(dirAbs, targetName);
|
|
809
|
+
if (existsSync(targetPath)) {
|
|
810
|
+
found.add(targetPath);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (mPath.endsWith("/")) {
|
|
814
|
+
const dirAbs = join9(root, mPath);
|
|
815
|
+
if (existsSync(dirAbs)) {
|
|
816
|
+
walkSync(dirAbs, targetName, found);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return [...found].map((abs) => relative3(root, abs).split(sep3).join("/"));
|
|
821
|
+
}
|
|
822
|
+
function getAncestorDirs(path) {
|
|
823
|
+
const clean = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
824
|
+
const parts = clean.split("/");
|
|
825
|
+
parts.pop();
|
|
826
|
+
const ancestors = [""];
|
|
827
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
828
|
+
ancestors.push(parts.slice(0, i).join("/"));
|
|
829
|
+
}
|
|
830
|
+
return ancestors;
|
|
831
|
+
}
|
|
832
|
+
function walkSync(dir, targetName, results) {
|
|
833
|
+
let entries;
|
|
834
|
+
try {
|
|
835
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
836
|
+
} catch {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
for (const entry of entries) {
|
|
840
|
+
if (entry.isDirectory() && (entry.name === ".git" || entry.name === ".sgit")) {
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const fullPath = join9(dir, entry.name);
|
|
844
|
+
if (entry.isDirectory()) {
|
|
845
|
+
walkSync(fullPath, targetName, results);
|
|
846
|
+
} else if (entry.name === targetName) {
|
|
847
|
+
results.add(fullPath);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function cleanStaleLock(lockPath) {
|
|
852
|
+
try {
|
|
853
|
+
unlinkSync(lockPath);
|
|
854
|
+
} catch {
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/commands/track/verify.ts
|
|
618
859
|
async function verifyMain(root) {
|
|
619
860
|
const manifest = await readManifest(root);
|
|
620
|
-
|
|
861
|
+
const violations = [];
|
|
862
|
+
if (!isGuardActive(root)) {
|
|
863
|
+
const stale = findStaleGitignores(root, manifest.paths);
|
|
864
|
+
violations.push(
|
|
865
|
+
...stale.map(
|
|
866
|
+
(f) => `${f} (stale .gitignore-sgit.bat \u2014 rename back to .gitignore)`
|
|
867
|
+
)
|
|
868
|
+
);
|
|
869
|
+
}
|
|
621
870
|
try {
|
|
622
|
-
staged = await execGit(["diff", "--cached", "--name-only"], {
|
|
623
|
-
gitDir:
|
|
871
|
+
const staged = await execGit(["diff", "--cached", "--name-only", "--diff-filter=ACMR"], {
|
|
872
|
+
gitDir: join10(root, ".git"),
|
|
624
873
|
workTree: root
|
|
625
874
|
});
|
|
875
|
+
if (staged.trim()) {
|
|
876
|
+
const stagedFiles = staged.trim().split("\n");
|
|
877
|
+
violations.push(
|
|
878
|
+
...stagedFiles.filter((f) => isPathCoveredByManifest(f, manifest.paths, manifest.ignore))
|
|
879
|
+
);
|
|
880
|
+
}
|
|
626
881
|
} catch {
|
|
627
|
-
return { valid: true, violations: [] };
|
|
628
882
|
}
|
|
629
|
-
if (!staged.trim()) return { valid: true, violations: [] };
|
|
630
|
-
const stagedFiles = staged.trim().split("\n");
|
|
631
|
-
const violations = stagedFiles.filter(
|
|
632
|
-
(f) => isPathCoveredByManifest(f, manifest.paths)
|
|
633
|
-
);
|
|
634
883
|
return { valid: violations.length === 0, violations };
|
|
635
884
|
}
|
|
636
885
|
async function verifyShadow(root) {
|
|
637
|
-
|
|
638
|
-
|
|
886
|
+
let manifest;
|
|
887
|
+
try {
|
|
888
|
+
manifest = await readManifest(root);
|
|
889
|
+
} catch {
|
|
890
|
+
return {
|
|
891
|
+
valid: false,
|
|
892
|
+
violations: [".sgit.json (missing or invalid on disk)"]
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const sgitDir = join10(root, ".sgit");
|
|
896
|
+
const violations = [];
|
|
897
|
+
if (!isGuardActive(root)) {
|
|
898
|
+
const stale = findStaleGitignores(root, manifest.paths);
|
|
899
|
+
violations.push(
|
|
900
|
+
...stale.map(
|
|
901
|
+
(f) => `${f} (stale .gitignore-sgit.bat \u2014 rename back to .gitignore)`
|
|
902
|
+
)
|
|
903
|
+
);
|
|
904
|
+
}
|
|
639
905
|
try {
|
|
640
|
-
|
|
641
|
-
gitDir:
|
|
906
|
+
const deleted = await execGit(["diff", "--cached", "--name-only", "--diff-filter=D"], {
|
|
907
|
+
gitDir: sgitDir,
|
|
642
908
|
workTree: root
|
|
643
909
|
});
|
|
910
|
+
if (deleted.trim().split("\n").includes(".sgit.json")) {
|
|
911
|
+
violations.push(".sgit.json (cannot delete sgit manifest)");
|
|
912
|
+
}
|
|
913
|
+
} catch {
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
const modified = await execGit(["diff", "--cached", "--name-only", "--diff-filter=M"], {
|
|
917
|
+
gitDir: sgitDir,
|
|
918
|
+
workTree: root
|
|
919
|
+
});
|
|
920
|
+
if (modified.trim().split("\n").includes(".sgit.json")) {
|
|
921
|
+
const content = await execGit(["show", ":0:.sgit.json"], {
|
|
922
|
+
gitDir: sgitDir,
|
|
923
|
+
workTree: root
|
|
924
|
+
});
|
|
925
|
+
try {
|
|
926
|
+
const parsed = JSON.parse(content);
|
|
927
|
+
if (!Array.isArray(parsed.paths)) {
|
|
928
|
+
violations.push('.sgit.json (invalid: missing "paths" array)');
|
|
929
|
+
}
|
|
930
|
+
} catch {
|
|
931
|
+
violations.push(".sgit.json (invalid JSON)");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
const staged = await execGit(["diff", "--cached", "--name-only"], {
|
|
938
|
+
gitDir: sgitDir,
|
|
939
|
+
workTree: root
|
|
940
|
+
});
|
|
941
|
+
if (staged.trim()) {
|
|
942
|
+
const stagedFiles = staged.trim().split("\n");
|
|
943
|
+
violations.push(
|
|
944
|
+
...stagedFiles.filter(
|
|
945
|
+
(f) => f !== ".sgit.json" && !isPathCoveredByManifest(f, manifest.paths, manifest.ignore)
|
|
946
|
+
)
|
|
947
|
+
);
|
|
948
|
+
}
|
|
644
949
|
} catch {
|
|
645
|
-
return { valid: true, violations: [] };
|
|
646
950
|
}
|
|
647
|
-
if (!staged.trim()) return { valid: true, violations: [] };
|
|
648
|
-
const stagedFiles = staged.trim().split("\n");
|
|
649
|
-
const violations = stagedFiles.filter(
|
|
650
|
-
(f) => f !== ".sgit.json" && !isPathCoveredByManifest(f, manifest.paths)
|
|
651
|
-
);
|
|
652
951
|
return { valid: violations.length === 0, violations };
|
|
653
952
|
}
|
|
654
953
|
async function trackVerify(opts) {
|
|
@@ -687,71 +986,359 @@ async function trackVerify(opts) {
|
|
|
687
986
|
}
|
|
688
987
|
}
|
|
689
988
|
|
|
690
|
-
// src/commands/track/
|
|
691
|
-
import { join as
|
|
692
|
-
import { readFile as readFile5, writeFile as writeFile5, rm as rm2 } from "fs/promises";
|
|
989
|
+
// src/commands/track/clone.ts
|
|
990
|
+
import { join as join11, extname } from "path";
|
|
991
|
+
import { access as access6, readFile as readFile5, writeFile as writeFile5, rename as rename2, mkdir as mkdir4, rm as rm2 } from "fs/promises";
|
|
992
|
+
import { constants as constants6 } from "fs";
|
|
693
993
|
import { createInterface } from "readline";
|
|
994
|
+
import { randomBytes } from "crypto";
|
|
995
|
+
async function trackClone(url, opts = {}) {
|
|
996
|
+
const root = await findRoot();
|
|
997
|
+
await validateNotWorktree(root);
|
|
998
|
+
await validateNotSubmodule(root);
|
|
999
|
+
await validateGitIdentity(root);
|
|
1000
|
+
const sgitDir = join11(root, ".sgit");
|
|
1001
|
+
try {
|
|
1002
|
+
await access6(sgitDir, constants6.F_OK);
|
|
1003
|
+
throw new AlreadyInitializedError();
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
if (err instanceof AlreadyInitializedError) throw err;
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
await execGit(["clone", "--bare", url, sgitDir]);
|
|
1009
|
+
} catch {
|
|
1010
|
+
try {
|
|
1011
|
+
await rm2(sgitDir, { recursive: true, force: true });
|
|
1012
|
+
} catch {
|
|
1013
|
+
}
|
|
1014
|
+
throw new SgitError(`failed to clone from ${url}`);
|
|
1015
|
+
}
|
|
1016
|
+
await execGit(["config", "core.bare", "false"], { gitDir: sgitDir });
|
|
1017
|
+
await execGit(["config", "core.worktree", root], { gitDir: sgitDir });
|
|
1018
|
+
try {
|
|
1019
|
+
const name = await execGit(["config", "user.name"], { cwd: root });
|
|
1020
|
+
if (name) {
|
|
1021
|
+
await execGit(["config", "user.name", name], { gitDir: sgitDir });
|
|
1022
|
+
}
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
const email = await execGit(["config", "user.email"], { cwd: root });
|
|
1027
|
+
if (email) {
|
|
1028
|
+
await execGit(["config", "user.email", email], { gitDir: sgitDir });
|
|
1029
|
+
}
|
|
1030
|
+
} catch {
|
|
1031
|
+
}
|
|
1032
|
+
let manifest;
|
|
1033
|
+
try {
|
|
1034
|
+
const manifestJson = await execGit(["show", "HEAD:.sgit.json"], { gitDir: sgitDir });
|
|
1035
|
+
manifest = JSON.parse(manifestJson);
|
|
1036
|
+
} catch {
|
|
1037
|
+
manifest = { paths: [], ignore: [] };
|
|
1038
|
+
}
|
|
1039
|
+
let trackedFiles = [];
|
|
1040
|
+
try {
|
|
1041
|
+
const output = await execGit(["ls-tree", "-r", "--name-only", "HEAD"], { gitDir: sgitDir });
|
|
1042
|
+
if (output.trim()) {
|
|
1043
|
+
trackedFiles = output.trim().split("\n");
|
|
1044
|
+
}
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1047
|
+
const conflicts = [];
|
|
1048
|
+
for (const file of trackedFiles) {
|
|
1049
|
+
try {
|
|
1050
|
+
await access6(join11(root, file), constants6.F_OK);
|
|
1051
|
+
conflicts.push(file);
|
|
1052
|
+
} catch {
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
let choice = 1;
|
|
1056
|
+
if (conflicts.length > 0) {
|
|
1057
|
+
if (opts.onConflict) {
|
|
1058
|
+
choice = opts.onConflict;
|
|
1059
|
+
} else {
|
|
1060
|
+
warn(`${conflicts.length} file(s) already exist on disk:`);
|
|
1061
|
+
for (const f of conflicts) {
|
|
1062
|
+
info(` ${f}`);
|
|
1063
|
+
}
|
|
1064
|
+
choice = await promptConflictResolution();
|
|
1065
|
+
}
|
|
1066
|
+
if (choice === 2) {
|
|
1067
|
+
const savedContent = /* @__PURE__ */ new Map();
|
|
1068
|
+
for (const file of conflicts) {
|
|
1069
|
+
savedContent.set(file, await readFile5(join11(root, file)));
|
|
1070
|
+
}
|
|
1071
|
+
if (trackedFiles.length > 0) {
|
|
1072
|
+
await execGit(["checkout", "HEAD", "--", "."], { gitDir: sgitDir, workTree: root });
|
|
1073
|
+
}
|
|
1074
|
+
for (const [file, content] of savedContent) {
|
|
1075
|
+
await mkdir4(join11(root, file, ".."), { recursive: true });
|
|
1076
|
+
await writeFile5(join11(root, file), content);
|
|
1077
|
+
}
|
|
1078
|
+
} else if (choice === 3) {
|
|
1079
|
+
for (const file of conflicts) {
|
|
1080
|
+
const renamed = generateOriginalName(file);
|
|
1081
|
+
await rename2(join11(root, file), join11(root, renamed));
|
|
1082
|
+
}
|
|
1083
|
+
if (trackedFiles.length > 0) {
|
|
1084
|
+
await execGit(["checkout", "HEAD", "--", "."], { gitDir: sgitDir, workTree: root });
|
|
1085
|
+
}
|
|
1086
|
+
} else {
|
|
1087
|
+
if (trackedFiles.length > 0) {
|
|
1088
|
+
await execGit(["checkout", "HEAD", "--", "."], { gitDir: sgitDir, workTree: root });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
if (trackedFiles.length > 0) {
|
|
1093
|
+
await execGit(["checkout", "HEAD", "--", "."], { gitDir: sgitDir, workTree: root });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
await syncExcludes(root);
|
|
1097
|
+
await installMainHook(root);
|
|
1098
|
+
await installShadowHook(sgitDir);
|
|
1099
|
+
await installShadowSyncHooks(sgitDir);
|
|
1100
|
+
const fileCount = trackedFiles.filter((f) => f !== ".sgit.json").length;
|
|
1101
|
+
success(`Shadow repository cloned from ${url}`);
|
|
1102
|
+
info(` ${fileCount} file(s) checked out`);
|
|
1103
|
+
if (conflicts.length > 0) {
|
|
1104
|
+
switch (choice) {
|
|
1105
|
+
case 1:
|
|
1106
|
+
info(` ${conflicts.length} conflicting file(s) overwritten`);
|
|
1107
|
+
break;
|
|
1108
|
+
case 2:
|
|
1109
|
+
info(` ${conflicts.length} local file(s) kept (shadow repo shows them as modified)`);
|
|
1110
|
+
break;
|
|
1111
|
+
case 3:
|
|
1112
|
+
info(` ${conflicts.length} local file(s) renamed with -original~ suffix`);
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
async function promptConflictResolution() {
|
|
1118
|
+
const rl = createInterface({
|
|
1119
|
+
input: process.stdin,
|
|
1120
|
+
output: process.stdout
|
|
1121
|
+
});
|
|
1122
|
+
return new Promise((resolve5) => {
|
|
1123
|
+
process.stdout.write("\nHow should conflicts be resolved?\n");
|
|
1124
|
+
process.stdout.write(" 1 - Keep sgit (overwrite local files)\n");
|
|
1125
|
+
process.stdout.write(" 2 - Keep originals (local files stay, sgit shows them as modified)\n");
|
|
1126
|
+
process.stdout.write(" 3 - Rename originals (add -original~ suffix, checkout sgit versions)\n");
|
|
1127
|
+
rl.question("\nChoice [1/2/3]: ", (answer) => {
|
|
1128
|
+
rl.close();
|
|
1129
|
+
const num = parseInt(answer.trim(), 10);
|
|
1130
|
+
if (num === 1 || num === 2 || num === 3) {
|
|
1131
|
+
resolve5(num);
|
|
1132
|
+
} else {
|
|
1133
|
+
resolve5(2);
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
function generateOriginalName(filePath) {
|
|
1139
|
+
const suffix = randomBytes(2).toString("hex");
|
|
1140
|
+
const ext = extname(filePath);
|
|
1141
|
+
if (ext) {
|
|
1142
|
+
const base = filePath.slice(0, filePath.length - ext.length);
|
|
1143
|
+
return `${base}-original~${suffix}${ext}`;
|
|
1144
|
+
}
|
|
1145
|
+
return `${filePath}-original~${suffix}`;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/commands/track/destroy.ts
|
|
1149
|
+
import { join as join12, dirname as dirname2 } from "path";
|
|
1150
|
+
import { readFile as readFile6, writeFile as writeFile6, rm as rm3, rmdir, unlink as unlink2 } from "fs/promises";
|
|
1151
|
+
import { createInterface as createInterface2 } from "readline";
|
|
694
1152
|
async function trackDestroy(opts) {
|
|
695
1153
|
const root = await findRoot();
|
|
696
1154
|
await validateShadowRepo(root);
|
|
1155
|
+
const sgitDir = join12(root, ".sgit");
|
|
1156
|
+
let trackedFiles = [];
|
|
1157
|
+
try {
|
|
1158
|
+
const output = await execGit(["ls-tree", "-r", "--name-only", "HEAD"], {
|
|
1159
|
+
gitDir: sgitDir,
|
|
1160
|
+
workTree: root
|
|
1161
|
+
});
|
|
1162
|
+
if (output.trim()) {
|
|
1163
|
+
trackedFiles = output.trim().split("\n").filter((f) => f !== ".sgit.json");
|
|
1164
|
+
}
|
|
1165
|
+
} catch {
|
|
1166
|
+
}
|
|
1167
|
+
let uncommittedStatus = "";
|
|
1168
|
+
try {
|
|
1169
|
+
uncommittedStatus = await execGit(["status", "--porcelain"], {
|
|
1170
|
+
gitDir: sgitDir,
|
|
1171
|
+
workTree: root
|
|
1172
|
+
});
|
|
1173
|
+
} catch {
|
|
1174
|
+
}
|
|
1175
|
+
const hasUncommitted = uncommittedStatus.trim().length > 0;
|
|
697
1176
|
if (!opts.force) {
|
|
698
|
-
|
|
699
|
-
"
|
|
700
|
-
|
|
1177
|
+
if (hasUncommitted) {
|
|
1178
|
+
warn("there are uncommitted changes in the shadow repository:");
|
|
1179
|
+
for (const line of uncommittedStatus.trim().split("\n")) {
|
|
1180
|
+
info(` ${line}`);
|
|
1181
|
+
}
|
|
1182
|
+
process.stdout.write("\n");
|
|
1183
|
+
}
|
|
1184
|
+
let message;
|
|
1185
|
+
if (opts.keepFiles) {
|
|
1186
|
+
message = hasUncommitted ? "This will remove the shadow repository. Tracked files remain on disk but shadow history will be lost. Continue?" : "This will remove the shadow repository. Tracked files will remain on disk. Continue?";
|
|
1187
|
+
} else {
|
|
1188
|
+
message = hasUncommitted ? `This will permanently remove the shadow repository and delete ${trackedFiles.length} tracked file(s). Uncommitted changes will be lost. Continue?` : `This will permanently remove the shadow repository and delete ${trackedFiles.length} tracked file(s). Continue?`;
|
|
1189
|
+
}
|
|
1190
|
+
const confirmed = await confirm(message);
|
|
701
1191
|
if (!confirmed) {
|
|
702
1192
|
info("Aborted.");
|
|
703
1193
|
return;
|
|
704
1194
|
}
|
|
705
1195
|
}
|
|
706
|
-
const excludePath =
|
|
1196
|
+
const excludePath = join12(root, ".git", "info", "exclude");
|
|
707
1197
|
try {
|
|
708
|
-
const content = await
|
|
1198
|
+
const content = await readFile6(excludePath, "utf-8");
|
|
709
1199
|
const lines = content.split("\n");
|
|
710
1200
|
const result = [];
|
|
1201
|
+
let insideBlock = false;
|
|
711
1202
|
let skipNext = false;
|
|
712
1203
|
for (const line of lines) {
|
|
713
|
-
if (line ===
|
|
714
|
-
|
|
1204
|
+
if (line === SGIT_BLOCK_START || line === "# sgit-managed (do not edit below this line)") {
|
|
1205
|
+
insideBlock = true;
|
|
715
1206
|
continue;
|
|
716
1207
|
}
|
|
717
|
-
if (line ===
|
|
718
|
-
|
|
1208
|
+
if (line === SGIT_BLOCK_END) {
|
|
1209
|
+
insideBlock = false;
|
|
719
1210
|
continue;
|
|
720
1211
|
}
|
|
721
|
-
if (
|
|
1212
|
+
if (insideBlock) continue;
|
|
1213
|
+
if (line === "# sgit") {
|
|
1214
|
+
skipNext = true;
|
|
722
1215
|
continue;
|
|
723
1216
|
}
|
|
724
1217
|
if (skipNext) {
|
|
725
1218
|
skipNext = false;
|
|
726
1219
|
continue;
|
|
727
1220
|
}
|
|
1221
|
+
if (line.endsWith(" # sgit")) continue;
|
|
728
1222
|
result.push(line);
|
|
729
1223
|
}
|
|
730
1224
|
const cleaned = result.join("\n");
|
|
731
|
-
await
|
|
1225
|
+
await writeFile6(excludePath, cleaned.replace(/\n+$/, "\n"));
|
|
732
1226
|
} catch {
|
|
733
1227
|
}
|
|
734
1228
|
await uninstallMainHook(root);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1229
|
+
if (!opts.keepFiles && trackedFiles.length > 0) {
|
|
1230
|
+
for (const file of trackedFiles) {
|
|
1231
|
+
try {
|
|
1232
|
+
await unlink2(join12(root, file));
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
1237
|
+
for (const file of trackedFiles) {
|
|
1238
|
+
let dir = dirname2(file);
|
|
1239
|
+
while (dir !== ".") {
|
|
1240
|
+
dirs.add(dir);
|
|
1241
|
+
dir = dirname2(dir);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const sortedDirs = [...dirs].sort((a, b) => b.length - a.length);
|
|
1245
|
+
for (const dir of sortedDirs) {
|
|
1246
|
+
try {
|
|
1247
|
+
await rmdir(join12(root, dir));
|
|
1248
|
+
} catch {
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
await rm3(join12(root, ".sgit"), { recursive: true, force: true });
|
|
1253
|
+
await rm3(join12(root, ".sgit.json"), { force: true });
|
|
1254
|
+
if (opts.keepFiles) {
|
|
1255
|
+
success("Shadow repository removed. Tracked files kept on disk.");
|
|
1256
|
+
} else {
|
|
1257
|
+
success("Shadow repository and tracked files removed.");
|
|
1258
|
+
}
|
|
738
1259
|
}
|
|
739
1260
|
async function confirm(message) {
|
|
740
|
-
const rl =
|
|
1261
|
+
const rl = createInterface2({
|
|
741
1262
|
input: process.stdin,
|
|
742
1263
|
output: process.stdout
|
|
743
1264
|
});
|
|
744
|
-
return new Promise((
|
|
1265
|
+
return new Promise((resolve5) => {
|
|
745
1266
|
rl.question(`${message} [y/N] `, (answer) => {
|
|
746
1267
|
rl.close();
|
|
747
|
-
|
|
1268
|
+
resolve5(answer.toLowerCase() === "y");
|
|
748
1269
|
});
|
|
749
1270
|
});
|
|
750
1271
|
}
|
|
751
1272
|
|
|
1273
|
+
// src/commands/track/ignore.ts
|
|
1274
|
+
import { join as join13 } from "path";
|
|
1275
|
+
async function trackIgnore(paths) {
|
|
1276
|
+
const root = await findRoot();
|
|
1277
|
+
await validateShadowRepo(root);
|
|
1278
|
+
const sgitDir = join13(root, ".sgit");
|
|
1279
|
+
const added = [];
|
|
1280
|
+
for (const inputPath of paths) {
|
|
1281
|
+
const normalized = await normalizePath(inputPath, root);
|
|
1282
|
+
const manifest = await readManifest(root);
|
|
1283
|
+
if (!isPathCoveredByManifest(normalized, manifest.paths)) {
|
|
1284
|
+
throw new PathNotTrackedError(normalized);
|
|
1285
|
+
}
|
|
1286
|
+
const wasAdded = await addIgnore(root, normalized);
|
|
1287
|
+
if (wasAdded) {
|
|
1288
|
+
added.push(normalized);
|
|
1289
|
+
const rmPath = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
1290
|
+
try {
|
|
1291
|
+
const rmArgs = normalized.endsWith("/") ? ["rm", "--cached", "-r", rmPath] : ["rm", "--cached", rmPath];
|
|
1292
|
+
await execGit(rmArgs, { gitDir: sgitDir, workTree: root });
|
|
1293
|
+
} catch {
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
await syncExcludes(root);
|
|
1298
|
+
if (added.length > 0) {
|
|
1299
|
+
success(`Ignoring ${added.length} path(s):`);
|
|
1300
|
+
for (const p of added) {
|
|
1301
|
+
info(` ${p}`);
|
|
1302
|
+
}
|
|
1303
|
+
} else {
|
|
1304
|
+
info("All paths already ignored.");
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/commands/track/unignore.ts
|
|
1309
|
+
import { resolve as resolve4, relative as relative4, sep as sep4 } from "path";
|
|
1310
|
+
async function trackUnignore(paths) {
|
|
1311
|
+
const root = await findRoot();
|
|
1312
|
+
await validateShadowRepo(root);
|
|
1313
|
+
const removed = [];
|
|
1314
|
+
for (const inputPath of paths) {
|
|
1315
|
+
const absolute = resolve4(process.cwd(), inputPath);
|
|
1316
|
+
let normalized = relative4(root, absolute).split(sep4).join("/");
|
|
1317
|
+
if (inputPath.endsWith("/") && !normalized.endsWith("/")) {
|
|
1318
|
+
normalized += "/";
|
|
1319
|
+
}
|
|
1320
|
+
const manifest = await readManifest(root);
|
|
1321
|
+
const exactPath = manifest.ignore.find((p) => p === normalized) || manifest.ignore.find((p) => p === normalized + "/") || (normalized.endsWith("/") ? manifest.ignore.find((p) => p === normalized.slice(0, -1)) : void 0);
|
|
1322
|
+
if (!exactPath) {
|
|
1323
|
+
throw new SgitError(`${normalized} is not in the ignore list`);
|
|
1324
|
+
}
|
|
1325
|
+
const wasRemoved = await removeIgnore(root, exactPath);
|
|
1326
|
+
if (wasRemoved) {
|
|
1327
|
+
removed.push(exactPath);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
await syncExcludes(root);
|
|
1331
|
+
if (removed.length > 0) {
|
|
1332
|
+
success(`Unignored ${removed.length} path(s):`);
|
|
1333
|
+
for (const p of removed) {
|
|
1334
|
+
info(` ${p}`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
752
1339
|
// src/commands/track/completion.ts
|
|
753
|
-
import { join as
|
|
754
|
-
import { mkdir as
|
|
1340
|
+
import { join as join14, dirname as dirname3 } from "path";
|
|
1341
|
+
import { mkdir as mkdir5, writeFile as writeFile7 } from "fs/promises";
|
|
755
1342
|
import { homedir } from "os";
|
|
756
1343
|
function bashCompletion() {
|
|
757
1344
|
return `# sgit bash completion
|
|
@@ -761,7 +1348,7 @@ _sgit() {
|
|
|
761
1348
|
|
|
762
1349
|
if [[ \${words[1]} == "track" ]]; then
|
|
763
1350
|
if [[ $cword -eq 2 ]]; then
|
|
764
|
-
COMPREPLY=($(compgen -W "init add remove list sync verify destroy completion" -- "$cur"))
|
|
1351
|
+
COMPREPLY=($(compgen -W "init clone add remove list sync verify ignore unignore destroy completion" -- "$cur"))
|
|
765
1352
|
return
|
|
766
1353
|
fi
|
|
767
1354
|
|
|
@@ -774,24 +1361,34 @@ _sgit() {
|
|
|
774
1361
|
_filedir
|
|
775
1362
|
fi
|
|
776
1363
|
;;
|
|
1364
|
+
clone)
|
|
1365
|
+
;;
|
|
777
1366
|
remove)
|
|
778
1367
|
local paths
|
|
779
1368
|
paths=$(sgit track list 2>/dev/null)
|
|
780
1369
|
COMPREPLY=($(compgen -W "$paths" -- "$cur"))
|
|
781
1370
|
;;
|
|
1371
|
+
ignore)
|
|
1372
|
+
_filedir
|
|
1373
|
+
;;
|
|
1374
|
+
unignore)
|
|
1375
|
+
local ignored
|
|
1376
|
+
ignored=$(python3 -c "import json; d=json.load(open('.sgit.json')); print(' '.join(d.get('ignore',[])))" 2>/dev/null)
|
|
1377
|
+
COMPREPLY=($(compgen -W "$ignored" -- "$cur"))
|
|
1378
|
+
;;
|
|
782
1379
|
verify)
|
|
783
1380
|
COMPREPLY=($(compgen -W "--main --shadow" -- "$cur"))
|
|
784
1381
|
;;
|
|
785
1382
|
destroy)
|
|
786
|
-
COMPREPLY=($(compgen -W "--force" -- "$cur"))
|
|
1383
|
+
COMPREPLY=($(compgen -W "--force --keep-files" -- "$cur"))
|
|
787
1384
|
;;
|
|
788
1385
|
completion)
|
|
789
1386
|
if [[ $prev == "--shell" ]]; then
|
|
790
1387
|
COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
|
|
791
|
-
elif [[ $
|
|
792
|
-
COMPREPLY=($(compgen -W "--shell" -- "$cur"))
|
|
1388
|
+
elif [[ $cword -eq 3 ]]; then
|
|
1389
|
+
COMPREPLY=($(compgen -W "install --shell" -- "$cur"))
|
|
793
1390
|
else
|
|
794
|
-
COMPREPLY=($(compgen -W "
|
|
1391
|
+
COMPREPLY=($(compgen -W "--shell" -- "$cur"))
|
|
795
1392
|
fi
|
|
796
1393
|
;;
|
|
797
1394
|
esac
|
|
@@ -824,11 +1421,14 @@ _sgit_track() {
|
|
|
824
1421
|
local -a subcmds
|
|
825
1422
|
subcmds=(
|
|
826
1423
|
'init:Initialize shadow repository'
|
|
1424
|
+
'clone:Clone shadow repository from remote URL'
|
|
827
1425
|
'add:Start shadow-tracking paths'
|
|
828
1426
|
'remove:Stop shadow-tracking paths'
|
|
829
1427
|
'list:List all shadow-tracked paths'
|
|
830
1428
|
'sync:Regenerate exclude files'
|
|
831
1429
|
'verify:Validate staged files'
|
|
1430
|
+
'ignore:Ignore paths within tracked directories'
|
|
1431
|
+
'unignore:Stop ignoring paths within tracked directories'
|
|
832
1432
|
'destroy:Remove shadow repository'
|
|
833
1433
|
'completion:Install shell completions'
|
|
834
1434
|
)
|
|
@@ -839,6 +1439,9 @@ _sgit_track() {
|
|
|
839
1439
|
fi
|
|
840
1440
|
|
|
841
1441
|
case "\${words[2]}" in
|
|
1442
|
+
clone)
|
|
1443
|
+
_arguments '1:url:'
|
|
1444
|
+
;;
|
|
842
1445
|
add)
|
|
843
1446
|
_arguments '--adopt[Move main-git-tracked files to shadow tracking]' '*:file:_files'
|
|
844
1447
|
;;
|
|
@@ -847,11 +1450,19 @@ _sgit_track() {
|
|
|
847
1450
|
paths=(\${(f)"$(sgit track list 2>/dev/null)"})
|
|
848
1451
|
_describe 'tracked path' paths
|
|
849
1452
|
;;
|
|
1453
|
+
ignore)
|
|
1454
|
+
_files
|
|
1455
|
+
;;
|
|
1456
|
+
unignore)
|
|
1457
|
+
local -a ignored
|
|
1458
|
+
ignored=(\${(f)"$(python3 -c "import json; d=json.load(open('.sgit.json')); print('\\\\n'.join(d.get('ignore',[])))" 2>/dev/null)"})
|
|
1459
|
+
_describe 'ignored path' ignored
|
|
1460
|
+
;;
|
|
850
1461
|
verify)
|
|
851
1462
|
_arguments '--main[Check main repo]' '--shadow[Check shadow repo]'
|
|
852
1463
|
;;
|
|
853
1464
|
destroy)
|
|
854
|
-
_arguments '--force[Skip confirmation]'
|
|
1465
|
+
_arguments '--force[Skip confirmation]' '--keep-files[Keep tracked files on disk]'
|
|
855
1466
|
;;
|
|
856
1467
|
completion)
|
|
857
1468
|
_arguments '--shell[Shell type]:shell:(bash zsh fish)' '1:action:(install)'
|
|
@@ -896,14 +1507,18 @@ complete -c sgit -w git
|
|
|
896
1507
|
complete -c sgit -n '__fish_use_subcommand' -a track -d 'Manage shadow-tracked paths'
|
|
897
1508
|
|
|
898
1509
|
# Track subcommands
|
|
899
|
-
|
|
900
|
-
complete -c sgit -n
|
|
901
|
-
complete -c sgit -n
|
|
902
|
-
complete -c sgit -n
|
|
903
|
-
complete -c sgit -n
|
|
904
|
-
complete -c sgit -n
|
|
905
|
-
complete -c sgit -n
|
|
906
|
-
complete -c sgit -n
|
|
1510
|
+
set -l __sgit_track_cmds init clone add remove list sync verify ignore unignore destroy completion
|
|
1511
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a init -d 'Initialize shadow repository'
|
|
1512
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a clone -d 'Clone shadow repository from remote URL'
|
|
1513
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a add -d 'Start shadow-tracking paths'
|
|
1514
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a remove -d 'Stop shadow-tracking paths'
|
|
1515
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a list -d 'List all shadow-tracked paths'
|
|
1516
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a sync -d 'Regenerate exclude files'
|
|
1517
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a verify -d 'Validate staged files'
|
|
1518
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a ignore -d 'Ignore paths within tracked directories'
|
|
1519
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a unignore -d 'Stop ignoring paths within tracked directories'
|
|
1520
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a destroy -d 'Remove shadow repository'
|
|
1521
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from $__sgit_track_cmds" -a completion -d 'Install shell completions'
|
|
907
1522
|
|
|
908
1523
|
# Track add options and file completions
|
|
909
1524
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from add' -l adopt -d 'Move main-git-tracked files'
|
|
@@ -912,15 +1527,22 @@ complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcomma
|
|
|
912
1527
|
# Track remove \u2014 complete from manifest
|
|
913
1528
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from remove' -a '(sgit track list 2>/dev/null)'
|
|
914
1529
|
|
|
1530
|
+
# Track ignore \u2014 file completions
|
|
1531
|
+
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from ignore' -F
|
|
1532
|
+
|
|
1533
|
+
# Track unignore \u2014 complete from .sgit.json ignore list
|
|
1534
|
+
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from unignore' -a "(python3 -c \\"import json; d=json.load(open('.sgit.json')); print(' '.join(d.get('ignore',[])))\\" 2>/dev/null)"
|
|
1535
|
+
|
|
915
1536
|
# Track verify options
|
|
916
1537
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from verify' -l main -d 'Check main repo'
|
|
917
1538
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from verify' -l shadow -d 'Check shadow repo'
|
|
918
1539
|
|
|
919
1540
|
# Track destroy options
|
|
920
1541
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from destroy' -l force -d 'Skip confirmation'
|
|
1542
|
+
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from destroy' -l keep-files -d 'Keep tracked files on disk'
|
|
921
1543
|
|
|
922
1544
|
# Track completion options
|
|
923
|
-
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from completion' -a install -d 'Install completions'
|
|
1545
|
+
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from completion; and not __fish_seen_subcommand_from install' -a install -d 'Install completions'
|
|
924
1546
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from completion' -l shell -d 'Shell type' -ra 'bash zsh fish'
|
|
925
1547
|
`;
|
|
926
1548
|
}
|
|
@@ -944,11 +1566,11 @@ function getInstallPath(shell) {
|
|
|
944
1566
|
const home = homedir();
|
|
945
1567
|
switch (shell) {
|
|
946
1568
|
case "zsh":
|
|
947
|
-
return
|
|
1569
|
+
return join14(home, ".zsh", "completions", "_sgit");
|
|
948
1570
|
case "fish":
|
|
949
|
-
return
|
|
1571
|
+
return join14(home, ".config", "fish", "completions", "sgit.fish");
|
|
950
1572
|
default:
|
|
951
|
-
return
|
|
1573
|
+
return join14(home, ".local", "share", "bash-completion", "completions", "sgit");
|
|
952
1574
|
}
|
|
953
1575
|
}
|
|
954
1576
|
async function trackCompletion(action, opts) {
|
|
@@ -960,8 +1582,8 @@ async function trackCompletion(action, opts) {
|
|
|
960
1582
|
}
|
|
961
1583
|
if (action === "install") {
|
|
962
1584
|
const installPath = getInstallPath(shell);
|
|
963
|
-
await
|
|
964
|
-
await
|
|
1585
|
+
await mkdir5(dirname3(installPath), { recursive: true });
|
|
1586
|
+
await writeFile7(installPath, script, { mode: 420 });
|
|
965
1587
|
success(`Installed ${shell} completions to ${installPath}`);
|
|
966
1588
|
if (shell === "zsh") {
|
|
967
1589
|
info(
|
|
@@ -978,6 +1600,7 @@ async function trackCompletion(action, opts) {
|
|
|
978
1600
|
function registerTrackCommands(program) {
|
|
979
1601
|
const track = program.command("track").description("Manage shadow-tracked paths");
|
|
980
1602
|
track.command("init").description("Initialize shadow repository").action(wrapAction(trackInit));
|
|
1603
|
+
track.command("clone").description("Clone a shadow repository from a remote URL").argument("<url>").action(wrapAction(async (url) => trackClone(url)));
|
|
981
1604
|
track.command("add").description("Start shadow-tracking paths").argument("<paths...>").option("--adopt", "Move main-git-tracked files to shadow tracking").action(wrapAction(async (paths, opts) => trackAdd(paths, opts)));
|
|
982
1605
|
track.command("remove").description("Stop shadow-tracking paths").argument("<paths...>").action(wrapAction(async (paths) => trackRemove(paths)));
|
|
983
1606
|
track.command("list").description("List all shadow-tracked paths").action(wrapAction(async () => {
|
|
@@ -989,7 +1612,9 @@ function registerTrackCommands(program) {
|
|
|
989
1612
|
async (opts) => trackVerify(opts)
|
|
990
1613
|
)
|
|
991
1614
|
);
|
|
992
|
-
track.command("
|
|
1615
|
+
track.command("ignore").description("Ignore paths within tracked directories").argument("<paths...>").action(wrapAction(async (paths) => trackIgnore(paths)));
|
|
1616
|
+
track.command("unignore").description("Stop ignoring paths within tracked directories").argument("<paths...>").action(wrapAction(async (paths) => trackUnignore(paths)));
|
|
1617
|
+
track.command("destroy").description("Remove shadow repository entirely").option("--force", "Skip confirmation prompt").option("--keep-files", "Keep tracked files on disk (default: delete them)").action(
|
|
993
1618
|
wrapAction(async (opts) => trackDestroy(opts))
|
|
994
1619
|
);
|
|
995
1620
|
track.command("completion").description("Install shell completion scripts").argument("[action]", "Action (install)").option("--shell <name>", "Shell (bash, zsh, fish)").action(
|
|
@@ -1012,20 +1637,81 @@ function wrapAction(fn) {
|
|
|
1012
1637
|
};
|
|
1013
1638
|
}
|
|
1014
1639
|
|
|
1640
|
+
// src/commands/debug/index.ts
|
|
1641
|
+
import { Command } from "commander";
|
|
1642
|
+
|
|
1643
|
+
// src/commands/debug/gitignores.ts
|
|
1644
|
+
async function debugGitignores() {
|
|
1645
|
+
const root = await findRoot();
|
|
1646
|
+
await validateShadowRepo(root);
|
|
1647
|
+
const manifest = await readManifest(root);
|
|
1648
|
+
const cleanup = setupGitignoreGuard(root, manifest.paths);
|
|
1649
|
+
try {
|
|
1650
|
+
info("gitignores disabled \u2014 waiting 10 seconds\u2026");
|
|
1651
|
+
for (let i = 10; i > 0; i--) {
|
|
1652
|
+
process.stdout.write(`\r ${i}s remaining `);
|
|
1653
|
+
await sleep(1e3);
|
|
1654
|
+
}
|
|
1655
|
+
process.stdout.write("\r \r");
|
|
1656
|
+
success("re-enabling gitignores");
|
|
1657
|
+
} finally {
|
|
1658
|
+
cleanup();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
function sleep(ms) {
|
|
1662
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/commands/debug/index.ts
|
|
1666
|
+
function registerDebugCommands(program) {
|
|
1667
|
+
const debug = new Command("debug").description("Debug utilities");
|
|
1668
|
+
debug.command("gitignores").description("Disable .gitignore files for 10 seconds, then re-enable").action(wrapAction2(debugGitignores));
|
|
1669
|
+
program.addCommand(debug, { hidden: true });
|
|
1670
|
+
}
|
|
1671
|
+
function wrapAction2(fn) {
|
|
1672
|
+
return async (...args) => {
|
|
1673
|
+
try {
|
|
1674
|
+
await fn(...args);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
if (err instanceof SgitError) {
|
|
1677
|
+
error(err.message);
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1680
|
+
throw err;
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1015
1685
|
// src/cli.ts
|
|
1686
|
+
var COMMANDER_COMMANDS = /* @__PURE__ */ new Set(["track", "debug"]);
|
|
1016
1687
|
async function run() {
|
|
1017
1688
|
const args = process.argv.slice(2);
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1689
|
+
const skipGuard = args[0] === "track" && (args[1] === "init" || args[1] === "clone") || args[0] === "debug";
|
|
1690
|
+
let guardCleanup;
|
|
1691
|
+
if (!skipGuard) {
|
|
1692
|
+
try {
|
|
1693
|
+
const root = await findRoot();
|
|
1694
|
+
const manifest = await readManifest(root);
|
|
1695
|
+
guardCleanup = setupGitignoreGuard(root, manifest.paths);
|
|
1696
|
+
} catch {
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
try {
|
|
1700
|
+
if (args.length === 0 || COMMANDER_COMMANDS.has(args[0]) || args[0].startsWith("-")) {
|
|
1701
|
+
const program = new Command2("sgit").description("Shadow Git \u2014 a secondary git repository for your project").version("0.1.0");
|
|
1702
|
+
registerTrackCommands(program);
|
|
1703
|
+
registerDebugCommands(program);
|
|
1704
|
+
program.addHelpText(
|
|
1705
|
+
"after",
|
|
1706
|
+
`
|
|
1024
1707
|
Track commands:
|
|
1025
1708
|
sgit track init Initialize shadow repository
|
|
1709
|
+
sgit track clone Clone shadow repository from remote URL
|
|
1026
1710
|
sgit track add Start shadow-tracking paths (--adopt to move from main git)
|
|
1027
1711
|
sgit track remove Stop shadow-tracking paths
|
|
1028
1712
|
sgit track list List all shadow-tracked paths
|
|
1713
|
+
sgit track ignore Ignore paths within tracked directories
|
|
1714
|
+
sgit track unignore Stop ignoring paths
|
|
1029
1715
|
sgit track sync Regenerate exclude files
|
|
1030
1716
|
sgit track verify Validate staged files
|
|
1031
1717
|
sgit track destroy Remove shadow repository
|
|
@@ -1033,13 +1719,18 @@ Track commands:
|
|
|
1033
1719
|
|
|
1034
1720
|
All other commands are forwarded to git in the shadow repository:
|
|
1035
1721
|
sgit status, sgit commit, sgit log, sgit diff, ...`
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1722
|
+
);
|
|
1723
|
+
await program.parseAsync(process.argv);
|
|
1724
|
+
} else {
|
|
1725
|
+
const root = await findRoot();
|
|
1726
|
+
await validateShadowRepo(root);
|
|
1727
|
+
const exitCode = forwardToGit(root, args);
|
|
1728
|
+
guardCleanup?.();
|
|
1729
|
+
guardCleanup = void 0;
|
|
1730
|
+
process.exit(exitCode);
|
|
1731
|
+
}
|
|
1732
|
+
} finally {
|
|
1733
|
+
guardCleanup?.();
|
|
1043
1734
|
}
|
|
1044
1735
|
}
|
|
1045
1736
|
run().catch((err) => {
|