Add repo/branch links to GitHub's "Comparing Changes" page

This adds a link to the fork's branch on the GitHub "Comparing changes" page (AKA the "Create a Pull Request" page). See https://stackoverflow.com/questions/77623282/how-do-i-get-my-remote-branch-url-from-the-github-create-pull-request-page for more details.

  1. // ==UserScript==
  2. // @name Add repo/branch links to GitHub's "Comparing Changes" page
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.8
  5. // @description This adds a link to the fork's branch on the GitHub "Comparing changes" page (AKA the "Create a Pull Request" page). See https://stackoverflow.com/questions/77623282/how-do-i-get-my-remote-branch-url-from-the-github-create-pull-request-page for more details.
  6. // @author DanKaplanSES
  7. // @match https://github.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
  9. // @require https://code.jquery.com/jquery-3.7.1.min.js
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. // Official update link: https://greatest.deepsurf.us/en/scripts/481652/versions/new
  15.  
  16. const browseRepoHtml = `<div class="browse-repo"><a href="#" target="_blank">Browse Repo</a></div>`;
  17. const browseBranchHtml = `<div class="browse-branch"><a href="#" target="_blank">Browse Branch</a></div>`;
  18.  
  19. jQuery.noConflict(true)(function ($) {
  20. function getEvents(jQueryElement) {
  21. const element = jQueryElement.get(0);
  22. const elemEvents = $._data(element, "events");
  23. const allDocEvnts = $._data(document, "events");
  24. function equalEvents(evt1, evt2) {
  25. return evt1.guid === evt2.guid;
  26. }
  27.  
  28. for (let evntType in allDocEvnts) {
  29. if (allDocEvnts.hasOwnProperty(evntType)) {
  30. const evts = allDocEvnts[evntType];
  31. for (let i = 0; i < evts.length; i++) {
  32. if ($(element).is(evts[i].selector)) {
  33. if (elemEvents == null) {
  34. elemEvents = {};
  35. }
  36. if (!elemEvents.hasOwnProperty(evntType)) {
  37. elemEvents[evntType] = [];
  38. }
  39. if (
  40. !elemEvents[evntType].some(function (evt) {
  41. return equalEvents(evt, evts[i]);
  42. })
  43. ) {
  44. elemEvents[evntType].push(evts[i]);
  45. }
  46. }
  47. }
  48. }
  49. }
  50. return elemEvents;
  51. }
  52.  
  53. $.fn.onFirst = function (name, fn) {
  54. this.bind(name, fn);
  55. for (let i = 0, _len = this.length; i < _len; i++) {
  56. let elem = this[i];
  57. let handlers = $._data(elem).events[name.split(".")[0]];
  58. handlers.unshift(handlers.pop());
  59. }
  60. };
  61.  
  62. let hrefs = [];
  63.  
  64. function modifyWhenReady(modificationFunction, isReady) {
  65. function modifyIfReady() {
  66. if (isReady()) {
  67. modificationFunction();
  68. }
  69. }
  70. modifyIfReady();
  71. setInterval(modifyIfReady, 1500);
  72. }
  73.  
  74. function modifyRangeEditor() {
  75. hrefs = [];
  76. $(`.range-cross-repo-pair details`).wrap(
  77. `<div class="details-container"></div>`
  78. );
  79.  
  80. const tempHrefs = [];
  81. $(`.details-container`).each(function (index) {
  82. const containerType =
  83. $(this).find(`summary.branch`).length > 0 ? `branch` : `repo`;
  84. const html = containerType === `repo` ? browseRepoHtml : browseBranchHtml;
  85. const href = hrefString(containerType, index);
  86. const containerContentsHidden = $(this).find(`details`).is(`:hidden`);
  87. $(this)
  88. .css({ display: containerContentsHidden ? `none` : `inline-block` })
  89. .append(html)
  90. .css({ "text-align": `center` })
  91. .find(`a`)
  92. .attr({ id: `browse-link-${index}`, href });
  93. tempHrefs.push(href);
  94. });
  95.  
  96. $(`.range-editor .pre-mergability, .range-editor .d-inline-block`).css({
  97. "vertical-align": `top`,
  98. });
  99.  
  100. hrefs = tempHrefs; // Only assigned when the loop has finished. This ensures modifyFiles only runs when all hrefs exist.
  101. }
  102.  
  103. function hrefString(containerType, index) {
  104. switch (containerType) {
  105. case "repo":
  106. return `/` + $(`.css-truncate-target`)[index].textContent;
  107. case "branch":
  108. return (
  109. `/` +
  110. $(`.css-truncate-target`)[index - 1].textContent +
  111. `/tree/` +
  112. $(`.css-truncate-target`)[index].textContent
  113. );
  114. default:
  115. throw new Error(`Unexpected containerType: ${containerType}`);
  116. }
  117. }
  118.  
  119. function modifyFiles() {
  120. const hrefOfBranchWithChanges = hrefs[hrefs.length - 1];
  121. hrefs = [];
  122. $(`#files a.Link--primary`).each(function (index) {
  123. const hrefToFile = $(this).attr("title");
  124. const detailsMenu = $(this).parents(`.file-header`).find(`details-menu`);
  125.  
  126. const viewFileButton = detailsMenu.find(`a:contains('View file')`);
  127. const viewFileHref = hrefOfBranchWithChanges + "/" + hrefToFile;
  128. viewFileButton.replaceWith(
  129. $(
  130. `<a href="${viewFileHref}" class="pl-5 dropdown-item btn-link" target="_blank">View file</a>`
  131. )
  132. );
  133.  
  134. const editFileButton = detailsMenu.find(`button:contains('Edit file')`);
  135. const editFileHref =
  136. hrefOfBranchWithChanges.replace(/\/tree\//, `/edit/`) +
  137. "/" +
  138. hrefToFile;
  139. editFileButton.replaceWith(
  140. $(
  141. `<a href="${editFileHref}" class="pl-5 dropdown-item btn-link" target="_blank">Edit file</a>`
  142. )
  143. );
  144. });
  145. }
  146.  
  147. function resetRangeEditor() {
  148. $(`.range-cross-repo-pair details`).each(function (index) {
  149. const parent = $(this).parents(`.range-cross-repo-pair`);
  150. parent.append($(this)); // make $(this) a direct child
  151. });
  152.  
  153. $(`.details-container`).remove();
  154. }
  155.  
  156. modifyWhenReady(modifyRangeEditor, () => {
  157. return (
  158. location.pathname.indexOf(`/compare/`) !== -1 &&
  159. $(`#browse-link-0`).length === 0 &&
  160. $(`.range-cross-repo-pair details`).length > 0
  161. );
  162. });
  163.  
  164. modifyWhenReady(modifyFiles, () => {
  165. return (
  166. location.pathname.indexOf(`/compare/`) !== -1 &&
  167. $(`#files`).length > 0 &&
  168. hrefs.length > 0
  169. );
  170. });
  171.  
  172. modifyWhenReady(
  173. () => {
  174. $(`.js-toggle-range-editor-cross-repo`).onFirst(
  175. "click",
  176. resetRangeEditor
  177. );
  178. },
  179. () => {
  180. return (
  181. location.pathname.indexOf(`/compare/`) !== -1 &&
  182. getEvents($(`.js-toggle-range-editor-cross-repo`)) === undefined
  183. );
  184. }
  185. );
  186.  
  187. $(`.js-toggle-range-editor-cross-repo`).onFirst("click", resetRangeEditor);
  188. });