feat(pr): add GitLab support

This commit is contained in:
2026-06-11 19:38:36 +08:00
parent c8626ff69a
commit 1dbfac7985
4 changed files with 82 additions and 9 deletions
+2 -1
View File
@@ -22,7 +22,7 @@ Generate **Conventional Commits** messages and pull request descriptions using A
- **Conventional Commits** — `feat(scope): description` format by default - **Conventional Commits** — `feat(scope): description` format by default
- **Interactive file selection** — ↑/↓ to navigate, space to select, top-level "Select all" - **Interactive file selection** — ↑/↓ to navigate, space to select, top-level "Select all"
- **Inline editing** — edit AI-generated messages right in the terminal with cursor movement - **Inline editing** — edit AI-generated messages right in the terminal with cursor movement
- **AI-generated PRs** — create GitHub or Gitea pull requests with generated title and body - **AI-generated PRs** — create GitHub, Gitea, or GitLab pull requests with generated title and body
- **OpenAI-compatible API** — works with DeepSeek, OpenAI, Ollama, OpenRouter, and more - **OpenAI-compatible API** — works with DeepSeek, OpenAI, Ollama, OpenRouter, and more
- **Review before commit** — confirm, edit, or abort the generated message - **Review before commit** — confirm, edit, or abort the generated message
- **Bun-native runtime** — built on Bun APIs with no runtime npm dependencies - **Bun-native runtime** — built on Bun APIs with no runtime npm dependencies
@@ -108,6 +108,7 @@ $ gai commit
- GitHub remotes use the `gh` CLI - GitHub remotes use the `gh` CLI
- Gitea remotes use the `tea` CLI - Gitea remotes use the `tea` CLI
- GitLab remotes use the `glab` CLI
- Unknown remotes prompt you to choose a platform - Unknown remotes prompt you to choose a platform
The command compares your current branch against the default branch, pushes the branch if needed, generates a PR title/body from the branch commits and diff, then asks for confirmation before creating the PR. The command compares your current branch against the default branch, pushes the branch if needed, generates a PR title/body from the branch commits and diff, then asks for confirmation before creating the PR.
+3 -1
View File
@@ -333,6 +333,7 @@ async function selectPlatform(hostname: string): Promise<Platform | null> {
items: [ items: [
{ label: "GitHub", value: "github" as Platform, description: "Use gh CLI" }, { label: "GitHub", value: "github" as Platform, description: "Use gh CLI" },
{ label: "Gitea", value: "gitea" as Platform, description: "Use tea CLI" }, { label: "Gitea", value: "gitea" as Platform, description: "Use tea CLI" },
{ label: "GitLab", value: "gitlab" as Platform, description: "Use glab CLI" },
], ],
}); });
} }
@@ -484,7 +485,8 @@ async function handlePR(draft: boolean): Promise<void> {
platform = chosen; platform = chosen;
} }
const platformLabel = platform === "github" ? "GitHub" : "Gitea"; const platformLabel =
platform === "github" ? "GitHub" : platform === "gitlab" ? "GitLab" : "Gitea";
console.log(` Using: ${CYAN}${platformLabel}${RESET}`); console.log(` Using: ${CYAN}${platformLabel}${RESET}`);
const cliError = checkCLI(platform); const cliError = checkCLI(platform);
+58 -7
View File
@@ -1,4 +1,4 @@
export type Platform = "github" | "gitea"; export type Platform = "github" | "gitea" | "gitlab";
export async function getDefaultBranch(): Promise<string> { export async function getDefaultBranch(): Promise<string> {
try { try {
@@ -93,16 +93,23 @@ function parseRemoteHostname(url: string): string | null {
return hostname || null; 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> { export async function detectPlatform(): Promise<Platform | null> {
try { try {
const url = await Bun.$`git remote get-url origin`.quiet().text(); const url = await Bun.$`git remote get-url origin`.quiet().text();
const hostname = parseRemoteHostname(url); const hostname = parseRemoteHostname(url);
if (!hostname) return null; return detectPlatformFromHostname(hostname);
if (hostname === "github.com") return "github";
if (hostname.includes("gitea")) return "gitea";
return null;
} catch { } catch {
return null; return null;
} }
@@ -118,12 +125,16 @@ export async function getRemoteHostname(): Promise<string | null> {
} }
export function checkCLI(platform: Platform): string | null { export function checkCLI(platform: Platform): string | null {
const bin = platform === "github" ? "gh" : "tea"; const bin =
platform === "github" ? "gh" : platform === "gitlab" ? "glab" : "tea";
const path = Bun.which(bin); const path = Bun.which(bin);
if (!path) { if (!path) {
if (platform === "github") { if (platform === "github") {
return "GitHub CLI (gh) not found. Install: brew install gh"; return "GitHub CLI (gh) not found. Install: brew install gh";
} }
if (platform === "gitlab") {
return "GitLab CLI (glab) not found. Install: brew install glab";
}
return "Gitea CLI (tea) not found. Install from: https://gitea.com/gitea/tea"; return "Gitea CLI (tea) not found. Install from: https://gitea.com/gitea/tea";
} }
return null; return null;
@@ -139,6 +150,15 @@ export async function checkAuth(platform: Platform): Promise<string | null> {
} }
} }
if (platform === "gitlab") {
try {
await Bun.$`glab auth status`.quiet();
return null;
} catch {
return "Not authenticated with GitLab CLI. Run: glab auth login";
}
}
try { try {
const result = await Bun.$`tea logins list`.quiet().text(); const result = await Bun.$`tea logins list`.quiet().text();
if (result.trim()) return null; if (result.trim()) return null;
@@ -186,6 +206,37 @@ export async function createPR(
return match?.[1] ?? stdout.trim(); 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 = [ const args = [
"pulls", "pulls",
"create", "create",
+19
View File
@@ -0,0 +1,19 @@
import { test, expect, describe } from "bun:test";
import { detectPlatformFromHostname } from "../src/pr";
describe("pr platform detection", () => {
test("detects supported hosted platforms", () => {
expect(detectPlatformFromHostname("github.com")).toBe("github");
expect(detectPlatformFromHostname("gitlab.com")).toBe("gitlab");
});
test("detects self-hosted platform hostnames", () => {
expect(detectPlatformFromHostname("gitlab.example.com")).toBe("gitlab");
expect(detectPlatformFromHostname("gitea.example.com")).toBe("gitea");
});
test("returns null for unknown hostnames", () => {
expect(detectPlatformFromHostname("git.example.com")).toBeNull();
expect(detectPlatformFromHostname(null)).toBeNull();
});
});