Add gitlab to provides

This commit is contained in:
Alex Selimov 2026-05-13 23:10:00 -04:00
parent 46315d9a62
commit 277e05fa2e
3 changed files with 101 additions and 2 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
test.html
activity.json
public/

6
get_history/.env.example Normal file
View file

@ -0,0 +1,6 @@
GH_API_KEY=
FJ_API_KEY=
FJ_USER=
FJ_URL=
GL_API_KEY=
GL_USER=

View file

@ -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<ActivityMap> {
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<ActivityMap> {
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<ActivityMap> {
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();