This commit is contained in:
JOLIMAITRE Matthieu 2024-04-12 17:34:57 +02:00
commit f159fab9a9
6 changed files with 195 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/okimeter
/token

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"deno.enable": true
}

9
build.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
set -e
cd "$(dirname "$(realpath "$0")")"
MAIN="main.ts"
ARGS="--allow-net"
BIN="okimeter"
deno compile "$ARGS" -o "$BIN" "$MAIN"

6
deno.json Normal file
View file

@ -0,0 +1,6 @@
{
"fmt": {
"useTabs": true,
"lineWidth": 120
}
}

37
deno.lock generated Normal file
View file

@ -0,0 +1,37 @@
{
"version": "3",
"redirects": {
"https://deno.land/x/crayon/mod.ts": "https://deno.land/x/crayon@3.3.3/mod.ts",
"https://deno.land/x/crayon/src/styles.ts": "https://deno.land/x/crayon@3.3.3/src/styles.ts"
},
"remote": {
"https://deno.land/std@0.222.1/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975",
"https://deno.land/std@0.222.1/assert/_diff.ts": "4bf42969aa8b1a33aaf23eb8e478b011bfaa31b82d85d2ff4b5c4662d8780d2b",
"https://deno.land/std@0.222.1/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4",
"https://deno.land/std@0.222.1/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834",
"https://deno.land/std@0.222.1/assert/assert_equals.ts": "cc1f4b0ff4ad511e69f965535b56a6cdbbbc0f086bf376e0243214df6039c883",
"https://deno.land/std@0.222.1/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917",
"https://deno.land/std@0.222.1/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47",
"https://deno.land/std@0.222.1/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376",
"https://deno.land/std@0.222.1/encoding/base64.ts": "dd59695391584c8ffc5a296ba82bcdba6dd8a84d41a6a539fbee8e5075286eaf",
"https://deno.land/std@0.222.1/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a",
"https://deno.land/x/crayon@3.3.3/mod.ts": "82ad225583a483c4837577971629cddaa22614093af8353da6426b9366de9780",
"https://deno.land/x/crayon@3.3.3/src/conversions.ts": "9bfd3b1fbe412bcba092890ac558b6beaad4c3aa399cd99d45fadb324d28afd6",
"https://deno.land/x/crayon@3.3.3/src/crayon.ts": "6b237baa08a31c903436e040afd2228a7fffaa5d11dddc58e3c402f79b3c1d04",
"https://deno.land/x/crayon@3.3.3/src/styles.ts": "aa588b57b2c0482dc5c6f53109b4287831a9827c0aeef9a88129beae1172c1ee",
"https://deno.land/x/crayon@3.3.3/src/util.ts": "af8884a917488de76ac0c2b92482093ade74514ece77a4c64e5eb5b0f6ed68e6",
"https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea",
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
"https://deno.land/x/zod@v3.22.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c",
"https://deno.land/x/zod@v3.22.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7",
"https://deno.land/x/zod@v3.22.4/helpers/parseUtil.ts": "f791e6e65a0340d85ad37d26cd7a3ba67126cd9957eac2b7163162155283abb1",
"https://deno.land/x/zod@v3.22.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7",
"https://deno.land/x/zod@v3.22.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e",
"https://deno.land/x/zod@v3.22.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774",
"https://deno.land/x/zod@v3.22.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268",
"https://deno.land/x/zod@v3.22.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c",
"https://deno.land/x/zod@v3.22.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4",
"https://deno.land/x/zod@v3.22.4/types.ts": "724185522fafe43ee56a52333958764c8c8cd6ad4effa27b42651df873fc151e"
}
}

138
main.ts Executable file
View file

