您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a floating widget to Nixpkgs PR pages to track which branches the PR has been merged into.
// ==UserScript== // @name Nixpkgs PR Branch Tracker // @namespace http://tampermonkey.net/ // @version 1.1 // @description Adds a floating widget to Nixpkgs PR pages to track which branches the PR has been merged into. // @license WTFPL // @author shouya // @match https://github.com/NixOS/nixpkgs/pull/* // @grant GM_setValue // @grant GM_getValue // @connect api.github.com // @run-at document-end // ==/UserScript== (function() { 'use strict'; const REPO = 'NixOS/nixpkgs'; const BRANCHES_TO_CHECK = [ "master", "nixpkgs-unstable", "nixos-unstable", ]; // --- UTILITY FUNCTIONS --- async function saveToken(token) { await GM_setValue('github_token', token); } async function getToken() { return await GM_getValue('github_token', ''); } async function getAuthHeaders() { const token = await getToken(); if (token) { return { 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json', }; } return { 'Accept': 'application/vnd.github.v3+json' }; } async function fetchPRData(prId) { const url = `https://api.github.com/repos/${REPO}/pulls/${prId}`; const headers = await getAuthHeaders(); const response = await fetch(url, { headers }); if (response.status === 404) { return { error: 'PR not found.' }; } if (response.status === 403) { return { error: 'API rate limit exceeded. Please set a token.' }; } if (response.status === 401) { return { error: 'Invalid token. Please correct it.' }; } const data = await response.json(); if (!data.merged) { return { error: 'PR is not merged.' }; } return { mergeCommitSha: data.merge_commit_sha, baseBranch: data.base?.ref, }; } async function isCommitInBranch(branch, commitSha) { const url = `https://api.github.com/repos/${REPO}/compare/${branch}...${commitSha}`; const headers = await getAuthHeaders(); const response = await fetch(url, { headers }); if (response.status === 404) { // This happens if the commit is too old or the branch is brand new return false; } const data = await response.json(); // "behind" means the branch is ahead of the commit, i.e., the commit is an ancestor. // "identical" means the branch HEAD is exactly at this commit. return data.status === 'identical' || data.status === 'behind'; } // --- UI CREATION --- function createWidget() { const widget = document.createElement('div'); widget.id = 'nixpkgs-tracker-widget'; widget.innerHTML = ` <div class="header"> <strong>Nixpkgs Branch Status</strong> </div> <div id="tracker-status-list"></div> <details> <summary>Configure Token</summary> <div class="token-area"> <input type="password" id="tracker-token-input" placeholder="Set GitHub PAT"/> <button id="tracker-token-save">Save</button> </div> </details> <div id="tracker-main-status" class="status-message">Loading PR data...</div> `; document.body.appendChild(widget); // Add styles const style = document.createElement('style'); style.innerHTML = ` #nixpkgs-tracker-widget { position: fixed; bottom: 20px; right: 20px; width: 250px; background-color: #f6f8fa; border: 1px solid #d1d5da; border-radius: 6px; padding: 12px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; font-size: 13px; z-index: 9999; box-shadow: 0 1px 15px rgba(27,31,35,.15); color: #24292e; } #nixpkgs-tracker-widget .header { font-size: 14px; border-bottom: 1px solid #e1e4e8; padding-bottom: 8px; margin-bottom: 8px; } #nixpkgs-tracker-widget .branch-status { display: flex; justify-content: space-between; padding: 4px 0; } #nixpkgs-tracker-widget .status-message { margin-top: 8px; font-style: italic; color: #586069; } #nixpkgs-tracker-widget .token-area { margin-top: 10px; display: flex; } #nixpkgs-tracker-widget #tracker-token-input { flex-grow: 1; min-width: 0; padding: 3px 6px; border: 1px solid #d1d5da; border-radius: 4px 0 0 4px; } #nixpkgs-tracker-widget #tracker-token-save { padding: 3px 8px; border: 1px solid #1b1f2326; border-left: 0; background-color: #fafbfc; cursor: pointer; border-radius: 0 4px 4px 0; } #nixpkgs-tracker-widget #tracker-token-save:hover { background-color: #f3f4f6; } `; document.head.appendChild(style); // Add event listener for the save button document.getElementById('tracker-token-save').addEventListener('click', async () => { const tokenInput = document.getElementById('tracker-token-input'); const token = tokenInput.value; await saveToken(token); tokenInput.value = ''; tokenInput.placeholder = 'Token saved!'; setTimeout(() => { tokenInput.placeholder = 'Set GitHub PAT'; // Reload checks with the new token runChecks(); }, 2000); }); } function updateStatus(branchName, status, message = '') { const element = document.getElementById(`status-${branchName}`); if (element) { let symbol = '⏳'; // Loading if (status === 'merged') symbol = '✅'; if (status === 'unmerged') symbol = '❌'; element.innerHTML = `<span>${branchName}</span><span>${symbol} ${message}</span>`; } } function setMainStatus(message) { document.getElementById('tracker-main-status').textContent = message; } // --- MAIN LOGIC --- async function runChecks() { const pathParts = window.location.pathname.split('/'); const prId = pathParts[4]; if (!prId || !/^\d+$/.test(prId)) { setMainStatus('Not a valid PR page.'); return; } // Initialize UI for branches const statusList = document.getElementById('tracker-status-list'); statusList.innerHTML = ''; // Clear previous results on re-run setMainStatus('Loading PR data...'); BRANCHES_TO_CHECK.forEach(branch => { const el = document.createElement('div'); el.className = 'branch-status'; el.id = `status-${branch}`; el.innerHTML = `<span>${branch}</span><span>⏳ Loading...</span>`; statusList.appendChild(el); }); const prData = await fetchPRData(prId); if (prData.error) { setMainStatus(prData.error); // Clear the loading state for branches BRANCHES_TO_CHECK.forEach(branch => { document.getElementById(`status-${branch}`).innerHTML = `<span>${branch}</span><span>-</span>`; }); return; } setMainStatus(`Checking commit ${prData.mergeCommitSha.slice(0, 7)}...`); // Check all branches concurrently const checks = BRANCHES_TO_CHECK.map(async (branch) => { try { const isMerged = await isCommitInBranch(branch, prData.mergeCommitSha); updateStatus(branch, isMerged ? 'merged' : 'unmerged', isMerged ? 'Merged' : 'Not Merged'); } catch (e) { updateStatus(branch, 'error', 'Error'); console.error(`Error checking branch ${branch}:`, e); } }); await Promise.all(checks); setMainStatus('Checks complete.'); } // --- SCRIPT EXECUTION --- createWidget(); runChecks(); })();