Scripts by hakubo

(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.

Discuss Post

Conventional comments

Install comment

// Menu: Conventional comment
// Description: Comments that are easy to grok and grep
// Author: Jakub Olek
// Twitter: @JakubOlek
// Shortcut: opt 0
// Based on: https://hemdan.hashnode.dev/conventional-comments
const type = await arg("Label", [
{
name: "👏 praise",
value: "**👏 praise**: ",
description:
"Praises highlight something positive. Try to leave at least one of these comments per review (if it exists :^)",
},
{
name: "🤓 nitpick",
value: "**🤓 nitpick**: ",
description:
"Nitpicks are small, trivial, but necessary changes. Distinguishing nitpick comments significantly helps direct the reader's attention to comments requiring more involvement.",
},
{
name: "🎯 suggestion",
value: "**🎯 suggestion**: ",
description:
"Suggestions are specific requests to improve the subject under review. It is assumed that we all want to do what's best, so these comments are never dismissed as “mere suggestions”, but are taken seriously.",
},
{
name: "🔨 issue",
value: "**🔨 issue**: ",
description:
"Issues represent user-facing problems. If possible, it's great to follow this kind of comment with a suggestion.",
},
{
name: "❔ question",
value: "**❔ question**: ",
description:
"Questions are appropriate if you have a potential concern but are not quite sure if it's relevant or not. Asking the author for clarification or investigation can lead to a quick resolution.",
},
{
name: "💭 thought",
value: "**💭 thought**: ",
description:
"Thoughts represent an idea that popped up from reviewing. These comments are non-blocking by nature, but they are extremely valuable and can lead to more focused initiatives and mentoring opportunities.",
},
{
name: "💣 chore",
value: "**💣 chore**: ",
description:
"Chores are simple tasks that must be done before the subject can be “officially” accepted. Usually, these comments reference some common processes. Try to leave a link to the process described so that the reader knows how to resolve the chore.",
},
]);
setSelectedText(type);

Simple list of conventional comments ready to be used. https://conventionalcomments.org/

Discuss Post

Snippets

Small snippets manager with builtin support for variables. Variables are defined as an array of function so they can be anything, arg, getSelectedText etc. Snippet can also be a simple string if it has no variables

Install Snippets

// Menu: Snippets
// Description: Snippets collection
// Author: Jakub Olek
// Twitter: @JakubOlek
// Shortcut: opt -
const { setSelectedText } = await kit("text");
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
const snippetMap = {
useState: {
args: [() => arg("variable name"), () => arg("variable value")],
template: (name, value) =>
`const [${name}, set${capitalizeFirstLetter(name)}] = useState(${value})`,
},
name: "Jakub Olek",
date: new Date().toLocaleDateString("en-GB", {
year: "numeric",
month: "long",
day: "numeric",
}),
test: {
args: [() => arg("should")],
template: (testName) => `test("should ${testName}", function() {
})`,
},
component: {
args: [() => arg("component name")],
template: (componentName) => `function ${capitalizeFirstLetter(
componentName
)}() {
return
}`,
},
};
const snippetName = await arg("Snippet", Object.keys(snippetMap));
let result = snippetMap[snippetName];
const { args, template } = result;
if (template) {
const variables = [];
if (args) {
for (let i = 0; i < args.length; i++) {
const variable = args[i];
if (typeof variable !== "string") {
variables.push(await variable());
}
}
}
setSelectedText(template(...variables));
} else {
setSelectedText(result);
}
Discuss Post

Simple Calculator

Hey, thanks for the awesome tool.

I constantly need to make some simple calculations and that's one of a feature that I liked about Alfred.

With ScriptKit I think I made it slightly better:

  • live preview of your calculation
  • select to copy to clipboard
  • remembers last 10 calculations
  • auto fixing , to . - so you don't have to do it manually

https://user-images.githubusercontent.com/1018759/116007328-8f126600-a60f-11eb-8588-4a978bac47c9.mov

// Menu: Simple calculator
// Description: Make simple calculations
// Author: Jakub Olek
// Twitter: @JakubOlek
// Shortcut: opt =
const calcDb = db('calc', {history: []});
function createResult(calculationResult, input) {
return {name: calculationResult, description: input, value: {calculationResult, input}}
}
const {calculationResult, input, ...rest} = await arg("calculate:", async (input) => {
const choices = [];
if (input) {
choices.push(createResult((await exec(`bc <<<"${input.replace(/\,/g, ".")}" -l`)).replace(/^\./, "0."), input))
}
return choices.concat(calcDb.get('history').value())
})
if (calculationResult) {
const history = calcDb.get('history').value();
history.unshift(createResult(calculationResult, input));
calcDb.set('history', history.filter(({description}, index) => index === 0 || description !== input).slice(0, 10)).write()
}
copy(calculationResult)
Discuss Post