Automatically add reviewers to GitLab merge requests
// ==UserScript==
// @name GitLab auto reviewers
// @namespace http://tampermonkey.net/
// @version 0.4
// @description Automatically add reviewers to GitLab merge requests
// @author Mohammad Sh
// @match https://gitlab.com/*/*/-/merge_requests/*/*
// @match https://gitlab.com/*/*/-/merge_requests/new*
// @match https://gitlab.com/*/merge_requests/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=gitlab.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ===== Configuration =====
// List of reviewers to add (usernames)
const backendReviewers = [
'mshabibR365',
];
// Timing configuration (in milliseconds)
const TIMING = {
SHORT_DELAY: 200, // Short delay for UI updates
MAX_WAIT_TIME: 5000, // Maximum wait time for search results
};
// CSS selectors for GitLab UI elements
const SELECTORS = {
REVIEWER_BLOCK: '.block.reviewer',
DROPDOWN: '.reviewers-dropdown.gl-ml-auto.gl-new-dropdown',
DROPDOWN_BUTTON: 'button',
SEARCH_INPUT: '.gl-listbox-search-input',
DROPDOWN_ITEM: '.gl-new-dropdown-item',
USERNAME_ELEMENT: '.gl-text-subtle',
ASSIGNEE_BLOCK: '.block.assignee'
};
// ===== Helper Functions =====
/**
* Sleep for a specified amount of time
* @param {number} ms - Time to sleep in milliseconds
* @returns {Promise} - Promise that resolves after the specified time
*/
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* Log a message with a timestamp
* @param {string} message - Message to log
* @param {string} type - Log type (log, error, warn)
*/
const log = (message, type = 'log') => {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
console[type](`[${timestamp}] GitLab Auto Reviewers: ${message}`);
};
/**
* Find an element in the DOM
* @param {string} selector - CSS selector
* @returns {Element|null} - Found element or null
*/
const findElement = (selector) => {
const element = document.querySelector(selector);
if (!element) {
log(`Element not found: ${selector}`, 'error');
}
return element;
};
/**
* Click on the reviewers dropdown button
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
const clickReviewersDropdown = async () => {
const dropdown = document.querySelector(SELECTORS.DROPDOWN);
if (!dropdown) {
log('Reviewers dropdown not found', 'error');
return false;
}
const button = dropdown.querySelector(SELECTORS.DROPDOWN_BUTTON);
if (!button) {
log('Dropdown button not found', 'error');
return false;
}
button.click();
await sleep(TIMING.SHORT_DELAY);
return true;
};
/**
* Search for a reviewer in the dropdown
* @param {string} reviewer - Reviewer username to search for
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
const searchForReviewer = async (reviewer) => {
// Find the search input
const searchInput = findElement(SELECTORS.SEARCH_INPUT);
if (!searchInput) return false;
// Clear any previous search
searchInput.value = '';
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
await sleep(TIMING.SHORT_DELAY);
// Enter the reviewer name
searchInput.value = reviewer;
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
return true;
};
/**
* Find and click on a reviewer in the dropdown
* @param {string} reviewer - Reviewer username to find
* @returns {Promise<boolean>} - True if found and clicked, false otherwise
*/
const findAndClickReviewer = async (reviewer) => {
const startTime = Date.now();
while (Date.now() - startTime < TIMING.MAX_WAIT_TIME) {
const listItems = document.querySelectorAll(SELECTORS.DROPDOWN_ITEM);
for (const item of listItems) {
const nameElement = item.querySelector(SELECTORS.USERNAME_ELEMENT);
if (nameElement && nameElement.textContent.trim().toLowerCase().includes(reviewer.toLowerCase())) {
log(`Found reviewer: ${reviewer}`);
item.click();
return true;
}
}
// Wait 100ms before trying again
await sleep(100);
}
log(`Reviewer not found: ${reviewer}`, 'error');
return false;
};
/**
* Add a single reviewer
* @param {string} reviewer - Reviewer username to add
* @returns {Promise<boolean>} - True if successful, false otherwise
*/
const addReviewer = async (reviewer) => {
try {
// Search for the reviewer
if (!await searchForReviewer(reviewer)) return false;
// Find and click on the reviewer
return await findAndClickReviewer(reviewer);
} catch (error) {
log(`Error adding reviewer ${reviewer}: ${error.message}`, 'error');
return false;
}
};
/**
* Add multiple reviewers
* @param {string[]} reviewers - List of reviewer usernames to add
* @returns {Promise<void>}
*/
const addReviewers = async (reviewers) => {
log(`Starting to add ${reviewers.length} reviewers`);
for (const reviewer of reviewers) {
await addReviewer(reviewer);
}
// Close the dropdown
await clickReviewersDropdown();
const assigneeBlock = findElement(SELECTORS.ASSIGNEE_BLOCK);
if (assigneeBlock) {
assigneeBlock.click();
}
log('Finished adding reviewers');
};
/**
* Create a button to add reviewers
* @param {string} repoName - Name to display on the button
* @param {string[]} reviewers - List of reviewer usernames to add
* @param {string} backgroundColor - Button background color
* @param {string} textColor - Button text color
* @returns {HTMLButtonElement} - Created button
*/
const createButton = (repoName, reviewers, backgroundColor, textColor) => {
const button = document.createElement("button");
button.innerHTML = `Add ${repoName} reviewers`;
button.style = `
background: ${backgroundColor};
color: ${textColor};
margin: 1em;
padding: 0.5em 1em;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
`;
button.onclick = (event) => {
event.stopPropagation();
event.preventDefault();
addReviewers(reviewers).catch(err => {
log(`Error in add reviewers process: ${err.message}`, 'error');
});
};
return button;
};
// ===== Main Initialization =====
// Find the reviewer block
const reviewerBlock = findElement(SELECTORS.REVIEWER_BLOCK);
if (!reviewerBlock) {
log('Reviewer block not found, script will not run', 'error');
return;
}
// Create and add the button
const backendReviewersButton = createButton('', backendReviewers, 'lime', 'black');
reviewerBlock.appendChild(backendReviewersButton);
log('GitLab Auto Reviewers script initialized');
})();