Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions apps/desktop/src/components/MergeButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
onclick: (method: MergeMethod, bypassRules?: boolean) => Promise<void>;
disabled?: boolean;
wide?: boolean;
tooltip?: string;
Expand All @@ -30,7 +30,13 @@
return persisted<MergeMethod>(MergeMethod.Merge, key + projectId);
}

function persistedBypassRules(projectId: string): Persisted<boolean> {
const key = 'projectMergeBypassRules';
return persisted<boolean>(false, key + projectId);
}

const action = persistedAction(projectId);
const bypassRules = persistedBypassRules(projectId);

let dropDown: ReturnType<typeof DropdownButton> | undefined;
let loading = $state(false);
Expand All @@ -47,7 +53,7 @@
onclick={async () => {
loading = true;
try {
await onclick?.($action);
await onclick?.($action, $bypassRules);
} finally {
loading = false;
}
Expand All @@ -72,5 +78,32 @@
/>
{/each}
</ContextMenuSection>
<ContextMenuSection>
<div class="bypass-checkbox-container">
<Checkbox
small
bind:checked={$bypassRules}
onchange={() => {
// The reactive store will handle the change
}}
/>
<span class="text-12 text-light bypass-label">Bypass branch protection rules</span>
</div>
</ContextMenuSection>
{/snippet}
</DropdownButton>

<style lang="postcss">
.bypass-checkbox-container {
display: flex;
align-items: center;
padding: 8px 12px;
gap: 8px;
cursor: pointer;
}

.bypass-label {
cursor: pointer;
user-select: none;
}
</style>
4 changes: 2 additions & 2 deletions apps/desktop/src/components/StackedPullRequestCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 26 additions & 17 deletions apps/desktop/src/lib/forge/github/githubPrService.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -194,23 +194,32 @@ function injectEndpoints(api: GitHubApi) {
}),
invalidatesTags: (result) => [invalidatesItem(ReduxTag.PullRequests, result?.number)]
}),
mergePr: build.mutation<void, { number: number; method: MergeMethod }>({
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<void, { number: number; method: MergeMethod; bypassRules?: boolean }>(
{
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,
{
Expand Down
22 changes: 17 additions & 5 deletions apps/desktop/src/lib/forge/gitlab/gitlabPrService.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -141,11 +141,23 @@ function injectEndpoints(api: GitLabApi) {
},
invalidatesTags: (result) => [invalidatesItem(ReduxTag.GitLabPullRequests, result?.number)]
}),
mergePr: build.mutation<undefined, { number: number; method: MergeMethod }>({
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) };
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/lib/forge/interface/forgePrService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface ForgePrService {
baseBranchName,
upstreamName
}: CreatePullRequestArgs): Promise<PullRequest>;
merge(method: MergeMethod, prNumber: number): Promise<void>;
merge(method: MergeMethod, prNumber: number, bypassRules?: boolean): Promise<void>;
reopen(prNumber: number): Promise<void>;
update(
prNumber: number,
Expand Down
Loading