Skip to content
Merged
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
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,7 @@ import {
groupTimelineEvents,
} from "#/components/details/grouped-label-event";
import { LabelPill } from "#/components/details/label-pill";
import { useMergeBypass } from "#/components/pulls/detail/use-merge-bypass";
import { formatRelativeTime } from "#/lib/format-relative-time";
import {
deleteBranch,
Expand DownExpand Up@@ -424,6 +425,7 @@ function MergeStatusCard({
baseRefName,
canUpdateBranch,
canBypassProtections,
canMerge,
} = status;

const approvedReviews = reviews.filter((r) => r.state === "APPROVED");
Expand All@@ -439,6 +441,17 @@ function MergeStatusCard({
const hasConflicts = mergeableState === "dirty";
const isMergeBlocked = mergeableState === "blocked" || mergeable === false;

const bypass = useMergeBypass({
isMergeBlocked,
canBypassProtections,
hasCheckFailures,
hasReviewIssue,
hasConflicts,
isBehind,
allChecksPassed,
totalChecks: checks.total,
});

return (
<div className="flex flex-col overflow-hidden rounded-lg border">
{/* Reviews section */}
Expand DownExpand Up@@ -490,7 +503,8 @@ function MergeStatusCard({
{/* Merge action footer */}
<MergeFooter
isMergeBlocked={isMergeBlocked}
canBypassProtections={canBypassProtections}
canMerge={canMerge}
bypass={bypass}
owner={owner}
repo={repo}
pullNumber={pullNumber}
Expand DownExpand Up@@ -990,13 +1004,15 @@ const MERGE_STRATEGIES = [

function MergeFooter({
isMergeBlocked,
canBypassProtections,
canMerge,
bypass,
owner,
repo,
pullNumber,
}: {
isMergeBlocked: boolean;
canBypassProtections: boolean;
canMerge: boolean;
bypass: ReturnType<typeof useMergeBypass>;
owner: string;
repo: string;
pullNumber: number;
Expand All@@ -1005,7 +1021,6 @@ function MergeFooter({
"squash",
);
const [isMerging, setIsMerging] = useState(false);
const [bypassChecks, setBypassChecks] = useState(false);
const queryClient = useQueryClient();

const currentStrategy =
Expand All@@ -1021,7 +1036,7 @@ function MergeFooter({
repo,
pullNumber,
mergeMethod,
bypassProtections: bypassChecks,
bypassProtections: bypass.shouldBypass,
},
});
if (result.ok) {
Expand All@@ -1037,7 +1052,8 @@ function MergeFooter({
}
};

const isDisabled = (isMergeBlocked && !bypassChecks) || isMerging;
const isDisabled =
!canMerge || (isMergeBlocked && !bypass.shouldBypass) || isMerging;

return (
<div className="flex flex-col gap-3 px-4 py-3">
Expand DownExpand Up@@ -1093,18 +1109,22 @@ function MergeFooter({
</DropdownMenu>
</div>
</div>
{isMergeBlocked && !bypassChecks && (
{!canMerge ? (
<p className="text-xs text-muted-foreground">
You don't have permission to merge this pull request.
</p>
) : isMergeBlocked && !bypass.shouldBypass ? (
<p className="text-xs text-muted-foreground">
Merging is blocked — all required conditions have not been met.
</p>
)}
) : null}
</div>
{isMergeBlocked && canBypassProtections && (
{bypass.showOption && (
<div className="flex items-center gap-2 text-xs text-yellow-600 dark:text-yellow-500">
<Checkbox
id="bypass-checks"
checked={bypassChecks}
onCheckedChange={(checked) => setBypassChecks(checked === true)}
checked={bypass.checked}
onCheckedChange={(checked) => bypass.setChecked(checked === true)}
/>
<label htmlFor="bypass-checks">
Merge without waiting for requirements to be met (bypass branch
Expand Down
43 changes: 43 additions & 0 deletions apps/dashboard/src/components/pulls/detail/use-merge-bypass.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
import { useEffect, useState } from "react";

export function useMergeBypass({
isMergeBlocked,
canBypassProtections,
hasCheckFailures,
hasReviewIssue,
hasConflicts,
isBehind,
allChecksPassed,
totalChecks,
}: {
isMergeBlocked: boolean;
canBypassProtections: boolean;
hasCheckFailures: boolean;
hasReviewIssue: boolean;
hasConflicts: boolean;
isBehind: boolean;
allChecksPassed: boolean;
totalChecks: number;
}) {
const [checked, setChecked] = useState(false);

useEffect(() => {
if (!isMergeBlocked) setChecked(false);
}, [isMergeBlocked]);

const allCriteriaMet =
!hasCheckFailures &&
!hasReviewIssue &&
!hasConflicts &&
!isBehind &&
(totalChecks === 0 || allChecksPassed);

const auto = canBypassProtections && isMergeBlocked && allCriteriaMet;

return {
shouldBypass: auto || checked,
showOption: isMergeBlocked && canBypassProtections && !allCriteriaMet,
checked,
setChecked,
};
}
8 changes: 8 additions & 0 deletions apps/dashboard/src/lib/github.functions.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3199,7 +3199,14 @@ async function computePullStatus(
behindBy = null;
}

const viewer = await getViewer(userContext ?? context);
const isViewerAuthor = pull.user?.login === viewer.login;
const canUpdateBranch =
isViewerAuthor ||
!permissions ||
permissions.push === true ||
permissions.admin === true;
const canMerge =
!permissions || permissions.push === true || permissions.admin === true;
const canBypassProtections = await getPullRequestBypassState({
branch: pull.base.ref,
Expand DownExpand Up@@ -3236,6 +3243,7 @@ async function computePullStatus(
baseRefName: pull.base.ref,
canUpdateBranch,
canBypassProtections,
canMerge,
};
}

Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/src/lib/github.types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,6 +223,7 @@ export type PullStatus = {
baseRefName: string;
canUpdateBranch: boolean;
canBypassProtections: boolean;
canMerge: boolean;
};

export type PullCommit = {
Expand Down