Greasy Fork is available in English.

GitHub Network Ninja

Full-viewport graph with searchable commit list on "GitHub repo network" pages.

  1. // ==UserScript==
  2. // @name GitHub Network Ninja
  3. // @version 2.0
  4. // @namespace https://github.com/maliayas
  5. // @author Ali Ayas <mali@maliayas.com>
  6. // @description Full-viewport graph with searchable commit list on "GitHub repo network" pages.
  7. // @license MIT
  8. // @include https://github.com/*/*/network
  9. // @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
  10. // @require https://code.jquery.com/jquery-2.0.3.min.js
  11. // @run-at document-end
  12. // ==/UserScript==
  13.  
  14. var $canvasContainer = $(".js-network-graph-container");
  15.  
  16. if ($canvasContainer.length) {
  17. // If there is a scrollbar, we should hide it in order to get the correct window width and height.
  18. $("body").css({"overflow": "hidden"});
  19.  
  20. // Make the container fixed to 0x0 point.
  21. $canvasContainer.removeClass("position-relative").css({
  22. "position": "fixed",
  23. "left": 0,
  24. "right": 0,
  25. "bottom": "-16px",
  26. "top": 0,
  27. "z-index": "1000",
  28. "background-color": "white"
  29. });
  30.  
  31. var $canvas = $canvasContainer.find("canvas");
  32.  
  33. var $win = $(window);
  34. var winWidth = $win.width();
  35. var winHeight = $win.height();
  36.  
  37. $canvas.attr("width", winWidth);
  38. $canvas.attr("height", winHeight);
  39. }
  40.  
  41. (function initCommitFilterBox() {
  42. var config = {
  43. /**
  44. * Number of commits to fetch in each API request.
  45. */
  46. "number_of_commits_per_request": 500,
  47.  
  48. /**
  49. * Number of milliseconds to wait between API requests.
  50. */
  51. "api_request_interval": 1000,
  52.  
  53. /**
  54. * Number of maximum commits to fetch from the API.
  55. */
  56. "number_of_max_commits": 10000
  57. };
  58.  
  59. insertCss(
  60. ".commit-filter-box-mask {"
  61. + "width: 100%;"
  62. + "height: 100%;"
  63.  
  64. + "position: fixed;"
  65. + "top: 0;"
  66. + "left: 0;"
  67. + "z-index: 1001;"
  68.  
  69. + "background-color: #000000;"
  70. + "opacity: 0.5;"
  71.  
  72. + "cursor: pointer;"
  73. + "}"
  74. + ".commit-filter-button {"
  75. + "position: fixed;"
  76. + "top: 5px;"
  77. + "right: 5px;"
  78. + "z-index: 1000;"
  79. + "}"
  80. + ".commit-filter-box {"
  81. + "padding: 20px;"
  82. + "border-radius: 10px;"
  83.  
  84. + "position: fixed;"
  85. + "left: 10%;"
  86. + "right: 10%;"
  87. + "top: 10%;"
  88. + "bottom: 10%;"
  89. + "overflow: hidden;"
  90. + "z-index: 1002;"
  91.  
  92. + "background-color: #eaf5ff;"
  93. + "}"
  94. + ".commit-filter-box input {"
  95. + "margin-bottom: 10px;"
  96. + "}"
  97. + ".commit-filter-box .table-container {"
  98. + "overflow: auto;"
  99. + "}"
  100. + ".commit-filter-box .loading {"
  101. + "position: relative;"
  102. + "top: 25%;"
  103.  
  104. + "color: #96a3ae;"
  105.  
  106. + "text-align: center;"
  107. + "}"
  108. + ".commit-filter-box .loading .flash {"
  109. + "margin-top: 20px;"
  110.  
  111. + "display: inline-block;"
  112. + "}"
  113. + ".commit-filter-box table {"
  114. + "width: 100%;"
  115.  
  116. + "display: none;"
  117. + "}"
  118. + ".commit-filter-box .author {"
  119. + "white-space: nowrap;"
  120. + "}"
  121. + ".commit-filter-box .message {"
  122. + "padding-left: 10px;"
  123. + "}"
  124. + ".commit-filter-box table tr.non-merged .message {"
  125. + "font-weight: bold;"
  126. + "}"
  127. + ".commit-filter-box .date {"
  128. + "padding-right: 10px;"
  129.  
  130. + "white-space: nowrap;"
  131. + "text-align: right;"
  132. + "}"
  133. );
  134.  
  135. var $commitFilterBoxMask, $commitFilterButton, $commitFilterBox, $commitFilterInput, $commitFilterLoading, $commitFilterTable;
  136.  
  137. var numberOfTotalCommits = 0;
  138.  
  139. var repo = getCurrentRepo();
  140.  
  141. /**
  142. * This is a hash string that's specific to the repo. It's required in order to
  143. * make API request for list of commits. However, -to my knowledge- it's not
  144. * possible to get it directly from somewhere. GitHub somehow creates/calculates
  145. * it. So we'll simply steal it from GitHub's runtime. See below for more
  146. * details.
  147. */
  148. var nethash;
  149.  
  150. /*
  151. * In order to find "nethash" value, we need to hook into GitHub's API requests.
  152. * So we're creating a proxy "fetch()" method below. Whenever we find a request
  153. * that includes the "nethash" in its URL we'll save it for future usage.
  154. */
  155. var originalFetch = fetch;
  156. fetch = function (input, init) {
  157. var match;
  158.  
  159. if (typeof nethash === "undefined" && (match = /[?&]nethash=([^&]+)/.exec(input.url))) {
  160. nethash = match[1];
  161. }
  162.  
  163. // Just do what regular fetch() does.
  164. return originalFetch(input, init);
  165. };
  166.  
  167. /**
  168. * Meta data related to axises of the network graph. See `fetchNetworkMetaData()`.
  169. */
  170. var networkMetaData;
  171.  
  172. $commitFilterButton = $(
  173. "<button class='commit-filter-button btn btn-sm btn-primary'>"
  174. + "List of commits"
  175. + "</button>").appendTo("body");
  176.  
  177. $commitFilterButton.click(showCommitFilterBox);
  178.  
  179. function showCommitFilterBox() {
  180. if (typeof $commitFilterBox !== "undefined") {
  181. // It's already created before.
  182.  
  183. $commitFilterBoxMask.show();
  184. $commitFilterBox.show();
  185. return;
  186. }
  187.  
  188. $commitFilterBoxMask = $("<div class='commit-filter-box-mask'></div>").appendTo("body");
  189. $commitFilterBoxMask.click(hideCommitFilterBox);
  190.  
  191. $commitFilterBox = $(
  192. "<div class='commit-filter-box'>"
  193. + "<input type='text' class='form-control input-block' />"
  194. + "<div class='table-container'>"
  195. + "<div class='loading'>"
  196. + "<p class='h1'><span class='number'>0</span> commits fetched...</p>"
  197. + "<p class='h3'>Please wait until all the commits are fetched.</p>"
  198. + "<p class='h5'>(Limit is " + config.number_of_max_commits + " commits. You can configure it in the source.)</p>"
  199. + "</div>"
  200. + "<table></table>"
  201. + "</div>"
  202. + "</div>").appendTo("body");
  203.  
  204. // Adjust height dynamically based on the viewport height. 44 is the total
  205. // height of the input box.
  206. $commitFilterBox.find(".table-container").height($commitFilterBox.height() - 44);
  207.  
  208. $commitFilterInput = $commitFilterBox.find("input");
  209. $commitFilterLoading = $commitFilterBox.find(".loading");
  210. $commitFilterTable = $commitFilterBox.find("table");
  211.  
  212. bindFilterFunctionOnInput();
  213.  
  214. fetchNetworkMetaData();
  215.  
  216. // Close commit filter box on escape.
  217. $(document).bind("keyup", function (e) {
  218. if (e.which == 27) {
  219. hideCommitFilterBox();
  220. return false;
  221. }
  222.  
  223. return true;
  224. });
  225. }
  226.  
  227. function hideCommitFilterBox() {
  228. $commitFilterBox.hide();
  229. $commitFilterBoxMask.hide();
  230. $commitFilterInput.val("").trigger("input");
  231. }
  232.  
  233. /**
  234. * Returns "user/repo".
  235. */
  236. function getCurrentRepo() {
  237. return document.location.href.replace(/^https:\/\/(www\.)?github\.com\/([^/]+\/[^/]+)(\/.*)?$/, "$2");
  238. }
  239.  
  240. function insertCss(css) {
  241. var style = document.createElement("style");
  242. style.type = "text/css";
  243. style.innerHTML = css;
  244. document.head.appendChild(style);
  245. }
  246.  
  247. var timeoutId;
  248. function bindFilterFunctionOnInput() {
  249. $commitFilterInput.on("input", function () {
  250. window.clearTimeout(timeoutId);
  251.  
  252. timeoutId = window.setTimeout(function () {
  253. var query = $commitFilterInput.val().toLowerCase();
  254.  
  255. $commitFilterTable.find("tr").each(function () {
  256. if (query === "" || this.getAttribute("search-text").includes(query)) {
  257. this.removeAttribute("hidden");
  258.  
  259. } else {
  260. this.setAttribute("hidden", "");
  261. }
  262. });
  263. }, 1000);
  264. });
  265. }
  266.  
  267. function fetchNetworkMetaData(retry) {
  268. if (typeof retry === "undefined") {
  269. // Number of retries for failed requests.
  270. retry = 3;
  271. }
  272.  
  273. $.getJSON("https://github.com/" + repo + "/network/meta")
  274. .done(function (response) {
  275. networkMetaData = response;
  276.  
  277. fetchCommits();
  278.  
  279. }).fail(function () {
  280. retry--;
  281.  
  282. if (! retry) {
  283. $commitFilterLoading.append("<div class='flash flash-error'>Network meta data could not be fetched. You can refresh the page to retry.</div>");
  284. return;
  285. }
  286.  
  287. // Retry.
  288. window.setTimeout(function () {
  289. fetchNetworkMetaData(retry);
  290. }, config.api_request_interval);
  291. });
  292. }
  293.  
  294. function fetchCommits(offset, retry) {
  295. if (typeof offset === "undefined") {
  296. offset = 0;
  297. }
  298.  
  299. if (typeof retry === "undefined") {
  300. // Number of retries for failed requests.
  301. retry = 3;
  302. }
  303.  
  304. /*
  305. * "start" and "end" URL parameters works this way: "start=0&end=2" returns
  306. * 3 items whose indexes are 0, 1 and 2. Hence the "-1" in the calculation
  307. * of "end".
  308. */
  309. $.getJSON("https://github.com/" + repo + "/network/chunk", {
  310. "nethash" : nethash,
  311. "start" : offset,
  312. "end" : offset + config.number_of_commits_per_request - 1
  313. }).done(function (response) {
  314. numberOfTotalCommits += response.commits.length;
  315.  
  316. $commitFilterLoading.find(".number").text(numberOfTotalCommits)
  317.  
  318. // Template object.
  319. var $tr = $("<tr><td class='author'></td><td class='message'></td><td class='date'></td></tr>");
  320.  
  321. for (var i = 0; i < response.commits.length; i++) {
  322. var $newTr = $tr.clone();
  323.  
  324. var commitUrl = "https://github.com/" + getUserRepoBySpace(response.commits[i]["space"]) + "/commit/" + response.commits[i]["id"];
  325. var $commitLink = $("<a target='_blank'></a>").attr("href", commitUrl).text(response.commits[i]["message"]);
  326.  
  327. // Check if the commit exists in the upstream repo.
  328. if (getUserBySpace(response.commits[i]["space"]) != getUserBySpace(0)) {
  329. $newTr.addClass("non-merged");
  330. }
  331.  
  332. $newTr.find(".author") .text(response.commits[i]["login"] || "(" + response.commits[i]["author"] + ")"); // "login" may be empty sometimes.
  333. $newTr.find(".message") .html($commitLink);
  334. $newTr.find(".date") .text(response.commits[i]["date"]);
  335.  
  336. $commitFilterTable.prepend($newTr);
  337. }
  338.  
  339. var fetchCompleted = false;
  340.  
  341. if (numberOfTotalCommits >= config.number_of_max_commits) {
  342. // We've reached the limit (defined by us).
  343. fetchCompleted = true;
  344. }
  345.  
  346. if (response.commits.length < config.number_of_commits_per_request) {
  347. // All the commits are fetched. Let's show them.
  348. fetchCompleted = true;
  349. }
  350.  
  351. if (fetchCompleted) {
  352. $commitFilterLoading.hide();
  353. $commitFilterTable.show();
  354.  
  355. /*
  356. * Searching uses `innerText` and `innerText` requires elements to
  357. * be visible in order to work. This is why we cache them in the
  358. * beginnig when all the rows are visible.
  359. */
  360. buildSearchIndex();
  361.  
  362. $commitFilterInput.attr("placeholder", "Search in " + numberOfTotalCommits + " commits from " + networkMetaData.users.length + " repos...")
  363. $commitFilterInput.focus();
  364.  
  365. } else {
  366. // There is more...
  367.  
  368. window.setTimeout(function () {
  369. fetchCommits(offset + config.number_of_commits_per_request);
  370. }, config.api_request_interval);
  371. }
  372.  
  373. }).fail(function () {
  374. retry--;
  375.  
  376. if (! retry) {
  377. $commitFilterLoading.append("<div class='flash flash-error'>Fetch operation failed. You can refresh the page to retry.</div>");
  378. return;
  379. }
  380.  
  381. // Retry the same request.
  382. window.setTimeout(function () {
  383. fetchCommits(offset, retry);
  384. }, config.api_request_interval);
  385. });
  386. }
  387.  
  388. function buildSearchIndex() {
  389. $commitFilterTable.find("tr").each(function () {
  390. this.setAttribute("search-text", this.innerText.toLowerCase().replace(/\s+/g, " "));
  391. });
  392. }
  393.  
  394. /**
  395. * "space" values are numbers returned from the GitHub API. They represent index
  396. * of the line which a commit's point resides on in the network graph. "0" is
  397. * the top (horizontal) line, "1" is the one below it, and so on.
  398. */
  399. function getUserBySpace(space) {
  400. for (var i = networkMetaData.blocks.length - 1; i >= 0; i--) {
  401. if (space < networkMetaData.blocks[i].start) {
  402. continue;
  403. }
  404.  
  405. return networkMetaData.blocks[i].name;
  406. }
  407.  
  408. return "_user_not_found_";
  409. }
  410.  
  411. function getUserRepo(user) {
  412. for (var i = 0; i < networkMetaData.users.length; i++) {
  413. if (networkMetaData.users[i].name == user) {
  414. return networkMetaData.users[i].name + "/" + networkMetaData.users[i].repo;
  415. }
  416. }
  417.  
  418. return user + "/_repo_not_found_";
  419. }
  420.  
  421. function getUserRepoBySpace(space) {
  422. return getUserRepo(getUserBySpace(space));
  423. }
  424. })();