// Menu: GitLab - next MR// Description: Open next MR that I have not approved// Author: Jakub Olek// Twitter: @JakubOlek// Shortcut: ctrl opt \const { request, gql, GraphQLClient } = await npm("graphql-request");const dayjs = await npm("dayjs");import relativeTime from "dayjs/plugin/relativeTime.js";dayjs.extend(relativeTime);const domain = await env("GITLAB_DOMAIN");const token = await env("GITLAB_TOKEN");const username = await env("GITLAB_USERNAME");const jiraDomain = await env("JIRA_DOMAIN");const requiredApprovals = Number(await env("GITLAB_REQUIRED_APPROVALS"));const debug = false;function log(...args) {if (debug) {console.log(...args);}}const graphQLClient = new GraphQLClient(domain + "/api/graphql", {headers: {"PRIVATE-TOKEN": token,},});const projects = gql`query($name: String!) {projects(search: $name, membership: true) {nodes {nameWithNamespacefullPath}}}`;if (!env.GITLAB_PROJECT_PATH) {const fullPath = await arg("Search project", async (input) => {return (await graphQLClient.request(projects, { name: input })).projects.nodes.map((project) => ({name: project.nameWithNamespace,description: project.fullPath,value: project.fullPath,}));});await cli("set-env-var", "GITLAB_PROJECT_PATH", fullPath);}const queryMrs = gql`query($projectPath: ID!) {project(fullPath: $projectPath) {mergeRequests(state: opened, sort: UPDATED_DESC) {nodes {titlewebUrliiddraftdescriptioncreatedAtapprovedBy {nodes {nameusername}}author {nameusernameavatarUrl}}}}}`;const query = gql`query($iid: String!, $projectPath: ID!) {project(fullPath: $projectPath) {mergeRequest(iid: $iid) {commitsWithoutMergeCommits(first: 1) {nodes {authoredDate}}headPipeline {status}notes {nodes {updatedAtauthor {username}}}}}}`;let nextMR;const myMrs = [];const drafts = [];const awaitingReview = [];const alreadyCommented = [];const haveAuthorCommented = [];const haveOthersCommented = [];const haveFailingPipeline = [];const alreadyApprovedByMe = [];const alreadyApprovedByOthers = [];const {project: {mergeRequests: { nodes: mergeRequests },},} = await graphQLClient.request(queryMrs, {projectPath: env.GITLAB_PROJECT_PATH,});arg("Processing...");log("Show list", flag.showList);log("Checking", mergeRequests.length, "MRs");for (let mr of mergeRequests) {log("Checking MR", mr.title, `(${mr.author.username})`);const approvedBy = mr.approvedBy.nodes.map((node) => node.username);if (mr.author.username === username) {log("^ This is my MR");myMrs.push(mr);continue;}if (mr.draft) {drafts.push(mr);log("^ This is a draft");continue;}if (approvedBy.includes(username)) {log("^ Approved by me");alreadyApprovedByMe.push(mr);continue;} else {if (approvedBy.length >= requiredApprovals) {log("^ Approved by others");alreadyApprovedByOthers.push(mr);continue;}const {project: { mergeRequest },} = await graphQLClient.request(query, {iid: mr.iid,projectPath: env.GITLAB_PROJECT_PATH,});const pipelineStatus = mergeRequest.headPipeline.status;if (pipelineStatus !== "SUCCESS") {log("^ Failed pipeline");haveFailingPipeline.push(mr);continue;}const comments = mergeRequest.notes.nodes;const anyLatestComment = comments[0];const myLatestComment = comments.find((comment) => comment.author.username === username);const authorLatestComment = comments.find((comment) => comment.author.username === mr.author.username);if (myLatestComment) {const latestCommitTime = dayjs(mergeRequest.commitsWithoutMergeCommits.nodes[0].authoredDate);const myLatestCommentTime = dayjs(myLatestComment.updatedAt);if (latestCommitTime.isBefore(myLatestCommentTime)) {log("^ awaits new commits after my comments");alreadyCommented.push(mr);continue;}if (authorLatestComment) {const authorLatestCommentTime = dayjs(authorLatestComment.updatedAt);if (authorLatestCommentTime.isAfter(myLatestComment.updatedAt)) {log("^ have some comments by the MR author after my comment");haveAuthorCommented.push(mr);continue;}}if (anyLatestComment) {const latestCommentTime = dayjs(anyLatestComment.updatedAt);if (latestCommentTime.isAfter(myLatestComment.updatedAt)) {log("^ have some comments by other after my comment");haveOthersCommented.push(mr);continue;}}}if (!flag.showList) {nextMR = mr;break;} else {awaitingReview.push(mr);}}}function createJiraLinks(text) {return text.replace(/[A-Z]{1,5}-[0-9]*/g,(ticketNumber) => `[${ticketNumber}](${jiraDomain}}/browse/${ticketNumber})`);}function getName(mr) {if (mr.author.username === username) {return `${!mr.draft && mr.approvedBy.nodes.length < 2 ? "!A " : ""}${mr.title}`;}return mr.title;}function getChoices(mrs, description) {return mrs.map((mr) => ({name: getName(mr),value: mr.webUrl,description: description,img: mr.author.avatarUrl.includes("http")? mr.author.avatarUrl: domain + mr.author.avatarUrl,preview: md(`# ${createJiraLinks(mr.title)}## Created ${dayjs(mr.createdAt).fromNow()} by ${mr.author.name}## ${description}## Approved by${mr.approvedBy.nodes.length? mr.approvedBy.nodes.map((user) => `* ${user.name}`).join(""): "- nobody"}${createJiraLinks(mr.description.replace(/\/uploads\//g,domain + "/uploads/" + env.GITLAB_PROJECT_PATH + "/"))}`),}));}if (nextMR) {await focusTab(nextMR.webUrl);} else {const choices = [...getChoices(awaitingReview, "Awaiting Review"),...getChoices(haveAuthorCommented, "Author have comments after you"),...getChoices(haveOthersCommented, "Someone have comments after you"),...getChoices(myMrs, "My merge request"),...getChoices(haveFailingPipeline, "Failing Pipeline"),...getChoices(alreadyCommented, "You have commented on this"),...getChoices(alreadyApprovedByOthers, "Already approved by others"),...getChoices(alreadyApprovedByMe, "Already approved by you"),...getChoices(drafts, "Draft"),];if (choices.length) {const mr = await arg("Open MR:", choices);if (mr) {focusTab(mr);}}}
This one I use every day at work. It checks a project for any MR that have no approvals and open it for me automatically. In case there is no MR that I should review - it opens arg with a list of all MRs that I might be interested in in this order:
First time you run it i'll ask you to configure it with gitlab domain, token and your username, jira domain and number of approvals required for each MR.