您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays GitHub forks ordered by stars, and with additional information and automatic filters.
// ==UserScript== // @name Useful Forks // @author payne911, odnar-dev // @version 1.8 // @license MIT // @namespace https://github.com/community-plugins/Userscripts // @description Displays GitHub forks ordered by stars, and with additional information and automatic filters. // @match *://github.com/*/* // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @require https://code.jquery.com/jquery-3.5.1.min.js // @icon https://useful-forks.github.io/assets/useful-forks-logo.png // @homepageURL https://github.com/community-plugins/Userscripts/tree/main/Useful%20Forks // @supportURL https://github.com/community-plugins/Userscripts/labels/Useful%20Forks // ==/UserScript== (function() { let GITHUB_ACCESS_TOKEN = GM_getValue('GITHUB_ACCESS_TOKEN') GM_registerMenuCommand("Set Github Access Token", setPersonalToken) GM_registerMenuCommand("Generate New Access Token", newPersonalToken); function setPersonalToken(){ var mess = "Personal Access Token"; var caseShow = GITHUB_ACCESS_TOKEN; var getpersonalToken = prompt(mess, caseShow); GITHUB_ACCESS_TOKEN = (getpersonalToken === null? GITHUB_ACCESS_TOKEN : getpersonalToken) GM_setValue("GITHUB_ACCESS_TOKEN", GITHUB_ACCESS_TOKEN) } function newPersonalToken(){ let tabControl = GM_openInTab("https://github.com/settings/tokens/new?description=useful-forks%20(no%20scope%20required)") tabControl.onclose = () => setPersonalToken(); } function valid(string) { return string && string.length > 0; } const UF_ID_WRAPPER = 'useful_forks_wrapper'; const UF_ID_TITLE = 'useful_forks_title'; const UF_ID_MSG = 'useful_forks_msg'; const UF_ID_DATA = 'useful_forks_data'; const UF_ID_TABLE = 'useful_forks_table'; const svg_literal_fork = '<svg class="octicon octicon-repo-forked v-align-text-bottom" viewBox="0 0 10 16" width="10" height="16" aria-hidden="true" role="img"><title>Amount of forks, or name of the repository</title><path fill-rule="evenodd" d="M8 1a1.993 1.993 0 00-1 3.72V6L5 8 3 6V4.72A1.993 1.993 0 002 1a1.993 1.993 0 00-1 3.72V6.5l3 3v1.78A1.993 1.993 0 005 15a1.993 1.993 0 001-3.72V9.5l3-3V4.72A1.993 1.993 0 008 1zM2 4.2C1.34 4.2.8 3.65.8 3c0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2zm3 10c-.66 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2zm3-10c-.66 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2z"></path></svg>'; const svg_literal_star = '<svg class="octicon octicon-star v-align-text-bottom" viewBox="0 0 14 16" width="14" height="16" aria-label="star" role="img"><title>Amount of stars</title><path fill-rule="evenodd" d="M14 6l-4.9-.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67 14 7 11.67 11.33 14l-.93-4.74L14 6z"></path></svg>'; const svg_literal_date = '<svg class="octicon octicon-history text-gray" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" role="img"><title>Date of the most recent push in ANY branch of the repository</title><path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"></path></svg>'; const UF_MSG_HEADER = "<b>Useful forks</b>"; const UF_MSG_NO_FORKS = "No one forked this specific repository."; const UF_MSG_SCANNING = "Currently scanning all the forks."; const UF_MSG_ERROR = "There seems to have been an error while scanning forks."; const UF_MSG_EMPTY_FILTER = "All the forks have been filtered out: you can now rest easy!"; const UF_MSG_API_RATE = "<b>Exceeded GitHub API rate-limits.</b>"; const UF_TABLE_SEPARATOR = "|"; const UF_MSG_ACCESS_TOKEN = 'You need to provide a personal Access Token.<br> If you don\'t already have one, you can create one now by clicking <a href="https://github.com/settings/tokens/new?description=useful-forks%20(no%20scope%20required)" target="_blank">here</a>'; const FORKS_PER_PAGE = 100; // enforced by GitHub API let REQUESTS_COUNTER = 0; // to know when it's over function allRequestsAreDone() { return REQUESTS_COUNTER <= 0; } function checkIfAllRequestsAreDone() { if (allRequestsAreDone()) { sortTable(); } } function getOnlyDate(full) { return full.split('T')[0]; } function extract_username_from_fork(combined_name) { return combined_name.split('/')[0]; } function badge_width(number) { return 70 * number.toString().length; // magic number 70 extracted from analyzing 'shields.io' } /** Credits to https://shields.io/ */ function ahead_badge(amount) { return '<svg xmlns="http://www.w3.org/2000/svg" width="88" height="24" role="img"><title>How far ahead this fork\'s default branch is compared to its parent\'s default branch</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="88" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="43" height="18" fill="#555"/><rect x="43" width="45" height="18" fill="#007ec6"/><rect width="88" height="18" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="225" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">ahead</text><text x="225" y="130" transform="scale(.1)" fill="#fff" textLength="330">ahead</text><text x="645" y="130" transform="scale(.1)" fill="#fff" textLength="' + badge_width(amount) + '">' + amount + '</text></g></svg>'; } /** Credits to https://shields.io/ */ function behind_badge(amount) { const color = amount === 0 ? '#4c1' : '#007ec6'; // green only when not behind, blue otherwise return '<svg xmlns="http://www.w3.org/2000/svg" width="92" height="24" role="img"><title>How far behind this fork\'s default branch is compared to its parent\'s default branch</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="92" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="47" height="18" fill="#555"/><rect x="47" width="45" height="18" fill="'+ color +'"/><rect width="92" height="18" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="245" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="370">behind</text><text x="245" y="130" transform="scale(.1)" fill="#fff" textLength="370">behind</text><text x="685" y="130" transform="scale(.1)" fill="#fff" textLength="' + badge_width(amount) + '">' + amount + '</text></g></svg>'; } function getElementById_$(id) { return $('#' + id); } function isEmpty(aList) { return (!aList || aList.length === 0); } function setMsg(msg) { getElementById_$(UF_ID_MSG).html(msg); } function clearMsg() { setMsg(""); } function getTableBody() { return getElementById_$(UF_ID_TABLE).find($("tbody")); } function getTdValue(rows, index, col) { return Number(rows.item(index).getElementsByTagName('td').item(col).getAttribute("value")); } function sortTable() { sortTableColumn(UF_ID_TABLE, 1); } /** 'sortColumn' index starts at 0. https://stackoverflow.com/a/37814596/9768291 */ function sortTableColumn(table_id, sortColumn){ let tableData = document.getElementById(table_id).getElementsByTagName('tbody').item(0); let rows = tableData.getElementsByTagName('tr'); for(let i = 0; i < rows.length - 1; i++) { for(let j = 0; j < rows.length - (i + 1); j++) { if(getTdValue(rows, j, sortColumn) < getTdValue(rows, j+1, sortColumn)) { tableData.insertBefore(rows.item(j+1), rows.item(j)); } } } } /** The secondary request which appends the badges. */ function commits_count(request, table_body, table_row, pushed_at) { return () => { const response = JSON.parse(request.responseText); if (response.total_commits === 0) { table_row.remove(); if (table_body.children().length === 0) { setMsg(UF_MSG_EMPTY_FILTER); } } else { table_row.append( $('<td>').html(UF_TABLE_SEPARATOR), $('<td>', {class: "uf_badge"}).html(ahead_badge(response.ahead_by)), $('<td>').html(UF_TABLE_SEPARATOR), $('<td>', {class: "uf_badge"}).html(behind_badge(response.behind_by)), $('<td>').html(UF_TABLE_SEPARATOR + svg_literal_date + ' ' + pushed_at) ); } /* Detection of final request. */ REQUESTS_COUNTER--; checkIfAllRequestsAreDone(); } } /** To remove erroneous repos. */ function commits_count_failure(table_row) { return () => { table_row.remove(); /* Detection of final request. */ REQUESTS_COUNTER--; checkIfAllRequestsAreDone(); } } /** To use the Access Token with a request. */ function authenticatedRequestHeaderFactory(url) { let request = new XMLHttpRequest(); request.open('GET', url); request.setRequestHeader("Accept", "application/vnd.github.v3+json"); request.setRequestHeader("Authorization", "token " + GITHUB_ACCESS_TOKEN); return request; } /** Defines the default behavior of a request. */ function onreadystatechangeFactory(xhr, successFn, failureFn) { return () => { if (xhr.readyState === 4) { if (xhr.status === 200) { successFn(); } else if (xhr.status === 403) { console.warn('Looks like the rate-limit was exceeded.'); setMsg(UF_MSG_API_RATE); } else { console.warn('GitHub API returned status:', xhr.status); failureFn(); } } else { // Request is still in progress } }; } /** Fills the first part of the rows. */ function build_fork_element_html(table_body, combined_name, num_stars, num_forks) { const NEW_ROW = $('<tr>', {id: extract_username_from_fork(combined_name), class: "useful_forks_repo"}); table_body.append( NEW_ROW.append( $('<td>').html(svg_literal_fork + ` <a href="https://github.com/${combined_name}" target="_blank" rel="noopener noreferrer">${combined_name}</a>`), $('<td>').html(UF_TABLE_SEPARATOR + svg_literal_star + ' x ' + num_stars).attr("value", num_stars), $('<td>').html(UF_TABLE_SEPARATOR + svg_literal_fork + ' x ' + num_forks).attr("value", num_forks) ) ); return NEW_ROW; } /** Prepares, appends, and updates dynamically a table row. */ function add_fork_elements(forkdata_array, user, repo, parentDefaultBranch) { if (isEmpty(forkdata_array)) return; clearMsg(); let table_body = getTableBody(); for (let i = 0; i < forkdata_array.length; ++i) { const currFork = forkdata_array[i]; /* Basic data (stars, watchers, forks). */ const NEW_ROW = build_fork_element_html(table_body, currFork.full_name, currFork.stargazers_count, currFork.forks_count); /* Commits diff data (ahead/behind). */ const API_REQUEST_URL = `https://api.github.com/repos/${user}/${repo}/compare/${parentDefaultBranch}...${extract_username_from_fork(currFork.full_name)}:${currFork.default_branch}`; let request = authenticatedRequestHeaderFactory(API_REQUEST_URL); request.onreadystatechange = onreadystatechangeFactory(request, commits_count(request, table_body, NEW_ROW, getOnlyDate(currFork.pushed_at)), commits_count_failure(NEW_ROW)); request.send(); /* Forks of forks. */ if (currFork.forks_count > 0) { request_fork_page(1, currFork.owner.login, currFork.name, currFork.default_branch); } } } /** Paginated request. Pages index start at 1. */ function request_fork_page(page_number, user, repo, defaultBranch) { const API_REQUEST_URL = `https://api.github.com/repos/${user}/${repo}/forks?sort=stargazers&per_page=${FORKS_PER_PAGE}&page=${page_number}`; let request = authenticatedRequestHeaderFactory(API_REQUEST_URL); request.onreadystatechange = onreadystatechangeFactory(request, () => { const response = JSON.parse(request.responseText); /* On empty response (repo has not been forked). */ if (isEmpty(response)) return; REQUESTS_COUNTER += response.length; // to keep track of when the query ends /* Pagination (beyond 100 forks). */ const link_header = request.getResponseHeader("link"); if (link_header) { let contains_next_page = link_header.indexOf('>; rel="next"'); if (contains_next_page !== -1) { request_fork_page(++page_number, user, repo, defaultBranch); } } sortTable(); /* Populate the table. */ add_fork_elements(response, user, repo, defaultBranch); }, () => { setMsg(UF_MSG_ERROR); checkIfAllRequestsAreDone(); }); request.send(); } /** Updates header with Queried Repo info, and initiates recursive forks search */ function initial_request(user, repo) { const API_REQUEST_URL = `https://api.github.com/repos/${user}/${repo}`; let request = authenticatedRequestHeaderFactory(API_REQUEST_URL); request.onreadystatechange = onreadystatechangeFactory(request, () => { const response = JSON.parse(request.responseText); if (isEmpty(response)) return; if (response.forks_count > 0) { request_fork_page(1, user, repo, response.default_branch); } else { setMsg(UF_MSG_NO_FORKS); enableQueryFields(); } }, () => setMsg(UF_MSG_ERROR) ); request.send(); } function prepare_display() { $('#network').prepend( $('<div>', {id: UF_ID_WRAPPER, class: "float-right"}).append( $('<h4>', {id: UF_ID_TITLE, html: UF_MSG_HEADER}), $('<div>', {id: UF_ID_MSG, html: UF_MSG_SCANNING}), $('<div>', {id: UF_ID_DATA}).append( $('<table>', {id: UF_ID_TABLE}).append( $('<tbody>') ) ) ) ); } /** To determine if Dark Mode is enabled. */ function getGitHubTheme() { let colorMode = document.querySelector('[data-color-mode]')?.dataset.colorMode; if (colorMode === 'dark') { return "dark"; } else if (colorMode === 'auto') { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return "dark"; } } return "light"; // default } function add_css() { const GITHUB_THEME = getGitHubTheme(); const TR_HOVER_COLOR = GITHUB_THEME === "dark" ? '#2f353e' : '#e2e2e2'; const TR_BG_COLOR = GITHUB_THEME === "dark" ? '#161b22' : '#f5f5f5'; const ADDITIONAL_CSS = ` .uf_badge svg { display: table-cell; padding-top: 3px; } tr:hover {background-color: ${TR_HOVER_COLOR} !important;} tr:nth-child(even) {background-color: ${TR_BG_COLOR};} #${UF_ID_MSG} {color: red;} `; let styleSheet = document.createElement('style'); styleSheet.type = "text/css"; styleSheet.innerText = ADDITIONAL_CSS; document.head.appendChild(styleSheet); } /** Entry point. */ function init() { const pathComponents = window.location.pathname.split('/'); if (pathComponents.length >= 3) { if (pathComponents[4] === "members") { const user = pathComponents[1], repo = pathComponents[2]; add_css(); prepare_display(); // only call if GITHUB_ACCESS_TOKEN has been set up if (valid(GITHUB_ACCESS_TOKEN)) { initial_request(user, repo); } else { setMsg(UF_MSG_ACCESS_TOKEN); } } } } init(); //When navigating between Insights pages, URL is manipulated through PJAX. document.addEventListener('pjax:end', init); document.addEventListener('turbo:render', init); })();