commit f159fab9a96ba0230bf34e791359afc1822521a8 Author: JOLIMAITRE Matthieu Date: Fri Apr 12 17:34:57 2024 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfc1c37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/okimeter +/token \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cbac569 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..f5558cf --- /dev/null +++ b/build.sh @@ -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" diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..f5a20a0 --- /dev/null +++ b/deno.json @@ -0,0 +1,6 @@ +{ + "fmt": { + "useTabs": true, + "lineWidth": 120 + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..81b3ddb --- /dev/null +++ b/deno.lock @@ -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" + } +} diff --git a/main.ts b/main.ts new file mode 100755 index 0000000..205c053 --- /dev/null +++ b/main.ts @@ -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(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();