git-sich 0.2.0 → 0.3.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.
- package/README.md +1 -0
- package/dist/cli.js +199 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,7 @@ setup
|
|
|
94
94
|
new <layer> [--remote <url> | --gh [name]]
|
|
95
95
|
create a layer (--gh: private GitHub repo)
|
|
96
96
|
attach <layer> <url> join an existing layer (collaborators)
|
|
97
|
+
detach <layer> [--force] [--yes] remove a layer here; files and remote stay
|
|
97
98
|
|
|
98
99
|
ownership
|
|
99
100
|
claim <layer> <path...> [--move] claim files/folders for a layer, stage them
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { realpathSync as realpathSync3, statSync } from "node:fs";
|
|
5
5
|
import { resolve as resolve6 } from "node:path";
|
|
6
6
|
// package.json
|
|
7
|
-
var version = "0.
|
|
7
|
+
var version = "0.3.0";
|
|
8
8
|
|
|
9
9
|
// src/ui.ts
|
|
10
10
|
class SichError extends Error {
|
|
@@ -42,6 +42,23 @@ function printError(message) {
|
|
|
42
42
|
process.stderr.write(`${ce.red("sich:")} ${message}
|
|
43
43
|
`);
|
|
44
44
|
}
|
|
45
|
+
async function confirm(question) {
|
|
46
|
+
process.stderr.write(`${question} [y/N] `);
|
|
47
|
+
let answer = "";
|
|
48
|
+
try {
|
|
49
|
+
for await (const chunk of process.stdin) {
|
|
50
|
+
answer += String(chunk);
|
|
51
|
+
if (answer.includes(`
|
|
52
|
+
`))
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
if (!process.stdin.isTTY)
|
|
57
|
+
process.stderr.write(`
|
|
58
|
+
`);
|
|
59
|
+
return /^y(es)?$/i.test(answer.split(`
|
|
60
|
+
`)[0].trim());
|
|
61
|
+
}
|
|
45
62
|
function plural(n, word, pluralWord = word + "s") {
|
|
46
63
|
return `${n} ${n === 1 ? word : pluralWord}`;
|
|
47
64
|
}
|
|
@@ -405,6 +422,7 @@ var COMMANDS = [
|
|
|
405
422
|
"init",
|
|
406
423
|
"new",
|
|
407
424
|
"attach",
|
|
425
|
+
"detach",
|
|
408
426
|
"claim",
|
|
409
427
|
"unclaim",
|
|
410
428
|
"add",
|
|
@@ -1164,7 +1182,7 @@ function cmdLs(ctx, args) {
|
|
|
1164
1182
|
|
|
1165
1183
|
// src/commands/layer.ts
|
|
1166
1184
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1167
|
-
import { existsSync as existsSync6, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1185
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1168
1186
|
import { basename as basename3, isAbsolute as isAbsolute2, join as join4, resolve as resolve5 } from "node:path";
|
|
1169
1187
|
function assertFree(ctx, layer) {
|
|
1170
1188
|
validateLayerName(layer);
|
|
@@ -1290,6 +1308,166 @@ function cmdAttach(ctx, args) {
|
|
|
1290
1308
|
installLayerHooks(ctx, layer);
|
|
1291
1309
|
return 0;
|
|
1292
1310
|
}
|
|
1311
|
+
var IN_PROGRESS = [
|
|
1312
|
+
["MERGE_HEAD", "merge"],
|
|
1313
|
+
["rebase-merge", "rebase"],
|
|
1314
|
+
["rebase-apply/applying", "am"],
|
|
1315
|
+
["rebase-apply", "rebase"],
|
|
1316
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
1317
|
+
["REVERT_HEAD", "revert"]
|
|
1318
|
+
];
|
|
1319
|
+
var preferredRemote = (remotes) => remotes.includes("origin") ? "origin" : remotes[0];
|
|
1320
|
+
function onlyInGitDir(repo) {
|
|
1321
|
+
const run = `sich ${repo.name}`;
|
|
1322
|
+
const losses = [];
|
|
1323
|
+
const count = (revs, allowFail = false) => Number(git(repo, ["rev-list", "--count", ...revs, "--"], { allowFail }).stdout.trim() || 0);
|
|
1324
|
+
const remote = preferredRemote(remotesOf(repo));
|
|
1325
|
+
if (!remote) {
|
|
1326
|
+
const n = count(["--exclude=refs/stash", "--all"]);
|
|
1327
|
+
losses.push({
|
|
1328
|
+
what: `${plural(n, "commit")} with no remote to push to`,
|
|
1329
|
+
fix: `${run} remote add origin <url>, then sich push ${repo.name}`
|
|
1330
|
+
});
|
|
1331
|
+
} else {
|
|
1332
|
+
const current = currentBranch(repo);
|
|
1333
|
+
const refs = lines(git(repo, ["for-each-ref", "--format=%(objectname) %(refname)"]).stdout).map((l) => {
|
|
1334
|
+
const [oid, ref] = l.split(" ");
|
|
1335
|
+
return { oid, ref };
|
|
1336
|
+
});
|
|
1337
|
+
for (const { ref } of refs.filter((r) => r.ref.startsWith("refs/heads/"))) {
|
|
1338
|
+
const n = count([ref, "--not", "--remotes"]);
|
|
1339
|
+
if (!n)
|
|
1340
|
+
continue;
|
|
1341
|
+
const branch = ref.slice("refs/heads/".length);
|
|
1342
|
+
losses.push({
|
|
1343
|
+
what: `${plural(n, "commit")} on ${branch} not on any remote`,
|
|
1344
|
+
fix: branch === current ? `sich push ${repo.name}` : `${run} push -u ${remote} ${shellQuote(branch)}`
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
let advertised;
|
|
1348
|
+
const remoteHas = (oid) => {
|
|
1349
|
+
if (advertised === undefined) {
|
|
1350
|
+
const r = git(repo, ["ls-remote", remote], { allowFail: true });
|
|
1351
|
+
advertised = r.code === 0 ? new Set(lines(r.stdout).map((l) => l.split("\t")[0])) : null;
|
|
1352
|
+
}
|
|
1353
|
+
return advertised?.has(oid) ?? false;
|
|
1354
|
+
};
|
|
1355
|
+
const others = refs.filter((r) => !/^refs\/(heads|remotes|prefetch)\//.test(r.ref) && r.ref !== "refs/stash");
|
|
1356
|
+
for (const { oid, ref } of others) {
|
|
1357
|
+
const n = count([`${ref}^{commit}`, "--not", "--remotes", "--branches"], true);
|
|
1358
|
+
if (!n || remoteHas(oid))
|
|
1359
|
+
continue;
|
|
1360
|
+
const name = ref.startsWith("refs/tags/") ? ref.slice("refs/tags/".length) : ref;
|
|
1361
|
+
const unchecked = advertised === null ? ` (could not reach ${remote} to check)` : "";
|
|
1362
|
+
losses.push({
|
|
1363
|
+
what: `${plural(n, "commit")} only on ${name === ref ? ref : `tag ${name}`}${unchecked}`,
|
|
1364
|
+
fix: `${run} push ${remote} ${shellQuote(name)}`
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
if (current === null) {
|
|
1368
|
+
const n = count(["HEAD", "--not", "--remotes", "--branches", "--tags"]);
|
|
1369
|
+
if (n) {
|
|
1370
|
+
losses.push({ what: `${plural(n, "commit")} only on the detached HEAD`, fix: `${run} switch -c <branch>` });
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const stashes = lines(git(repo, ["stash", "list"]).stdout).length;
|
|
1375
|
+
if (stashes) {
|
|
1376
|
+
losses.push({ what: plural(stashes, "stash entry", "stash entries"), fix: `${run} stash pop, then commit` });
|
|
1377
|
+
}
|
|
1378
|
+
const staged = splitZ(git(repo, ["diff", "--cached", "--name-only", "-z", "--no-renames", "--diff-filter=du"]).stdout);
|
|
1379
|
+
const changed = new Set(splitZ(git(repo, ["diff", "--name-only", "-z", "--no-renames"]).stdout));
|
|
1380
|
+
for (const e of splitZ(git(repo, ["ls-files", "-v", "-z"]).stdout)) {
|
|
1381
|
+
if (/^[a-zS] /.test(e))
|
|
1382
|
+
changed.add(e.slice(2));
|
|
1383
|
+
}
|
|
1384
|
+
const stagedOnly = staged.filter((f) => changed.has(f));
|
|
1385
|
+
if (stagedOnly.length) {
|
|
1386
|
+
losses.push({
|
|
1387
|
+
what: `staged versions of ${listSome(stagedOnly)} that differ from what is on disk`,
|
|
1388
|
+
fix: `${run} commit -m <msg>, then sich push ${repo.name}`
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
const worktrees = lines(git(repo, ["worktree", "list", "--porcelain"]).stdout).filter((l) => l.startsWith("worktree ")).slice(1).map((l) => l.slice("worktree ".length));
|
|
1392
|
+
if (worktrees.length) {
|
|
1393
|
+
losses.push({
|
|
1394
|
+
what: `${plural(worktrees.length, "linked worktree")}: ${listSome(worktrees)}`,
|
|
1395
|
+
fix: `${run} worktree remove <path>`
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
const modules = join4(repo.gitDir, "modules");
|
|
1399
|
+
if (existsSync6(modules) && readdirSync2(modules).length) {
|
|
1400
|
+
losses.push({ what: `submodule repos in .sich/${repo.name}/modules/`, fix: `push and remove the submodules` });
|
|
1401
|
+
}
|
|
1402
|
+
const op = IN_PROGRESS.find(([file]) => existsSync6(join4(repo.gitDir, file)))?.[1];
|
|
1403
|
+
if (op)
|
|
1404
|
+
losses.push({ what: `a ${op} in progress`, fix: `finish it, or ${run} ${op} --abort` });
|
|
1405
|
+
return losses;
|
|
1406
|
+
}
|
|
1407
|
+
async function cmdDetach(ctx, args) {
|
|
1408
|
+
const p = parseArgs(args, { "--force": "bool", "--yes": "bool" }, { "-y": "--yes" });
|
|
1409
|
+
const [layer, ...extra] = p.positionals;
|
|
1410
|
+
if (!layer || extra.length)
|
|
1411
|
+
fail("usage: sich detach <layer> [--force] [--yes]");
|
|
1412
|
+
const repo = requireLayer(ctx, layer);
|
|
1413
|
+
const losses = onlyInGitDir(repo);
|
|
1414
|
+
if (losses.length && !p.flags["--force"]) {
|
|
1415
|
+
for (const l of losses)
|
|
1416
|
+
out(`${c.red("✗")} ${l.what} (fix: ${l.fix})`);
|
|
1417
|
+
fail(`detaching ${layer} would lose what exists only in .sich/${layer}/ (see above); --force discards it`);
|
|
1418
|
+
}
|
|
1419
|
+
const claims = readManifest(manifestPath(ctx, layer), layer).claims;
|
|
1420
|
+
const remote = preferredRemote(remotesOf(repo));
|
|
1421
|
+
const url = remote && git(repo, ["remote", "get-url", remote], { allowFail: true }).stdout.trim();
|
|
1422
|
+
if (p.flags["--yes"]) {
|
|
1423
|
+
for (const l of losses)
|
|
1424
|
+
warn(`discarding ${l.what}`);
|
|
1425
|
+
} else {
|
|
1426
|
+
const onDisk = claims.filter((cl) => existsSync6(join4(ctx.root, bare(cl))));
|
|
1427
|
+
const row = (label, text) => out(` ${label}${" ".repeat(10 - label.length)}${text}`);
|
|
1428
|
+
out(`detaching ${c.bold(layer)}:`);
|
|
1429
|
+
row("deletes", `.sich/${layer}/ (its git data: local history, index, hooks, config)`);
|
|
1430
|
+
if (existsSync6(manifestPath(ctx, layer))) {
|
|
1431
|
+
row("deletes", `${manifestRel(layer)} (claims list: ${claims.length ? listSome(claims) : "empty"})`);
|
|
1432
|
+
}
|
|
1433
|
+
for (const l of losses)
|
|
1434
|
+
row("discards", l.what);
|
|
1435
|
+
row("keeps", onDisk.length ? `${listSome(onDisk)} on disk, no longer hidden from base` : "files: none on disk");
|
|
1436
|
+
if (url)
|
|
1437
|
+
row("keeps", `remote ${remote} (${url}) and everything pushed to it`);
|
|
1438
|
+
if (!await confirm(`Detach ${layer}?`)) {
|
|
1439
|
+
fail(process.stdin.isTTY ? "not detached" : "not detached (no answer on stdin; --yes skips the question)");
|
|
1440
|
+
}
|
|
1441
|
+
const shown = new Set(losses.map((l) => l.what));
|
|
1442
|
+
const added = onlyInGitDir(repo).filter((l) => !shown.has(l.what));
|
|
1443
|
+
if (added.length) {
|
|
1444
|
+
for (const l of added)
|
|
1445
|
+
out(`${c.red("✗")} ${l.what} (fix: ${l.fix})`);
|
|
1446
|
+
fail(`${layer} changed while waiting for the answer (see above); not detached`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
const manifest = existsSync6(manifestPath(ctx, layer));
|
|
1450
|
+
rmSync(repo.gitDir, { recursive: true, force: true });
|
|
1451
|
+
rmSync(manifestPath(ctx, layer), { force: true });
|
|
1452
|
+
const deleted = `git data -> .sich/${layer}/${manifest ? `, claims list -> ${manifestRel(layer)}` : ""}`;
|
|
1453
|
+
out(`${c.green("detached")} layer ${c.bold(layer)}: deleted ${deleted}`);
|
|
1454
|
+
for (const t of excludeTargets(ctx)) {
|
|
1455
|
+
if (!applyExclude(t, true))
|
|
1456
|
+
continue;
|
|
1457
|
+
const file = excludeFileShown(t.repo);
|
|
1458
|
+
out(t.repo.name === "base" ? `no longer hidden from base -> ${file}` : `updated the excludes of ${t.repo.name} -> ${file}`);
|
|
1459
|
+
}
|
|
1460
|
+
const untracked = ["ls-files", "-z", "--others", "--exclude-standard", "--directory", "--no-empty-directory"];
|
|
1461
|
+
const visible = claims.length ? splitZ(git(ctx.base, [...untracked, "--", ...claims]).stdout) : [];
|
|
1462
|
+
if (visible.length) {
|
|
1463
|
+
warn(`base now sees ${listSome(visible)} as untracked ` + `(delete, .gitignore or claim ${visible.length === 1 ? "it" : "them"} before committing base)`);
|
|
1464
|
+
}
|
|
1465
|
+
if (url) {
|
|
1466
|
+
const away = claims.some((cl) => existsSync6(join4(ctx.root, bare(cl)))) ? "move its files away, then " : "";
|
|
1467
|
+
note(`its remote is untouched; to attach again: ${away}sich attach ${layer} ${shellQuote(url)}`);
|
|
1468
|
+
}
|
|
1469
|
+
return 0;
|
|
1470
|
+
}
|
|
1293
1471
|
|
|
1294
1472
|
// src/commands/status.ts
|
|
1295
1473
|
function parseStatus(raw) {
|
|
@@ -1525,6 +1703,7 @@ setup
|
|
|
1525
1703
|
new <layer> [--remote <url> | --gh [name]]
|
|
1526
1704
|
create a layer (--gh: private GitHub repo)
|
|
1527
1705
|
attach <layer> <url> join an existing layer (collaborators)
|
|
1706
|
+
detach <layer> [--force] [--yes] remove a layer here; files and remote stay
|
|
1528
1707
|
|
|
1529
1708
|
ownership
|
|
1530
1709
|
claim <layer> <path...> [--move] claim files/folders for a layer, stage them
|
|
@@ -1587,6 +1766,21 @@ its pre-commit and pre-merge-commit hooks (see sich init).
|
|
|
1587
1766
|
Joins an existing layer: fetches <url> and checks out its default branch into
|
|
1588
1767
|
this working tree, and installs its pre-commit and pre-merge-commit hooks (see
|
|
1589
1768
|
sich init). Refuses to overwrite existing files.`,
|
|
1769
|
+
detach: `usage: sich detach <layer> [--force] [--yes]
|
|
1770
|
+
|
|
1771
|
+
The opposite of attach: removes a layer from this working tree by deleting its
|
|
1772
|
+
git data (.sich/<layer>/) and claims list (.sich/<layer>.paths). Its files stay
|
|
1773
|
+
on disk, no longer hidden from base (nothing stops base from committing them
|
|
1774
|
+
any more), and its remote is left alone: attach it again any time.
|
|
1775
|
+
Refuses if that would lose what exists only in .sich/<layer>/: commits on no
|
|
1776
|
+
remote (for branches: as of the last fetch or push), stashes, staged versions
|
|
1777
|
+
of files that changed again on disk, linked worktrees, submodule repos, or a
|
|
1778
|
+
merge, rebase or similar in progress. Uncommitted changes to files don't
|
|
1779
|
+
count, since the files stay.
|
|
1780
|
+
Lists what it deletes and what it keeps, then asks before doing it (no answer,
|
|
1781
|
+
e.g. without a terminal, means no).
|
|
1782
|
+
--force detach anyway, discarding all of that
|
|
1783
|
+
-y, --yes don't ask`,
|
|
1590
1784
|
claim: `usage: sich claim <layer> <path...> [--move]
|
|
1591
1785
|
|
|
1592
1786
|
Claims paths (files or directories, relative to the current directory) for a
|
|
@@ -1669,6 +1863,7 @@ var HANDLERS = {
|
|
|
1669
1863
|
init: cmdInit,
|
|
1670
1864
|
new: cmdNew,
|
|
1671
1865
|
attach: cmdAttach,
|
|
1866
|
+
detach: cmdDetach,
|
|
1672
1867
|
claim: cmdClaim,
|
|
1673
1868
|
unclaim: cmdUnclaim,
|
|
1674
1869
|
add: cmdClaim,
|
|
@@ -1695,7 +1890,7 @@ function changeDir(cwd, dir) {
|
|
|
1695
1890
|
fail(`-C ${dir}: no such directory`);
|
|
1696
1891
|
}
|
|
1697
1892
|
}
|
|
1698
|
-
function main(argv) {
|
|
1893
|
+
async function main(argv) {
|
|
1699
1894
|
let cwd = realpathSync3.native(process.cwd());
|
|
1700
1895
|
let i = 0;
|
|
1701
1896
|
for (;i < argv.length; i++) {
|
|
@@ -1754,7 +1949,7 @@ function main(argv) {
|
|
|
1754
1949
|
}
|
|
1755
1950
|
var code;
|
|
1756
1951
|
try {
|
|
1757
|
-
code = main(process.argv.slice(2));
|
|
1952
|
+
code = await main(process.argv.slice(2));
|
|
1758
1953
|
} catch (e) {
|
|
1759
1954
|
if (e instanceof SichError)
|
|
1760
1955
|
printError(e.message);
|