#!/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(path: string, parser: z.ZodType) { const unkn = await this.fetch(path); return parser.parse(unkn); } async fetch_parse_paginated(path: string, parser: z.ZodType) { 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; 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 { return await client.fetch_parse_paginated(`api/v4/merge_requests`, merge_request_parser); } function sum(numbers: Iterable) { 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(inputs: I[], operation: (item: I) => Promise) { const promises = [] as Promise[]; 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();