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
42 changes: 31 additions & 11 deletions apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
Original file line number Diff line number Diff 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export type PullStatus = {
baseRefName: string;
canUpdateBranch: boolean;
canBypassProtections: boolean;
canMerge: boolean;
};

export type PullCommit = {
Expand Down