138 lines
4.1 KiB
TypeScript
Executable file
138 lines
4.1 KiB
TypeScript
Executable file
#!/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", total, "oki");
|
|
}
|
|
|
|
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();
|