@ -0,0 +1,138 @@
#!/bin/env -S deno run -A
import { encodeBase64 } from "https://deno.land/std@0.222.1/encoding/base64.ts";
import { assert } from "https://deno.land/std@0.222.1/assert/assert.ts";
import { assertEquals } from "https://deno.land/std@0.222.1/assert/assert_equals.ts";
import { crayon } from "https://deno.land/x/crayon@3.3.3/mod.ts";
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts";
async function main() {
let { token } = parse_args(Deno.args);
if (token === undefined) token = prompt_args();
const client = new ApiClient("gitlab.cri.epita.fr", token);
const merge_requests = await get_all_merge_requests(client);
const total = sum(await parallel(merge_requests, (mr) => count_in_mr(mr, client)));
console.log("Total 'oki'", total);
}
function parse_args(args: string[]) {
const [token_part] = args;
const token = token_part as string | undefined;
return { token };
}
function prompt_args() {
const [bl, gr] = [crayon.blue, crayon.lightBlack];
const message = `
${bl("Please enter a read-allowed gitlab token :")}
${gr("You can get one from https://gitlab.cri.epita.fr/-/user_settings/personal_access_tokens")}
${gr("with at least the permission 'read_api', format is :")}
${gr("> xxxxxxxxxxxxxxxxxxxx")}
>`.trim();
const result = prompt(message);
assert(result !== null);
return result.trim();
}
class ApiClient {
url;
token;
constructor(hostname: string, token: string) {
this.url = `https://${hostname}`;
this.token = token;
}
async fetch(path: string) {
console.log(crayon.lightBlack("\tfetching", crayon.bold(path)));
const headers = new Headers([["PRIVATE-TOKEN", this.token]]);
const result = await fetch(this.url + "/" + path, { headers });
const parsed = await result.json();
return parsed as unknown;
}
async fetch_parse<T>(path: string, parser: z.ZodType<T>) {
const unkn = await this.fetch(path);
return parser.parse(unkn);
}
async fetch_parse_paginated<T>(path: string, parser: z.ZodType<T>) {
const result = [] as T[];
let page = 1;
while (true) {
const fetched = await this.fetch_parse(path + "?per_page=100&page=" + page, parser.array());
if (fetched.length === 0) break;
result.push(...fetched);
page += 1;
}
return result;
}
}
type MergeRequest = z.infer<typeof merge_request_parser>;
const merge_request_parser = z.object({
iid: z.number(),
project_id: z.number(),
reviewers: z.array(z.object({
username: z.string(),
})),
});
async function get_all_merge_requests(client: ApiClient): Promise<MergeRequest[]> {
return await client.fetch_parse_paginated(`api/v4/merge_requests`, merge_request_parser);
}
function sum(numbers: Iterable<number>) {
let result = 0;
for (const item of numbers) result += item;
return result;
}
Deno.test("test_sum", () => {
assertEquals(sum([1, 2, 3]), 6);
});
async function parallel<I, O>(inputs: I[], operation: (item: I) => Promise<O>) {
const promises = [] as Promise<O>[];
for (const input of inputs) promises.push(operation(input));
return await Promise.all(promises);
}
async function count_in_mr(mr: MergeRequest, client: ApiClient) {
let total = 0;
const mr_path = `api/v4/projects/${mr.project_id}/merge_requests/${mr.iid}`;
const all_notes = await client.fetch_parse_paginated(`${mr_path}/notes`, note_parser);
for (const c of all_notes) {
if (!await is_relevant(c.author.username)) continue;
if (c.body.toLowerCase().includes("oki")) total += 1;
}
return total;
}
const note_parser = z.object({
body: z.string(),
author: z.object({
username: z.string(),
}),
});
async function hash(namespace: string) {
const buffer = new TextEncoder().encode(namespace);
const hash = await crypto.subtle.digest("SHA-512", buffer);
const base64 = encodeBase64(hash);
return base64;
}
async function is_relevant(text: string) {
return (await hash(text)) === relevant_hash;
}
const relevant_hash = "Qa4xy8yRPnBsQf8U2UUE7tse2u4Fi/Aqxbt" +
"6aq56m5ofcmCkTi8PbgMBF1OtHC6qkXDF2tz2qKprYCIAMzTSQQ==";
Deno.test("encode_namespace", async () => {
const namespace = "CHANGEME";
const base64 = await hash(namespace);
console.log({ namespace, base64 });
});
if (import.meta.main) await main();