From 1dbfac7985263d0841a965aacecab8305d3ef689 Mon Sep 17 00:00:00 2001 From: Mplan Date: Thu, 11 Jun 2026 19:38:36 +0800 Subject: [PATCH] feat(pr): add GitLab support --- README.md | 3 ++- index.ts | 4 ++- src/pr.ts | 65 +++++++++++++++++++++++++++++++++++++++++++------ test/pr.test.ts | 19 +++++++++++++++ 4 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 test/pr.test.ts diff --git a/README.md b/README.md index ed9fa52..7f27de8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Generate **Conventional Commits** messages and pull request descriptions using A - **Conventional Commits** — `feat(scope): description` format by default - **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 -- **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 - **Review before commit** — confirm, edit, or abort the generated message - **Bun-native runtime** — built on Bun APIs with no runtime npm dependencies @@ -108,6 +108,7 @@ $ gai commit - GitHub remotes use the `gh` CLI - Gitea remotes use the `tea` CLI +- GitLab remotes use the `glab` CLI - 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. diff --git a/index.ts b/index.ts index 696246f..e1f0758 100644 --- a/index.ts +++ b/index.ts @@ -333,6 +333,7 @@ async function selectPlatform(hostname: string): Promise { items: [ { label: "GitHub", value: "github" as Platform, description: "Use gh 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 { platform = chosen; } - const platformLabel = platform === "github" ? "GitHub" : "Gitea"; + const platformLabel = + platform === "github" ? "GitHub" : platform === "gitlab" ? "GitLab" : "Gitea"; console.log(` Using: ${CYAN}${platformLabel}${RESET}`); const cliError = checkCLI(platform); diff --git a/src/pr.ts b/src/pr.ts index 1bd758e..e035462 100644 --- a/src/pr.ts +++ b/src/pr.ts @@ -1,4 +1,4 @@ -export type Platform = "github" | "gitea"; +export type Platform = "github" | "gitea" | "gitlab"; export async function getDefaultBranch(): Promise { try { @@ -93,16 +93,23 @@ function parseRemoteHostname(url: string): string | 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 { try { const url = await Bun.$`git remote get-url origin`.quiet().text(); const hostname = parseRemoteHostname(url); - if (!hostname) return null; - - if (hostname === "github.com") return "github"; - if (hostname.includes("gitea")) return "gitea"; - return null; + return detectPlatformFromHostname(hostname); } catch { return null; } @@ -118,12 +125,16 @@ export async function getRemoteHostname(): Promise { } 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); if (!path) { if (platform === "github") { 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 null; @@ -139,6 +150,15 @@ export async function checkAuth(platform: Platform): Promise { } } + if (platform === "gitlab") { + try { + await Bun.$`glab auth status`.quiet(); + return null; + } catch { + return "Not authenticated with GitLab CLI. Run: glab auth login"; + } + } + try { const result = await Bun.$`tea logins list`.quiet().text(); if (result.trim()) return null; @@ -186,6 +206,37 @@ export async function createPR( 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", diff --git a/test/pr.test.ts b/test/pr.test.ts new file mode 100644 index 0000000..8d9fd19 --- /dev/null +++ b/test/pr.test.ts @@ -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(); + }); +});