Skip to content

Commit 40002e6

Browse files
authored
feat: GitLab merge request review support (#364)
* feat: GitLab merge request review support Add full GitLab MR review parity with existing GitHub PR review: - Auto-detect platform from URL (github.com vs any GitLab host) - Extract GitHub logic into pr-github.ts, new pr-gitlab.ts implementation - Widen PRRef/PRMetadata to discriminated unions for type safety - Dispatch functions route to correct platform implementation - Platform-aware UI labels (PR/MR, #/!, GitHub/GitLab icons) - Self-hosted GitLab support via --hostname flag - Normalize glab diff output to standard git format - Handle glab CLI differences (no --jq, Content-Type header for --input) - Defensive JSON parsing for GitLab context API responses Tested against gitlab.com with inline comments, multi-line ranges, approval, and PR context tabs (summary, comments, checks). For provenance purposes, this commit was AI assisted. * fix: correct GitLab enum mappings and add shared file path encoding - Map GitLab job statuses to UI-expected enums (failed→FAILURE, canceled→NEUTRAL) - Map GitLab detailed_merge_status to CLEAN/BLOCKED/BEHIND/DIRTY/UNKNOWN - Fix false approval state on repos without required approvers - Add shared encodeApiFilePath helper used by both GitHub and GitLab For provenance purposes, this commit was AI assisted. * fix: align panel headers and refine file tree selection style - Use shared --panel-header-h CSS variable for consistent header heights across file tree search, file header, and annotations panel - Update GitLab icon to use official tanuki SVG paths with currentColor - Replace solid primary fill on active file tree items with 30% tinted background for better readability and semantic color preservation For provenance purposes, this commit was AI assisted.
1 parent d3cb2b9 commit 40002e6

File tree

15 files changed

+1120
-388
lines changed

15 files changed

+1120
-388
lines changed

apps/hook/server/index.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
handleAnnotateServerReady,
4545
} from "@plannotator/server/annotate";
4646
import { getGitContext, runGitDiff } from "@plannotator/server/git";
47-
import { parsePRUrl, checkGhAuth, fetchPR } from "@plannotator/server/pr";
47+
import { parsePRUrl, checkAuth, fetchPR, getCliName, getCliInstallUrl, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr";
4848
import { writeRemoteShareLink } from "@plannotator/server/share-url";
4949
import { resolveMarkdownFile } from "@plannotator/server/resolve-file";
5050
import { registerSession, unregisterSession, listSessions } from "@plannotator/server/sessions";
@@ -149,29 +149,34 @@ if (args[0] === "sessions") {
149149
// --- PR Review Mode ---
150150
const prRef = parsePRUrl(urlArg);
151151
if (!prRef) {
152-
console.error(`Invalid PR URL: ${urlArg}`);
153-
console.error("Supported formats: https://github.com/owner/repo/pull/123");
152+
console.error(`Invalid PR/MR URL: ${urlArg}`);
153+
console.error("Supported formats:");
154+
console.error(" GitHub: https://github.com/owner/repo/pull/123");
155+
console.error(" GitLab: https://gitlab.com/group/project/-/merge_requests/42");
154156
process.exit(1);
155157
}
156158

159+
const cliName = getCliName(prRef);
160+
const cliUrl = getCliInstallUrl(prRef);
161+
157162
try {
158-
await checkGhAuth();
163+
await checkAuth(prRef);
159164
} catch (err) {
160165
const msg = err instanceof Error ? err.message : String(err);
161166
if (msg.includes("not found") || msg.includes("ENOENT")) {
162-
console.error("GitHub CLI (gh) is not installed.");
163-
console.error("Install it from https://cli.github.com");
167+
console.error(`${cliName === "gh" ? "GitHub" : "GitLab"} CLI (${cliName}) is not installed.`);
168+
console.error(`Install it from ${cliUrl}`);
164169
} else {
165170
console.error(msg);
166171
}
167172
process.exit(1);
168173
}
169174

170-
console.error(`Fetching PR #${prRef.number} from ${prRef.owner}/${prRef.repo}...`);
175+
console.error(`Fetching ${getMRLabel(prRef)} ${getMRNumberLabel(prRef)} from ${getDisplayRepo(prRef)}...`);
171176
try {
172177
const pr = await fetchPR(prRef);
173178
rawPatch = pr.rawPatch;
174-
gitRef = `PR #${prRef.number}`;
179+
gitRef = `${getMRLabel(prRef)} ${getMRNumberLabel(prRef)}`;
175180
prMetadata = pr.metadata;
176181
} catch (err) {
177182
console.error(err instanceof Error ? err.message : "Failed to fetch PR");
@@ -216,7 +221,7 @@ if (args[0] === "sessions") {
216221
mode: "review",
217222
project: reviewProject,
218223
startedAt: new Date().toISOString(),
219-
label: isPRMode ? `pr-review-${prMetadata!.owner}/${prMetadata!.repo}#${prMetadata!.number}` : `review-${reviewProject}`,
224+
label: isPRMode ? `${getMRLabel(prMetadata!).toLowerCase()}-review-${getDisplayRepo(prMetadata!)}${getMRNumberLabel(prMetadata!)}` : `review-${reviewProject}`,
220225
});
221226

222227
// Wait for user feedback

apps/opencode-plugin/commands.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
handleAnnotateServerReady,
1515
} from "@plannotator/server/annotate";
1616
import { getGitContext, runGitDiffWithContext } from "@plannotator/server/git";
17-
import { parsePRUrl, checkGhAuth, fetchPR } from "@plannotator/server/pr";
17+
import { parsePRUrl, checkAuth, fetchPR, getCliName, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr";
1818
import { resolveMarkdownFile } from "@plannotator/server/resolve-file";
1919

2020
/** Shared dependencies injected by the plugin */
@@ -44,28 +44,29 @@ export async function handleReviewCommand(
4444
let prMetadata: Awaited<ReturnType<typeof fetchPR>>["metadata"] | undefined;
4545

4646
if (isPRMode) {
47-
client.app.log({ level: "info", message: "Fetching PR for review..." });
48-
4947
const prRef = parsePRUrl(urlArg);
5048
if (!prRef) {
51-
client.app.log({ level: "error", message: `Invalid PR URL: ${urlArg}` });
49+
client.app.log({ level: "error", message: `Invalid PR/MR URL: ${urlArg}` });
5250
return;
5351
}
5452

53+
client.app.log({ level: "info", message: `Fetching ${getMRLabel(prRef)} ${getMRNumberLabel(prRef)} from ${getDisplayRepo(prRef)}...` });
54+
5555
try {
56-
await checkGhAuth();
56+
await checkAuth(prRef);
5757
} catch (err) {
58-
client.app.log({ level: "error", message: err instanceof Error ? err.message : "GitHub CLI auth check failed" });
58+
const cliName = getCliName(prRef);
59+
client.app.log({ level: "error", message: err instanceof Error ? err.message : `${cliName} auth check failed` });
5960
return;
6061
}
6162

6263
try {
6364
const pr = await fetchPR(prRef);
6465
rawPatch = pr.rawPatch;
65-
gitRef = `PR #${prRef.number}`;
66+
gitRef = `${getMRLabel(prRef)} ${getMRNumberLabel(prRef)}`;
6667
prMetadata = pr.metadata;
6768
} catch (err) {
68-
client.app.log({ level: "error", message: err instanceof Error ? err.message : "Failed to fetch PR" });
69+
client.app.log({ level: "error", message: err instanceof Error ? err.message : `Failed to fetch ${getMRLabel(prRef)} ${getMRNumberLabel(prRef)}` });
6970
return;
7071
}
7172
} else {

bun.lock

Lines changed: 3 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)