Files
gai/src/pr.ts
T

224 lines
5.6 KiB
TypeScript

export type Platform = "github" | "gitea" | "gitlab";
export async function getDefaultBranch(): Promise<string> {
try {
const result =
await Bun.$`git symbolic-ref refs/remotes/origin/HEAD`.quiet().text();
return result.trim().replace("refs/remotes/origin/", "");
} catch {
try {
const branches = await Bun.$`git branch -r`.quiet().text();
for (const line of branches.split("\n")) {
const trimmed = line.trim();
if (trimmed === "origin/main" || trimmed === "origin/master") {
return trimmed.replace("origin/", "");
}
}
} catch {}
return "main";
}
}
export async function getBranchName(): Promise<string> {
const result =
await Bun.$`git rev-parse --abbrev-ref HEAD`.quiet().text();
return result.trim();
}
export async function getBranchPushStatus(): Promise<{
pushed: boolean;
upstream: string | null;
}> {
try {
const upstream = (await Bun.$`git rev-parse --abbrev-ref --symbolic-full-name @{u}`.quiet().text()).trim();
const localHead = (await Bun.$`git rev-parse HEAD`.quiet().text()).trim();
const remoteHead = (await Bun.$`git rev-parse @{u}`.quiet().text()).trim();
return { pushed: localHead === remoteHead, upstream };
} catch {
return { pushed: false, upstream: null };
}
}
export async function pushCurrentBranch(branch: string, upstream: string | null): Promise<void> {
const args = upstream
? ["push"]
: ["push", "-u", "origin", branch];
const proc = Bun.spawn(["git", ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
if (exitCode !== 0) {
throw new Error(stderr.trim() || stdout.trim() || `git push failed (exit code ${exitCode})`);
}
}
export async function getBranchCommits(base: string): Promise<string[]> {
try {
const result =
await Bun.$`git log --oneline origin/${base}..HEAD`.quiet().text();
return result.trim().split("\n").filter(Boolean);
} catch {
try {
const result =
await Bun.$`git log --oneline ${base}..HEAD`.quiet().text();
return result.trim().split("\n").filter(Boolean);
} catch {
return [];
}
}
}
export async function getBranchDiff(base: string): Promise<string> {
try {
const result =
await Bun.$`git diff ${base}...HEAD`.quiet().text();
return result.trim();
} catch {
return "";
}
}
function parseRemoteHostname(url: string): string | null {
const hostname = url
.trim()
.toLowerCase()
.replace(/^(https?:\/\/|ssh:\/\/|git:\/\/)/, "")
.replace(/^[^@]+@/, "")
.split(/[:/]/)[0];
return hostname || null;
}
export function detectPlatformFromHostname(hostname: string | null): Platform | null {
if (!hostname) return null;
if (hostname === "github.com") return "github";
if (hostname === "gitlab.com" || hostname.includes("gitlab")) {
return "gitlab";
}
if (hostname.includes("gitea")) return "gitea";
return null;
}
export async function detectPlatform(): Promise<Platform | null> {
try {
const url = await Bun.$`git remote get-url origin`.quiet().text();
const hostname = parseRemoteHostname(url);
return detectPlatformFromHostname(hostname);
} catch {
return null;
}
}
export async function getRemoteHostname(): Promise<string | null> {
try {
const url = await Bun.$`git remote get-url origin`.quiet().text();
return parseRemoteHostname(url);
} catch {
return null;
}
}
export async function createPR(
platform: Platform,
title: string,
body: string,
base: string,
draft: boolean,
): Promise<string> {
if (platform === "github") {
const args = [
"pr",
"create",
"--title",
title,
"--body",
body,
"--base",
base,
];
if (draft) args.push("--draft");
const proc = Bun.spawn(["gh", ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
if (exitCode !== 0) {
throw new Error(
stderr.trim() || `gh pr create failed (exit code ${exitCode})`,
);
}
const match = stdout.match(/(https?:\/\/[^\s]+)/);
return match?.[1] ?? stdout.trim();
}
if (platform === "gitlab") {
const args = [
"mr",
"create",
"--title",
title,
"--description",
body,
"--target-branch",
base,
];
if (draft) args.push("--draft");
const proc = Bun.spawn(["glab", ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
if (exitCode !== 0) {
throw new Error(
stderr.trim() || `glab mr create failed (exit code ${exitCode})`,
);
}
const match = stdout.match(/(https?:\/\/[^\s]+)/);
return match?.[1] ?? stdout.trim();
}
const args = [
"pulls",
"create",
"--title",
title,
"--description",
body,
"--base",
base,
];
const proc = Bun.spawn(["tea", ...args], {
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
if (exitCode !== 0) {
throw new Error(
stderr.trim() || `tea pulls create failed (exit code ${exitCode})`,
);
}
const match = stdout.match(/(https?:\/\/[^\s]+)/);
return match?.[1] ?? stdout.trim();
}