Merge origin/main into dev (second round)
Resolve 21 conflicts from latest main (provider profiles, Windows fixes, 0.1.55-rc.1). Fix all type errors and syntax issues from merge artifacts.
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
import { exec, execFile } from "child_process";
|
||||
import { spawnProcess } from "./spawn.js";
|
||||
import { promisify } from "util";
|
||||
import { resolve, dirname, basename } from "path";
|
||||
import { existsSync, realpathSync } from "fs";
|
||||
import { open as openFile, stat as statFile } from "fs/promises";
|
||||
@@ -9,17 +6,15 @@ import { z } from "zod";
|
||||
import type { ParsedDiffFile } from "../server/utils/diff-highlighter.js";
|
||||
import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
|
||||
import { findExecutable } from "./executable.js";
|
||||
import { runGitCommand } from "./run-git-command.js";
|
||||
import { execCommand } from "./spawn.js";
|
||||
import { isPaseoOwnedWorktreeCwd } from "./worktree.js";
|
||||
import { requirePaseoWorktreeBaseRefName } from "./worktree-metadata.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
const READ_ONLY_GIT_ENV: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
GIT_OPTIONAL_LOCKS: "0",
|
||||
};
|
||||
|
||||
const SMALL_OUTPUT_MAX_BUFFER = 20 * 1024 * 1024; // 20MB
|
||||
const DEFAULT_PULL_REQUEST_STATUS_CACHE_TTL_MS = 30_000;
|
||||
const PULL_REQUEST_STATUS_CACHE_MAX = 1_000;
|
||||
const DEFAULT_SHORTSTAT_CACHE_TTL_MS = 15_000;
|
||||
@@ -97,91 +92,6 @@ export function __setCheckoutShortstatCacheTtlForTests(ttlMs: number): void {
|
||||
shortstatInFlight.clear();
|
||||
}
|
||||
|
||||
async function execGit(
|
||||
command: string,
|
||||
options: { cwd: string; env?: NodeJS.ProcessEnv },
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
return execAsync(command, { ...options, maxBuffer: SMALL_OUTPUT_MAX_BUFFER });
|
||||
}
|
||||
|
||||
type LimitedTextResult = {
|
||||
text: string;
|
||||
truncated: boolean;
|
||||
exitCode: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
};
|
||||
|
||||
async function spawnLimitedText(params: {
|
||||
cmd: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
maxBytes: number;
|
||||
acceptExitCodes?: number[];
|
||||
}): Promise<LimitedTextResult> {
|
||||
const accept = new Set(params.acceptExitCodes ?? [0]);
|
||||
|
||||
return new Promise((resolvePromise, rejectPromise) => {
|
||||
const child = spawnProcess(params.cmd, params.args, {
|
||||
cwd: params.cwd,
|
||||
env: params.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
let stdoutBytes = 0;
|
||||
let truncated = false;
|
||||
|
||||
const stop = () => {
|
||||
if (child.killed) return;
|
||||
try {
|
||||
child.kill("SIGKILL");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
child.stdout!.on("data", (chunk: Buffer) => {
|
||||
if (truncated) return;
|
||||
stdoutBytes += chunk.length;
|
||||
if (stdoutBytes > params.maxBytes) {
|
||||
truncated = true;
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
stdoutChunks.push(chunk);
|
||||
});
|
||||
|
||||
// We don't buffer stderr (it can be large too). Keep it minimal for debugging.
|
||||
let stderrPreview = "";
|
||||
child.stderr!.on("data", (chunk: Buffer) => {
|
||||
if (stderrPreview.length > 2048) return;
|
||||
stderrPreview += chunk.toString("utf8");
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
rejectPromise(error);
|
||||
});
|
||||
|
||||
child.on("close", (code, signal) => {
|
||||
if (code !== null && !accept.has(code) && !truncated) {
|
||||
rejectPromise(
|
||||
new Error(
|
||||
`Command failed: ${params.cmd} ${params.args.join(" ")} (code ${code})\n${stderrPreview}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolvePromise({
|
||||
text: Buffer.concat(stdoutChunks).toString("utf8"),
|
||||
truncated,
|
||||
exitCode: code,
|
||||
signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type CheckoutFileChange = {
|
||||
path: string;
|
||||
oldPath?: string;
|
||||
@@ -221,8 +131,13 @@ interface GitRef {
|
||||
}
|
||||
|
||||
async function listGitRefs(cwd: string, refPrefix: string): Promise<GitRef[]> {
|
||||
const { stdout } = await execGit(
|
||||
`git for-each-ref --sort=-committerdate --format="%(refname)%09%(committerdate:unix)" ${refPrefix}`,
|
||||
const { stdout } = await runGitCommand(
|
||||
[
|
||||
"for-each-ref",
|
||||
"--sort=-committerdate",
|
||||
"--format=%(refname)%09%(committerdate:unix)",
|
||||
refPrefix,
|
||||
],
|
||||
{ cwd, env: READ_ONLY_GIT_ENV },
|
||||
);
|
||||
return stdout
|
||||
@@ -324,17 +239,12 @@ async function listCheckoutFileChanges(
|
||||
): Promise<CheckoutFileChange[]> {
|
||||
const changes: CheckoutFileChange[] = [];
|
||||
|
||||
const { stdout: nameStatusOut } = await execFileAsync(
|
||||
"git",
|
||||
const { stdout: nameStatusOut } = await runGitCommand(
|
||||
buildGitDiffArgs({
|
||||
ignoreWhitespace,
|
||||
extra: ["--name-status", ref],
|
||||
}),
|
||||
{
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
maxBuffer: SMALL_OUTPUT_MAX_BUFFER,
|
||||
},
|
||||
{ cwd, env: READ_ONLY_GIT_ENV },
|
||||
);
|
||||
for (const line of nameStatusOut
|
||||
.split("\n")
|
||||
@@ -371,10 +281,13 @@ async function listCheckoutFileChanges(
|
||||
});
|
||||
}
|
||||
|
||||
const { stdout: untrackedOut } = await execGit("git ls-files --others --exclude-standard", {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
const { stdout: untrackedOut } = await runGitCommand(
|
||||
["ls-files", "--others", "--exclude-standard"],
|
||||
{
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
},
|
||||
);
|
||||
for (const file of untrackedOut
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
@@ -409,10 +322,9 @@ async function readGitFileContentAtRef(
|
||||
path: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync("git", ["show", `${ref}:${path}`], {
|
||||
const { stdout } = await runGitCommand(["show", `${ref}:${path}`], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
maxBuffer: SMALL_OUTPUT_MAX_BUFFER,
|
||||
});
|
||||
return stdout;
|
||||
} catch {
|
||||
@@ -422,7 +334,7 @@ async function readGitFileContentAtRef(
|
||||
|
||||
async function tryResolveMergeBase(cwd: string, baseRef: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execGit(`git merge-base ${baseRef} HEAD`, {
|
||||
const { stdout } = await runGitCommand(["merge-base", baseRef, "HEAD"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -462,20 +374,21 @@ async function getTrackedNumstatByPath(
|
||||
ref: string,
|
||||
ignoreWhitespace = false,
|
||||
): Promise<Map<string, FileStat>> {
|
||||
const result = await spawnLimitedText({
|
||||
cmd: "git",
|
||||
args: buildGitDiffArgs({
|
||||
const result = await runGitCommand(
|
||||
buildGitDiffArgs({
|
||||
ignoreWhitespace,
|
||||
extra: ["--numstat", ref],
|
||||
}),
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
maxBytes: TRACKED_DIFF_NUMSTAT_MAX_BYTES,
|
||||
acceptExitCodes: [0],
|
||||
});
|
||||
{
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
maxOutputBytes: TRACKED_DIFF_NUMSTAT_MAX_BYTES,
|
||||
acceptExitCodes: [0],
|
||||
},
|
||||
);
|
||||
|
||||
const stats = new Map<string, FileStat>();
|
||||
const lines = result.text
|
||||
const lines = result.stdout
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
@@ -671,14 +584,14 @@ function isGitError(error: unknown): boolean {
|
||||
|
||||
async function requireGitRepo(cwd: string): Promise<void> {
|
||||
try {
|
||||
await execAsync("git rev-parse --git-dir", { cwd, env: READ_ONLY_GIT_ENV });
|
||||
await runGitCommand(["rev-parse", "--git-dir"], { cwd, env: READ_ONLY_GIT_ENV });
|
||||
} catch (error) {
|
||||
throw new NotGitRepoError(cwd);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCurrentBranch(cwd: string): Promise<string | null> {
|
||||
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
||||
const { stdout } = await runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -688,10 +601,13 @@ export async function getCurrentBranch(cwd: string): Promise<string | null> {
|
||||
|
||||
async function getWorktreeRoot(cwd: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git rev-parse --path-format=absolute --show-toplevel", {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-parse", "--path-format=absolute", "--show-toplevel"],
|
||||
{
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
},
|
||||
);
|
||||
const root = stdout.trim();
|
||||
return root.length > 0 ? root : null;
|
||||
} catch {
|
||||
@@ -700,8 +616,8 @@ async function getWorktreeRoot(cwd: string): Promise<string | null> {
|
||||
}
|
||||
|
||||
export async function getMainRepoRoot(cwd: string): Promise<string> {
|
||||
const { stdout: commonDirOut } = await execAsync(
|
||||
"git rev-parse --path-format=absolute --git-common-dir",
|
||||
const { stdout: commonDirOut } = await runGitCommand(
|
||||
["rev-parse", "--path-format=absolute", "--git-common-dir"],
|
||||
{ cwd, env: READ_ONLY_GIT_ENV },
|
||||
);
|
||||
const commonDir = commonDirOut.trim();
|
||||
@@ -711,7 +627,7 @@ export async function getMainRepoRoot(cwd: string): Promise<string> {
|
||||
return dirname(normalized);
|
||||
}
|
||||
|
||||
const { stdout: worktreeOut } = await execAsync("git worktree list --porcelain", {
|
||||
const { stdout: worktreeOut } = await runGitCommand(["worktree", "list", "--porcelain"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -777,7 +693,7 @@ export function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
||||
|
||||
async function getWorktreePathForBranch(cwd: string, branchName: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git worktree list --porcelain", {
|
||||
const { stdout } = await runGitCommand(["worktree", "list", "--porcelain"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -800,8 +716,9 @@ export async function renameCurrentBranch(
|
||||
throw new Error("Cannot rename branch in detached HEAD state");
|
||||
}
|
||||
|
||||
await execAsync(`git branch -m "${newName}"`, {
|
||||
await runGitCommand(["branch", "-m", newName], {
|
||||
cwd,
|
||||
timeout: 120_000,
|
||||
});
|
||||
|
||||
const currentBranch = await getCurrentBranch(cwd);
|
||||
@@ -834,7 +751,7 @@ async function getConfiguredBaseRefForCwd(
|
||||
}
|
||||
|
||||
async function isWorkingTreeDirty(cwd: string): Promise<boolean> {
|
||||
const { stdout } = await execAsync("git status --porcelain", {
|
||||
const { stdout } = await runGitCommand(["status", "--porcelain"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -843,7 +760,7 @@ async function isWorkingTreeDirty(cwd: string): Promise<boolean> {
|
||||
|
||||
export async function getOriginRemoteUrl(cwd: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git config --get remote.origin.url", {
|
||||
const { stdout } = await runGitCommand(["config", "--get", "remote.origin.url"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -861,7 +778,7 @@ export async function hasOriginRemote(cwd: string): Promise<boolean> {
|
||||
|
||||
export async function resolveAbsoluteGitDir(cwd: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git rev-parse --absolute-git-dir", {
|
||||
const { stdout } = await runGitCommand(["rev-parse", "--absolute-git-dir"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -884,7 +801,7 @@ async function abortGitPullConflictState(cwd: string): Promise<void> {
|
||||
|
||||
if (existsSync(mergeHeadPath)) {
|
||||
try {
|
||||
await execAsync("git merge --abort", { cwd });
|
||||
await runGitCommand(["merge", "--abort"], { cwd, timeout: 120_000 });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -892,7 +809,7 @@ async function abortGitPullConflictState(cwd: string): Promise<void> {
|
||||
|
||||
if (existsSync(rebaseMergePath) || existsSync(rebaseApplyPath)) {
|
||||
try {
|
||||
await execAsync("git rebase --abort", { cwd });
|
||||
await runGitCommand(["rebase", "--abort"], { cwd, timeout: 120_000 });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -901,10 +818,13 @@ async function abortGitPullConflictState(cwd: string): Promise<void> {
|
||||
|
||||
export async function resolveRepositoryDefaultBranch(repoRoot: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git symbolic-ref --quiet refs/remotes/origin/HEAD", {
|
||||
cwd: repoRoot,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
const { stdout } = await runGitCommand(
|
||||
["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
},
|
||||
);
|
||||
const ref = stdout.trim();
|
||||
if (ref) {
|
||||
// Prefer a local branch name (e.g. "main") over the remote-tracking ref (e.g. "origin/main")
|
||||
@@ -914,7 +834,7 @@ export async function resolveRepositoryDefaultBranch(repoRoot: string): Promise<
|
||||
? remoteShort.slice("origin/".length)
|
||||
: remoteShort;
|
||||
try {
|
||||
await execAsync(`git show-ref --verify --quiet refs/heads/${localName}`, {
|
||||
await runGitCommand(["show-ref", "--verify", "--quiet", `refs/heads/${localName}`], {
|
||||
cwd: repoRoot,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -927,7 +847,7 @@ export async function resolveRepositoryDefaultBranch(repoRoot: string): Promise<
|
||||
// ignore
|
||||
}
|
||||
|
||||
const { stdout } = await execAsync("git branch --format='%(refname:short)'", {
|
||||
const { stdout } = await runGitCommand(["branch", "--format=%(refname:short)"], {
|
||||
cwd: repoRoot,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -956,7 +876,7 @@ function normalizeLocalBranchRefName(input: string): string {
|
||||
|
||||
async function doesGitRefExist(cwd: string, fullRef: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync(`git show-ref --verify --quiet ${fullRef}`, {
|
||||
await runGitCommand(["show-ref", "--verify", "--quiet", fullRef], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -987,8 +907,8 @@ async function resolveBestComparisonBaseRef(
|
||||
|
||||
// Both exist: choose the ref with more unique commits compared to the other.
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`git rev-list --left-right --count ${normalizedBaseRef}...origin/${normalizedBaseRef}`,
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-list", "--left-right", "--count", `${normalizedBaseRef}...origin/${normalizedBaseRef}`],
|
||||
{ cwd, env: READ_ONLY_GIT_ENV },
|
||||
);
|
||||
const [localOnlyRaw, originOnlyRaw] = stdout.trim().split(/\s+/);
|
||||
@@ -1014,8 +934,8 @@ async function getAheadBehind(
|
||||
return null;
|
||||
}
|
||||
const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
|
||||
const { stdout } = await execAsync(
|
||||
`git rev-list --left-right --count ${comparisonBaseRef}...${currentBranch}`,
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-list", "--left-right", "--count", `${comparisonBaseRef}...${currentBranch}`],
|
||||
{ cwd, env: READ_ONLY_GIT_ENV },
|
||||
);
|
||||
const [behindRaw, aheadRaw] = stdout.trim().split(/\s+/);
|
||||
@@ -1032,15 +952,15 @@ async function getAheadOfOrigin(cwd: string, currentBranch: string): Promise<num
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`git rev-list --count origin/${currentBranch}..${currentBranch}`,
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-list", "--count", `origin/${currentBranch}..${currentBranch}`],
|
||||
{ cwd, env: READ_ONLY_GIT_ENV },
|
||||
);
|
||||
const count = Number.parseInt(stdout.trim(), 10);
|
||||
return Number.isNaN(count) ? null : count;
|
||||
} catch {
|
||||
try {
|
||||
const { stdout } = await execAsync(`git rev-list --count ${currentBranch}`, {
|
||||
const { stdout } = await runGitCommand(["rev-list", "--count", currentBranch], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -1057,8 +977,8 @@ async function getBehindOfOrigin(cwd: string, currentBranch: string): Promise<nu
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`git rev-list --count ${currentBranch}..origin/${currentBranch}`,
|
||||
const { stdout } = await runGitCommand(
|
||||
["rev-list", "--count", `${currentBranch}..origin/${currentBranch}`],
|
||||
{ cwd, env: READ_ONLY_GIT_ENV },
|
||||
);
|
||||
const count = Number.parseInt(stdout.trim(), 10);
|
||||
@@ -1196,19 +1116,20 @@ async function getUntrackedDiffText(
|
||||
// Fall through to git diff path if metadata probing fails.
|
||||
}
|
||||
|
||||
const result = await spawnLimitedText({
|
||||
cmd: "git",
|
||||
args: buildGitDiffArgs({
|
||||
const result = await runGitCommand(
|
||||
buildGitDiffArgs({
|
||||
ignoreWhitespace,
|
||||
extra: ["--no-index", "/dev/null", "--", change.path],
|
||||
}),
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
maxBytes: PER_FILE_DIFF_MAX_BYTES,
|
||||
acceptExitCodes: [0, 1],
|
||||
});
|
||||
{
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
maxOutputBytes: PER_FILE_DIFF_MAX_BYTES,
|
||||
acceptExitCodes: [0, 1],
|
||||
},
|
||||
);
|
||||
return {
|
||||
text: result.text,
|
||||
text: result.stdout,
|
||||
truncated: result.truncated,
|
||||
stat: { additions: 0, deletions: 0, isBinary: false },
|
||||
};
|
||||
@@ -1335,7 +1256,7 @@ async function getCheckoutShortstatUncached(
|
||||
);
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(`git merge-base HEAD ${comparisonBaseRef}`, {
|
||||
const { stdout } = await runGitCommand(["merge-base", "HEAD", comparisonBaseRef], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -1360,7 +1281,7 @@ async function getCheckoutShortstatUncached(
|
||||
|
||||
try {
|
||||
// Omit HEAD so the diff includes uncommitted (staged + unstaged) changes
|
||||
const { stdout } = await execAsync(`git diff --shortstat ${diffTarget}`, {
|
||||
const { stdout } = await runGitCommand(["diff", "--shortstat", diffTarget], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -1530,17 +1451,18 @@ export async function getCheckoutDiff(
|
||||
let trackedDiffText = "";
|
||||
let trackedDiffTruncated = false;
|
||||
if (trackedDiffPaths.length > 0) {
|
||||
const trackedDiffResult = await spawnLimitedText({
|
||||
cmd: "git",
|
||||
args: buildGitDiffArgs({
|
||||
const trackedDiffResult = await runGitCommand(
|
||||
buildGitDiffArgs({
|
||||
ignoreWhitespace,
|
||||
extra: [refForDiff, "--", ...trackedDiffPaths],
|
||||
}),
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
maxBytes: TOTAL_DIFF_MAX_BYTES,
|
||||
});
|
||||
trackedDiffText = trackedDiffResult.text;
|
||||
{
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
maxOutputBytes: TOTAL_DIFF_MAX_BYTES,
|
||||
},
|
||||
);
|
||||
trackedDiffText = trackedDiffResult.stdout;
|
||||
trackedDiffTruncated = trackedDiffResult.truncated;
|
||||
appendDiff(trackedDiffText);
|
||||
if (trackedDiffTruncated) {
|
||||
@@ -1695,10 +1617,11 @@ export async function commitChanges(
|
||||
): Promise<void> {
|
||||
await requireGitRepo(cwd);
|
||||
if (options.addAll ?? true) {
|
||||
await execFileAsync("git", ["add", "-A"], { cwd });
|
||||
await runGitCommand(["add", "-A"], { cwd, timeout: 120_000 });
|
||||
}
|
||||
await execFileAsync("git", ["-c", "commit.gpgsign=false", "commit", "-m", options.message], {
|
||||
await runGitCommand(["-c", "commit.gpgsign=false", "commit", "-m", options.message], {
|
||||
cwd,
|
||||
timeout: 120_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1737,16 +1660,23 @@ export async function mergeToBase(
|
||||
const originalBranch = await getCurrentBranch(operationCwd);
|
||||
const mode = options.mode ?? "merge";
|
||||
try {
|
||||
await execAsync(`git checkout ${normalizedBaseRef}`, { cwd: operationCwd });
|
||||
await runGitCommand(["checkout", normalizedBaseRef], {
|
||||
cwd: operationCwd,
|
||||
timeout: 120_000,
|
||||
});
|
||||
if (mode === "squash") {
|
||||
await execAsync(`git merge --squash ${currentBranch}`, { cwd: operationCwd });
|
||||
await runGitCommand(["merge", "--squash", currentBranch], {
|
||||
cwd: operationCwd,
|
||||
timeout: 120_000,
|
||||
});
|
||||
const message =
|
||||
options.commitMessage ?? `Squash merge ${currentBranch} into ${normalizedBaseRef}`;
|
||||
await execFileAsync("git", ["-c", "commit.gpgsign=false", "commit", "-m", message], {
|
||||
await runGitCommand(["-c", "commit.gpgsign=false", "commit", "-m", message], {
|
||||
cwd: operationCwd,
|
||||
timeout: 120_000,
|
||||
});
|
||||
} else {
|
||||
await execAsync(`git merge ${currentBranch}`, { cwd: operationCwd });
|
||||
await runGitCommand(["merge", currentBranch], { cwd: operationCwd, timeout: 120_000 });
|
||||
}
|
||||
} catch (error) {
|
||||
const errorDetails =
|
||||
@@ -1755,9 +1685,9 @@ export async function mergeToBase(
|
||||
: String(error);
|
||||
try {
|
||||
const [unmergedOutput, lsFilesOutput, statusOutput] = await Promise.all([
|
||||
execAsync("git diff --name-only --diff-filter=U", { cwd: operationCwd }),
|
||||
execAsync("git ls-files -u", { cwd: operationCwd }),
|
||||
execAsync("git status --porcelain", { cwd: operationCwd }),
|
||||
runGitCommand(["diff", "--name-only", "--diff-filter=U"], { cwd: operationCwd }),
|
||||
runGitCommand(["ls-files", "-u"], { cwd: operationCwd }),
|
||||
runGitCommand(["status", "--porcelain"], { cwd: operationCwd }),
|
||||
]);
|
||||
const statusConflicts = statusOutput.stdout
|
||||
.split("\n")
|
||||
@@ -1781,7 +1711,7 @@ export async function mergeToBase(
|
||||
conflicts.length > 0 || /CONFLICT|Automatic merge failed/i.test(errorDetails);
|
||||
if (conflictDetected) {
|
||||
try {
|
||||
await execAsync("git merge --abort", { cwd: operationCwd });
|
||||
await runGitCommand(["merge", "--abort"], { cwd: operationCwd, timeout: 120_000 });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -1802,7 +1732,10 @@ export async function mergeToBase(
|
||||
} finally {
|
||||
if (isSameCheckout && originalBranch && originalBranch !== normalizedBaseRef) {
|
||||
try {
|
||||
await execAsync(`git checkout ${originalBranch}`, { cwd: operationCwd });
|
||||
await runGitCommand(["checkout", originalBranch], {
|
||||
cwd: operationCwd,
|
||||
timeout: 120_000,
|
||||
});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -1832,7 +1765,7 @@ export async function mergeFromBase(
|
||||
|
||||
const requireCleanTarget = options.requireCleanTarget ?? true;
|
||||
if (requireCleanTarget) {
|
||||
const { stdout } = await execAsync("git status --porcelain", {
|
||||
const { stdout } = await runGitCommand(["status", "--porcelain"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -1848,7 +1781,7 @@ export async function mergeFromBase(
|
||||
}
|
||||
|
||||
try {
|
||||
await execAsync(`git merge ${bestBaseRef}`, { cwd });
|
||||
await runGitCommand(["merge", bestBaseRef], { cwd, timeout: 120_000 });
|
||||
} catch (error) {
|
||||
const errorDetails =
|
||||
error instanceof Error
|
||||
@@ -1856,9 +1789,9 @@ export async function mergeFromBase(
|
||||
: String(error);
|
||||
try {
|
||||
const [unmergedOutput, lsFilesOutput, statusOutput] = await Promise.all([
|
||||
execAsync("git diff --name-only --diff-filter=U", { cwd }),
|
||||
execAsync("git ls-files -u", { cwd }),
|
||||
execAsync("git status --porcelain", { cwd }),
|
||||
runGitCommand(["diff", "--name-only", "--diff-filter=U"], { cwd }),
|
||||
runGitCommand(["ls-files", "-u"], { cwd }),
|
||||
runGitCommand(["status", "--porcelain"], { cwd }),
|
||||
]);
|
||||
const statusConflicts = statusOutput.stdout
|
||||
.split("\n")
|
||||
@@ -1882,7 +1815,7 @@ export async function mergeFromBase(
|
||||
conflicts.length > 0 || /CONFLICT|Automatic merge failed/i.test(errorDetails);
|
||||
if (conflictDetected) {
|
||||
try {
|
||||
await execAsync("git merge --abort", { cwd });
|
||||
await runGitCommand(["merge", "--abort"], { cwd, timeout: 120_000 });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -1914,7 +1847,7 @@ export async function pullCurrentBranch(cwd: string): Promise<void> {
|
||||
throw new Error("Remote 'origin' is not configured.");
|
||||
}
|
||||
try {
|
||||
await execAsync("git pull", { cwd });
|
||||
await runGitCommand(["pull"], { cwd, timeout: 120_000 });
|
||||
} catch (error) {
|
||||
await abortGitPullConflictState(cwd);
|
||||
throw error;
|
||||
@@ -1931,7 +1864,7 @@ export async function pushCurrentBranch(cwd: string): Promise<void> {
|
||||
if (!hasRemote) {
|
||||
throw new Error("Remote 'origin' is not configured.");
|
||||
}
|
||||
await execAsync(`git push -u origin ${currentBranch}`, { cwd });
|
||||
await runGitCommand(["push", "-u", "origin", currentBranch], { cwd, timeout: 120_000 });
|
||||
}
|
||||
|
||||
export interface CreatePullRequestOptions {
|
||||
@@ -2016,7 +1949,6 @@ const ReviewDecisionSchema = z
|
||||
type CheckRunNode = z.infer<typeof CheckRunNodeSchema>;
|
||||
type StatusContextNode = z.infer<typeof StatusContextNodeSchema>;
|
||||
|
||||
|
||||
export async function resolveGhPath(): Promise<string> {
|
||||
if (cachedGhPath === undefined) {
|
||||
cachedGhPath = await findExecutable("gh");
|
||||
@@ -2048,10 +1980,7 @@ function isGhAuthError(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function mapCheckRunStatus(
|
||||
status: unknown,
|
||||
conclusion: unknown,
|
||||
): PullRequestCheck["status"] {
|
||||
function mapCheckRunStatus(status: unknown, conclusion: unknown): PullRequestCheck["status"] {
|
||||
if (status !== "COMPLETED") {
|
||||
return "pending";
|
||||
}
|
||||
@@ -2203,7 +2132,7 @@ function mapReviewDecision(value: unknown): ReviewDecision {
|
||||
|
||||
async function resolveGitHubRepo(cwd: string): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git config --get remote.origin.url", {
|
||||
const { stdout } = await runGitCommand(["config", "--get", "remote.origin.url"], {
|
||||
cwd,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
@@ -2265,7 +2194,7 @@ export async function createPullRequest(
|
||||
throw new Error(`Base ref mismatch: expected ${base}, got ${options.base}`);
|
||||
}
|
||||
|
||||
await execAsync(`git push -u origin ${head}`, { cwd });
|
||||
await runGitCommand(["push", "-u", "origin", head], { cwd, timeout: 120_000 });
|
||||
|
||||
const ghEnv: NodeJS.ProcessEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
||||
const args = ["api", "-X", "POST", `repos/${repo}/pulls`, "-f", `title=${options.title}`];
|
||||
@@ -2274,7 +2203,7 @@ export async function createPullRequest(
|
||||
if (options.body) {
|
||||
args.push("-f", `body=${options.body}`);
|
||||
}
|
||||
const { stdout } = await execFileAsync(ghPath, args, { cwd, env: ghEnv });
|
||||
const { stdout } = await execCommand(ghPath, args, { cwd, env: ghEnv });
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
if (!parsed?.url || !parsed?.number) {
|
||||
throw new Error("GitHub CLI did not return PR url/number");
|
||||
@@ -2326,7 +2255,7 @@ async function getPullRequestStatusUncached(cwd: string): Promise<PullRequestSta
|
||||
};
|
||||
}
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
const { stdout } = await execCommand(
|
||||
ghPath,
|
||||
[
|
||||
"pr",
|
||||
@@ -2407,7 +2336,7 @@ export async function searchGitHubIssuesAndPrs(
|
||||
|
||||
try {
|
||||
const [issuesResult, prsResult] = await Promise.allSettled([
|
||||
execFileAsync(
|
||||
execCommand(
|
||||
ghPath,
|
||||
[
|
||||
"issue",
|
||||
@@ -2421,7 +2350,7 @@ export async function searchGitHubIssuesAndPrs(
|
||||
],
|
||||
{ cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
|
||||
),
|
||||
execFileAsync(
|
||||
execCommand(
|
||||
ghPath,
|
||||
[
|
||||
"pr",
|
||||
|
||||
Reference in New Issue
Block a user