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
112 changes: 66 additions & 46 deletions src/mergify.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,48 @@ export function resetForNavigation() {
lastPullRequestUrl = null;
}

function injectRowIntoMergeBox() {
// New merge box — the mergebox-partial may be inside discussion-timeline-actions
// or a separate element in the page (GitHub layout varies)
const mergeBoxPartial = document.querySelector(
'[data-testid="mergebox-partial"]',
);
if (mergeBoxPartial) {
const mergeBoxContainer =
mergeBoxPartial.querySelector(".border.rounded-2");
if (mergeBoxContainer) {
mergeBoxContainer.appendChild(buildMergifyRow());
debug("Mergify section injected inside merge box container");
return true;
}
}

let detailSection = document.querySelector(
"div[class=discussion-timeline-actions]",
);
if (detailSection) {
const mergeBoxContainer = detailSection.querySelector(
".merge-pr .border.rounded-2",
);
if (mergeBoxContainer) {
mergeBoxContainer.appendChild(buildMergifyRow());
debug("Mergify section injected inside merge-pr container");
return true;
}
debug("Merge box container not found yet, waiting for render");
return false;
}

// Classic merge box
detailSection = document.querySelector(".mergeability-details");
if (detailSection) {
detailSection.insertBefore(buildMergifyRow(), detailSection.firstChild);
debug("Mergify section injected in classic merge box");
return true;
}
return false;
}

