#!/usr/bin/env /usr/local/bin/node
// jshint asi:true
// jshint esversion: 8
// <xbar.title>GitLab Merge Requests</xbar.title>
// <xbar.version>v1.0</xbar.version>
// <xbar.author>Simeon Cheeseman</xbar.author>
// <xbar.author.github>simeonc</xbar.author.github>
// <xbar.desc>List of your assigned and created merge requests.
// The menu bar shows a count of MRs requiring your approval (🔎), WIP MRs (🛠️), Un-mergable MRs (⛔), MRs with failed pipelines (⚠️), MRs with unresolved discussions (🚧), MRs with running pipeline (🚀), MRs pending approval (💬) and Approved/Ready to Merge (❇️).
// Each MR in the dropdown is grouped by project and displays `<Unmergable> "Title"; "Pipeline Status" : "Approval Status"`, clicking on an MR opens it in the browser</xbar.desc>
// <xbar.dependencies>node.js</xbar.dependencies>
// <xbar.image>https://i.imgur.com/t0TtQXO.png</xbar.image>
// <xbar.var>string(GITLAB_API_TOKEN=""): Gitlab token with read_user and read_api scopes. https://gitlab.com/profile/personal_access_tokens</xbar.var>
// <xbar.var>string(GITLAB_HOST="gitlab.com"): The domain your instance is hosted on. Leave the default if using gitlab.com</xbar.var>
// <xbar.var>number(FONT_SIZE=15): Font size of the MR title and status</xbar.var>
// <xbar.var>number(PROJECT_FONT_SIZE=13): Font size of the project heading</xbar.var>
// <xbar.var>select(DARK_MODE="auto"): Choose dark mode settings [auto, dark, light]</xbar.var>
/**
* Information
*
* This was inspired by the work of Shelton Koskie (@eightygrit), who made the
* first version of the "GITLAB Projects" for API v4.
*
* @see GitLab API Documentation https://docs.gitlab.com/ee/api/README.html
* @see Create GitLab Access Token https://gitlab.com/profile/personal_access_tokens
* @see BitBar Node Module Docs https://github.com/sindresorhus/bitbar
*/
const gitlab_domain = process.env.GITLAB_HOST;
const private_token = process.env.GITLAB_API_TOKEN;
const font_size = process.env.FONT_SIZE;
// <xbar.var>boolean(MENU_BAR_SHOW_NEW_MR=true): Show new MR count and icon in the menubar, these are branches that have been pushed today but have no connected MR</xbar.var>
// <xbar.var>boolean(MENU_BAR_SHOW_REQUESTED_REVIEWER=true): show requested reviews count and icon in the menubar</xbar.var>
// <xbar.var>boolean(MENU_BAR_SHOW_WIP=true): show Work In Progress/Draft count and icon in the menubar</xbar.var>
// <xbar.var>boolean(MENU_BAR_SHOW_UNMERGABLE=true): show Unmergable MR count and icon in the menubar</xbar.var>
// <xbar.var>boolean(MENU_BAR_SHOW_UNRESOLVED_DISCUSSION=true): show MRs with unresolved discussion count and icon in the menubar</xbar.var>
// <xbar.var>boolean(MENU_BAR_SHOW_APPROVED=true): show Approved with passing CI MR count and icon in the menubar</xbar.var>
// <xbar.var>boolean(MENU_BAR_SHOW_CI_FAILED=true): show Failed pipeline count and icon in the menubar</xbar.var>
// <xbar.var>boolean(MENU_BAR_SHOW_CI_RUNNING=true): show Pipeline running count and icon in the menubar</xbar.var>
// <xbar.var>boolean(MENU_BAR_SHOW_CI_PENDING=true): show Pending pipeline count and icon in the menubar</xbar.var>
/**
* Change this array to choose which statuses show and in what order on the menu bar.
*
* @type {string[]}
*/
const MENU_BAR_ORDER = [
process.env.MENU_BAR_SHOW_NEW_MR === "true" ? "newMR" : false,
process.env.MENU_BAR_SHOW_REQUESTED_REVIEWER === "true" ? "reviewer" : false,
process.env.MENU_BAR_SHOW_WIP === "true" ? "wip" : false,
process.env.MENU_BAR_SHOW_UNMERGABLE === "true" ? "unmergable" : false,
process.env.MENU_BAR_SHOW_CI_FAILED === "true" ? "failed" : false,
process.env.MENU_BAR_SHOW_UNRESOLVED === "true" ? "unresolved" : false,
process.env.MENU_BAR_SHOW_CI_RUNNING === "true" ? "ciRunning" : false,
process.env.MENU_BAR_SHOW_CI_PENDING === "true" ? "pending" : false,
process.env.MENU_BAR_SHOW_APPROVED === "true" ? "approved" : false,
].filter((k) => !!k);
/**
* MAX length of the title string in the toolbar
*
* @type {number}
*/
const MAX_LENGTH = 60;
/////////////////////////////////////////////////////////////////////////
// Do not edit below this line unless you know what you're doing. :) //
///////////////////////////////////////////////////////////////////////
const fs = require("fs");
const path = require("path");
const SETTINGS_FILE_PATH = path.resolve(
__dirname,
"./.gitlab_merge_requests.settings.json"
);
let bitbar;
// Verify bitbar node module is available or try to install it globally.
try {
bitbar = require("bitbar");
} catch (e) {
try {
bitbar = globalRequire("bitbar");
} catch (e) {
installBitbarModule();
// Not catching error if one is thrown.
bitbar = globalRequire("bitbar");
}
}
const textColor = (() => {
switch (process.env.DARK_MODE) {
case "dark":
return "white";
case "light":
return "black";
case "auto":
default:
return bitbar.darkMode ? "white" : "black";
}
})();
// Converts the gitlab status to emoji
// see https://emojipedia.org/ for customisation
function stateIcon(status) {
return {
created: "💤",
pending: "💤",
running: "🚀",
failed: "⚠️",
success: "✅",
skipped: "🚀",
manual: "⏯️",
}[status];
}
const gitlabIconBase64 =
"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";
function request(path) {
const httpTransport = require("https");
const responseEncoding = "utf8";
const httpOptions = {
hostname: gitlab_domain,
port: "443",
path,
method: "GET",
headers: { "PRIVATE-TOKEN": private_token },
};
httpOptions.headers["User-Agent"] =
"bitbar/gitlab_projects - node " + process.version;
return new Promise((resolve, reject) => {
const request = httpTransport
.request(httpOptions, (res) => {
let responseBufs = [];
let responseStr = "";
res
.on("data", (chunk) => {
if (Buffer.isBuffer(chunk)) {
responseBufs.push(chunk);
} else {
responseStr = responseStr + chunk;
}
})
.on("end", () => {
if (responseBufs.length > 0) {
responseStr = Buffer.concat(responseBufs).toString(responseEncoding);
}
resolve(JSON.parse(responseStr));
});
})
.setTimeout(0)
.on("error", (error) => {
reject(error);
});
request.write("");
request.end();
});
}
const cachedProjectNames = {};
async function registerProject(project_id) {
if (!cachedProjectNames[project_id]) {
const project = await request(`/api/v4/projects/${project_id}`);
cachedProjectNames[project_id] = project.name_with_namespace;
}
}
async function getSettings() {
let settings = {};
if (fs.existsSync(SETTINGS_FILE_PATH)) {
settings = JSON.parse(fs.readFileSync(SETTINGS_FILE_PATH));
}
if (!settings.userId) {
const myUser = await request("/api/v4/user");
settings.userId = myUser.id;
fs.writeFileSync(
SETTINGS_FILE_PATH,
JSON.stringify({
settings,
})
);
}
return settings;
}
async function getMRs() {
const { userId, todaysUUID, todaysExcludedEvents = [] } = await getSettings();
const statusCount = {};
function incrementStatusCount(key) {
statusCount[key] = statusCount[key] || 0;
statusCount[key] += 1;
}
const mergeRequestsByProjectId = {};
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayParam = `${yesterday.getFullYear()}-${
yesterday.getMonth() + 1
}-${yesterday.getDate()}`;
let excludedEvents = [];
if (todaysUUID === yesterdayParam) {
excludedEvents = todaysExcludedEvents;
}
const [todaysEvents, createdMRs, assignedMRs, approvalMRs] =
await Promise.all([
request(
`/api/v4/users/${userId}/events?after=${yesterdayParam}&action=pushed`
),
request("/api/v4/merge_requests?scope=created_by_me&state=opened"),
request("/api/v4/merge_requests?scope=assigned_to_me&state=opened"),
request(
`/api/v4/merge_requests?scope=all&state=opened&approver_ids[]=${userId}`
),
request(
`/api/v4/merge_requests?scope=all&state=opened&reviewer_id[]=${userId}`
),
]);
const allMRs = assignedMRs
.concat(approvalMRs, createdMRs)
.reduce((uniqueMRs, currentMR) => {
if (!uniqueMRs.find(({ id }) => id === currentMR.id)) {
uniqueMRs.push(currentMR);
}
return uniqueMRs;
}, []);
if (allMRs.length === 0) {
// Hide when no open MRs
return bitbar([
{
image: gitlabIconBase64,
text: `0 MR`,
color: textColor,
dropdown: true,
},
]);
}
await Promise.all(
allMRs
.map((mergeRequest) => {
const { project_id, iid } = mergeRequest;
return Promise.all([
Promise.resolve(mergeRequest),
request(
`/api/v4/projects/${project_id}/merge_requests/${iid}/pipelines`
),
request(
`/api/v4/projects/${project_id}/merge_requests/${iid}/approval_state`
),
registerProject(project_id),
]);
})
.map(async (promises) => {
const [mergeRequest, pipelines, approvals] = await promises;
const {
web_url,
merge_status,
title,
project_id,
iid,
blocking_discussions_resolved,
work_in_progress,
assignee,
assignees,
author,
source_branch,
} = mergeRequest;
excludedEvents.push(`${project_id}_${source_branch}`);
const isReviewer =
(!author || author.id !== userId) &&
(!assignee || assignee.id !== userId) &&
(!assignees || !assignees.find(({ id }) => id === userId));
mergeRequestsByProjectId[project_id] =
mergeRequestsByProjectId[project_id] || [];
let pipelineStatus = "";
if (pipelines.length) {
pipelineStatus = stateIcon(pipelines[0].status);
}
const isApproved = approvals.rules.reduce(
(isApproved, { approved }) => isApproved && approved,
true
);
const hasCurrentUserApproved = approvals.rules.reduce(
(currentUserHasApproved, { approved_by }) =>
currentUserHasApproved ||
!!approved_by.find(({ id }) => id === userId),
false
);
const canMerge = merge_status === "can_be_merged";
if (
isReviewer &&
(work_in_progress ||
!canMerge ||
isApproved ||
hasCurrentUserApproved)
)
return;
let mergeStatus = " ";
if (!canMerge) {
mergeStatus = "⛔ ";
} else if (isReviewer) {
mergeStatus = "🔎 ";
}
let approvalStatus = "💬";
if (work_in_progress) {
approvalStatus = "🛠️";
} else if (!blocking_discussions_resolved) {
approvalStatus = "🚧";
} else if (isApproved) {
approvalStatus = "❇️";
}
if (isReviewer) {
incrementStatusCount("reviewer");
} else if (work_in_progress) {
incrementStatusCount("wip");
} else if (merge_status !== "can_be_merged") {
incrementStatusCount("unmergable");
} else if (pipelines.length && pipelines[0].status === "failed") {
incrementStatusCount("failed");
} else if (!blocking_discussions_resolved) {
incrementStatusCount("unresolved");
} else if (pipelines.length && pipelines[0].status === "running") {
incrementStatusCount("ciRunning");
} else if (!isApproved) {
incrementStatusCount("pending");
} else {
incrementStatusCount("approved");
}
let statusString = "";
if (pipelineStatus || approvals.rules.length) {
const statusStrings = [];
if (pipelineStatus) statusStrings.push(pipelineStatus);
if (approvals.rules.length) statusStrings.push(approvalStatus);
statusString = ` ${statusStrings.join(" ")}`;
}
let trimmedTitle = title.substring(0, MAX_LENGTH);
if (title.length > MAX_LENGTH) {
trimmedTitle += "...";
}
mergeRequestsByProjectId[project_id].push(
`${mergeStatus}!${iid}: ${trimmedTitle}${statusString} | href="${web_url}" size=${font_size}`
);
})
);
for (let i = 0; i < todaysEvents.length; i += 1) {
const {
project_id,
push_data: { action, ref, ref_type, commit_to },
} = todaysEvents[i];
if (excludedEvents.indexOf(`${project_id}_${ref}`) !== -1) continue;
if (!(action === "created" && ref_type === "branch")) continue;
const mrCheck = await request(
`/api/v4/projects/${project_id}/repository/commits/${commit_to}/merge_requests`
);
if (mrCheck.length) {
excludedEvents.push(`${project_id}_${ref}`);
continue;
}
await registerProject(project_id);
const { web_url } = await request(`/api/v4/projects/${project_id}`);
const params = [
`merge_request%5Bsource_branch%5D=${ref}`,
`merge_request%5Bsource_project_id%5D=${project_id}`,
`merge_request%5Btarget_project_id%5D=${project_id}`,
];
const createMRUrl = `${web_url}/-/merge_requests/new?${params.join("&")}`;
mergeRequestsByProjectId[project_id] =
mergeRequestsByProjectId[project_id] || [];
mergeRequestsByProjectId[project_id].unshift(
`📝: ${ref} | href="${createMRUrl}" size=${font_size}`
);
incrementStatusCount("newMR");
}
const content = [];
const statusMap = {
reviewer: "🔎",
unmergable: "⛔",
failed: "⚠️",
ciRunning: "🚀",
pending: "💬",
approved: "❇️",
wip: "🛠️",
unresolved: "🚧",
newMR: "📝",
};
const statusString = MENU_BAR_ORDER.map((key) => {
if (statusCount[key]) {
return `${statusMap[key]}[${statusCount[key]}]`;
}
})
.filter((s) => !!s)
.join(", ");
content.push({
image: gitlabIconBase64,
text: `${statusString}`,
color: textColor,
dropdown: true,
});
Object.keys(mergeRequestsByProjectId).forEach((projectId) => {
if (!mergeRequestsByProjectId[projectId].length) return;
content.push(bitbar.separator);
content.push({
text: cachedProjectNames[projectId],
size: process.env.PROJECT_FONT_SIZE,
});
mergeRequestsByProjectId[projectId].forEach((text) =>
text ? content.push({ text }) : null
);
});
// Update settings file for speed later
fs.writeFileSync(
SETTINGS_FILE_PATH,
JSON.stringify({
userId,
todaysUUID: yesterdayParam,
todaysExcludedEvents: excludedEvents.reduce((uniqueEvents, event) => {
if (uniqueEvents.indexOf(event) === -1) {
uniqueEvents.push(event);
}
return uniqueEvents;
}, []),
})
);
try {
bitbar(content);
} catch (error) {
console.log(content);
throw error;
}
}
getMRs().catch((error) => {
console.error(error);
});
// These utility functions taken from another plugin;
// https://github.com/matryer/xbar-plugins/blob/f1004a74a0d887b9655c71a1d52ba4e02b37fd77/Dev/Gitlab/gitlab_projects.js
/**
* Sets up the ability to require global node packages.
*
* @return {object} Returns the required node package object
*/
function globalRequire(packageName) {
const childProcess = require("child_process");
const path = require("path");
const fs = require("fs");
const env = Object.assign({}, process.env);
env.PATH = path.resolve("/usr/local/bin") + ":" + env.PATH;
const globalNodeModulesDir =
childProcess
.execSync(npmBin() + " root -g", { env: env })
.toString()
.trim() + "/";
let packageDir = path.join(globalNodeModulesDir, packageName, "/");
//find package required by older versions of npm
if (!fs.existsSync(packageDir)) {
packageDir = path.join(
globalNodeModulesDir,
"npm/node_modules/",
packageName
);
}
// Package not found
if (!fs.existsSync(packageDir)) {
throw new Error("Cannot find global module '" + packageName + "'");
}
const packageMeta = JSON.parse(
fs.readFileSync(path.join(packageDir, "package.json")).toString()
);
const main = path.join(packageDir, packageMeta.files[0]);
return require(main);
}
/**
* Installs Bitbar node module if it doesn't exits.
*
* @see BitBar node module on github https://github.com/sindresorhus/bitbar
*/
function installBitbarModule() {
// Allows one to run the npm command as if on the command line.
const childProcess = require("child_process");
const execSync = childProcess.execSync;
const path = require("path");
const env = Object.assign({}, process.env);
env.PATH = path.resolve("/usr/local/bin") + ":" + env.PATH;
// Get the path to npm bin
const npm = npmBin();
// The install command
const cmd = npm + " install -g bitbar@1";
console.log("Installing the BitBar Node module...");
execSync(cmd, {
cwd: process.cwd(),
env: env,
})
.toString("utf8")
.trim();
console.log("Installation complete.");
}
/**
* Gets the path to your npm executable.
*
* @return {string} The full path to your npm executable
*/
function npmBin() {
const path = require("path");
const childProcess = require("child_process");
const execSync = childProcess.execSync;
const env = Object.assign({}, process.env);
env.PATH = path.resolve("/usr/local/bin") + ":" + env.PATH;
// Get the path to npm bin
return execSync("which npm", { env: env }).toString("utf8").trim();
}