feat(git): Forgejo commit API client
Verified against the live instance: status 200, total 787 commits, first sha 1bc0da40... 'feat(customers): lift active/inactive toggle to top of customer form'.
This commit is contained in:
parent
a05c1b8ae0
commit
809983ee5c
1 changed files with 90 additions and 0 deletions
90
src/lib/git/forgejo.ts
Normal file
90
src/lib/git/forgejo.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import "server-only";
|
||||
|
||||
/**
|
||||
* Forgejo REST client, against the Gitea 1.22 API shape.
|
||||
*
|
||||
* Everything here is measured against the live instance rather than recalled:
|
||||
* the pagination headers, the query parameters, and the response fields are
|
||||
* all confirmed. See the header of the Phase 2a plan.
|
||||
*/
|
||||
|
||||
export interface ForgejoCommit {
|
||||
sha: string;
|
||||
html_url: string;
|
||||
commit: {
|
||||
message: string;
|
||||
author: { name: string; email: string; date: string };
|
||||
committer: { name: string; email: string; date: string };
|
||||
};
|
||||
parents: Array<{ sha: string }>;
|
||||
stats?: { total: number; additions: number; deletions: number };
|
||||
}
|
||||
|
||||
export interface CommitPage {
|
||||
commits: ForgejoCommit[];
|
||||
page: number;
|
||||
pageCount: number;
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
/** Forgejo caps limit at 50 on a default install; asking for more silently caps. */
|
||||
export const PAGE_SIZE = 50;
|
||||
|
||||
function baseUrl(): string {
|
||||
const url = process.env.FORGEJO_URL;
|
||||
if (!url) throw new Error("FORGEJO_URL is not set");
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function headers(): HeadersInit {
|
||||
const token = process.env.FORGEJO_TOKEN;
|
||||
return {
|
||||
accept: "application/json",
|
||||
// The instance currently serves commits anonymously. Sending a token when
|
||||
// one exists means a private repository works with no code change.
|
||||
...(token ? { authorization: `token ${token}` } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One page of commits, newest first.
|
||||
*
|
||||
* stat/files/verification are disabled: each commit otherwise carries its full
|
||||
* file list, which for a backfill of several hundred commits is a great deal
|
||||
* of payload for data this phase does not use.
|
||||
*/
|
||||
export async function fetchCommitPage(
|
||||
owner: string,
|
||||
repo: string,
|
||||
page: number
|
||||
): Promise<CommitPage> {
|
||||
const url =
|
||||
`${baseUrl()}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits` +
|
||||
`?page=${page}&limit=${PAGE_SIZE}&stat=false&files=false&verification=false`;
|
||||
|
||||
const response = await fetch(url, { headers: headers(), cache: "no-store" });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Forgejo returned ${response.status} for ${owner}/${repo} page ${page}`
|
||||
);
|
||||
}
|
||||
|
||||
const commits = (await response.json()) as ForgejoCommit[];
|
||||
|
||||
return {
|
||||
commits,
|
||||
page: Number(response.headers.get("x-page") ?? page),
|
||||
pageCount: Number(response.headers.get("x-pagecount") ?? 1),
|
||||
total: Number(response.headers.get("x-total-count") ?? commits.length),
|
||||
hasMore: response.headers.get("x-hasmore") === "true",
|
||||
};
|
||||
}
|
||||
|
||||
/** Confirms a repository exists and is readable before a backfill is started. */
|
||||
export async function repoExists(owner: string, repo: string): Promise<boolean> {
|
||||
const url = `${baseUrl()}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
||||
const response = await fetch(url, { headers: headers(), cache: "no-store" });
|
||||
return response.ok;
|
||||
}
|
||||
Loading…
Reference in a new issue