您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Full-viewport graph with searchable commit list on "GitHub repo network" pages.
// ==UserScript== // @name GitHub Network Ninja // @version 2.0 // @namespace https://github.com/maliayas // @author Ali Ayas <[email protected]> // @description Full-viewport graph with searchable commit list on "GitHub repo network" pages. // @license MIT // @include https://github.com/*/*/network // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAAB8hJR8iJiAiJiAjJyIkKCMlKSMmKSQmKiUnKyUoKyYoLCcqLSgqLSgrLiksLyosMCsuMCsuMSwuMCwuMS4wMy8xNC8xNTEyNjI1NzM1ODU4Oj0%2FQj5AQz9AREFCRUJDR0JER0dITEhJTElLTk9QU09RVFBRVFFSVVJUVlJUV1RWWFtcXltcX1xdYF1eYF5eYV5fYmFiZWNkZmRlZ2dpa2prbWprbmtsbmtsb2xucG5vcW9wc3JydXt7fnx8fnx9f3x%2Bf35%2BgH5%2FgYCAg4GChIKDhYOEhoaGiIaHiYeIioiJi4mKi4qLjY6OkI6PkZCQkpGRk5GSk5GSlJWVl5aWmJqanJycnp6dn5%2Ben56foKGhoqOjpKOjpaOkpaeoqaurrK2sra2trq2ur66vsLCwsba0tbW0tra2t7i4uby7vLy8vcHBwsTCw8TDxMXExcXFxsbGx8rKysvKy83MzM7Mzc3NztTS0tXV1dXV1tfW19jX19zb293c3N7d3t%2Fe39%2Ff4ODe3uLg4OLh4ePi4uTi4uXj4%2BXk5Obl5efm5ujl5ejm5unn5%2Bro6Ovp6evq6uzq6uzr6%2B3s7O7t7e%2Fu7u%2Fu7%2FDu7vHv7%2FHw8PLx8fLy8vPz8%2FTz8%2FX09PX19fb29vf39%2Fj39%2Fj4%2BPn5%2Bfr6%2Bvv7%2B%2Fz8%2FP39%2Ff7%2B%2Fv%2F%2F%2FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC0HIHQAAAEAdFJOU%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwBT9wclAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4xLjFjKpxLAAACEElEQVQ4T4WQCXPSQBiGiyQVL4g3nvVurVLvq94K3lpq61lvUcFirfdttbWF2kQJVIUIQbLBxl%2B5ft8mDEnGGZ%2BZZHff99nsThrof7ALetX4Axi6bgVIXdCNiVfXTx883BH7YBh1pSYQI9u1fI7F6mvqb80qLEGvXl4000ZTYoKYjSmQb1sEF2HNNJhAci1WbKNdY6egUP61IRAQhICN2fCEq2VL0M%2FC8mghvg6GBUF4besvh2BI4CdAUN%2FP9fv9twmp9L5UCBl%2F%2FogQchyiJkVlgnYAFv6Y5uAEZlfgjAb6czw4A%2BiuONiNWXMFBSUxHVgslx28noXpgAKCemoa0KW62InpnQIIpR04TZZcnMe0swRCMTQVeFx0cRXTSBHvEJoC3FdcdGMawTsUtvuAcwUXezDtzIOQPzkZWPsj70BaiGlMBiETb0RufndwDDPfWxS%2BSvN5QLiRqyOfacRsTY796uw%2BXri1medbLzwRs8CnZMcyrHm%2BJ8ME6U2AW%2FFwL8dxwXco9PEwRZaIEhOoHOW4XQ9aOW6%2FzFhl9vw96JkwKrV5ff0veuNPM4xNXkY4UxPo2GCzd1402ffsC2Mj69ulNHZMoOnBrZOAIxKjDaZcRGS9JdAR8WLQ4wmLjPUez9K7YspsLIGmxI%2FRlZdM4VBLTyo9ahU1gdKhsTqfh63QLuBNRrAdHqrtRuzCP6D0L3BZI7iJ8A14AAAAAElFTkSuQmCC // @require https://code.jquery.com/jquery-2.0.3.min.js // @run-at document-end // ==/UserScript== var $canvasContainer = $(".js-network-graph-container"); if ($canvasContainer.length) { // If there is a scrollbar, we should hide it in order to get the correct window width and height. $("body").css({"overflow": "hidden"}); // Make the container fixed to 0x0 point. $canvasContainer.removeClass("position-relative").css({ "position": "fixed", "left": 0, "right": 0, "bottom": "-16px", "top": 0, "z-index": "1000", "background-color": "white" }); var $canvas = $canvasContainer.find("canvas"); var $win = $(window); var winWidth = $win.width(); var winHeight = $win.height(); $canvas.attr("width", winWidth); $canvas.attr("height", winHeight); } (function initCommitFilterBox() { var config = { /** * Number of commits to fetch in each API request. */ "number_of_commits_per_request": 500, /** * Number of milliseconds to wait between API requests. */ "api_request_interval": 1000, /** * Number of maximum commits to fetch from the API. */ "number_of_max_commits": 10000 }; insertCss( ".commit-filter-box-mask {" + "width: 100%;" + "height: 100%;" + "position: fixed;" + "top: 0;" + "left: 0;" + "z-index: 1001;" + "background-color: #000000;" + "opacity: 0.5;" + "cursor: pointer;" + "}" + ".commit-filter-button {" + "position: fixed;" + "top: 5px;" + "right: 5px;" + "z-index: 1000;" + "}" + ".commit-filter-box {" + "padding: 20px;" + "border-radius: 10px;" + "position: fixed;" + "left: 10%;" + "right: 10%;" + "top: 10%;" + "bottom: 10%;" + "overflow: hidden;" + "z-index: 1002;" + "background-color: #eaf5ff;" + "}" + ".commit-filter-box input {" + "margin-bottom: 10px;" + "}" + ".commit-filter-box .table-container {" + "overflow: auto;" + "}" + ".commit-filter-box .loading {" + "position: relative;" + "top: 25%;" + "color: #96a3ae;" + "text-align: center;" + "}" + ".commit-filter-box .loading .flash {" + "margin-top: 20px;" + "display: inline-block;" + "}" + ".commit-filter-box table {" + "width: 100%;" + "display: none;" + "}" + ".commit-filter-box .author {" + "white-space: nowrap;" + "}" + ".commit-filter-box .message {" + "padding-left: 10px;" + "}" + ".commit-filter-box table tr.non-merged .message {" + "font-weight: bold;" + "}" + ".commit-filter-box .date {" + "padding-right: 10px;" + "white-space: nowrap;" + "text-align: right;" + "}" ); var $commitFilterBoxMask, $commitFilterButton, $commitFilterBox, $commitFilterInput, $commitFilterLoading, $commitFilterTable; var numberOfTotalCommits = 0; var repo = getCurrentRepo(); /** * This is a hash string that's specific to the repo. It's required in order to * make API request for list of commits. However, -to my knowledge- it's not * possible to get it directly from somewhere. GitHub somehow creates/calculates * it. So we'll simply steal it from GitHub's runtime. See below for more * details. */ var nethash; /* * In order to find "nethash" value, we need to hook into GitHub's API requests. * So we're creating a proxy "fetch()" method below. Whenever we find a request * that includes the "nethash" in its URL we'll save it for future usage. */ var originalFetch = fetch; fetch = function (input, init) { var match; if (typeof nethash === "undefined" && (match = /[?&]nethash=([^&]+)/.exec(input.url))) { nethash = match[1]; } // Just do what regular fetch() does. return originalFetch(input, init); }; /** * Meta data related to axises of the network graph. See `fetchNetworkMetaData()`. */ var networkMetaData; $commitFilterButton = $( "<button class='commit-filter-button btn btn-sm btn-primary'>" + "List of commits" + "</button>").appendTo("body"); $commitFilterButton.click(showCommitFilterBox); function showCommitFilterBox() { if (typeof $commitFilterBox !== "undefined") { // It's already created before. $commitFilterBoxMask.show(); $commitFilterBox.show(); return; } $commitFilterBoxMask = $("<div class='commit-filter-box-mask'></div>").appendTo("body"); $commitFilterBoxMask.click(hideCommitFilterBox); $commitFilterBox = $( "<div class='commit-filter-box'>" + "<input type='text' class='form-control input-block' />" + "<div class='table-container'>" + "<div class='loading'>" + "<p class='h1'><span class='number'>0</span> commits fetched...</p>" + "<p class='h3'>Please wait until all the commits are fetched.</p>" + "<p class='h5'>(Limit is " + config.number_of_max_commits + " commits. You can configure it in the source.)</p>" + "</div>" + "<table></table>" + "</div>" + "</div>").appendTo("body"); // Adjust height dynamically based on the viewport height. 44 is the total // height of the input box. $commitFilterBox.find(".table-container").height($commitFilterBox.height() - 44); $commitFilterInput = $commitFilterBox.find("input"); $commitFilterLoading = $commitFilterBox.find(".loading"); $commitFilterTable = $commitFilterBox.find("table"); bindFilterFunctionOnInput(); fetchNetworkMetaData(); // Close commit filter box on escape. $(document).bind("keyup", function (e) { if (e.which == 27) { hideCommitFilterBox(); return false; } return true; }); } function hideCommitFilterBox() { $commitFilterBox.hide(); $commitFilterBoxMask.hide(); $commitFilterInput.val("").trigger("input"); } /** * Returns "user/repo". */ function getCurrentRepo() { return document.location.href.replace(/^https:\/\/(www\.)?github\.com\/([^/]+\/[^/]+)(\/.*)?$/, "$2"); } function insertCss(css) { var style = document.createElement("style"); style.type = "text/css"; style.innerHTML = css; document.head.appendChild(style); } var timeoutId; function bindFilterFunctionOnInput() { $commitFilterInput.on("input", function () { window.clearTimeout(timeoutId); timeoutId = window.setTimeout(function () { var query = $commitFilterInput.val().toLowerCase(); $commitFilterTable.find("tr").each(function () { if (query === "" || this.getAttribute("search-text").includes(query)) { this.removeAttribute("hidden"); } else { this.setAttribute("hidden", ""); } }); }, 1000); }); } function fetchNetworkMetaData(retry) { if (typeof retry === "undefined") { // Number of retries for failed requests. retry = 3; } $.getJSON("https://github.com/" + repo + "/network/meta") .done(function (response) { networkMetaData = response; fetchCommits(); }).fail(function () { retry--; if (! retry) { $commitFilterLoading.append("<div class='flash flash-error'>Network meta data could not be fetched. You can refresh the page to retry.</div>"); return; } // Retry. window.setTimeout(function () { fetchNetworkMetaData(retry); }, config.api_request_interval); }); } function fetchCommits(offset, retry) { if (typeof offset === "undefined") { offset = 0; } if (typeof retry === "undefined") { // Number of retries for failed requests. retry = 3; } /* * "start" and "end" URL parameters works this way: "start=0&end=2" returns * 3 items whose indexes are 0, 1 and 2. Hence the "-1" in the calculation * of "end". */ $.getJSON("https://github.com/" + repo + "/network/chunk", { "nethash" : nethash, "start" : offset, "end" : offset + config.number_of_commits_per_request - 1 }).done(function (response) { numberOfTotalCommits += response.commits.length; $commitFilterLoading.find(".number").text(numberOfTotalCommits) // Template object. var $tr = $("<tr><td class='author'></td><td class='message'></td><td class='date'></td></tr>"); for (var i = 0; i < response.commits.length; i++) { var $newTr = $tr.clone(); var commitUrl = "https://github.com/" + getUserRepoBySpace(response.commits[i]["space"]) + "/commit/" + response.commits[i]["id"]; var $commitLink = $("<a target='_blank'></a>").attr("href", commitUrl).text(response.commits[i]["message"]); // Check if the commit exists in the upstream repo. if (getUserBySpace(response.commits[i]["space"]) != getUserBySpace(0)) { $newTr.addClass("non-merged"); } $newTr.find(".author") .text(response.commits[i]["login"] || "(" + response.commits[i]["author"] + ")"); // "login" may be empty sometimes. $newTr.find(".message") .html($commitLink); $newTr.find(".date") .text(response.commits[i]["date"]); $commitFilterTable.prepend($newTr); } var fetchCompleted = false; if (numberOfTotalCommits >= config.number_of_max_commits) { // We've reached the limit (defined by us). fetchCompleted = true; } if (response.commits.length < config.number_of_commits_per_request) { // All the commits are fetched. Let's show them. fetchCompleted = true; } if (fetchCompleted) { $commitFilterLoading.hide(); $commitFilterTable.show(); /* * Searching uses `innerText` and `innerText` requires elements to * be visible in order to work. This is why we cache them in the * beginnig when all the rows are visible. */ buildSearchIndex(); $commitFilterInput.attr("placeholder", "Search in " + numberOfTotalCommits + " commits from " + networkMetaData.users.length + " repos...") $commitFilterInput.focus(); } else { // There is more... window.setTimeout(function () { fetchCommits(offset + config.number_of_commits_per_request); }, config.api_request_interval); } }).fail(function () { retry--; if (! retry) { $commitFilterLoading.append("<div class='flash flash-error'>Fetch operation failed. You can refresh the page to retry.</div>"); return; } // Retry the same request. window.setTimeout(function () { fetchCommits(offset, retry); }, config.api_request_interval); }); } function buildSearchIndex() { $commitFilterTable.find("tr").each(function () { this.setAttribute("search-text", this.innerText.toLowerCase().replace(/\s+/g, " ")); }); } /** * "space" values are numbers returned from the GitHub API. They represent index * of the line which a commit's point resides on in the network graph. "0" is * the top (horizontal) line, "1" is the one below it, and so on. */ function getUserBySpace(space) { for (var i = networkMetaData.blocks.length - 1; i >= 0; i--) { if (space < networkMetaData.blocks[i].start) { continue; } return networkMetaData.blocks[i].name; } return "_user_not_found_"; } function getUserRepo(user) { for (var i = 0; i < networkMetaData.users.length; i++) { if (networkMetaData.users[i].name == user) { return networkMetaData.users[i].name + "/" + networkMetaData.users[i].repo; } } return user + "/_repo_not_found_"; } function getUserRepoBySpace(space) { return getUserRepo(getUserBySpace(space)); } })();