From 277e05fa2e5d9183e0ec02233666be4819574401 Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Wed, 13 May 2026 23:10:00 -0400 Subject: [PATCH] Add gitlab to provides --- .gitignore | 1 + get_history/.env.example | 6 +++ get_history/index.ts | 96 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 get_history/.env.example diff --git a/.gitignore b/.gitignore index cab0d2f..e5a81d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ test.html activity.json +public/ diff --git a/get_history/.env.example b/get_history/.env.example new file mode 100644 index 0000000..eb06bba --- /dev/null +++ b/get_history/.env.example @@ -0,0 +1,6 @@ +GH_API_KEY= +FJ_API_KEY= +FJ_USER= +FJ_URL= +GL_API_KEY= +GL_USER= diff --git a/get_history/index.ts b/get_history/index.ts index e775f33..fe2a0b3 100644 --- a/get_history/index.ts +++ b/get_history/index.ts @@ -9,6 +9,9 @@ const config = { forgejoApiKey: requireEnv("FJ_API_KEY"), forgejoUsername: requireEnv("FJ_USER"), forgejoURL: requireEnv("FJ_URL"), + gitlabApiKey: process.env["GL_API_KEY"], + gitlabUsername: process.env["GL_USER"], + gitlabURL: process.env["GL_URL"] ?? "https://gitlab.com", }; const now = new Date(); @@ -269,13 +272,102 @@ async function getGitHubActivityMap(): Promise { return map; } +function parseGitLabEvents(events: any[]): ActivityMap { + const result: ActivityMap = {}; + + for (const event of events) { + const pushData = event.push_data; + if (!pushData) continue; + // "deleted" is a branch deletion — no commits to count + if (pushData.action === "deleted") continue; + + // New branch pushes report commit_count=0 even though commits exist; + // fall back to 1 when there's a known head commit. + const commitCount: number = + pushData.commit_count > 0 + ? pushData.commit_count + : pushData.commit_to + ? 1 + : 0; + if (commitCount === 0) continue; + + // GitLab doesn't include a nested project object in the events API response; + // construct a name from the fields that are always present. + const repoFullName: string = + event.project?.path_with_namespace ?? + (event.author_username && event.target_title + ? `${event.author_username}/${event.target_title}` + : "unknown"); + const isPrivate: boolean = event.project?.visibility === "private"; + const displayName = isPrivate ? "private" : repoFullName; + + const day: string = event.created_at.split("T")[0]!; + + if (!result[day]) result[day] = {}; + result[day]![displayName] = (result[day]![displayName] ?? 0) + commitCount; + } + + return result; +} + +async function getGitLabActivityMap(): Promise { + if (!config.gitlabApiKey || !config.gitlabUsername) { + console.log("[GitLab] Skipping: GL_API_KEY or GL_USER not set"); + return {}; + } + + const pageLimit = 100; + let page = 1; + const map: ActivityMap = {}; + const afterDate = oneYearAgo.toISOString().split("T")[0]!; + + console.log(`[GitLab] Fetching events for user "${config.gitlabUsername}" from ${config.gitlabURL} after ${afterDate}`); + + while (true) { + const url = `${config.gitlabURL}/api/v4/users/${config.gitlabUsername}/events?action=pushed&after=${afterDate}&per_page=${pageLimit}&page=${page}`; + console.log(`[GitLab] GET ${url}`); + + const response = await fetch(url, { + headers: { "PRIVATE-TOKEN": config.gitlabApiKey }, + }); + + console.log(`[GitLab] Response: ${response.status} ${response.statusText}`); + + if (!response.ok) { + const body = await response.text(); + console.error(`[GitLab] Error body: ${body}`); + throw new Error(`GitLab API error: ${response.status} ${response.statusText}`); + } + + const events: any[] = await response.json(); + console.log(`[GitLab] Page ${page}: received ${events.length} events`); + + if (events.length > 0) { + console.log(`[GitLab] First event sample:`, JSON.stringify(events[0], null, 2)); + } + + if (events.length === 0) break; + + const parsed = parseGitLabEvents(events); + console.log(`[GitLab] Page ${page}: parsed ${Object.keys(parsed).length} days of activity`); + mergeInto(map, parsed); + + if (events.length < pageLimit) break; + page++; + } + + return map; +} + async function makeActivityMap(): Promise { - const [ghMap, fjMap] = await Promise.all([ + const [ghMap, fjMap, glMap] = await Promise.all([ getGitHubActivityMap(), getForgejoActivityMap(), + getGitLabActivityMap(), ]); - return deduplicateAcrossPlatforms(ghMap, fjMap); + const merged = deduplicateAcrossPlatforms(ghMap, fjMap); + return deduplicateAcrossPlatforms(merged, glMap); } const activityMap = await makeActivityMap();