-
Notifications
You must be signed in to change notification settings - Fork 355
feat: add GitLab MR support with --glab flag #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| export * from "./branch.ts"; | ||
| export * from "./worktree.ts"; | ||
| export * from "./pr.ts"; | ||
| export * from "./mr.ts"; | ||
| export * from "./merge.ts"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { describe, expect, it } from "bun:test"; | ||
| import { parseArgs } from "../cli/args.ts"; | ||
|
|
||
| describe("--glab flag parsing", () => { | ||
| it("should set createMr to true when --glab is passed", () => { | ||
| const { options } = parseArgs(["node", "ralphy", "--glab"]); | ||
|
|
||
| expect(options.createMr).toBe(true); | ||
| }); | ||
|
|
||
| it("should imply createPr when --glab is passed", () => { | ||
| const { options } = parseArgs(["node", "ralphy", "--glab"]); | ||
|
|
||
| expect(options.createPr).toBe(true); | ||
| }); | ||
|
|
||
| it("should not set createMr when --glab is not passed", () => { | ||
| const { options } = parseArgs(["node", "ralphy"]); | ||
|
|
||
| expect(options.createMr).toBe(false); | ||
| }); | ||
|
|
||
| it("should keep createPr false when neither --create-pr nor --glab is passed", () => { | ||
| const { options } = parseArgs(["node", "ralphy"]); | ||
|
|
||
| expect(options.createPr).toBe(false); | ||
| }); | ||
|
|
||
| it("should allow --glab with --draft-pr", () => { | ||
| const { options } = parseArgs(["node", "ralphy", "--glab", "--draft-pr"]); | ||
|
|
||
| expect(options.createMr).toBe(true); | ||
| expect(options.createPr).toBe(true); | ||
| expect(options.draftPr).toBe(true); | ||
| }); | ||
|
|
||
| it("should allow --glab with --base-branch", () => { | ||
| const { options } = parseArgs(["node", "ralphy", "--glab", "--base-branch", "develop"]); | ||
|
|
||
| expect(options.createMr).toBe(true); | ||
| expect(options.baseBranch).toBe("develop"); | ||
| }); | ||
|
|
||
| it("should allow --glab with --branch-per-task", () => { | ||
| const { options } = parseArgs(["node", "ralphy", "--glab", "--branch-per-task"]); | ||
|
|
||
| expect(options.createMr).toBe(true); | ||
| expect(options.branchPerTask).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe("settings display with --glab", () => { | ||
| it("should show mr setting when createMr is true", async () => { | ||
| const { buildActiveSettings } = await import("../ui/settings.ts"); | ||
| const { DEFAULT_OPTIONS } = await import("../config/types.ts"); | ||
|
|
||
| const settings = buildActiveSettings({ | ||
| ...DEFAULT_OPTIONS, | ||
| createPr: true, | ||
| createMr: true, | ||
| }); | ||
|
|
||
| expect(settings).toContain("mr"); | ||
| expect(settings).not.toContain("pr"); | ||
| }); | ||
|
|
||
| it("should show pr setting when createPr is true but createMr is false", async () => { | ||
| const { buildActiveSettings } = await import("../ui/settings.ts"); | ||
| const { DEFAULT_OPTIONS } = await import("../config/types.ts"); | ||
|
|
||
| const settings = buildActiveSettings({ | ||
| ...DEFAULT_OPTIONS, | ||
| createPr: true, | ||
| createMr: false, | ||
| }); | ||
|
|
||
| expect(settings).toContain("pr"); | ||
| expect(settings).not.toContain("mr"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||||||||||||||||
| import simpleGit, { type SimpleGit } from "simple-git"; | ||||||||||||||||||||||||||||||||||||
| import { execCommand } from "../engines/base.ts"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Push a branch to origin for GitLab | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| export async function pushBranchGlab(branch: string, workDir = process.cwd()): Promise<boolean> { | ||||||||||||||||||||||||||||||||||||
| const git: SimpleGit = simpleGit(workDir); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| await git.push("origin", branch, ["--set-upstream"]); | ||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Create a merge request using glab CLI | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| export async function createMergeRequest( | ||||||||||||||||||||||||||||||||||||
| branch: string, | ||||||||||||||||||||||||||||||||||||
| baseBranch: string, | ||||||||||||||||||||||||||||||||||||
| title: string, | ||||||||||||||||||||||||||||||||||||
| body: string, | ||||||||||||||||||||||||||||||||||||
| draft = false, | ||||||||||||||||||||||||||||||||||||
| workDir = process.cwd(), | ||||||||||||||||||||||||||||||||||||
| ): Promise<string | null> { | ||||||||||||||||||||||||||||||||||||
| // Push branch first | ||||||||||||||||||||||||||||||||||||
| const pushed = await pushBranchGlab(branch, workDir); | ||||||||||||||||||||||||||||||||||||
| if (!pushed) { | ||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Build glab mr create command args | ||||||||||||||||||||||||||||||||||||
| const args = [ | ||||||||||||||||||||||||||||||||||||
| "mr", | ||||||||||||||||||||||||||||||||||||
| "create", | ||||||||||||||||||||||||||||||||||||
| "--target-branch", | ||||||||||||||||||||||||||||||||||||
| baseBranch, | ||||||||||||||||||||||||||||||||||||
| "--source-branch", | ||||||||||||||||||||||||||||||||||||
| branch, | ||||||||||||||||||||||||||||||||||||
| "--title", | ||||||||||||||||||||||||||||||||||||
| title, | ||||||||||||||||||||||||||||||||||||
| "--description", | ||||||||||||||||||||||||||||||||||||
| body, | ||||||||||||||||||||||||||||||||||||
| "--no-editor", | ||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (draft) { | ||||||||||||||||||||||||||||||||||||
| args.push("--draft"); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Execute glab CLI | ||||||||||||||||||||||||||||||||||||
| const { stdout, exitCode } = await execCommand("glab", args, workDir); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (exitCode !== 0) { | ||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Return the MR URL (glab outputs the URL on success) | ||||||||||||||||||||||||||||||||||||
| return stdout.trim() || null; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Consider extracting the URL from the output:
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: cli/src/git/mr.ts
Line: 55:62
Comment:
**`glab` output includes more than a URL**
`glab mr create` outputs multi-line text (e.g. `"Creating merge request for branch into main in group/project\n\nhttps://gitlab.com/..."`) whereas `gh pr create` outputs only the URL. Using `stdout.trim()` here will return the entire multi-line output, not just the MR URL. This will result in messy log output and an incorrect return value if it's ever consumed as a URL.
Consider extracting the URL from the output:
```suggestion
const { stdout, exitCode } = await execCommand("glab", args, workDir);
if (exitCode !== 0) {
return null;
}
// glab outputs multi-line text; extract the URL (last line or first https:// match)
const urlMatch = stdout.match(/https?:\/\/\S+/);
return urlMatch ? urlMatch[0].trim() : stdout.trim() || null;
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Check if glab CLI is available and authenticated | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| export async function isGlabAvailable(): Promise<boolean> { | ||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| const { exitCode } = await execCommand("glab", ["auth", "status"], process.cwd()); | ||||||||||||||||||||||||||||||||||||
| return exitCode === 0; | ||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+75
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This function is defined but never used. The Prompt To Fix With AIThis is a comment left during a code review.
Path: cli/src/git/mr.ts
Line: 65:75
Comment:
**`isGlabAvailable()` is never called**
This function is defined but never used. The `--glab` flag will silently fail if the `glab` CLI is not installed or not authenticated — `createMergeRequest` returns `null` and the user gets no feedback. Consider calling `isGlabAvailable()` early in the execution pipeline (similar to `isEngineAvailable`) to provide a clear error message before task execution begins.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent failure when MR creation fails
When
createMergeRequestreturnsnull(e.g. push failure,glabnot installed, auth failure), there is no error or warning logged — the code just silently continues. This makes debugging difficult for users. Consider adding an else branch to log a warning:The same issue exists in the existing PR path (
prUrlnull case), but given this is new code, it's worth addressing here.Prompt To Fix With AI