Files
affaan-m--everything-claude…/scripts/lib/worktree-lifecycle/git.js
T
wehub-resource-sync d48cda4081
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:55:55 +08:00

152 lines
5.1 KiB
JavaScript

'use strict';
const { spawnSync } = require('child_process');
// Thin, injectable git command layer. All git interaction in the lifecycle
// service goes through a runner so tests can feed canned output without a real
// repository. The default runner shells out with spawnSync.
// Git env vars that, if inherited (e.g. when this service runs inside a git
// hook), would override the -C/cwd target and point git at the host repo.
const INHERITED_GIT_ENV = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR', 'GIT_PREFIX'];
function hermeticGitEnv() {
const env = { ...process.env };
for (const key of INHERITED_GIT_ENV) {
delete env[key];
}
return env;
}
function defaultRunImpl(args, options = {}) {
const result = spawnSync('git', args, {
cwd: options.cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
maxBuffer: 32 * 1024 * 1024,
env: hermeticGitEnv()
});
return {
status: typeof result.status === 'number' ? result.status : 1,
stdout: result.stdout || '',
stderr: result.stderr || ''
};
}
function createGitRunner(repoRoot, runImpl = defaultRunImpl) {
function run(args) {
return runImpl(args, { cwd: repoRoot });
}
function succeeds(args) {
return run(args).status === 0;
}
function stdoutOf(args) {
const result = run(args);
return result.status === 0 ? (result.stdout || '').trim() : '';
}
return {
repoRoot,
run,
succeeds,
isGitRepo() {
return succeeds(['rev-parse', '--is-inside-work-tree']);
},
branchExists(branch) {
return succeeds(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]);
},
// Porcelain worktree list -> [{ path, branch, head, detached, bare }]
listWorktrees() {
const result = run(['worktree', 'list', '--porcelain']);
if (result.status !== 0) {
return [];
}
const worktrees = [];
let current = null;
for (const rawLine of (result.stdout || '').split('\n')) {
const line = rawLine.trim();
if (line.startsWith('worktree ')) {
if (current) {
worktrees.push(current);
}
current = { path: line.slice('worktree '.length).trim(), branch: null, head: null, detached: false, bare: false };
} else if (current && line.startsWith('branch ')) {
current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '').trim();
} else if (current && line.startsWith('HEAD ')) {
current.head = line.slice('HEAD '.length).trim();
} else if (current && line === 'detached') {
current.detached = true;
} else if (current && line === 'bare') {
current.bare = true;
}
}
if (current) {
worktrees.push(current);
}
return worktrees;
},
// Uncommitted changes in a worktree (status --porcelain over its own path).
isDirty(worktreePath) {
const result = runImpl(['status', '--porcelain'], { cwd: worktreePath });
return result.status === 0 ? (result.stdout || '').trim().length > 0 : false;
},
// Commits a branch is ahead of / behind its base. Returns null if either
// ref is missing (e.g. branch already merged-and-deleted).
aheadBehind(branch, baseBranch) {
const out = stdoutOf(['rev-list', '--left-right', '--count', `${baseBranch}...${branch}`]);
const match = out.match(/^(\d+)\s+(\d+)$/);
if (!match) {
return null;
}
return { behind: Number(match[1]), ahead: Number(match[2]) };
},
// Last commit time (epoch ms) on a branch.
lastCommitMs(branch) {
const out = stdoutOf(['log', '-1', '--format=%ct', branch]);
const seconds = Number(out);
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : null;
},
// Predict merge conflicts WITHOUT touching the working tree using
// `git merge-tree`. Prefers the modern --write-tree form (Git 2.38+),
// which exits non-zero and lists conflicted paths on conflict.
predictMergeConflicts(branch, baseBranch) {
const modern = run(['merge-tree', '--write-tree', '--name-only', baseBranch, branch]);
if (modern.status === 0) {
return { conflicted: false, files: [], method: 'merge-tree' };
}
// Non-zero with parseable output => real conflict (modern form).
if (modern.stdout && /\S/.test(modern.stdout)) {
const lines = modern.stdout.split('\n').map(l => l.trim()).filter(Boolean);
// First line is the tree oid; subsequent lines are conflicted paths.
const files = lines.slice(1).filter(line => !/^[0-9a-f]{40}$/i.test(line));
return { conflicted: true, files, method: 'merge-tree' };
}
// Fallback to the legacy form (older Git): conflict markers in output.
const legacy = run(['merge-tree', baseBranch, baseBranch, branch]);
const hasMarkers = /^<{7}|^={7}|^>{7}/m.test(legacy.stdout || '')
|| /\+<{7}|changed in both/m.test(legacy.stdout || '');
return { conflicted: hasMarkers, files: [], method: 'merge-tree-legacy' };
}
};
}
module.exports = {
createGitRunner,
defaultRunImpl
};