@reviewflow/sdk 6.24.0 → 6.27.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/build/git-node.mjs +725 -9
- package/build/index-browser.es.js +219 -17
- package/build/index-node.mjs +219 -17
- package/package.json +1 -1
- package/src/checkout/overview.ts +109 -0
- package/src/checkout/sync.ts +85 -0
- package/src/checkout/worktreeRemoval.ts +136 -0
- package/src/config/defaultRepoPaths.ts +2 -0
- package/src/config/insertCheckoutEntry.ts +9 -15
- package/src/config/parseConfigText.ts +23 -0
- package/src/config/repoPathsSetting.ts +44 -0
- package/src/diff/readingOrder.ts +5 -4
- package/src/diff/themes.ts +29 -1
- package/src/diff/types.ts +112 -0
- package/src/git/checkoutBranchDiff.ts +191 -0
- package/src/git/checkoutBranches.ts +187 -0
- package/src/git/checkoutCommits.ts +223 -0
- package/src/git/checkoutSync.ts +179 -0
- package/src/git/groupDiffPaths.ts +3 -3
- package/src/git/readBranchDiff.ts +9 -10
- package/src/git/run.ts +55 -1
- package/src/git/worktreeRemoval.ts +316 -0
- package/src/git.ts +52 -1
- package/src/github/openPullRequests.ts +154 -0
- package/src/index.ts +9 -0
package/build/git-node.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
|
-
import { env } from 'node:process';
|
|
2
|
+
import { env, pid } from 'node:process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
-
import { lstat } from 'node:fs/promises';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
4
|
+
import { lstat, readdir } from 'node:fs/promises';
|
|
5
|
+
import { join, dirname, relative, isAbsolute } from 'node:path';
|
|
6
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
7
7
|
import { parseSync } from 'oxc-parser';
|
|
8
8
|
import { builtinModules } from 'node:module';
|
|
9
9
|
import { setImmediate } from 'node:timers/promises';
|
|
10
10
|
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
11
|
+
const execFileAsync$1 = promisify(execFile);
|
|
12
12
|
const timeoutMs = 1e4;
|
|
13
13
|
const networkTimeoutMs = 6e4;
|
|
14
14
|
const defaultMaxBuffer = 4 * 1024 * 1024;
|
|
@@ -31,10 +31,39 @@ const failureReason = (error, timeout) => {
|
|
|
31
31
|
return "stderr" in error ? reasonLine(String(error.stderr)) : "";
|
|
32
32
|
};
|
|
33
33
|
const isExitOneAnswer = (error) => typeof error === "object" && error !== null && "code" in error && error.code === 1 && "stdout" in error && typeof error.stdout === "string" && error.stdout !== "";
|
|
34
|
+
const runUninterruptible = async (cwd, args) => new Promise((resolve, reject) => {
|
|
35
|
+
const child = spawn("git", args, {
|
|
36
|
+
cwd,
|
|
37
|
+
env,
|
|
38
|
+
detached: true,
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
40
|
+
});
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
44
|
+
stdout += chunk;
|
|
45
|
+
});
|
|
46
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
47
|
+
stderr += chunk;
|
|
48
|
+
});
|
|
49
|
+
child.on("error", reject);
|
|
50
|
+
child.on("close", (code) => {
|
|
51
|
+
if (code === 0) {
|
|
52
|
+
resolve(stdout);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
reject(
|
|
56
|
+
Object.assign(new Error(`git exited with ${String(code)}`), {
|
|
57
|
+
stderr
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
34
62
|
const git = async (cwd, args, options = {}) => {
|
|
35
63
|
const timeout = options.network ? networkTimeoutMs : timeoutMs;
|
|
36
64
|
try {
|
|
37
|
-
|
|
65
|
+
if (options.uninterruptible) return await runUninterruptible(cwd, args);
|
|
66
|
+
const { stdout } = await execFileAsync$1("git", args, {
|
|
38
67
|
cwd,
|
|
39
68
|
timeout,
|
|
40
69
|
maxBuffer: options.maxBuffer ?? defaultMaxBuffer,
|
|
@@ -403,7 +432,693 @@ const groupDiffPaths = (isPackage, paths) => {
|
|
|
403
432
|
if (group === void 0) groups.set(name, [path]);
|
|
404
433
|
else group.push(path);
|
|
405
434
|
}
|
|
406
|
-
return [...groups].toSorted(([left], [right]) => Number(left
|
|
435
|
+
return [...groups].toSorted(([left], [right]) => Number(left === "") - Number(right === "")).map(([name, groupPaths]) => ({ name, paths: groupPaths }));
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const readHeadBranch = async (cwd) => {
|
|
439
|
+
const branch = await gitOrNull(cwd, [
|
|
440
|
+
"symbolic-ref",
|
|
441
|
+
"--quiet",
|
|
442
|
+
"--short",
|
|
443
|
+
"HEAD"
|
|
444
|
+
]);
|
|
445
|
+
return branch === null || branch.trim() === "" ? null : branch.trim();
|
|
446
|
+
};
|
|
447
|
+
const readHeadSha = async (cwd, ref = "HEAD") => {
|
|
448
|
+
const sha = await git(cwd, ["rev-parse", ref]);
|
|
449
|
+
return sha.trim();
|
|
450
|
+
};
|
|
451
|
+
const readDefaultBranch = async (cwd) => {
|
|
452
|
+
const ref = await gitOrNull(cwd, [
|
|
453
|
+
"symbolic-ref",
|
|
454
|
+
"--quiet",
|
|
455
|
+
"--short",
|
|
456
|
+
"refs/remotes/origin/HEAD"
|
|
457
|
+
]);
|
|
458
|
+
const name = ref?.trim().replace(/^origin\//, "") ?? "";
|
|
459
|
+
return name === "" ? null : name;
|
|
460
|
+
};
|
|
461
|
+
const readCommit = async (cwd, ref) => {
|
|
462
|
+
const sha = await gitOrNull(cwd, [
|
|
463
|
+
"rev-parse",
|
|
464
|
+
"--verify",
|
|
465
|
+
"--quiet",
|
|
466
|
+
`${ref}^{commit}`
|
|
467
|
+
]);
|
|
468
|
+
return sha === null ? null : sha.trim();
|
|
469
|
+
};
|
|
470
|
+
const resolveReadHead = async (cwd, branch) => {
|
|
471
|
+
if (branch === void 0) {
|
|
472
|
+
return { ref: "HEAD", branch: await readHeadBranch(cwd) };
|
|
473
|
+
}
|
|
474
|
+
const ref = `refs/heads/${branch}`;
|
|
475
|
+
if (await readCommit(cwd, ref) === null) {
|
|
476
|
+
throw new Error(`no local branch ${branch} in ${cwd}`);
|
|
477
|
+
}
|
|
478
|
+
return { ref, branch };
|
|
479
|
+
};
|
|
480
|
+
const resolveDiffBase = async (cwd, name, head = "HEAD") => {
|
|
481
|
+
const candidates = [
|
|
482
|
+
{ ref: `origin/${name}`, fullRef: `refs/remotes/origin/${name}` },
|
|
483
|
+
{ ref: name, fullRef: `refs/heads/${name}` }
|
|
484
|
+
];
|
|
485
|
+
for (const { ref, fullRef } of candidates) {
|
|
486
|
+
const sha = await readCommit(cwd, fullRef);
|
|
487
|
+
if (sha === null) continue;
|
|
488
|
+
const mergeBase = await gitOrNull(cwd, ["merge-base", sha, head]);
|
|
489
|
+
if (mergeBase === null) {
|
|
490
|
+
throw new Error(`${head} shares no history with ${ref}`);
|
|
491
|
+
}
|
|
492
|
+
return { base: { name, ref, sha }, mergeBase: mergeBase.trim() };
|
|
493
|
+
}
|
|
494
|
+
throw new Error(
|
|
495
|
+
`neither origin/${name} nor ${name} exists in ${cwd}: fetch the repository, or check its default branch`
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
const readScopeStart = async ({
|
|
499
|
+
cwd,
|
|
500
|
+
scope,
|
|
501
|
+
baseName,
|
|
502
|
+
head
|
|
503
|
+
}) => {
|
|
504
|
+
if (scope === "uncommitted") return null;
|
|
505
|
+
const name = baseName ?? await readDefaultBranch(cwd) ?? "main";
|
|
506
|
+
return resolveDiffBase(cwd, name, head);
|
|
507
|
+
};
|
|
508
|
+
const readCheckoutBranchDiff = async ({
|
|
509
|
+
cwd,
|
|
510
|
+
baseName,
|
|
511
|
+
scope,
|
|
512
|
+
branch: branchName,
|
|
513
|
+
whitespace = "show",
|
|
514
|
+
limits
|
|
515
|
+
}) => {
|
|
516
|
+
if (branchName !== void 0 && scope === "uncommitted") {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`${branchName} is not checked out here: it has nothing uncommitted`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const head = await resolveReadHead(cwd, branchName);
|
|
522
|
+
const [start, headSha] = await Promise.all([
|
|
523
|
+
readScopeStart({ cwd, scope, baseName, head: head.ref }),
|
|
524
|
+
readHeadSha(cwd, head.ref)
|
|
525
|
+
]);
|
|
526
|
+
const { paths, ...diff } = await readBranchDiff({
|
|
527
|
+
cwd,
|
|
528
|
+
mergeBase: start?.mergeBase ?? headSha,
|
|
529
|
+
head: head.ref,
|
|
530
|
+
scope: scope === "uncommitted" ? "workingTree" : "committed",
|
|
531
|
+
whitespace,
|
|
532
|
+
limits
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
scope,
|
|
536
|
+
whitespace,
|
|
537
|
+
branch: head.branch,
|
|
538
|
+
headSha,
|
|
539
|
+
base: start?.base ?? null,
|
|
540
|
+
mergeBase: start?.mergeBase ?? null,
|
|
541
|
+
...diff,
|
|
542
|
+
groups: groupDiffPaths(packagesOnDisk(cwd), paths)
|
|
543
|
+
};
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const defaultRecentCommits = 30;
|
|
547
|
+
const fieldSeparator$1 = "";
|
|
548
|
+
const recordSeparator = "";
|
|
549
|
+
const logFormat = ["%H", "%P", "%an", "%cI", "%s", "%b"].join("%x1f");
|
|
550
|
+
const parseLog = (output) => output.split(recordSeparator).map((record) => record.replace(/^\n/, "")).filter((record) => record !== "").map((record) => {
|
|
551
|
+
const [
|
|
552
|
+
sha = "",
|
|
553
|
+
parents = "",
|
|
554
|
+
author = "",
|
|
555
|
+
committedAt = "",
|
|
556
|
+
headline = "",
|
|
557
|
+
body = ""
|
|
558
|
+
] = record.split(fieldSeparator$1);
|
|
559
|
+
return {
|
|
560
|
+
sha,
|
|
561
|
+
parents: parents === "" ? [] : parents.split(" "),
|
|
562
|
+
author,
|
|
563
|
+
committedAt,
|
|
564
|
+
headline,
|
|
565
|
+
body: body.trim()
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
const readBranchStart = async ({
|
|
569
|
+
cwd,
|
|
570
|
+
baseName,
|
|
571
|
+
limit,
|
|
572
|
+
head
|
|
573
|
+
}) => {
|
|
574
|
+
const name = baseName ?? await readDefaultBranch(cwd) ?? "main";
|
|
575
|
+
try {
|
|
576
|
+
const { base, mergeBase } = await resolveDiffBase(cwd, name, head);
|
|
577
|
+
const range = `${mergeBase}..${head}`;
|
|
578
|
+
const [listed, count] = await Promise.all([
|
|
579
|
+
git(cwd, ["rev-list", `--max-count=${String(limit)}`, range]),
|
|
580
|
+
git(cwd, ["rev-list", "--count", range])
|
|
581
|
+
]);
|
|
582
|
+
return {
|
|
583
|
+
base,
|
|
584
|
+
onBranch: new Set(listed.split("\n").filter((sha) => sha !== "")),
|
|
585
|
+
count: Number(count.trim())
|
|
586
|
+
};
|
|
587
|
+
} catch {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const readUnpushed = async (cwd, limit, head) => {
|
|
592
|
+
const remotes = await git(cwd, ["remote"]);
|
|
593
|
+
if (remotes.trim() === "") return null;
|
|
594
|
+
const listed = await gitOrNull(cwd, [
|
|
595
|
+
"rev-list",
|
|
596
|
+
`--max-count=${String(limit)}`,
|
|
597
|
+
head,
|
|
598
|
+
"--not",
|
|
599
|
+
"--remotes"
|
|
600
|
+
]);
|
|
601
|
+
return new Set((listed ?? "").split("\n").filter((sha) => sha !== ""));
|
|
602
|
+
};
|
|
603
|
+
const readRecentCommits = async ({
|
|
604
|
+
cwd,
|
|
605
|
+
limit = defaultRecentCommits,
|
|
606
|
+
baseName,
|
|
607
|
+
branch: branchName
|
|
608
|
+
}) => {
|
|
609
|
+
const head = await resolveReadHead(cwd, branchName);
|
|
610
|
+
const [start, unpushed, log] = await Promise.all([
|
|
611
|
+
readBranchStart({ cwd, baseName, limit, head: head.ref }),
|
|
612
|
+
readUnpushed(cwd, limit, head.ref),
|
|
613
|
+
gitOrNull(cwd, [
|
|
614
|
+
"log",
|
|
615
|
+
"--no-color",
|
|
616
|
+
`--max-count=${String(limit)}`,
|
|
617
|
+
`--format=${logFormat}%x1e`,
|
|
618
|
+
head.ref
|
|
619
|
+
])
|
|
620
|
+
]);
|
|
621
|
+
const { branch } = head;
|
|
622
|
+
const commits = parseLog(log ?? "").map((commit) => ({
|
|
623
|
+
oid: commit.sha,
|
|
624
|
+
parentOid: commit.parents[0] ?? null,
|
|
625
|
+
merge: commit.parents.length > 1,
|
|
626
|
+
headline: commit.headline,
|
|
627
|
+
body: commit.body,
|
|
628
|
+
author: { login: commit.author, avatarUrl: null },
|
|
629
|
+
committedAt: commit.committedAt,
|
|
630
|
+
onBranch: start?.onBranch.has(commit.sha) ?? false,
|
|
631
|
+
pushed: unpushed === null ? null : !unpushed.has(commit.sha)
|
|
632
|
+
}));
|
|
633
|
+
return {
|
|
634
|
+
branch,
|
|
635
|
+
base: start?.base ?? null,
|
|
636
|
+
onBranchCount: start?.count ?? null,
|
|
637
|
+
commits
|
|
638
|
+
};
|
|
639
|
+
};
|
|
640
|
+
const emptyTree = async (cwd) => {
|
|
641
|
+
const tree = await git(cwd, ["hash-object", "-t", "tree", "/dev/null"]);
|
|
642
|
+
return tree.trim();
|
|
643
|
+
};
|
|
644
|
+
const readCommitDiff = async ({
|
|
645
|
+
cwd,
|
|
646
|
+
sha,
|
|
647
|
+
whitespace = "show",
|
|
648
|
+
limits
|
|
649
|
+
}) => {
|
|
650
|
+
const listed = await git(cwd, [
|
|
651
|
+
"rev-list",
|
|
652
|
+
"--parents",
|
|
653
|
+
"--max-count=1",
|
|
654
|
+
`${sha}^{commit}`
|
|
655
|
+
]);
|
|
656
|
+
const [commit = sha, parentSha = null] = listed.trim().split(" ");
|
|
657
|
+
const { paths, patch, omitted } = await readBranchDiff({
|
|
658
|
+
cwd,
|
|
659
|
+
mergeBase: parentSha ?? await emptyTree(cwd),
|
|
660
|
+
head: commit,
|
|
661
|
+
scope: "committed",
|
|
662
|
+
whitespace,
|
|
663
|
+
limits
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
sha: commit,
|
|
667
|
+
parentSha,
|
|
668
|
+
whitespace,
|
|
669
|
+
patch,
|
|
670
|
+
omitted,
|
|
671
|
+
groups: groupDiffPaths(packagesOnDisk(cwd), paths)
|
|
672
|
+
};
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const upstreamGone = (upstream) => `${upstream} no longer exists on the remote`;
|
|
676
|
+
const pullRefusal = (status) => {
|
|
677
|
+
if (status.branch === null) return "a detached HEAD has nothing to pull into";
|
|
678
|
+
if (status.upstream === null) {
|
|
679
|
+
return `${status.branch} tracks no remote branch`;
|
|
680
|
+
}
|
|
681
|
+
if (status.behind === null) return upstreamGone(status.upstream);
|
|
682
|
+
if (status.behind === 0) {
|
|
683
|
+
return `${status.branch} is not behind ${status.upstream}`;
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
};
|
|
687
|
+
const pushRefusal = (status, hasOrigin) => {
|
|
688
|
+
if (status.branch === null) return "a detached HEAD has no branch to push";
|
|
689
|
+
if (status.upstream === null) {
|
|
690
|
+
return hasOrigin ? null : "this repository has no origin remote";
|
|
691
|
+
}
|
|
692
|
+
if (status.ahead === null) return upstreamGone(status.upstream);
|
|
693
|
+
if (status.ahead === 0) {
|
|
694
|
+
return `${status.branch} has nothing to push to ${status.upstream}`;
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const headerValue = (line, key) => line.startsWith(`# ${key} `) ? line.slice(`# ${key} `.length) : null;
|
|
700
|
+
const parseStatusV2 = (stdout) => {
|
|
701
|
+
const status = {
|
|
702
|
+
branch: null,
|
|
703
|
+
hasCommit: true,
|
|
704
|
+
upstream: null,
|
|
705
|
+
ahead: null,
|
|
706
|
+
behind: null,
|
|
707
|
+
modified: 0,
|
|
708
|
+
untracked: 0
|
|
709
|
+
};
|
|
710
|
+
for (const line of stdout.split("\n")) {
|
|
711
|
+
if (line.startsWith("# ")) {
|
|
712
|
+
const oid = headerValue(line, "branch.oid");
|
|
713
|
+
if (oid !== null) status.hasCommit = oid !== "(initial)";
|
|
714
|
+
const head = headerValue(line, "branch.head");
|
|
715
|
+
if (head !== null) status.branch = head === "(detached)" ? null : head;
|
|
716
|
+
const upstream = headerValue(line, "branch.upstream");
|
|
717
|
+
if (upstream !== null) status.upstream = upstream;
|
|
718
|
+
const ab = headerValue(line, "branch.ab");
|
|
719
|
+
const counts = ab === null ? null : /^\+(\d+) -(\d+)$/.exec(ab);
|
|
720
|
+
if (counts) {
|
|
721
|
+
status.ahead = Number(counts[1]);
|
|
722
|
+
status.behind = Number(counts[2]);
|
|
723
|
+
}
|
|
724
|
+
} else if (line.startsWith("? ")) {
|
|
725
|
+
status.untracked += 1;
|
|
726
|
+
} else if (/^[12u] /.test(line)) {
|
|
727
|
+
status.modified += 1;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return status;
|
|
731
|
+
};
|
|
732
|
+
const readHeadCommit = async (cwd) => {
|
|
733
|
+
const stdout = await gitOrNull(cwd, [
|
|
734
|
+
"log",
|
|
735
|
+
"-1",
|
|
736
|
+
"--format=%h%x00%s%x00%cI",
|
|
737
|
+
"--no-patch"
|
|
738
|
+
]);
|
|
739
|
+
const [shortSha, subject, committedAt] = (stdout?.trim() ?? "").split("\0");
|
|
740
|
+
if (!shortSha || subject === void 0 || !committedAt) return null;
|
|
741
|
+
return {
|
|
742
|
+
shortSha,
|
|
743
|
+
subject,
|
|
744
|
+
committedAt: new Date(committedAt).toISOString()
|
|
745
|
+
};
|
|
746
|
+
};
|
|
747
|
+
const readCheckoutStatus = async (cwd) => {
|
|
748
|
+
const { hasCommit, ...status } = parseStatusV2(
|
|
749
|
+
await git(cwd, ["status", "--porcelain=v2", "--branch"])
|
|
750
|
+
);
|
|
751
|
+
return {
|
|
752
|
+
path: cwd,
|
|
753
|
+
...status,
|
|
754
|
+
commit: hasCommit ? await readHeadCommit(cwd) : null
|
|
755
|
+
};
|
|
756
|
+
};
|
|
757
|
+
const messageOf$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
758
|
+
const hasOriginRemote = async (cwd) => await gitOrNull(cwd, ["remote", "get-url", "origin"]) !== null;
|
|
759
|
+
const fetchFirst = async (cwd, fetch) => {
|
|
760
|
+
if (fetch === "never") return null;
|
|
761
|
+
const upstreamSha = await gitOrNull(cwd, [
|
|
762
|
+
"rev-parse",
|
|
763
|
+
"--verify",
|
|
764
|
+
"--quiet",
|
|
765
|
+
"@{upstream}"
|
|
766
|
+
]);
|
|
767
|
+
if (upstreamSha === null) return null;
|
|
768
|
+
if (fetch !== "always" && upstreamSha.trim() === fetch.unlessUpstreamAt) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
await git(cwd, ["fetch", "--quiet"], { network: true });
|
|
773
|
+
return null;
|
|
774
|
+
} catch (error) {
|
|
775
|
+
return messageOf$1(error);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
const readCheckoutSync = async ({
|
|
779
|
+
cwd,
|
|
780
|
+
fetch = "never"
|
|
781
|
+
}) => {
|
|
782
|
+
const fetchError = await fetchFirst(cwd, fetch);
|
|
783
|
+
const [status, hasOrigin] = await Promise.all([
|
|
784
|
+
readCheckoutStatus(cwd),
|
|
785
|
+
hasOriginRemote(cwd)
|
|
786
|
+
]);
|
|
787
|
+
return { status, hasOrigin, fetchError };
|
|
788
|
+
};
|
|
789
|
+
const pullCheckout = async (cwd) => {
|
|
790
|
+
const refusal = pullRefusal(await readCheckoutStatus(cwd));
|
|
791
|
+
if (refusal !== null) throw new Error(refusal);
|
|
792
|
+
await git(cwd, ["pull", "--quiet", "--ff-only"], { network: true });
|
|
793
|
+
};
|
|
794
|
+
const pushCheckout = async (cwd) => {
|
|
795
|
+
const [status, hasOrigin] = await Promise.all([
|
|
796
|
+
readCheckoutStatus(cwd),
|
|
797
|
+
hasOriginRemote(cwd)
|
|
798
|
+
]);
|
|
799
|
+
const refusal = pushRefusal(status, hasOrigin);
|
|
800
|
+
if (refusal !== null) throw new Error(refusal);
|
|
801
|
+
await git(
|
|
802
|
+
cwd,
|
|
803
|
+
status.upstream === null && status.branch !== null ? ["push", "--quiet", "--set-upstream", "origin", status.branch] : ["push", "--quiet"],
|
|
804
|
+
{ network: true }
|
|
805
|
+
);
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const zeroSha = /^0+$/;
|
|
809
|
+
const parseWorktreeList = (output) => output.split(/\n{2,}/).map((block) => block.split("\n").filter((line) => line !== "")).filter((lines) => lines[0]?.startsWith("worktree ") === true).map((lines) => {
|
|
810
|
+
const value = (key) => {
|
|
811
|
+
const line = lines.find(
|
|
812
|
+
(candidate) => candidate === key || candidate.startsWith(`${key} `)
|
|
813
|
+
);
|
|
814
|
+
return line === void 0 ? null : line.slice(key.length + 1);
|
|
815
|
+
};
|
|
816
|
+
const head = value("HEAD");
|
|
817
|
+
const branch = value("branch");
|
|
818
|
+
return {
|
|
819
|
+
path: value("worktree") ?? "",
|
|
820
|
+
branch: branch === null ? null : branch.replace(/^refs\/heads\//, ""),
|
|
821
|
+
headSha: head === null || zeroSha.test(head) ? null : head,
|
|
822
|
+
bare: value("bare") !== null,
|
|
823
|
+
lock: value("locked"),
|
|
824
|
+
prunable: value("prunable") !== null
|
|
825
|
+
};
|
|
826
|
+
});
|
|
827
|
+
const readWorktrees = async (cwd) => {
|
|
828
|
+
const listed = parseWorktreeList(
|
|
829
|
+
await git(cwd, ["worktree", "list", "--porcelain"])
|
|
830
|
+
);
|
|
831
|
+
return Promise.all(
|
|
832
|
+
listed.map((worktree, index) => ({ ...worktree, main: index === 0 })).filter(({ bare }) => !bare).map(async ({ bare: _bare, ...worktree }) => ({
|
|
833
|
+
...worktree,
|
|
834
|
+
status: worktree.prunable ? null : await readCheckoutStatus(worktree.path).catch(() => null)
|
|
835
|
+
}))
|
|
836
|
+
);
|
|
837
|
+
};
|
|
838
|
+
const fieldSeparator = "\0";
|
|
839
|
+
const branchFormat = [
|
|
840
|
+
"%(refname:lstrip=2)",
|
|
841
|
+
"%(objectname)",
|
|
842
|
+
"%(upstream:short)",
|
|
843
|
+
"%(upstream:track)",
|
|
844
|
+
"%(committerdate:iso-strict)",
|
|
845
|
+
"%(worktreepath)",
|
|
846
|
+
"%(subject)"
|
|
847
|
+
].join("%00");
|
|
848
|
+
const parseTrack = (track, hasUpstream) => {
|
|
849
|
+
if (!hasUpstream) return { ahead: null, behind: null, gone: false };
|
|
850
|
+
if (track === "[gone]") return { ahead: null, behind: null, gone: true };
|
|
851
|
+
const count = (word) => Number(new RegExp(`${word} (\\d+)`).exec(track)?.[1] ?? 0);
|
|
852
|
+
return { ahead: count("ahead"), behind: count("behind"), gone: false };
|
|
853
|
+
};
|
|
854
|
+
const readMergedBranches = async (cwd, start) => {
|
|
855
|
+
if (start === null) return /* @__PURE__ */ new Set();
|
|
856
|
+
const listed = await git(cwd, [
|
|
857
|
+
"for-each-ref",
|
|
858
|
+
`--merged=${start.base.sha}`,
|
|
859
|
+
"--format=%(refname:lstrip=2)",
|
|
860
|
+
"refs/heads"
|
|
861
|
+
]);
|
|
862
|
+
return new Set(
|
|
863
|
+
listed.split("\n").filter((name) => name !== "" && name !== start.base.name)
|
|
864
|
+
);
|
|
865
|
+
};
|
|
866
|
+
const readBase = async (cwd, baseName) => {
|
|
867
|
+
const name = baseName ?? await readDefaultBranch(cwd) ?? "main";
|
|
868
|
+
return resolveDiffBase(cwd, name).catch(() => null);
|
|
869
|
+
};
|
|
870
|
+
const readBranches = async ({
|
|
871
|
+
cwd,
|
|
872
|
+
baseName
|
|
873
|
+
}) => {
|
|
874
|
+
const start = await readBase(cwd, baseName);
|
|
875
|
+
const [listing, merged, worktrees] = await Promise.all([
|
|
876
|
+
gitOrNull(cwd, [
|
|
877
|
+
"for-each-ref",
|
|
878
|
+
"--sort=-committerdate",
|
|
879
|
+
`--format=${branchFormat}`,
|
|
880
|
+
"refs/heads"
|
|
881
|
+
]),
|
|
882
|
+
readMergedBranches(cwd, start),
|
|
883
|
+
readWorktrees(cwd)
|
|
884
|
+
]);
|
|
885
|
+
const branches = (listing ?? "").split("\n").filter((line) => line !== "").map((line) => {
|
|
886
|
+
const [
|
|
887
|
+
name = "",
|
|
888
|
+
sha = "",
|
|
889
|
+
upstream = "",
|
|
890
|
+
track = "",
|
|
891
|
+
committedAt = "",
|
|
892
|
+
worktreePath = "",
|
|
893
|
+
subject = ""
|
|
894
|
+
] = line.split(fieldSeparator);
|
|
895
|
+
const { ahead, behind, gone } = parseTrack(track, upstream !== "");
|
|
896
|
+
return {
|
|
897
|
+
name,
|
|
898
|
+
sha,
|
|
899
|
+
upstream: upstream === "" ? null : upstream,
|
|
900
|
+
ahead,
|
|
901
|
+
behind,
|
|
902
|
+
upstreamGone: gone,
|
|
903
|
+
merged: merged.has(name),
|
|
904
|
+
worktreePath: worktreePath === "" ? null : worktreePath,
|
|
905
|
+
lastCommit: {
|
|
906
|
+
subject,
|
|
907
|
+
committedAt: new Date(committedAt).toISOString()
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
return { base: start?.base ?? null, worktrees, branches };
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
const fileCount = (count, kind) => count === 0 ? null : `${String(count)} ${kind} file${count === 1 ? "" : "s"}`;
|
|
915
|
+
const mainWorktreeRefusal = "This is the main checkout, which holds the repository: it is never removed.";
|
|
916
|
+
const dirtyRefusal = ({
|
|
917
|
+
modified,
|
|
918
|
+
untracked
|
|
919
|
+
}) => {
|
|
920
|
+
const parts = [
|
|
921
|
+
fileCount(modified, "modified"),
|
|
922
|
+
fileCount(untracked, "untracked")
|
|
923
|
+
].filter((part) => part !== null);
|
|
924
|
+
if (parts.length === 0) return null;
|
|
925
|
+
return `${parts.join(" and ")}: git would refuse to remove this worktree. Commit or discard them first.`;
|
|
926
|
+
};
|
|
927
|
+
const unpushedRefusal = (count) => {
|
|
928
|
+
if (count === 0) return null;
|
|
929
|
+
const subject = count === 1 ? "1 commit exists" : `${String(count)} commits exist`;
|
|
930
|
+
return `${subject} only in this clone, on no remote branch and in no pull request. Push them first.`;
|
|
931
|
+
};
|
|
932
|
+
const lockRefusal = (lock) => {
|
|
933
|
+
if (lock === null) return null;
|
|
934
|
+
const reason = lock === "" ? "" : ` (${lock})`;
|
|
935
|
+
return `git holds this worktree locked${reason}, usually by a Claude Code process still running in it. Exit it first.`;
|
|
936
|
+
};
|
|
937
|
+
const processesRefusal = (count) => {
|
|
938
|
+
if (count === null || count === 0) return null;
|
|
939
|
+
const subject = count === 1 ? "1 process runs" : `${String(count)} processes run`;
|
|
940
|
+
return `${subject} inside this worktree (a shell, an editor or an agent): removing it would pull the directory out from under them. Exit them first.`;
|
|
941
|
+
};
|
|
942
|
+
const worktreeRemovalRefusals = (status) => {
|
|
943
|
+
if (status.main) return [mainWorktreeRefusal];
|
|
944
|
+
return [
|
|
945
|
+
dirtyRefusal(status),
|
|
946
|
+
unpushedRefusal(status.unpushedCommits),
|
|
947
|
+
lockRefusal(status.lock),
|
|
948
|
+
processesRefusal(status.processesInside)
|
|
949
|
+
].filter((refusal) => refusal !== null);
|
|
950
|
+
};
|
|
951
|
+
const worktreeRemovalRefusal = (status) => worktreeRemovalRefusals(status)[0] ?? null;
|
|
952
|
+
|
|
953
|
+
const execFileAsync = promisify(execFile);
|
|
954
|
+
const realpathOrSelf = (path) => {
|
|
955
|
+
try {
|
|
956
|
+
return realpathSync.native(path);
|
|
957
|
+
} catch {
|
|
958
|
+
return path;
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
const isInsideDirectory = (directory, path) => {
|
|
962
|
+
const relativePath = relative(directory, path);
|
|
963
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
964
|
+
};
|
|
965
|
+
const readGitDirectory = async (worktree, flag) => {
|
|
966
|
+
const directory = await git(worktree, [
|
|
967
|
+
"rev-parse",
|
|
968
|
+
"--path-format=absolute",
|
|
969
|
+
flag
|
|
970
|
+
]);
|
|
971
|
+
return realpathOrSelf(directory.trim());
|
|
972
|
+
};
|
|
973
|
+
const readBranch = async (worktree) => {
|
|
974
|
+
const branch = await gitOrNull(worktree, [
|
|
975
|
+
"rev-parse",
|
|
976
|
+
"--abbrev-ref",
|
|
977
|
+
"HEAD"
|
|
978
|
+
]);
|
|
979
|
+
const name = branch?.trim() ?? "";
|
|
980
|
+
return name === "" || name === "HEAD" ? null : name;
|
|
981
|
+
};
|
|
982
|
+
const countChanges = async (worktree) => {
|
|
983
|
+
const porcelain = await git(worktree, ["status", "--porcelain"]);
|
|
984
|
+
const lines = porcelain.split("\n").filter((line) => line.trim() !== "");
|
|
985
|
+
const untracked = lines.filter((line) => line.startsWith("??")).length;
|
|
986
|
+
return { modified: lines.length - untracked, untracked };
|
|
987
|
+
};
|
|
988
|
+
const hasCommit$1 = async (worktree, sha) => await gitOrNull(worktree, ["cat-file", "-e", `${sha}^{commit}`]) !== null;
|
|
989
|
+
const countUnpushedCommits = async (worktree, keptShas) => {
|
|
990
|
+
const present = await Promise.all(
|
|
991
|
+
keptShas.filter((sha) => /^[\da-f]{7,64}$/i.test(sha)).map(async (sha) => await hasCommit$1(worktree, sha) ? sha : null)
|
|
992
|
+
);
|
|
993
|
+
const count = await gitOrNull(worktree, [
|
|
994
|
+
"rev-list",
|
|
995
|
+
"--count",
|
|
996
|
+
"HEAD",
|
|
997
|
+
"--not",
|
|
998
|
+
"--remotes",
|
|
999
|
+
...present.filter((sha) => sha !== null)
|
|
1000
|
+
]);
|
|
1001
|
+
return count === null ? 0 : Number(count.trim());
|
|
1002
|
+
};
|
|
1003
|
+
const readLock = async (worktree) => {
|
|
1004
|
+
const listed = parseWorktreeList(
|
|
1005
|
+
await git(worktree, ["worktree", "list", "--porcelain"])
|
|
1006
|
+
);
|
|
1007
|
+
const own = listed.find(
|
|
1008
|
+
({ path }) => realpathOrSelf(path) === realpathOrSelf(worktree)
|
|
1009
|
+
);
|
|
1010
|
+
return own?.lock ?? null;
|
|
1011
|
+
};
|
|
1012
|
+
const countProcessesInside = async (directory) => {
|
|
1013
|
+
const stdout = await execFileAsync("lsof", ["-n", "-d", "cwd", "-F", "pn"], {
|
|
1014
|
+
timeout: 1e4,
|
|
1015
|
+
maxBuffer: 16 * 1024 * 1024
|
|
1016
|
+
}).then(
|
|
1017
|
+
({ stdout: answer }) => answer,
|
|
1018
|
+
(error) => {
|
|
1019
|
+
if (typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string" && error.stdout !== "") {
|
|
1020
|
+
return error.stdout;
|
|
1021
|
+
}
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
);
|
|
1025
|
+
if (stdout === null) return null;
|
|
1026
|
+
const root = realpathOrSelf(directory);
|
|
1027
|
+
const inside = /* @__PURE__ */ new Set();
|
|
1028
|
+
let processId = "";
|
|
1029
|
+
for (const line of stdout.split("\n")) {
|
|
1030
|
+
if (line.startsWith("p")) processId = line.slice(1);
|
|
1031
|
+
else if (line.startsWith("n") && processId !== String(pid) && isInsideDirectory(root, line.slice(1))) {
|
|
1032
|
+
inside.add(processId);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return inside.size;
|
|
1036
|
+
};
|
|
1037
|
+
const ignoredPrefix = "!! ";
|
|
1038
|
+
const listFiles = async (worktree, relativeDirectory) => {
|
|
1039
|
+
const entries = await readdir(join(worktree, relativeDirectory), {
|
|
1040
|
+
recursive: true,
|
|
1041
|
+
withFileTypes: true
|
|
1042
|
+
}).catch(() => []);
|
|
1043
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => relative(worktree, join(entry.parentPath, entry.name))).toSorted();
|
|
1044
|
+
};
|
|
1045
|
+
const listLocalOnlyIgnored = async (worktree, mainCheckout) => {
|
|
1046
|
+
const porcelain = await git(worktree, [
|
|
1047
|
+
"status",
|
|
1048
|
+
"--porcelain",
|
|
1049
|
+
"--ignored",
|
|
1050
|
+
"-z"
|
|
1051
|
+
]);
|
|
1052
|
+
const ignored = porcelain.split("\0").filter((entry) => entry.startsWith(ignoredPrefix)).map((entry) => entry.slice(ignoredPrefix.length));
|
|
1053
|
+
const localOnly = await Promise.all(
|
|
1054
|
+
ignored.map(async (relativePath) => {
|
|
1055
|
+
const entry = await lstat(join(worktree, relativePath)).catch(() => null);
|
|
1056
|
+
if (entry === null || entry.isSymbolicLink()) return [];
|
|
1057
|
+
if (existsSync(join(mainCheckout, relativePath))) return [];
|
|
1058
|
+
return entry.isDirectory() ? listFiles(worktree, relativePath) : [relativePath];
|
|
1059
|
+
})
|
|
1060
|
+
);
|
|
1061
|
+
return localOnly.flat();
|
|
1062
|
+
};
|
|
1063
|
+
const readWorktreeRemovalStatus = async ({
|
|
1064
|
+
worktree,
|
|
1065
|
+
keptShas = []
|
|
1066
|
+
}) => {
|
|
1067
|
+
const [gitDirectory, commonDirectory] = await Promise.all([
|
|
1068
|
+
readGitDirectory(worktree, "--git-dir"),
|
|
1069
|
+
readGitDirectory(worktree, "--git-common-dir")
|
|
1070
|
+
]);
|
|
1071
|
+
const [branch, changes, unpushedCommits, lock, ignored] = await Promise.all([
|
|
1072
|
+
readBranch(worktree),
|
|
1073
|
+
countChanges(worktree),
|
|
1074
|
+
countUnpushedCommits(worktree, keptShas),
|
|
1075
|
+
readLock(worktree),
|
|
1076
|
+
listLocalOnlyIgnored(worktree, dirname(commonDirectory))
|
|
1077
|
+
]);
|
|
1078
|
+
const processesInside = await countProcessesInside(worktree);
|
|
1079
|
+
return {
|
|
1080
|
+
path: worktree,
|
|
1081
|
+
main: gitDirectory === commonDirectory,
|
|
1082
|
+
branch,
|
|
1083
|
+
...changes,
|
|
1084
|
+
unpushedCommits,
|
|
1085
|
+
lock,
|
|
1086
|
+
processesInside,
|
|
1087
|
+
localOnlyIgnored: ignored
|
|
1088
|
+
};
|
|
1089
|
+
};
|
|
1090
|
+
const prepareWorktreeRemoval = async ({
|
|
1091
|
+
deleteBranch,
|
|
1092
|
+
...params
|
|
1093
|
+
}) => {
|
|
1094
|
+
const status = await readWorktreeRemovalStatus(params);
|
|
1095
|
+
const refusal = worktreeRemovalRefusal(status);
|
|
1096
|
+
if (refusal !== null) throw new Error(refusal);
|
|
1097
|
+
return {
|
|
1098
|
+
path: status.path,
|
|
1099
|
+
commonDirectory: await readGitDirectory(
|
|
1100
|
+
params.worktree,
|
|
1101
|
+
"--git-common-dir"
|
|
1102
|
+
),
|
|
1103
|
+
branch: deleteBranch ? status.branch : null
|
|
1104
|
+
};
|
|
1105
|
+
};
|
|
1106
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
1107
|
+
const completeWorktreeRemoval = async ({
|
|
1108
|
+
path,
|
|
1109
|
+
commonDirectory,
|
|
1110
|
+
branch
|
|
1111
|
+
}) => {
|
|
1112
|
+
await git(commonDirectory, ["worktree", "remove", path], {
|
|
1113
|
+
uninterruptible: true
|
|
1114
|
+
});
|
|
1115
|
+
if (branch === null) return { path, branch, branchError: null };
|
|
1116
|
+
try {
|
|
1117
|
+
await git(commonDirectory, ["branch", "-d", branch]);
|
|
1118
|
+
return { path, branch, branchError: null };
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
return { path, branch, branchError: messageOf(error) };
|
|
1121
|
+
}
|
|
407
1122
|
};
|
|
408
1123
|
|
|
409
1124
|
const cEscapes = {
|
|
@@ -782,7 +1497,7 @@ const orderPackages = (names, dependsOn) => {
|
|
|
782
1497
|
const isDependedOnByRemaining = (name) => [...remaining].some(
|
|
783
1498
|
(other) => other !== name && dependencies.get(other).has(name)
|
|
784
1499
|
);
|
|
785
|
-
const ordered =
|
|
1500
|
+
const ordered = [];
|
|
786
1501
|
while (remaining.size > 0) {
|
|
787
1502
|
const sorted = [...remaining].toSorted(
|
|
788
1503
|
(left, right) => left.localeCompare(right)
|
|
@@ -791,6 +1506,7 @@ const orderPackages = (names, dependsOn) => {
|
|
|
791
1506
|
ordered.push(next);
|
|
792
1507
|
remaining.delete(next);
|
|
793
1508
|
}
|
|
1509
|
+
if (names.includes("")) ordered.push("");
|
|
794
1510
|
return ordered;
|
|
795
1511
|
};
|
|
796
1512
|
const orderForReading = ({
|
|
@@ -1788,5 +2504,5 @@ const readImpacts = async ({
|
|
|
1788
2504
|
};
|
|
1789
2505
|
};
|
|
1790
2506
|
|
|
1791
|
-
export { createBindingsCache, defaultDiffLimits, git, gitFailedCode, gitOrNull, groupDiffPaths, maxTypescriptFiles, noPackages, packagesAtTree, packagesOnDisk, parseNumstat, readBranchDiff, readFileAtSha, readImpacts, readPullRequestDiff, readReadingOrder, reasonLine };
|
|
2507
|
+
export { completeWorktreeRemoval, countProcessesInside, createBindingsCache, defaultDiffLimits, defaultRecentCommits, git, gitFailedCode, gitOrNull, groupDiffPaths, maxTypescriptFiles, noPackages, packagesAtTree, packagesOnDisk, parseNumstat, parseStatusV2, parseTrack, parseWorktreeList, prepareWorktreeRemoval, pullCheckout, pushCheckout, readBranchDiff, readBranches, readCheckoutBranchDiff, readCheckoutStatus, readCheckoutSync, readCommitDiff, readDefaultBranch, readFileAtSha, readHeadBranch, readHeadSha, readImpacts, readPullRequestDiff, readReadingOrder, readRecentCommits, readWorktreeRemovalStatus, readWorktrees, reasonLine, resolveDiffBase };
|
|
1792
2508
|
//# sourceMappingURL=git-node.mjs.map
|