GitLab auto reviewers

Automatically add reviewers to GitLab merge requests

  1. // ==UserScript==
  2. // @name GitLab auto reviewers
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.4
  5. // @description Automatically add reviewers to GitLab merge requests
  6. // @author Mohammad Sh
  7. // @match https://gitlab.com/*/*/-/merge_requests/*/*
  8. // @match https://gitlab.com/*/*/-/merge_requests/new*
  9. // @match https://gitlab.com/*/merge_requests/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=gitlab.com
  11. // @grant none
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // ===== Configuration =====
  19. // List of reviewers to add (usernames)
  20. const backendReviewers = [
  21. 'mshabibR365',
  22. ];
  23. // Timing configuration (in milliseconds)
  24. const TIMING = {
  25. SHORT_DELAY: 200, // Short delay for UI updates
  26. MAX_WAIT_TIME: 5000, // Maximum wait time for search results
  27. };
  28.  
  29. // CSS selectors for GitLab UI elements
  30. const SELECTORS = {
  31. REVIEWER_BLOCK: '.block.reviewer',
  32. DROPDOWN: '.reviewers-dropdown.gl-ml-auto.gl-new-dropdown',
  33. DROPDOWN_BUTTON: 'button',
  34. SEARCH_INPUT: '.gl-listbox-search-input',
  35. DROPDOWN_ITEM: '.gl-new-dropdown-item',
  36. USERNAME_ELEMENT: '.gl-text-subtle',
  37. ASSIGNEE_BLOCK: '.block.assignee'
  38. };
  39.  
  40. // ===== Helper Functions =====
  41. /**
  42. * Sleep for a specified amount of time
  43. * @param {number} ms - Time to sleep in milliseconds
  44. * @returns {Promise} - Promise that resolves after the specified time
  45. */
  46. const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  47. /**
  48. * Log a message with a timestamp
  49. * @param {string} message - Message to log
  50. * @param {string} type - Log type (log, error, warn)
  51. */
  52. const log = (message, type = 'log') => {
  53. const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
  54. console[type](`[${timestamp}] GitLab Auto Reviewers: ${message}`);
  55. };
  56. /**
  57. * Find an element in the DOM
  58. * @param {string} selector - CSS selector
  59. * @returns {Element|null} - Found element or null
  60. */
  61. const findElement = (selector) => {
  62. const element = document.querySelector(selector);
  63. if (!element) {
  64. log(`Element not found: ${selector}`, 'error');
  65. }
  66. return element;
  67. };
  68. /**
  69. * Click on the reviewers dropdown button
  70. * @returns {Promise<boolean>} - True if successful, false otherwise
  71. */
  72. const clickReviewersDropdown = async () => {
  73. const dropdown = document.querySelector(SELECTORS.DROPDOWN);
  74. if (!dropdown) {
  75. log('Reviewers dropdown not found', 'error');
  76. return false;
  77. }
  78. const button = dropdown.querySelector(SELECTORS.DROPDOWN_BUTTON);
  79. if (!button) {
  80. log('Dropdown button not found', 'error');
  81. return false;
  82. }
  83. button.click();
  84. await sleep(TIMING.SHORT_DELAY);
  85. return true;
  86. };
  87. /**
  88. * Search for a reviewer in the dropdown
  89. * @param {string} reviewer - Reviewer username to search for
  90. * @returns {Promise<boolean>} - True if successful, false otherwise
  91. */
  92. const searchForReviewer = async (reviewer) => {
  93. // Find the search input
  94. const searchInput = findElement(SELECTORS.SEARCH_INPUT);
  95. if (!searchInput) return false;
  96. // Clear any previous search
  97. searchInput.value = '';
  98. searchInput.dispatchEvent(new Event('input', { bubbles: true }));
  99. await sleep(TIMING.SHORT_DELAY);
  100. // Enter the reviewer name
  101. searchInput.value = reviewer;
  102. searchInput.dispatchEvent(new Event('input', { bubbles: true }));
  103. return true;
  104. };
  105. /**
  106. * Find and click on a reviewer in the dropdown
  107. * @param {string} reviewer - Reviewer username to find
  108. * @returns {Promise<boolean>} - True if found and clicked, false otherwise
  109. */
  110. const findAndClickReviewer = async (reviewer) => {
  111. const startTime = Date.now();
  112. while (Date.now() - startTime < TIMING.MAX_WAIT_TIME) {
  113. const listItems = document.querySelectorAll(SELECTORS.DROPDOWN_ITEM);
  114. for (const item of listItems) {
  115. const nameElement = item.querySelector(SELECTORS.USERNAME_ELEMENT);
  116. if (nameElement && nameElement.textContent.trim().toLowerCase().includes(reviewer.toLowerCase())) {
  117. log(`Found reviewer: ${reviewer}`);
  118. item.click();
  119. return true;
  120. }
  121. }
  122. // Wait 100ms before trying again
  123. await sleep(100);
  124. }
  125. log(`Reviewer not found: ${reviewer}`, 'error');
  126. return false;
  127. };
  128. /**
  129. * Add a single reviewer
  130. * @param {string} reviewer - Reviewer username to add
  131. * @returns {Promise<boolean>} - True if successful, false otherwise
  132. */
  133. const addReviewer = async (reviewer) => {
  134. try {
  135. // Search for the reviewer
  136. if (!await searchForReviewer(reviewer)) return false;
  137. // Find and click on the reviewer
  138. return await findAndClickReviewer(reviewer);
  139. } catch (error) {
  140. log(`Error adding reviewer ${reviewer}: ${error.message}`, 'error');
  141. return false;
  142. }
  143. };
  144. /**
  145. * Add multiple reviewers
  146. * @param {string[]} reviewers - List of reviewer usernames to add
  147. * @returns {Promise<void>}
  148. */
  149. const addReviewers = async (reviewers) => {
  150. log(`Starting to add ${reviewers.length} reviewers`);
  151. for (const reviewer of reviewers) {
  152. await addReviewer(reviewer);
  153. }
  154. // Close the dropdown
  155. await clickReviewersDropdown();
  156. const assigneeBlock = findElement(SELECTORS.ASSIGNEE_BLOCK);
  157. if (assigneeBlock) {
  158. assigneeBlock.click();
  159. }
  160. log('Finished adding reviewers');
  161. };
  162. /**
  163. * Create a button to add reviewers
  164. * @param {string} repoName - Name to display on the button
  165. * @param {string[]} reviewers - List of reviewer usernames to add
  166. * @param {string} backgroundColor - Button background color
  167. * @param {string} textColor - Button text color
  168. * @returns {HTMLButtonElement} - Created button
  169. */
  170. const createButton = (repoName, reviewers, backgroundColor, textColor) => {
  171. const button = document.createElement("button");
  172. button.innerHTML = `Add ${repoName} reviewers`;
  173. button.style = `
  174. background: ${backgroundColor};
  175. color: ${textColor};
  176. margin: 1em;
  177. padding: 0.5em 1em;
  178. border: none;
  179. border-radius: 4px;
  180. cursor: pointer;
  181. font-weight: bold;
  182. `;
  183. button.onclick = (event) => {
  184. event.stopPropagation();
  185. event.preventDefault();
  186. addReviewers(reviewers).catch(err => {
  187. log(`Error in add reviewers process: ${err.message}`, 'error');
  188. });
  189. };
  190. return button;
  191. };
  192. // ===== Main Initialization =====
  193. // Find the reviewer block
  194. const reviewerBlock = findElement(SELECTORS.REVIEWER_BLOCK);
  195. if (!reviewerBlock) {
  196. log('Reviewer block not found, script will not run', 'error');
  197. return;
  198. }
  199. // Create and add the button
  200. const backendReviewersButton = createButton('', backendReviewers, 'lime', 'black');
  201. reviewerBlock.appendChild(backendReviewersButton);
  202. log('GitLab Auto Reviewers script initialized');
  203. })();