Created
October 23, 2025 23:19
-
-
Save ldthorne/c57db0d37e83af3fe977e2e0d10d53cd to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name GitHub Merge Queue Tab | |
| // @version 1.0.0 | |
| // @description Adds a "Merge queue" tab to GitHub repository navigation if merge queue is enabled | |
| // @author @ldthorne | |
| // @match https://github.com/*/* | |
| // @grant GM_xmlhttpRequest | |
| // @connect github.com | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| // Check if we're on a repository page | |
| const pathParts = window.location.pathname.split("/").filter((p) => p); | |
| if (pathParts.length < 2) return; | |
| const excludedPages = [ | |
| "settings", | |
| "users", | |
| "orgs", | |
| "marketplace", | |
| "topics", | |
| "search", | |
| "notifications", | |
| ]; | |
| if (excludedPages.includes(pathParts[0])) return; | |
| const owner = pathParts[0]; | |
| const repo = pathParts[1]; | |
| const repoKey = `${owner}/${repo}`; | |
| // Flag to prevent concurrent checks | |
| let isChecking = false; | |
| let hasAddedTab = false; | |
| let __isMergeQueueEnabled = null; | |
| // Check if merge queue is enabled | |
| async function isMergeQueueEnabled() { | |
| if (__isMergeQueueEnabled !== null) { | |
| return __isMergeQueueEnabled; | |
| } | |
| try { | |
| // Try accessing the merge queue page directly (fastest check) | |
| const mergeQueueUrl = `/${owner}/${repo}/queue`; | |
| const pageResponse = await fetch(mergeQueueUrl, { | |
| method: "HEAD", | |
| credentials: "include", | |
| }); | |
| // If we get a 200, the page exists | |
| if (pageResponse.ok) { | |
| __isMergeQueueEnabled = true; | |
| return true; | |
| } | |
| // Cache false result to avoid repeated checks | |
| __isMergeQueueEnabled = false; | |
| return false; | |
| } catch (error) { | |
| console.log("Error checking merge queue status:", error); | |
| return false; | |
| } | |
| } | |
| // Function to add the merge queue tab | |
| async function addMergeQueueTab() { | |
| // Prevent concurrent execution | |
| if (isChecking || hasAddedTab) return; | |
| // Find the navigation container | |
| const nav = document.querySelector('nav[aria-label="Repository"]'); | |
| if (!nav) return; | |
| // Check if tab already exists | |
| if (document.querySelector("[data-merge-queue-tab]")) { | |
| hasAddedTab = true; | |
| return; | |
| } | |
| // Set flag to prevent concurrent checks | |
| isChecking = true; | |
| try { | |
| // Check if merge queue is enabled | |
| const isEnabled = await isMergeQueueEnabled(); | |
| if (!isEnabled) { | |
| hasAddedTab = true; // Don't keep trying | |
| return; | |
| } | |
| // Find the tab list | |
| const tabList = nav.querySelector("ul"); | |
| if (!tabList) return; | |
| const mergeQueueUrl = `/${owner}/${repo}/queue`; | |
| // Check if we're currently on the merge queue page | |
| const isActive = window.location.pathname.includes("/queue/") || window.location.pathname.endsWith("/queue"); | |
| // Create the tab list item | |
| const tabItem = document.createElement("li"); | |
| tabItem.setAttribute("data-merge-queue-tab", "true"); | |
| tabItem.className = "d-inline-flex"; | |
| // Create the link | |
| const link = document.createElement("a"); | |
| link.href = mergeQueueUrl; | |
| link.className = `UnderlineNav-item ${isActive ? "selected" : ""}`; | |
| link.setAttribute("data-tab-item", "merge-queue-tab"); | |
| link.setAttribute("data-turbo-frame", "repo-content-turbo-frame"); | |
| if (isActive) { | |
| // find other active tab, remove the aria-current attribute from it | |
| const activeTabs = tabList.querySelectorAll(".selected"); | |
| activeTabs.forEach((tab) => { | |
| tab.removeAttribute("aria-current"); | |
| tab.classList.remove("selected"); | |
| }); | |
| link.setAttribute("aria-current", "page"); | |
| } | |
| // Add icon | |
| const iconSvg = document.createElementNS( | |
| "http://www.w3.org/2000/svg", | |
| "svg" | |
| ); | |
| iconSvg.setAttribute("aria-hidden", "true"); | |
| iconSvg.setAttribute("height", "16"); | |
| iconSvg.setAttribute("viewBox", "0 0 16 16"); | |
| iconSvg.setAttribute("width", "16"); | |
| iconSvg.setAttribute( | |
| "class", | |
| "octicon octicon-stack UnderlineNav-octicon d-none d-sm-inline" | |
| ); | |
| iconSvg.innerHTML = `<path d="M7.122.392a1.75 1.75 0 0 1 1.756 0l5.25 3.045c.54.313.872.89.872 1.514V7.25a.75.75 0 0 1-1.5 0V5.677L8.75 8.432v6.384a1 | |
| 1 0 0 1-1.502.865L.872 12.563A1.75 1.75 0 0 1 0 11.049V4.951c0-.624.332-1.2.872-1.514L7.122.392ZM7.125 1.69l4.63 2.685L7 7.133 2.245 4.375l4.63-2.685a.25.25 | |
| 0 0 1 .25 0ZM1.5 11.049V5.677l5.75 3.368v6.384L1.872 12.563a.25.25 0 0 1-.372-.215Zm13.5.177v2.225a.75.75 0 0 1-1.5 0v-2.225a.75.75 0 0 1 1.5 0Z"/>`; | |
| // Add text | |
| const text = document.createElement("span"); | |
| text.textContent = "Merge queue"; | |
| text.setAttribute("data-content", "Merge queue"); | |
| link.appendChild(iconSvg); | |
| link.appendChild(text); | |
| tabItem.appendChild(link); | |
| // Insert after Pull requests tab | |
| const pullRequestsTab = Array.from(tabList.children).find((li) => | |
| li.textContent.includes("Pull requests") | |
| ); | |
| if (pullRequestsTab) { | |
| pullRequestsTab.after(tabItem); | |
| } else { | |
| tabList.appendChild(tabItem); | |
| } | |
| console.log("Merge queue tab added successfully"); | |
| hasAddedTab = true; | |
| } finally { | |
| isChecking = false; | |
| } | |
| } | |
| // Add the tab initially | |
| addMergeQueueTab(); | |
| // Debounce function to limit observer calls | |
| let observerTimeout; | |
| function debouncedAddTab() { | |
| clearTimeout(observerTimeout); | |
| observerTimeout = setTimeout(() => { | |
| if (!hasAddedTab) { | |
| addMergeQueueTab(); | |
| } | |
| }, 500); | |
| } | |
| // Watch for navigation changes (only if we haven't added the tab yet) | |
| const observer = new MutationObserver(() => { | |
| if (!hasAddedTab) { | |
| debouncedAddTab(); | |
| } else { | |
| // Once tab is added, stop observing | |
| observer.disconnect(); | |
| } | |
| }); | |
| // Only observe the main content area, not the entire body | |
| const mainContent = document.querySelector('main') || document.body; | |
| observer.observe(mainContent, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| // Listen for popstate/navigation events | |
| window.addEventListener("popstate", () => { | |
| // Reset flags on navigation to allow tab to be added on new page | |
| hasAddedTab = false; | |
| isChecking = false; | |
| __isMergeQueueEnabled = null; | |
| setTimeout(addMergeQueueTab, 100); | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment