您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allow GitHub forks to be configured to default to self for pull request base
// ==UserScript== // @name GitHub Fork Default // @namespace https://github.com/logiclrd/GitHubForkDefault/ // @version 2025-08-01 // @description Allow GitHub forks to be configured to default to self for pull request base // @author You // @match *://*/* // @icon https://github.com/logiclrd/GitHubForkDefault/blob/main/PRIcon.png?raw=true // @license MIT // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @grant GM_getValue // @grant GM_setValue // @grant GM_getTab // @grant GM_saveTab // @grant GM.getValue // @grant GM.setValue // @grant GM.getTab // @grant GM.saveTab // ==/UserScript== (function() { 'use strict'; var config = { DefaultToSelfRepos: [""] }; async function updateConfig(defaultToSelfReposStr) { try { config.DefaultToSelfRepos = defaultToSelfReposStr.split(/ ,\n/g).filter(i => i); await GM.setValue("DefaultToSelfRepos", JSON.stringify(config.DefaultToSelfRepos)); } catch { } } try { config.DefaultToSelfRepos = JSON.parse(GM_getValue("DefaultToSelfRepos", "[]")); } catch { } var gmc = new GM_config( { "id": "GitHubPRDefaults", "title": "GitHub PR Defaults", "css": { "basic": "width: 75%; height: 25%;" }, "fields": { "DefaultToSelfRepos": { "section": "List of repositories that should default to self for pull requests", "label": "Default-to-Self Repos (one per line and/or comma-separated)", "type": "textarea", "rows": 15, "cols": 60, "default": "myusername/myreponame..." } }, "events": { "init": function() { this.set("DefaultToSelfRepos", config.DefaultToSelfRepos.join('\n')); }, "save": function() { updateConfig(this.get("DefaultToSelfRepos")); window.location.reload(); } } }); var tabData = {}; function pullComponent(path, separator) { var index = path.indexOf(separator); if (index >= 0) return [path.substring(0, index), path.substring(index + separator.length).trimStart(separator)]; else return [path, '']; } function isValidOwnerName(name) { return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(name); } function isValidRepoName(name) { return /[a-z\d.-_]+/i.test(name); } function isValidBranchName(name) { return ( new RegExp("^(?!/|.*(?:[/.]\\.|//|@\\{|\\\\))[^\\040\\177 ~^:?*[]+(?<!\\.lock)(?<![/.])$").test(name) && !/[0-9a-f]{40}/i.test(name) && // GitHub-specific !name.startsWith("refs/")); // GitHub-specific } function decodePullRequestURL(url) { if (url.host != "github.com") return null; var path = url.pathname.replace(/^\/+/g, ''); var baseOwner, baseRepo, operation, baseBranch, headOwner, headRepo, headBranch; [baseOwner, path] = pullComponent(path, '/'); [baseRepo, path] = pullComponent(path, '/'); [operation, path] = pullComponent(path, '/'); [baseBranch, path] = pullComponent(path, '...'); [headOwner, path] = pullComponent(path, ':'); if (path.indexOf(':') < 0) [headRepo, headBranch, path] = [baseRepo, path, '']; else { [headRepo, path] = pullComponent(path, ':'); [headBranch, path] = [path, '']; } if (operation !== "compare") return null; if (!isValidOwnerName(baseOwner) || !isValidRepoName(baseRepo) || !isValidBranchName(baseBranch)) return null; if (!isValidOwnerName(headOwner) || !isValidRepoName(headRepo) || !isValidBranchName(headBranch)) return null; var decoded = { Base: { Owner: baseOwner, Repo: baseRepo, Branch: baseBranch }, Head: { Owner: headOwner, Repo: headRepo, Branch: headBranch }, Params: url.search }; return decoded; } function encodePullRequestURL(pr) { return `https://github.com/${pr.Base.Owner}/${pr.Base.Repo}/compare/${pr.Base.Branch}...${pr.Head.Owner}:${pr.Head.Repo}:${pr.Head.Branch}${pr.Params}`; } function isPullRequestURL(url) { return decodePullRequestURL(url) != null; } function isBadPullRequestURL(url) { var pr = decodePullRequestURL(url); if (pr == null) return false; var baseRepoQualifiedName = pr.Base.Owner + '/' + pr.Base.Repo; var headRepoQualifiedName = pr.Head.Owner + '/' + pr.Head.Repo; if (!config.DefaultToSelfRepos.includes(headRepoQualifiedName)) return false; return baseRepoQualifiedName !== headRepoQualifiedName; } function isGoodPullRequestURL(url) { var pr = decodePullRequestURL(url); if (pr == null) return false; var baseRepoQualifiedName = pr.Base.Owner + '/' + pr.Base.Repo; var headRepoQualifiedName = pr.Head.Owner + '/' + pr.Head.Repo; if (!config.DefaultToSelfRepos.includes(headRepoQualifiedName)) return false; return baseRepoQualifiedName === headRepoQualifiedName; } function convertBadToGood(url) { var pr = decodePullRequestURL(url); if (pr == null) return url; pr.Base.Owner = pr.Head.Owner; pr.Base.Repo = pr.Head.Repo; return encodePullRequestURL(pr); } async function check() { // https://github.com/schismtracker/schismtracker/compare/master...logiclrd:schismtracker:test-pr-thinger?expand=1 var urlRaw = window.location.href; var url = URL.parse(urlRaw); var tabData = await GM.getTab(); if (!("LastURL" in tabData)) tabData.LastURL = "about:"; var lastURL = URL.parse(tabData.LastURL); tabData.LastURL = urlRaw; await GM.saveTab(tabData); if (isPullRequestURL(url)) { var configButton = document.createElement("button"); configButton.className = "btn Button--small"; configButton.innerText = "Configure Base Defaults"; configButton.onclick = () => gmc.open(); var panelCandidates = document.getElementsByClassName("range-editor"); if (panelCandidates.length < 1) alert("Couldn't insert configuration button: can't find range editor"); else { var panel = panelCandidates[0]; panel.appendChild(configButton); } } if (isBadPullRequestURL(url) && !isGoodPullRequestURL(lastURL)) window.location.href = convertBadToGood(url); } window.addEventListener("load", check); var originalPushState = window.history.pushState; window.history.pushState = function () { originalPushState.apply(window.history, arguments); setTimeout(check, 10); }; })();