spidersan 0.20.1 → 0.20.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/CHANGELOG.md +8 -0
- package/dist/bin/command-registry.d.ts +25 -0
- package/dist/bin/command-registry.d.ts.map +1 -0
- package/dist/bin/command-registry.js +29 -0
- package/dist/bin/command-registry.js.map +1 -0
- package/dist/bin/spidersan.d.ts +16 -0
- package/dist/bin/spidersan.d.ts.map +1 -0
- package/dist/bin/spidersan.js +98 -0
- package/dist/bin/spidersan.js.map +1 -0
- package/dist/commands/abandon.d.ts +8 -0
- package/dist/commands/abandon.d.ts.map +1 -0
- package/dist/commands/abandon.js +43 -0
- package/dist/commands/abandon.js.map +1 -0
- package/dist/commands/cleanup.d.ts +8 -0
- package/dist/commands/cleanup.d.ts.map +1 -0
- package/dist/commands/cleanup.js +50 -0
- package/dist/commands/cleanup.js.map +1 -0
- package/dist/commands/config.d.ts +15 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +309 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/conflicts.d.ts +34 -0
- package/dist/commands/conflicts.d.ts.map +1 -0
- package/dist/commands/conflicts.js +773 -0
- package/dist/commands/conflicts.js.map +1 -0
- package/dist/commands/context.d.ts +8 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +104 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/cross-conflicts.d.ts +17 -0
- package/dist/commands/cross-conflicts.d.ts.map +1 -0
- package/dist/commands/cross-conflicts.js +273 -0
- package/dist/commands/cross-conflicts.js.map +1 -0
- package/dist/commands/depends.d.ts +11 -0
- package/dist/commands/depends.d.ts.map +1 -0
- package/dist/commands/depends.js +63 -0
- package/dist/commands/depends.js.map +1 -0
- package/dist/commands/doctor.d.ts +30 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +558 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/git-watch.d.ts +24 -0
- package/dist/commands/git-watch.d.ts.map +1 -0
- package/dist/commands/git-watch.js +84 -0
- package/dist/commands/git-watch.js.map +1 -0
- package/dist/commands/github-sync.d.ts +42 -0
- package/dist/commands/github-sync.d.ts.map +1 -0
- package/dist/commands/github-sync.js +191 -0
- package/dist/commands/github-sync.js.map +1 -0
- package/dist/commands/index.d.ts +25 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +29 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +23 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +45 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/log.d.ts +7 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +110 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/merge-order.d.ts +9 -0
- package/dist/commands/merge-order.d.ts.map +1 -0
- package/dist/commands/merge-order.js +100 -0
- package/dist/commands/merge-order.js.map +1 -0
- package/dist/commands/merged.d.ts +8 -0
- package/dist/commands/merged.d.ts.map +1 -0
- package/dist/commands/merged.js +37 -0
- package/dist/commands/merged.js.map +1 -0
- package/dist/commands/pr-check.d.ts +24 -0
- package/dist/commands/pr-check.d.ts.map +1 -0
- package/dist/commands/pr-check.js +85 -0
- package/dist/commands/pr-check.js.map +1 -0
- package/dist/commands/ready-check.d.ts +11 -0
- package/dist/commands/ready-check.d.ts.map +1 -0
- package/dist/commands/ready-check.js +150 -0
- package/dist/commands/ready-check.js.map +1 -0
- package/dist/commands/rebase-helper.d.ts +3 -0
- package/dist/commands/rebase-helper.d.ts.map +1 -0
- package/dist/commands/rebase-helper.js +52 -0
- package/dist/commands/rebase-helper.js.map +1 -0
- package/dist/commands/register.d.ts +10 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +176 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/registry-sync.d.ts +9 -0
- package/dist/commands/registry-sync.d.ts.map +1 -0
- package/dist/commands/registry-sync.js +129 -0
- package/dist/commands/registry-sync.js.map +1 -0
- package/dist/commands/stale.d.ts +17 -0
- package/dist/commands/stale.d.ts.map +1 -0
- package/dist/commands/stale.js +136 -0
- package/dist/commands/stale.js.map +1 -0
- package/dist/commands/sync.d.ts +21 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +78 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/verify-trunk.d.ts +18 -0
- package/dist/commands/verify-trunk.d.ts.map +1 -0
- package/dist/commands/verify-trunk.js +77 -0
- package/dist/commands/verify-trunk.js.map +1 -0
- package/dist/commands/watch.d.ts +8 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +509 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/activity.d.ts +53 -0
- package/dist/lib/activity.d.ts.map +1 -0
- package/dist/lib/activity.js +271 -0
- package/dist/lib/activity.js.map +1 -0
- package/dist/lib/agent-errors.d.ts +41 -0
- package/dist/lib/agent-errors.d.ts.map +1 -0
- package/dist/lib/agent-errors.js +145 -0
- package/dist/lib/agent-errors.js.map +1 -0
- package/dist/lib/ai/context-builder.d.ts +16 -0
- package/dist/lib/ai/context-builder.d.ts.map +1 -0
- package/dist/lib/ai/context-builder.js +229 -0
- package/dist/lib/ai/context-builder.js.map +1 -0
- package/dist/lib/ai/event-handler.d.ts +21 -0
- package/dist/lib/ai/event-handler.d.ts.map +1 -0
- package/dist/lib/ai/event-handler.js +111 -0
- package/dist/lib/ai/event-handler.js.map +1 -0
- package/dist/lib/ai/index.d.ts +13 -0
- package/dist/lib/ai/index.d.ts.map +1 -0
- package/dist/lib/ai/index.js +11 -0
- package/dist/lib/ai/index.js.map +1 -0
- package/dist/lib/ai/llm-client.d.ts +37 -0
- package/dist/lib/ai/llm-client.d.ts.map +1 -0
- package/dist/lib/ai/llm-client.js +225 -0
- package/dist/lib/ai/llm-client.js.map +1 -0
- package/dist/lib/ai/reasoner.d.ts +11 -0
- package/dist/lib/ai/reasoner.d.ts.map +1 -0
- package/dist/lib/ai/reasoner.js +227 -0
- package/dist/lib/ai/reasoner.js.map +1 -0
- package/dist/lib/ai/setup.d.ts +40 -0
- package/dist/lib/ai/setup.d.ts.map +1 -0
- package/dist/lib/ai/setup.js +154 -0
- package/dist/lib/ai/setup.js.map +1 -0
- package/dist/lib/ai/types.d.ts +135 -0
- package/dist/lib/ai/types.d.ts.map +1 -0
- package/dist/lib/ai/types.js +42 -0
- package/dist/lib/ai/types.js.map +1 -0
- package/dist/lib/ast.d.ts +44 -0
- package/dist/lib/ast.d.ts.map +1 -0
- package/dist/lib/ast.js +160 -0
- package/dist/lib/ast.js.map +1 -0
- package/dist/lib/carries.d.ts +22 -0
- package/dist/lib/carries.d.ts.map +1 -0
- package/dist/lib/carries.js +56 -0
- package/dist/lib/carries.js.map +1 -0
- package/dist/lib/colony-subscriber.d.ts +56 -0
- package/dist/lib/colony-subscriber.d.ts.map +1 -0
- package/dist/lib/colony-subscriber.js +265 -0
- package/dist/lib/colony-subscriber.js.map +1 -0
- package/dist/lib/config.d.ts +62 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +235 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/conflict-analyzer.d.ts +33 -0
- package/dist/lib/conflict-analyzer.d.ts.map +1 -0
- package/dist/lib/conflict-analyzer.js +114 -0
- package/dist/lib/conflict-analyzer.js.map +1 -0
- package/dist/lib/conflict-renderer.d.ts +7 -0
- package/dist/lib/conflict-renderer.d.ts.map +1 -0
- package/dist/lib/conflict-renderer.js +145 -0
- package/dist/lib/conflict-renderer.js.map +1 -0
- package/dist/lib/conflict-tier.d.ts +20 -0
- package/dist/lib/conflict-tier.d.ts.map +1 -0
- package/dist/lib/conflict-tier.js +49 -0
- package/dist/lib/conflict-tier.js.map +1 -0
- package/dist/lib/crypto.d.ts +71 -0
- package/dist/lib/crypto.d.ts.map +1 -0
- package/dist/lib/crypto.js +176 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/ecosystem-env.d.ts +33 -0
- package/dist/lib/ecosystem-env.d.ts.map +1 -0
- package/dist/lib/ecosystem-env.js +62 -0
- package/dist/lib/ecosystem-env.js.map +1 -0
- package/dist/lib/errors.d.ts +35 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +71 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/git-events-subscriber.d.ts +86 -0
- package/dist/lib/git-events-subscriber.d.ts.map +1 -0
- package/dist/lib/git-events-subscriber.js +870 -0
- package/dist/lib/git-events-subscriber.js.map +1 -0
- package/dist/lib/git-merge-analyzer.d.ts +38 -0
- package/dist/lib/git-merge-analyzer.d.ts.map +1 -0
- package/dist/lib/git-merge-analyzer.js +23 -0
- package/dist/lib/git-merge-analyzer.js.map +1 -0
- package/dist/lib/git.d.ts +103 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +670 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/github.d.ts +151 -0
- package/dist/lib/github.d.ts.map +1 -0
- package/dist/lib/github.js +425 -0
- package/dist/lib/github.js.map +1 -0
- package/dist/lib/graph.d.ts +38 -0
- package/dist/lib/graph.d.ts.map +1 -0
- package/dist/lib/graph.js +165 -0
- package/dist/lib/graph.js.map +1 -0
- package/dist/lib/machine.d.ts +14 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/machine.js +38 -0
- package/dist/lib/machine.js.map +1 -0
- package/dist/lib/notify.d.ts +68 -0
- package/dist/lib/notify.d.ts.map +1 -0
- package/dist/lib/notify.js +111 -0
- package/dist/lib/notify.js.map +1 -0
- package/dist/lib/outline.d.ts +6 -0
- package/dist/lib/outline.d.ts.map +1 -0
- package/dist/lib/outline.js +60 -0
- package/dist/lib/outline.js.map +1 -0
- package/dist/lib/push-registry.d.ts +22 -0
- package/dist/lib/push-registry.d.ts.map +1 -0
- package/dist/lib/push-registry.js +21 -0
- package/dist/lib/push-registry.js.map +1 -0
- package/dist/lib/reconcile.d.ts +100 -0
- package/dist/lib/reconcile.d.ts.map +1 -0
- package/dist/lib/reconcile.js +121 -0
- package/dist/lib/reconcile.js.map +1 -0
- package/dist/lib/regex-utils.d.ts +6 -0
- package/dist/lib/regex-utils.d.ts.map +1 -0
- package/dist/lib/regex-utils.js +24 -0
- package/dist/lib/regex-utils.js.map +1 -0
- package/dist/lib/register-renderer.d.ts +10 -0
- package/dist/lib/register-renderer.d.ts.map +1 -0
- package/dist/lib/register-renderer.js +19 -0
- package/dist/lib/register-renderer.js.map +1 -0
- package/dist/lib/remote-drift.d.ts +61 -0
- package/dist/lib/remote-drift.d.ts.map +1 -0
- package/dist/lib/remote-drift.js +227 -0
- package/dist/lib/remote-drift.js.map +1 -0
- package/dist/lib/security.d.ts +72 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +180 -0
- package/dist/lib/security.js.map +1 -0
- package/dist/lib/semantic-analysis.d.ts +31 -0
- package/dist/lib/semantic-analysis.d.ts.map +1 -0
- package/dist/lib/semantic-analysis.js +120 -0
- package/dist/lib/semantic-analysis.js.map +1 -0
- package/dist/lib/supabase-credentials.d.ts +36 -0
- package/dist/lib/supabase-credentials.d.ts.map +1 -0
- package/dist/lib/supabase-credentials.js +43 -0
- package/dist/lib/supabase-credentials.js.map +1 -0
- package/dist/lib/trunk.d.ts +39 -0
- package/dist/lib/trunk.d.ts.map +1 -0
- package/dist/lib/trunk.js +116 -0
- package/dist/lib/trunk.js.map +1 -0
- package/dist/lib/update-check.d.ts +11 -0
- package/dist/lib/update-check.d.ts.map +1 -0
- package/dist/lib/update-check.js +113 -0
- package/dist/lib/update-check.js.map +1 -0
- package/dist/lib/watch-renderer.d.ts +13 -0
- package/dist/lib/watch-renderer.d.ts.map +1 -0
- package/dist/lib/watch-renderer.js +35 -0
- package/dist/lib/watch-renderer.js.map +1 -0
- package/dist/storage/adapter.d.ts +81 -0
- package/dist/storage/adapter.d.ts.map +1 -0
- package/dist/storage/adapter.js +8 -0
- package/dist/storage/adapter.js.map +1 -0
- package/dist/storage/branch-registry-store.d.ts +13 -0
- package/dist/storage/branch-registry-store.d.ts.map +1 -0
- package/dist/storage/branch-registry-store.js +2 -0
- package/dist/storage/branch-registry-store.js.map +1 -0
- package/dist/storage/factory.d.ts +6 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +14 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/index.d.ts +10 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +11 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/json-branch-registry-store.d.ts +19 -0
- package/dist/storage/json-branch-registry-store.d.ts.map +1 -0
- package/dist/storage/json-branch-registry-store.js +138 -0
- package/dist/storage/json-branch-registry-store.js.map +1 -0
- package/dist/storage/local.d.ts +20 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/local.js +40 -0
- package/dist/storage/local.js.map +1 -0
- package/dist/storage/memory-branch-registry-store.d.ts +16 -0
- package/dist/storage/memory-branch-registry-store.d.ts.map +1 -0
- package/dist/storage/memory-branch-registry-store.js +68 -0
- package/dist/storage/memory-branch-registry-store.js.map +1 -0
- package/dist/storage/supabase-registry-sync-client-impl.d.ts +40 -0
- package/dist/storage/supabase-registry-sync-client-impl.d.ts.map +1 -0
- package/dist/storage/supabase-registry-sync-client-impl.js +278 -0
- package/dist/storage/supabase-registry-sync-client-impl.js.map +1 -0
- package/dist/storage/supabase-registry-sync-client.d.ts +9 -0
- package/dist/storage/supabase-registry-sync-client.d.ts.map +1 -0
- package/dist/storage/supabase-registry-sync-client.js +2 -0
- package/dist/storage/supabase-registry-sync-client.js.map +1 -0
- package/dist/storage/supabase.d.ts +28 -0
- package/dist/storage/supabase.d.ts.map +1 -0
- package/dist/storage/supabase.js +56 -0
- package/dist/storage/supabase.js.map +1 -0
- package/dist/types/cloud.d.ts +102 -0
- package/dist/types/cloud.d.ts.map +1 -0
- package/dist/types/cloud.js +13 -0
- package/dist/types/cloud.js.map +1 -0
- package/package.json +1 -1
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { validateFilePath, validateBranchName } from './security.js';
|
|
4
|
+
export class GitError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(message, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = 'GitError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function gitEnv() {
|
|
13
|
+
return { ...process.env };
|
|
14
|
+
}
|
|
15
|
+
export function execGit(args, options) {
|
|
16
|
+
return execFileSync('git', args, {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
env: gitEnv(),
|
|
19
|
+
...options,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function getErrorText(error) {
|
|
23
|
+
if (!(error instanceof Error)) {
|
|
24
|
+
return String(error);
|
|
25
|
+
}
|
|
26
|
+
const gitError = error;
|
|
27
|
+
const parts = [gitError.message];
|
|
28
|
+
if (typeof gitError.stderr === 'string') {
|
|
29
|
+
parts.push(gitError.stderr);
|
|
30
|
+
}
|
|
31
|
+
else if (Buffer.isBuffer(gitError.stderr)) {
|
|
32
|
+
parts.push(gitError.stderr.toString('utf-8'));
|
|
33
|
+
}
|
|
34
|
+
if (typeof gitError.stdout === 'string') {
|
|
35
|
+
parts.push(gitError.stdout);
|
|
36
|
+
}
|
|
37
|
+
else if (Buffer.isBuffer(gitError.stdout)) {
|
|
38
|
+
parts.push(gitError.stdout.toString('utf-8'));
|
|
39
|
+
}
|
|
40
|
+
return parts.filter(Boolean).join('\n');
|
|
41
|
+
}
|
|
42
|
+
function isNotRepoError(error) {
|
|
43
|
+
return /not a git repository/i.test(getErrorText(error));
|
|
44
|
+
}
|
|
45
|
+
function isInvalidRefError(error) {
|
|
46
|
+
return /(ambiguous argument|bad revision|invalid object name|Needed a single revision|unknown revision|unknown option `verify'|not a valid object name)/i.test(getErrorText(error));
|
|
47
|
+
}
|
|
48
|
+
function isMissingPathAtRefError(error) {
|
|
49
|
+
return /(does not exist in|exists on disk, but not in)/i.test(getErrorText(error));
|
|
50
|
+
}
|
|
51
|
+
function toGitError(error, message, fallbackCode) {
|
|
52
|
+
if (error instanceof GitError) {
|
|
53
|
+
return error;
|
|
54
|
+
}
|
|
55
|
+
if (isNotRepoError(error)) {
|
|
56
|
+
return new GitError('Not in a git repository', 'NOT_A_REPO');
|
|
57
|
+
}
|
|
58
|
+
if (fallbackCode === 'INVALID_REF' || isInvalidRefError(error)) {
|
|
59
|
+
return new GitError(message, 'INVALID_REF');
|
|
60
|
+
}
|
|
61
|
+
return new GitError(message, 'EXEC_FAILED');
|
|
62
|
+
}
|
|
63
|
+
export function getCurrentBranch() {
|
|
64
|
+
try {
|
|
65
|
+
return execGit(['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw toGitError(error, 'Failed to determine current branch', 'EXEC_FAILED');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Repo name as used for the `repo_name` column in spider_registries.
|
|
73
|
+
*
|
|
74
|
+
* Must stay identical across push and read paths: `registry-sync --push` stamps
|
|
75
|
+
* rows with this value and `conflicts`/`cross-conflicts` filter on it, so any
|
|
76
|
+
* divergence makes a pull silently return zero rows (tb-ly0b).
|
|
77
|
+
*/
|
|
78
|
+
export function getRepoName() {
|
|
79
|
+
try {
|
|
80
|
+
return basename(execGit(['rev-parse', '--show-toplevel']).trim());
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return basename(process.cwd());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every repo_name a read should match: the clone's directory name (what registry-sync
|
|
88
|
+
* writes) plus the lowercased GitHub repo name from `origin` (what the spidersan-webhook
|
|
89
|
+
* edge function writes for pushed branches — it never sees the local directory). They
|
|
90
|
+
* differ for spidersan-oss cloned as `spidersan`, or `toak` cloned as `Toak`.
|
|
91
|
+
*/
|
|
92
|
+
export function registryRepoNames() {
|
|
93
|
+
const names = [getRepoName()];
|
|
94
|
+
try {
|
|
95
|
+
const url = execGit(['remote', 'get-url', 'origin']).trim();
|
|
96
|
+
const m = url.match(/[/:]([^/]+?)(?:\.git)?$/);
|
|
97
|
+
if (m && !names.includes(m[1].toLowerCase()))
|
|
98
|
+
names.push(m[1].toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
catch { /* no origin: directory name only */ }
|
|
101
|
+
return names;
|
|
102
|
+
}
|
|
103
|
+
export function getChangedFiles(options) {
|
|
104
|
+
const baseRef = options?.base ?? 'main';
|
|
105
|
+
const strategies = [`${baseRef}...HEAD`, 'HEAD~1', 'HEAD'];
|
|
106
|
+
for (const strategy of strategies) {
|
|
107
|
+
try {
|
|
108
|
+
const output = execGit(['-c', 'core.quotepath=off', 'diff', '--name-only', strategy], {
|
|
109
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
110
|
+
});
|
|
111
|
+
// A successful diff is authoritative — an empty result means no changes,
|
|
112
|
+
// not "try the next strategy" (HEAD~1 would report already-merged files).
|
|
113
|
+
return output.trim().split('\n').filter(Boolean);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (isNotRepoError(error)) {
|
|
117
|
+
throw new GitError('Not in a git repository', 'NOT_A_REPO');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
export function getFileAtRef(ref, filePath) {
|
|
124
|
+
let safeFilePath = filePath;
|
|
125
|
+
try {
|
|
126
|
+
safeFilePath = validateFilePath(filePath);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Historical registry entries may contain invalid file names. We still use argv-safe
|
|
130
|
+
// git execution here so callers can inspect those refs without shell interpolation.
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
execGit(['rev-parse', '--verify', `${ref}^{commit}`], {
|
|
134
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw toGitError(error, `Invalid git ref: ${ref}`, 'INVALID_REF');
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return execGit(['show', `${ref}:${safeFilePath}`]);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (isNotRepoError(error)) {
|
|
145
|
+
throw new GitError('Not in a git repository', 'NOT_A_REPO');
|
|
146
|
+
}
|
|
147
|
+
if (isMissingPathAtRefError(error)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
throw toGitError(error, `Failed to read ${safeFilePath} at ref ${ref}`, 'EXEC_FAILED');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function getRemoteHead(remote, branch) {
|
|
154
|
+
const safeBranch = validateBranchName(branch);
|
|
155
|
+
try {
|
|
156
|
+
// ls-remote patterns match ref suffixes ("main" also matches "feature/main"),
|
|
157
|
+
// so query the full refname and match the exact line.
|
|
158
|
+
const refName = `refs/heads/${safeBranch}`;
|
|
159
|
+
const output = execGit(['ls-remote', '--heads', remote, refName], {
|
|
160
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
161
|
+
}).trim();
|
|
162
|
+
const line = output.split('\n').find((l) => l.endsWith(`\t${refName}`));
|
|
163
|
+
if (!line) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const [sha] = line.split(/\s+/, 1);
|
|
167
|
+
return sha || null;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Read stdout off a thrown execFileSync error (utf-8). */
|
|
174
|
+
function getThrownStdout(error) {
|
|
175
|
+
const gitError = error;
|
|
176
|
+
if (typeof gitError?.stdout === 'string') {
|
|
177
|
+
return gitError.stdout;
|
|
178
|
+
}
|
|
179
|
+
if (Buffer.isBuffer(gitError?.stdout)) {
|
|
180
|
+
return gitError.stdout.toString('utf-8');
|
|
181
|
+
}
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
/** "unknown option" / usage failure → modern --write-tree unsupported, fall back to legacy. */
|
|
185
|
+
function isUnknownOptionError(error) {
|
|
186
|
+
return /(unknown option|usage: git merge-tree|is not a git command)/i.test(getErrorText(error));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Resolve a ref leniently to a commit SHA.
|
|
190
|
+
* Accepts a local branch / SHA, and falls back to `origin/<ref>`.
|
|
191
|
+
* Returns null if the ref cannot be resolved (caller turns this into `error`).
|
|
192
|
+
*/
|
|
193
|
+
function resolveRefToSha(ref) {
|
|
194
|
+
const candidates = ref.startsWith('origin/') ? [ref] : [ref, `origin/${ref}`];
|
|
195
|
+
for (const candidate of candidates) {
|
|
196
|
+
try {
|
|
197
|
+
return execGit(['rev-parse', '--verify', `${candidate}^{commit}`], {
|
|
198
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
199
|
+
}).trim();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// try next candidate
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
/** Probe once whether this git supports `git merge-tree --write-tree`. */
|
|
208
|
+
let writeTreeSupportedCache = null;
|
|
209
|
+
function supportsWriteTree() {
|
|
210
|
+
if (writeTreeSupportedCache !== null) {
|
|
211
|
+
return writeTreeSupportedCache;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
// Empty-tree against itself is a trivial, side-effect-free probe.
|
|
215
|
+
const empty = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
216
|
+
execGit(['merge-tree', '--write-tree', empty, empty], {
|
|
217
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
218
|
+
});
|
|
219
|
+
writeTreeSupportedCache = true;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
// Exit-1 here would still mean the option IS supported (just "conflict"/no-op);
|
|
223
|
+
// only a usage/unknown-option failure means it's unsupported.
|
|
224
|
+
writeTreeSupportedCache = !isUnknownOptionError(error);
|
|
225
|
+
}
|
|
226
|
+
return writeTreeSupportedCache;
|
|
227
|
+
}
|
|
228
|
+
const KIND_FROM_LABEL = {
|
|
229
|
+
content: 'content',
|
|
230
|
+
'add/add': 'add/add',
|
|
231
|
+
'modify/delete': 'delete/modify',
|
|
232
|
+
'delete/modify': 'delete/modify',
|
|
233
|
+
distinct: 'rename',
|
|
234
|
+
rename: 'rename',
|
|
235
|
+
'rename/rename': 'rename',
|
|
236
|
+
'rename/add': 'rename',
|
|
237
|
+
'rename/delete': 'rename',
|
|
238
|
+
};
|
|
239
|
+
function mapKind(label) {
|
|
240
|
+
const normalized = label.trim().toLowerCase();
|
|
241
|
+
return KIND_FROM_LABEL[normalized] ?? 'content';
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Parse modern `git merge-tree --write-tree <base> <branch>` output.
|
|
245
|
+
* Format: first line = written tree OID; then a blank line; then an info block
|
|
246
|
+
* with `CONFLICT (<kind>): ... in <path>` lines (and `Auto-merging <path>`).
|
|
247
|
+
* The CONFLICT lines carry BOTH the path and the kind, so we parse those.
|
|
248
|
+
*/
|
|
249
|
+
function parseWriteTreeConflicts(output) {
|
|
250
|
+
const conflicts = new Map();
|
|
251
|
+
const lines = output.split('\n');
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
// CONFLICT (content): Merge conflict in <path>
|
|
254
|
+
// CONFLICT (add/add): Merge conflict in <path>
|
|
255
|
+
const merge = line.match(/^CONFLICT \(([^)]+)\): Merge conflict in (.+)$/);
|
|
256
|
+
if (merge) {
|
|
257
|
+
const [, label, path] = merge;
|
|
258
|
+
conflicts.set(path, mapKind(label));
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// CONFLICT (modify/delete): <path> deleted in X and modified in Y. ...
|
|
262
|
+
// CONFLICT (rename/delete): <path> renamed ... but deleted in ...
|
|
263
|
+
// Terminate on the multi-word PHRASE that follows the path (" deleted in ",
|
|
264
|
+
// " renamed to ", …), NOT a bare keyword — a path like "my added notes.txt"
|
|
265
|
+
// contains the word "added" but not the phrase " added in ", so phrases avoid
|
|
266
|
+
// truncating the captured path.
|
|
267
|
+
const other = line.match(/^CONFLICT \(([^)]+)\): (.+?)(?: deleted in | renamed to | added in | modified in | left in tree)/);
|
|
268
|
+
if (other) {
|
|
269
|
+
const [, label, path] = other;
|
|
270
|
+
conflicts.set(path, mapKind(label));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return Array.from(conflicts, ([file, kind]) => ({ file, kind }));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Parse legacy `git merge-tree <mergeBase> <base> <branch>` output.
|
|
277
|
+
* Output is a per-file record stream. Records begin with an unindented header
|
|
278
|
+
* (`changed in both`, `added in both`, `removed in remote`, `merged`, …) followed
|
|
279
|
+
* by indented ` base/our/their <mode> <oid> <path>` lines and a diff hunk.
|
|
280
|
+
*
|
|
281
|
+
* A `changed in both`/`added in both` header signals BOTH sides touched the path,
|
|
282
|
+
* but is only an actual CONFLICT when its hunk contains `<<<<<<<` markers
|
|
283
|
+
* (e.g. same-file/different-region merges cleanly yet still shows `changed in both`).
|
|
284
|
+
* We track the header record for the path (handling spaces) and require a marker.
|
|
285
|
+
*
|
|
286
|
+
* KNOWN LIMITATION (legacy/git<2.38 only — modern --write-tree path is preferred and
|
|
287
|
+
* always taken on git>=2.38): marker-less conflict classes (modify/delete, rename/*)
|
|
288
|
+
* emit no `<<<<<<<` and are under-reported here. Tracked in STATE.json known-issues;
|
|
289
|
+
* not fixed because legacy git is effectively absent in this fleet (CI/dev on 2.50).
|
|
290
|
+
*/
|
|
291
|
+
function parseLegacyConflicts(output) {
|
|
292
|
+
const conflicts = new Map();
|
|
293
|
+
const lines = output.split('\n');
|
|
294
|
+
let currentHeader = null;
|
|
295
|
+
let currentPath = null;
|
|
296
|
+
let currentKind = 'content';
|
|
297
|
+
let sawMarker = false;
|
|
298
|
+
const flush = () => {
|
|
299
|
+
if (currentHeader && currentPath && sawMarker) {
|
|
300
|
+
conflicts.set(currentPath, currentKind);
|
|
301
|
+
}
|
|
302
|
+
currentHeader = null;
|
|
303
|
+
currentPath = null;
|
|
304
|
+
currentKind = 'content';
|
|
305
|
+
sawMarker = false;
|
|
306
|
+
};
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
// Unindented record header (starts a new record).
|
|
309
|
+
const header = line.match(/^(changed in both|added in both|removed in (?:both|remote|local)|added in (?:remote|local)|merged|renamed)\b/);
|
|
310
|
+
if (header) {
|
|
311
|
+
flush();
|
|
312
|
+
currentHeader = header[1];
|
|
313
|
+
if (currentHeader === 'added in both') {
|
|
314
|
+
currentKind = 'add/add';
|
|
315
|
+
}
|
|
316
|
+
else if (currentHeader.startsWith('removed in')) {
|
|
317
|
+
currentKind = 'delete/modify';
|
|
318
|
+
}
|
|
319
|
+
else if (currentHeader === 'renamed') {
|
|
320
|
+
currentKind = 'rename';
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
currentKind = 'content';
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// Indented file record line: ` our 100644 <oid> <path>` — extract the path.
|
|
328
|
+
// Path may contain spaces; take everything after the oid token.
|
|
329
|
+
const fileLine = line.match(/^\s+(?:base|our|their)\s+\d{6}\s+[0-9a-f]{7,64}\s+(.+)$/);
|
|
330
|
+
if (fileLine && currentHeader && !currentPath) {
|
|
331
|
+
currentPath = fileLine[1];
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (line.includes('<<<<<<<')) {
|
|
335
|
+
sawMarker = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
flush();
|
|
339
|
+
return Array.from(conflicts, ([file, kind]) => ({ file, kind }));
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Real conflicts merging `branch` into `base`. Prefers `--write-tree` (git ≥2.38),
|
|
343
|
+
* falls back to the legacy 3-arg form. NEVER throws for "they conflict" — only sets
|
|
344
|
+
* `error` for environmental failures (bad ref / not a repo / unsupported tooling).
|
|
345
|
+
*/
|
|
346
|
+
export function getMergeConflicts(base, branch) {
|
|
347
|
+
const baseResult = {
|
|
348
|
+
base,
|
|
349
|
+
branch,
|
|
350
|
+
mergeBase: null,
|
|
351
|
+
conflicts: [],
|
|
352
|
+
clean: false,
|
|
353
|
+
method: 'write-tree',
|
|
354
|
+
error: undefined,
|
|
355
|
+
};
|
|
356
|
+
// Resolve both refs leniently first; a missing ref is an env error, not a throw.
|
|
357
|
+
let baseSha;
|
|
358
|
+
let branchSha;
|
|
359
|
+
try {
|
|
360
|
+
baseSha = resolveRefToSha(base);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (isNotRepoError(error)) {
|
|
364
|
+
return { ...baseResult, error: 'Not in a git repository' };
|
|
365
|
+
}
|
|
366
|
+
baseSha = null;
|
|
367
|
+
}
|
|
368
|
+
if (baseSha === null) {
|
|
369
|
+
return { ...baseResult, error: `ref not found: ${base}` };
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
branchSha = resolveRefToSha(branch);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (isNotRepoError(error)) {
|
|
376
|
+
return { ...baseResult, error: 'Not in a git repository' };
|
|
377
|
+
}
|
|
378
|
+
branchSha = null;
|
|
379
|
+
}
|
|
380
|
+
if (branchSha === null) {
|
|
381
|
+
return { ...baseResult, error: `ref not found: ${branch}` };
|
|
382
|
+
}
|
|
383
|
+
// Compute merge-base (best-effort; not fatal if unrelated histories).
|
|
384
|
+
let mergeBase;
|
|
385
|
+
try {
|
|
386
|
+
mergeBase = execGit(['merge-base', baseSha, branchSha], {
|
|
387
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
388
|
+
}).trim() || null;
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
mergeBase = null;
|
|
392
|
+
}
|
|
393
|
+
// ── Modern path: git merge-tree --write-tree <base> <branch> ──
|
|
394
|
+
if (supportsWriteTree()) {
|
|
395
|
+
try {
|
|
396
|
+
const output = execGit(['merge-tree', '--write-tree', baseSha, branchSha], {
|
|
397
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
398
|
+
});
|
|
399
|
+
// Exit 0 → clean merge (first line is the tree OID, no conflict block).
|
|
400
|
+
return {
|
|
401
|
+
...baseResult,
|
|
402
|
+
mergeBase,
|
|
403
|
+
conflicts: [],
|
|
404
|
+
clean: true,
|
|
405
|
+
method: 'write-tree',
|
|
406
|
+
rawOutput: output,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (isNotRepoError(error)) {
|
|
411
|
+
return { ...baseResult, mergeBase, error: 'Not in a git repository' };
|
|
412
|
+
}
|
|
413
|
+
if (isUnknownOptionError(error)) {
|
|
414
|
+
// Tooling regression mid-run — fall through to legacy.
|
|
415
|
+
writeTreeSupportedCache = false;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
// Exit non-zero WITH parseable conflict output = a valid conflict RESULT.
|
|
419
|
+
const stdout = getThrownStdout(error);
|
|
420
|
+
if (stdout) {
|
|
421
|
+
const conflicts = parseWriteTreeConflicts(stdout);
|
|
422
|
+
if (conflicts.length > 0) {
|
|
423
|
+
return {
|
|
424
|
+
...baseResult,
|
|
425
|
+
mergeBase,
|
|
426
|
+
conflicts,
|
|
427
|
+
clean: false,
|
|
428
|
+
method: 'write-tree',
|
|
429
|
+
rawOutput: stdout,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
// Non-zero but no parseable conflict (e.g. unrelated histories) → env error.
|
|
433
|
+
return {
|
|
434
|
+
...baseResult,
|
|
435
|
+
mergeBase,
|
|
436
|
+
method: 'write-tree',
|
|
437
|
+
rawOutput: stdout,
|
|
438
|
+
error: getErrorText(error).split('\n')[0] || 'git merge-tree failed',
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
...baseResult,
|
|
443
|
+
mergeBase,
|
|
444
|
+
method: 'write-tree',
|
|
445
|
+
error: getErrorText(error).split('\n')[0] || 'git merge-tree failed',
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// ── Legacy path: git merge-tree <mergeBase> <base> <branch> ──
|
|
451
|
+
if (mergeBase === null) {
|
|
452
|
+
// Legacy form requires a merge-base; without one we cannot analyze.
|
|
453
|
+
return {
|
|
454
|
+
...baseResult,
|
|
455
|
+
mergeBase,
|
|
456
|
+
method: 'legacy',
|
|
457
|
+
error: `no merge-base between ${base} and ${branch}`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const output = execGit(['merge-tree', mergeBase, baseSha, branchSha], {
|
|
462
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
463
|
+
});
|
|
464
|
+
const conflicts = parseLegacyConflicts(output);
|
|
465
|
+
return {
|
|
466
|
+
...baseResult,
|
|
467
|
+
mergeBase,
|
|
468
|
+
conflicts,
|
|
469
|
+
clean: conflicts.length === 0,
|
|
470
|
+
method: 'legacy',
|
|
471
|
+
rawOutput: output,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
if (isNotRepoError(error)) {
|
|
476
|
+
return { ...baseResult, mergeBase, method: 'legacy', error: 'Not in a git repository' };
|
|
477
|
+
}
|
|
478
|
+
// Legacy form may still produce output on a thrown error in some gits.
|
|
479
|
+
const stdout = getThrownStdout(error);
|
|
480
|
+
if (stdout) {
|
|
481
|
+
const conflicts = parseLegacyConflicts(stdout);
|
|
482
|
+
return {
|
|
483
|
+
...baseResult,
|
|
484
|
+
mergeBase,
|
|
485
|
+
conflicts,
|
|
486
|
+
clean: conflicts.length === 0,
|
|
487
|
+
method: 'legacy',
|
|
488
|
+
rawOutput: stdout,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
...baseResult,
|
|
493
|
+
mergeBase,
|
|
494
|
+
method: 'legacy',
|
|
495
|
+
error: getErrorText(error).split('\n')[0] || 'git merge-tree failed',
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
export function getAheadBehind(remote, branch) {
|
|
500
|
+
let upstreamRef;
|
|
501
|
+
if (remote && branch) {
|
|
502
|
+
upstreamRef = `${remote}/${validateBranchName(branch)}`;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
try {
|
|
506
|
+
upstreamRef = execGit(['rev-parse', '--abbrev-ref', '@{u}'], {
|
|
507
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
508
|
+
}).trim();
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (isNotRepoError(error)) {
|
|
512
|
+
throw new GitError('Not in a git repository', 'NOT_A_REPO');
|
|
513
|
+
}
|
|
514
|
+
return { ahead: 0, behind: 0 };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const output = execGit(['rev-list', '--left-right', '--count', `HEAD...${upstreamRef}`], {
|
|
519
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
520
|
+
}).trim();
|
|
521
|
+
const [aheadRaw, behindRaw] = output.split(/\s+/);
|
|
522
|
+
const ahead = Number.parseInt(aheadRaw ?? '', 10);
|
|
523
|
+
const behind = Number.parseInt(behindRaw ?? '', 10);
|
|
524
|
+
if (!Number.isFinite(ahead) || !Number.isFinite(behind)) {
|
|
525
|
+
throw new GitError(`Failed to parse ahead/behind counts for ${upstreamRef}`, 'EXEC_FAILED');
|
|
526
|
+
}
|
|
527
|
+
return { ahead, behind };
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
if (error instanceof GitError) {
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
if (isNotRepoError(error)) {
|
|
534
|
+
throw new GitError('Not in a git repository', 'NOT_A_REPO');
|
|
535
|
+
}
|
|
536
|
+
if (!remote && !branch) {
|
|
537
|
+
return { ahead: 0, behind: 0 };
|
|
538
|
+
}
|
|
539
|
+
throw toGitError(error, `Failed to determine ahead/behind for ${upstreamRef}`, 'EXEC_FAILED');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Resolve a usable git ref for a branch name, preferring the remote-tracking
|
|
544
|
+
* ref (`refs/remotes/<remote>/<name>`) over the local ref (`refs/heads/<name>`).
|
|
545
|
+
*
|
|
546
|
+
* Returns the resolved ref string, or `null` if neither a remote nor a local
|
|
547
|
+
* ref exists — i.e. the branch is gone from this checkout AND the remote
|
|
548
|
+
* (an orphan). Preferring the remote ref means a stale local branch doesn't
|
|
549
|
+
* mask a delete, and vice-versa.
|
|
550
|
+
*
|
|
551
|
+
* Invalid branch names resolve to `null` rather than throwing, so a poisoned
|
|
552
|
+
* registry entry degrades to "orphan" instead of crashing reconcile-on-read.
|
|
553
|
+
*/
|
|
554
|
+
export function resolveBranchRef(name, remote = 'origin') {
|
|
555
|
+
let branch;
|
|
556
|
+
try {
|
|
557
|
+
branch = validateBranchName(name);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
for (const ref of [`refs/remotes/${remote}/${branch}`, `refs/heads/${branch}`]) {
|
|
563
|
+
try {
|
|
564
|
+
execGit(['rev-parse', '--verify', '--quiet', ref], {
|
|
565
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
566
|
+
});
|
|
567
|
+
return ref;
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Ref doesn't exist — try the next candidate.
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Branch names that exist on `remote` right now (`git ls-remote --heads`), or
|
|
577
|
+
* `null` when the remote can't be asked (offline, no remote, timeout). A missing
|
|
578
|
+
* local/tracking ref only proves "not fetched here"; this is the call that
|
|
579
|
+
* proves "deleted". One network round-trip for the whole registry.
|
|
580
|
+
*/
|
|
581
|
+
export function listRemoteHeads(remote = 'origin') {
|
|
582
|
+
try {
|
|
583
|
+
const out = execGit(['ls-remote', '--heads', remote], { stdio: ['pipe', 'pipe', 'ignore'], timeout: 15_000 });
|
|
584
|
+
return new Set(out.split('\n').map(l => l.split('\trefs/heads/')[1]).filter((b) => !!b));
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/** Resolve only the LOCAL ref (`refs/heads/<name>`), or `null` if none. */
|
|
591
|
+
export function resolveLocalBranchRef(name) {
|
|
592
|
+
let branch;
|
|
593
|
+
try {
|
|
594
|
+
branch = validateBranchName(name);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const ref = `refs/heads/${branch}`;
|
|
600
|
+
try {
|
|
601
|
+
execGit(['rev-parse', '--verify', '--quiet', ref], {
|
|
602
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
603
|
+
});
|
|
604
|
+
return ref;
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* True if `ref` is an ancestor of `ancestorOf` — i.e. `ref` is fully merged
|
|
612
|
+
* into it. Backed by `git merge-base --is-ancestor` (exit 0 = ancestor,
|
|
613
|
+
* exit 1 = not). Any failure (not-an-ancestor, bad ref, not a repo) returns
|
|
614
|
+
* `false`, so the caller treats "can't prove merged" as "still live" — the
|
|
615
|
+
* safe default for conflict detection.
|
|
616
|
+
*
|
|
617
|
+
* NOTE: detects merge-commit / fast-forward merges, where the branch tip is
|
|
618
|
+
* an ancestor of trunk. Squash merges rewrite history, so the tip is NOT an
|
|
619
|
+
* ancestor — use {@link isSquashMergedInto} for those.
|
|
620
|
+
*/
|
|
621
|
+
export function isMergedInto(ref, ancestorOf) {
|
|
622
|
+
try {
|
|
623
|
+
execGit(['merge-base', '--is-ancestor', ref, ancestorOf], {
|
|
624
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
625
|
+
});
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* True if `ref`'s content landed in `ancestorOf` via a SQUASH merge (tb-tr1z).
|
|
634
|
+
*
|
|
635
|
+
* Squash merges rewrite history, so `merge-base --is-ancestor` can never see
|
|
636
|
+
* them. Instead we ask whether trunk contains a patch-id-equivalent change:
|
|
637
|
+
* commit the branch's tree onto its merge-base as one synthetic commit (the
|
|
638
|
+
* exact diff a squash merge lands), then `git cherry` it against trunk — a
|
|
639
|
+
* leading `-` means an equivalent patch is already there.
|
|
640
|
+
*
|
|
641
|
+
* The synthetic commit is a dangling object git eventually gc's; author and
|
|
642
|
+
* dates are pinned so repeat probes reuse the identical object instead of
|
|
643
|
+
* accreting new ones. Any failure returns `false` — "can't prove merged"
|
|
644
|
+
* stays "live", same as {@link isMergedInto}.
|
|
645
|
+
*/
|
|
646
|
+
export function isSquashMergedInto(ref, ancestorOf) {
|
|
647
|
+
const quiet = { stdio: ['pipe', 'pipe', 'ignore'] };
|
|
648
|
+
try {
|
|
649
|
+
const base = execGit(['merge-base', ancestorOf, ref], quiet).trim();
|
|
650
|
+
const tree = execGit(['rev-parse', `${ref}^{tree}`], quiet).trim();
|
|
651
|
+
const probe = execGit(['commit-tree', tree, '-p', base, '-m', 'spidersan squash probe'], {
|
|
652
|
+
...quiet,
|
|
653
|
+
env: {
|
|
654
|
+
...gitEnv(),
|
|
655
|
+
GIT_AUTHOR_NAME: 'spidersan',
|
|
656
|
+
GIT_AUTHOR_EMAIL: 'probe@spidersan',
|
|
657
|
+
GIT_COMMITTER_NAME: 'spidersan',
|
|
658
|
+
GIT_COMMITTER_EMAIL: 'probe@spidersan',
|
|
659
|
+
GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
|
|
660
|
+
GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
|
|
661
|
+
},
|
|
662
|
+
}).trim();
|
|
663
|
+
const cherry = execGit(['cherry', ancestorOf, probe], quiet).trim();
|
|
664
|
+
return cherry !== '' && cherry.split('\n').every(line => line.startsWith('-'));
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
//# sourceMappingURL=git.js.map
|