From 161eb99b42643dcffe299d4705539cad63c29416 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Sep 2025 09:25:45 +0200 Subject: [PATCH] Implement bypass merge rules checkbox feature Co-authored-by: Byron <63622+Byron@users.noreply.github.com> --- .../desktop/src/components/MergeButton.svelte | 39 +++++++++++++++-- .../components/StackedPullRequestCard.svelte | 4 +- .../forge/github/githubPrService.svelte.ts | 43 +++++++++++-------- .../forge/gitlab/gitlabPrService.svelte.ts | 22 +++++++--- .../src/lib/forge/interface/forgePrService.ts | 2 +- 5 files changed, 82 insertions(+), 28 deletions(-) diff --git a/apps/desktop/src/components/MergeButton.svelte b/apps/desktop/src/components/MergeButton.svelte index 7fed1184cc..cdab7cf823 100644 --- a/apps/desktop/src/components/MergeButton.svelte +++ b/apps/desktop/src/components/MergeButton.svelte @@ -2,12 +2,12 @@ import { MergeMethod } from '$lib/forge/interface/types'; import { persisted, type Persisted } from '@gitbutler/shared/persisted'; - import { ContextMenuItem, ContextMenuSection, DropdownButton } from '@gitbutler/ui'; + import { ContextMenuItem, ContextMenuSection, DropdownButton, Checkbox } from '@gitbutler/ui'; import type { ButtonProps } from '@gitbutler/ui'; interface Props { projectId: string; - onclick: (method: MergeMethod) => Promise; + onclick: (method: MergeMethod, bypassRules?: boolean) => Promise; disabled?: boolean; wide?: boolean; tooltip?: string; @@ -30,7 +30,13 @@ return persisted(MergeMethod.Merge, key + projectId); } + function persistedBypassRules(projectId: string): Persisted { + const key = 'projectMergeBypassRules'; + return persisted(false, key + projectId); + } + const action = persistedAction(projectId); + const bypassRules = persistedBypassRules(projectId); let dropDown: ReturnType | undefined; let loading = $state(false); @@ -47,7 +53,7 @@ onclick={async () => { loading = true; try { - await onclick?.($action); + await onclick?.($action, $bypassRules); } finally { loading = false; } @@ -72,5 +78,32 @@ /> {/each} + +
+ { + // The reactive store will handle the change + }} + /> + Bypass branch protection rules +
+
{/snippet} + + diff --git a/apps/desktop/src/components/StackedPullRequestCard.svelte b/apps/desktop/src/components/StackedPullRequestCard.svelte index c39315ca3b..6bb5f17458 100644 --- a/apps/desktop/src/components/StackedPullRequestCard.svelte +++ b/apps/desktop/src/components/StackedPullRequestCard.svelte @@ -70,9 +70,9 @@ await prService?.reopen(pr.number); } - async function handleMerge(method: MergeMethod) { + async function handleMerge(method: MergeMethod, bypassRules?: boolean) { if (!pr) return; - await prService?.merge(method, pr.number); + await prService?.merge(method, pr.number, bypassRules); // In a stack, after merging, update the new bottom PR target // base branch to master if necessary diff --git a/apps/desktop/src/lib/forge/github/githubPrService.svelte.ts b/apps/desktop/src/lib/forge/github/githubPrService.svelte.ts index 78fccc0aa6..2ab8004dfe 100644 --- a/apps/desktop/src/lib/forge/github/githubPrService.svelte.ts +++ b/apps/desktop/src/lib/forge/github/githubPrService.svelte.ts @@ -85,8 +85,8 @@ export class GitHubPrService implements ForgePrService { return this.api.endpoints.getPr.useQuery({ number }, options); } - async merge(method: MergeMethod, number: number) { - await this.api.endpoints.mergePr.mutate({ method, number }); + async merge(method: MergeMethod, number: number, bypassRules?: boolean) { + await this.api.endpoints.mergePr.mutate({ method, number, bypassRules }); } async reopen(number: number) { @@ -194,23 +194,32 @@ function injectEndpoints(api: GitHubApi) { }), invalidatesTags: (result) => [invalidatesItem(ReduxTag.PullRequests, result?.number)] }), - mergePr: build.mutation({ - queryFn: async ({ number, method: method }, api) => { - const result = await ghQuery({ - domain: 'pulls', - action: 'merge', - parameters: { pull_number: number, merge_method: method }, - extra: api.extra - }); + mergePr: build.mutation( + { + queryFn: async ({ number, method: method, bypassRules }, api) => { + const parameters: any = { pull_number: number, merge_method: method }; - if (result.error) { - return { error: result.error }; - } + // Add bypass parameter if requested and available + if (bypassRules) { + parameters.bypass_required_pr_reviews = true; + } - return { data: undefined }; - }, - invalidatesTags: [invalidatesList(ReduxTag.PullRequests)] - }), + const result = await ghQuery({ + domain: 'pulls', + action: 'merge', + parameters, + extra: api.extra + }); + + if (result.error) { + return { error: result.error }; + } + + return { data: undefined }; + }, + invalidatesTags: [invalidatesList(ReduxTag.PullRequests)] + } + ), updatePr: build.mutation< void, { diff --git a/apps/desktop/src/lib/forge/gitlab/gitlabPrService.svelte.ts b/apps/desktop/src/lib/forge/gitlab/gitlabPrService.svelte.ts index 256451fd72..48103048f7 100644 --- a/apps/desktop/src/lib/forge/gitlab/gitlabPrService.svelte.ts +++ b/apps/desktop/src/lib/forge/gitlab/gitlabPrService.svelte.ts @@ -78,8 +78,8 @@ export class GitLabPrService implements ForgePrService { return this.api.endpoints.getPr.useQuery({ number }, options); } - async merge(method: MergeMethod, number: number) { - await this.api.endpoints.mergePr.mutate({ method, number }); + async merge(method: MergeMethod, number: number, bypassRules?: boolean) { + await this.api.endpoints.mergePr.mutate({ method, number, bypassRules }); } async reopen(number: number) { @@ -141,11 +141,23 @@ function injectEndpoints(api: GitLabApi) { }, invalidatesTags: (result) => [invalidatesItem(ReduxTag.GitLabPullRequests, result?.number)] }), - mergePr: build.mutation({ - queryFn: async ({ number }, query) => { + mergePr: build.mutation< + undefined, + { number: number; method: MergeMethod; bypassRules?: boolean } + >({ + queryFn: async ({ number, bypassRules }, query) => { try { const { api, upstreamProjectId } = gitlab(query.extra); - await api.MergeRequests.merge(upstreamProjectId, number); + const options: any = {}; + + // GitLab supports bypassing merge checks with force_remove_source_branch + // and should_remove_source_branch options + if (bypassRules) { + options.skip_ci = true; + options.merge_when_pipeline_succeeds = false; + } + + await api.MergeRequests.merge(upstreamProjectId, number, options); return { data: undefined }; } catch (e: unknown) { return { error: toSerializable(e) }; diff --git a/apps/desktop/src/lib/forge/interface/forgePrService.ts b/apps/desktop/src/lib/forge/interface/forgePrService.ts index fb75240fdf..58900400e9 100644 --- a/apps/desktop/src/lib/forge/interface/forgePrService.ts +++ b/apps/desktop/src/lib/forge/interface/forgePrService.ts @@ -32,7 +32,7 @@ export interface ForgePrService { baseBranchName, upstreamName }: CreatePullRequestArgs): Promise; - merge(method: MergeMethod, prNumber: number): Promise; + merge(method: MergeMethod, prNumber: number, bypassRules?: boolean): Promise; reopen(prNumber: number): Promise; update( prNumber: number,