async function _tryInject() {
if (!isGitHubPullRequestPage()) {
// SPA-navigated away from a PR (e.g., back to /pulls). The
Expand All @@ -81,70 +123,48 @@ async function _tryInject() {
lastPullRequestUrl = currentUrl;
}

const hasMergifyConfiguration = await getMergifyConfigurationStatus();
if (!isMergifyEnabledOnTheRepo(hasMergifyConfiguration)) {
// Try synchronous Mergify-enabled detection first (Mergify app icon in
// DOM, or cached repo result). Only fall back to the /search fetch when
// we have no synchronous signal — that fetch is the single biggest
// contributor to first-row latency on cold loads.
let mergifyEnabled = isMergifyEnabledOnTheRepo(false);
if (!mergifyEnabled) {
const hasMergifyConfiguration = await getMergifyConfigurationStatus();
mergifyEnabled = isMergifyEnabledOnTheRepo(hasMergifyConfiguration);
}
if (!mergifyEnabled) {
debug("Mergify is not enabled on the repo");
return;
}

const _data = getPullRequestData();
await renderMergifyContext({
const contextPayload = {
org: _data.org,
repo: _data.repo,
number: Number.parseInt(_data.pull, 10),
subpath: _data.subpath,
});
};

const existingRow = document.querySelector("#mergify");
if (existingRow) {
updateMergifyRow(existingRow);
void renderMergifyContext(contextPayload);
return;
}

// Fetch queue state before first render (only once per PR page visit)
await fetchQueueStateIfNeeded();
// Inject the row immediately with state derived synchronously from the
// DOM. The queue-state and stack-context fetches run in parallel in the
// background — when fetchQueueStateIfNeeded resolves, we re-derive the
// button state and swap it in if it changed.
const injected = injectRowIntoMergeBox();

// New merge box — the mergebox-partial may be inside discussion-timeline-actions
// or a separate element in the page (GitHub layout varies)
const mergeBoxPartial = document.querySelector(
'[data-testid="mergebox-partial"]',
);
if (mergeBoxPartial) {
const mergeBoxContainer =
mergeBoxPartial.querySelector(".border.rounded-2");
if (mergeBoxContainer) {
mergeBoxContainer.appendChild(buildMergifyRow());
debug("Mergify section injected inside merge box container");
scheduleQueueStatePoll();
return;
}
}
void renderMergifyContext(contextPayload);
void fetchQueueStateIfNeeded().then(() => {
const row = document.querySelector("#mergify");
if (row) updateMergifyRow(row);
});

let detailSection = document.querySelector(
"div[class=discussion-timeline-actions]",
);
if (detailSection) {
// Fallback: look for merge box inside discussion-timeline-actions
const mergeBoxContainer = detailSection.querySelector(
".merge-pr .border.rounded-2",
);
if (mergeBoxContainer) {
mergeBoxContainer.appendChild(buildMergifyRow());
debug("Mergify section injected inside merge-pr container");
scheduleQueueStatePoll();
} else {
debug("Merge box container not found yet, waiting for render");
}
return;
}
// Classic merge box
detailSection = document.querySelector(".mergeability-details");
if (detailSection) {
detailSection.insertBefore(buildMergifyRow(), detailSection.firstChild);
debug("Mergify section injected in classic merge box");
scheduleQueueStatePoll();
return;
}
if (injected) scheduleQueueStatePoll();
}

function tryInject() {
Expand Down
108 changes: 55 additions & 53 deletions src/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { getPullRequestData, isGitHubPullRequestPage } from "./dom.js";
import { getLogoSvg, parseSvg } from "./logo.js";
import { resetStackState } from "./stacks.js";

const QUEUE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm3.75-1.5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Zm0 5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Zm0 5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5ZM3 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm-1 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>`;

const LOGS_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 4.75a.75.75 0 0 1 1.5 0v3.44l1.97 1.97a.75.75 0 1 1-1.06 1.06l-2.19-2.19a.75.75 0 0 1-.22-.53Z"/></svg>`;

export const BUTTONS = [
{
command: "refresh",
Expand Down Expand Up @@ -365,7 +369,9 @@ export function buildQueueButton(state) {
case "dequeuing":
btn = buildMergeBoxButton(
state === "queuing" ? "queue" : "dequeue",
state === "queuing" ? "Queuing…" : "Dequeuing…",
state === "queuing"
? "Adding to merge queue…"
: "Removing from merge queue…",
"Waiting for Mergify to process…",
true,
"secondary",
Expand All @@ -374,7 +380,7 @@ export function buildQueueButton(state) {
case "queued":
btn = buildMergeBoxButton(
"dequeue",
"Dequeue",
"Remove from merge queue",
"Remove the pull request from the merge queue",
false,
"danger",
Expand All @@ -384,7 +390,7 @@ export function buildQueueButton(state) {
default:
btn = buildMergeBoxButton(
"queue",
"Queue",
"Add to merge queue",
"Add the pull request to the merge queue",
false,
"primary",
Expand All @@ -411,76 +417,72 @@ export function buildMergifyRow() {

const row = document.createElement("div");
row.id = "mergify";
row.className =
"bgColor-muted borderColor-muted border-top rounded-bottom-2";
row.style.cssText =
"padding:10px 16px;border-top:1px solid var(--borderColor-default, #30363d);display:flex;align-items:center;gap:10px;";
"display:flex;align-items:center;gap:8px;padding:var(--base-size-16,16px)!important;";

const logo = document.createElement("div");
logo.style.cssText = "flex-shrink:0;display:flex;";
const svgEl = parseSvg(getLogoSvg());
svgEl.setAttribute("width", "20");
svgEl.setAttribute("height", "20");
logo.appendChild(svgEl);
row.appendChild(logo);
if (state !== "merged" && state !== "closed") {
const buttons = document.createElement("div");
buttons.style.cssText = "display:flex;gap:6px;";
buttons.appendChild(buildQueueButton(state));
for (const btn of BUTTONS) {
buttons.appendChild(
buildMergeBoxButton(
btn.command,
btn.label,
btn.tooltip,
false,
"secondary",
),
);
}
row.appendChild(buttons);
}

const info = document.createElement("div");
info.style.cssText =
"flex:1;display:flex;align-items:center;gap:6px;font-size:13px;";

const label = document.createElement("span");
label.style.fontWeight = "600";
label.textContent = "Mergify";
info.appendChild(label);

function appendDot() {
const dot = document.createElement("span");
dot.style.color = "var(--fgColor-muted, #7d8590)";
dot.textContent = "·";
info.appendChild(dot);
}
"display:flex;align-items:center;gap:12px;font-size:14px;margin-left:auto;";

function appendLink(href, text) {
appendDot();
function appendLink(href, text, iconSvg) {
const link = document.createElement("a");
link.href = href;
link.target = "_blank";
link.rel = "noopener noreferrer";
link.textContent = text;
link.style.cssText =
"color:var(--fgColor-accent, #58a6ff);text-decoration:none;font-size:12px;";
"color:var(--fgColor-accent, #58a6ff);text-decoration:none;display:inline-flex;align-items:center;gap:4px;";
link.appendChild(parseSvg(iconSvg));
link.appendChild(document.createTextNode(text));
info.appendChild(link);
}

appendLink(getMergeQueueLink(), "queue");
appendLink(getEventLogLink(), "logs");

row.appendChild(info);
appendLink(getMergeQueueLink(), "queue", QUEUE_ICON_SVG);
appendLink(getEventLogLink(), "logs", LOGS_ICON_SVG);

if (state === "merged") {
const status = document.createElement("span");
status.style.cssText =
"color:var(--fgColor-muted, #7d8590);font-size:13px;";
status.style.color = "var(--fgColor-muted, #7d8590)";
status.textContent = getMergedMessage();
row.appendChild(status);
} else if (state !== "closed") {
const buttons = document.createElement("div");
buttons.style.cssText = "display:flex;gap:6px;";

buttons.appendChild(buildQueueButton(state));
info.appendChild(status);
}

for (const btn of BUTTONS) {
buttons.appendChild(
buildMergeBoxButton(
btn.command,
btn.label,
btn.tooltip,
false,
"secondary",
),
);
}
const brand = document.createElement("a");
brand.href = "https://dashboard.mergify.com";
brand.target = "_blank";
brand.rel = "noopener noreferrer";
brand.title = "Open Mergify dashboard";
brand.style.cssText =
"display:flex;align-items:center;gap:6px;margin-left:8px;color:var(--fgColor-default, #e6edf3);text-decoration:none;font-weight:600;";
const svgEl = parseSvg(getLogoSvg());
svgEl.setAttribute("width", "20");
svgEl.setAttribute("height", "20");
brand.appendChild(svgEl);
const brandText = document.createElement("span");
brandText.textContent = "Mergify";
brand.appendChild(brandText);
info.appendChild(brand);

row.appendChild(buttons);
}
row.appendChild(info);

return row;
}
Expand Down