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);
}
}
}