Greasy Fork is available in English.

Github Bors Merge

Adds a button to easily start/stop PR merge when using bors

  1. // ==UserScript==
  2. // @name Github Bors Merge
  3. // @namespace https://sanketmishra.me
  4. // @version 0.1
  5. // @description Adds a button to easily start/stop PR merge when using bors
  6. // @author Sanket Mishra
  7. // @match https://github.com/*
  8. // @match https://*.github.com/*
  9. // @icon https://github.githubassets.com/favicons/favicon-dark.png
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. "use strict";
  15.  
  16. const BORS_MERGE_COMMENT = "bors r+";
  17. const BORS_CANCEL_COMMENT = "bors r-";
  18. const BORS_BUTTON_ID = "bors-button-submit";
  19. const BORS_MERGE_BUTTON_LABEL = "Bors Merge";
  20. const BORS_CANCEL_BUTTON_LABEL = "Bors Cancel";
  21. const BORS_PENDING_PREFIX = "borspending";
  22. const INTERVAL_DURATION = 3 * 1000; // 3 sec
  23. const PULL_REQUEST_COMMENTS_PAGE_REGEX = /\/pull\/\d+$/;
  24.  
  25. const BORS_BUTTON_TYPE = "submit"; // change to "button" for testing
  26.  
  27. let intervalId = null;
  28.  
  29. function isPrCommentsPage() {
  30. return PULL_REQUEST_COMMENTS_PAGE_REGEX.test(window.location.pathname);
  31. }
  32.  
  33. function isPrOpen() {
  34. return document.getElementsByClassName("State--open").length > 0;
  35. }
  36.  
  37. function isBorsRunning() {
  38. const mergeStatusItems = [
  39. ...document.getElementsByClassName("merge-status-item"),
  40. ];
  41. return mergeStatusItems.some((item) => {
  42. const textContent = item.innerText
  43. .replaceAll(/[\W\n]+/g, "")
  44. .trim()
  45. .toLowerCase();
  46. return textContent.startsWith(BORS_PENDING_PREFIX);
  47. });
  48. }
  49.  
  50. function getExistingBorsButton() {
  51. return document.getElementById(BORS_BUTTON_ID);
  52. }
  53.  
  54. function isBorsButtonPresent() {
  55. return getExistingBorsButton() !== null;
  56. }
  57.  
  58. function getExistingBorsButtonText() {
  59. return getExistingBorsButton()?.innerText ?? "";
  60. }
  61.  
  62. function getBorsBaseButton() {
  63. const borsButton = document.createElement("button");
  64. borsButton.className = "btn-primary btn mr-1";
  65. borsButton.setAttribute("type", BORS_BUTTON_TYPE);
  66. borsButton.setAttribute("id", BORS_BUTTON_ID);
  67. return borsButton;
  68. }
  69.  
  70. function getBorsButtonLabel(isBorsAlreadyRunning) {
  71. return isBorsAlreadyRunning
  72. ? BORS_CANCEL_BUTTON_LABEL
  73. : BORS_MERGE_BUTTON_LABEL;
  74. }
  75.  
  76. function getBorsButtonCommentText(isBorsAlreadyRunning) {
  77. return isBorsAlreadyRunning ? BORS_CANCEL_COMMENT : BORS_MERGE_COMMENT;
  78. }
  79.  
  80. function addOrReplaceBorsButton(borsButton, borsButtonParent) {
  81. if (isBorsButtonPresent()) {
  82. borsButtonParent.replaceChild(
  83. borsButton,
  84. borsButtonParent.firstElementChild
  85. );
  86. } else {
  87. borsButtonParent.insertBefore(
  88. borsButton,
  89. borsButtonParent.firstElementChild
  90. );
  91. }
  92. }
  93.  
  94. function handleBorsButton() {
  95. const commentTextArea = document.getElementById("new_comment_field");
  96. const commentBoxButtonsParent = document.getElementById(
  97. "partial-new-comment-form-actions"
  98. ).firstElementChild;
  99. const commentButton = [
  100. ...commentBoxButtonsParent.getElementsByTagName("button"),
  101. ].filter((btn) => btn.innerText.toLowerCase() === "comment")[0];
  102.  
  103. if (!commentButton) {
  104. return;
  105. }
  106.  
  107. const isBorsAlreadyRunning = isBorsRunning();
  108. const borsButton = getBorsBaseButton();
  109.  
  110. borsButton.innerText = getBorsButtonLabel(isBorsAlreadyRunning);
  111. borsButton.addEventListener("click", () => {
  112. commentTextArea.value = getBorsButtonCommentText(isBorsAlreadyRunning);
  113. });
  114.  
  115. addOrReplaceBorsButton(borsButton, commentBoxButtonsParent);
  116. }
  117.  
  118. function shouldHandleBorsButton() {
  119. if (!isPrCommentsPage()) {
  120. return false;
  121. }
  122.  
  123. if (!isPrOpen()) {
  124. return false;
  125. }
  126.  
  127. if (!isBorsButtonPresent()) {
  128. return true;
  129. }
  130.  
  131. const borsButtonActualText = getExistingBorsButtonText();
  132. const borsButtonExpectedText = isBorsRunning()
  133. ? BORS_CANCEL_BUTTON_LABEL
  134. : BORS_MERGE_BUTTON_LABEL;
  135.  
  136. return borsButtonActualText !== borsButtonExpectedText;
  137. }
  138.  
  139. function execute() {
  140. if (shouldHandleBorsButton()) {
  141. handleBorsButton();
  142. }
  143. }
  144.  
  145. function runOnInterval() {
  146. if (intervalId !== null) {
  147. clearInterval(intervalId);
  148. intervalId = null;
  149. }
  150.  
  151. // Run it immediately for the first time
  152. execute();
  153.  
  154. // Setup an interval to check whether the bors button is in DOM
  155. // Add it to the DOM if not present. Do nothing if present.
  156. intervalId = setInterval(() => {
  157. execute();
  158. }, INTERVAL_DURATION);
  159. }
  160.  
  161. // Main function starts here
  162. runOnInterval();
  163. })();