(Gitlab) Open next MR that awaits approval

Install nextMR

// 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 {
nameWithNamespace
fullPath
}
}
}
`;
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 {
title
webUrl
iid
draft
description
createdAt
approvedBy {
nodes {
name
username
}
}
author {
name
username
avatarUrl
}
}
}
}
}
`;
const query = gql`
query($iid: String!, $projectPath: ID!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
commitsWithoutMergeCommits(first: 1) {
nodes {
authoredDate
}
}
headPipeline {
status
}
notes {
nodes {
updatedAt
author {
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:

  1. All MRs that I approved but author have comments after me
  2. All MRs that I approved but someone have comments after me
  3. All my MRs
  4. All MRs that have a pipeline failing
  5. All MRs that I have already commented
  6. All MRs that is already approved by others
  7. All MR s that is already approved by me
  8. All Draft Mrs.

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.