import axios from "axios"; const rawGiteaBaseUrl = import.meta.env.VITE_GITEA_BASE_URL?.trim() ?? ""; const giteaToken = import.meta.env.VITE_GITEA_TOKEN?.trim() ?? ""; const rawGiteaRepoOwner = import.meta.env.VITE_GITEA_REPO_OWNER?.trim() ?? ""; const rawGiteaRepoName = import.meta.env.VITE_GITEA_REPO_NAME?.trim() ?? ""; export interface GiteaUser { id: number; login: string; full_name?: string; email?: string; } interface GiteaRepoOwner { login: string; } interface GiteaRepoResponse { name: string; full_name?: string; description?: string; html_url?: string; clone_url?: string; ssh_url?: string; updated_at?: string; default_branch?: string; private?: boolean; owner?: GiteaRepoOwner; } interface GiteaBranchResponse { name: string; commit?: { id?: string; message?: string; timestamp?: string; url?: string; }; } interface GiteaIssueResponse { number?: number; title?: string; body?: string; state?: string; html_url?: string; created_at?: string; updated_at?: string; user?: { login?: string; }; comments?: number; } interface GiteaIssueCommentResponse { id?: number; body?: string; html_url?: string; created_at?: string; updated_at?: string; user?: { login?: string; }; } interface GiteaLabelResponse { id: number; name: string; } interface GiteaAttachmentResponse { id?: number; name?: string; browser_download_url?: string; download_url?: string; size?: number; created_at?: string; } export interface CreatedGiteaIssue { number: number; webUrl: string; } export interface GiteaIssueSummary { number: number; title: string; body: string; state: string; webUrl: string; createdAt: string; updatedAt: string; author: string; comments: number; } export interface GiteaIssueAttachment { id: number; name: string; downloadUrl: string; size: number; createdAt: string; } export interface GiteaIssueComment { id: number; body: string; webUrl: string; createdAt: string; updatedAt: string; author: string; } export interface GiteaIssueDetail { issue: GiteaIssueSummary; comments: GiteaIssueComment[]; attachments: GiteaIssueAttachment[]; } export interface GiteaRepoSummary { name: string; fullName: string; description: string; webUrl: string; cloneUrl: string; defaultBranch: string; isPrivate: boolean; lastCommitAt: string; lastCommitSha: string; lastCommitMessage: string; } export const giteaConfig = { baseUrl: rawGiteaBaseUrl, hasToken: giteaToken.length > 0, repoOwner: rawGiteaRepoOwner, repoName: rawGiteaRepoName, targetRepo: rawGiteaRepoOwner.length > 0 && rawGiteaRepoName.length > 0 ? `${rawGiteaRepoOwner}/${rawGiteaRepoName}` : "", isConfigured: rawGiteaBaseUrl.length > 0 && giteaToken.length > 0 }; export function getApiBaseUrl(baseUrl: string): string { const normalized = baseUrl.replace(/\/+$/, ""); return normalized.endsWith("/api/v1") ? normalized : `${normalized}/api/v1`; } function assertGiteaConfigured(): void { if (!giteaConfig.isConfigured) { throw new Error("Set VITE_GITEA_BASE_URL and VITE_GITEA_TOKEN before testing the connection."); } } function createGiteaClient() { assertGiteaConfigured(); return axios.create({ baseURL: getApiBaseUrl(giteaConfig.baseUrl), headers: { Authorization: `token ${giteaToken}` } }); } export async function getCurrentGiteaUser(): Promise { const client = createGiteaClient(); const response = await client.get("/user"); return response.data; } export async function createGiteaIssue( owner: string, repo: string, title: string, body: string, labelIds: number[] = [] ): Promise { const client = createGiteaClient(); const response = await client.post(`/repos/${owner}/${repo}/issues`, { title, body, labels: labelIds }); const number = response.data.number; if (!number) { throw new Error("Gitea did not return an issue number."); } return { number, webUrl: response.data.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${number}` }; } export async function getGiteaIssues(owner: string, repo: string): Promise { const client = createGiteaClient(); const response = await client.get(`/repos/${owner}/${repo}/issues`, { params: { limit: 25, page: 1, state: "all", type: "issues" } }); return response.data .filter((issue) => issue.number !== undefined) .map((issue) => ({ number: issue.number ?? 0, title: issue.title || "Untitled issue", body: issue.body || "", state: issue.state || "unknown", webUrl: issue.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${issue.number}`, createdAt: issue.created_at || "", updatedAt: issue.updated_at || "", author: issue.user?.login || "unknown", comments: issue.comments ?? 0 })); } export async function getGiteaIssueDetail(owner: string, repo: string, issueNumber: number): Promise { const client = createGiteaClient(); const [issueResponse, commentsResponse, attachmentsResponse] = await Promise.all([ client.get(`/repos/${owner}/${repo}/issues/${issueNumber}`), client.get(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { params: { limit: 100, page: 1 } }), client.get(`/repos/${owner}/${repo}/issues/${issueNumber}/assets`, { params: { limit: 100, page: 1 } }) ]); const issue = issueResponse.data; return { issue: { number: issue.number ?? issueNumber, title: issue.title || "Untitled issue", body: issue.body || "", state: issue.state || "unknown", webUrl: issue.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${issueNumber}`, createdAt: issue.created_at || "", updatedAt: issue.updated_at || "", author: issue.user?.login || "unknown", comments: issue.comments ?? commentsResponse.data.length }, comments: commentsResponse.data.map((comment) => ({ id: comment.id ?? 0, body: comment.body || "", webUrl: comment.html_url || "", createdAt: comment.created_at || "", updatedAt: comment.updated_at || "", author: comment.user?.login || "unknown" })), attachments: attachmentsResponse.data.map((attachment) => ({ id: attachment.id ?? 0, name: attachment.name || "attachment", downloadUrl: attachment.browser_download_url || attachment.download_url || "", size: attachment.size ?? 0, createdAt: attachment.created_at || "" })) }; } export async function uploadGiteaIssueAttachment( owner: string, repo: string, issueNumber: number, file: Blob, fileName: string ): Promise { const client = createGiteaClient(); const formData = new FormData(); formData.append("attachment", file, fileName); const response = await client.post( `/repos/${owner}/${repo}/issues/${issueNumber}/assets`, formData ); return { id: response.data.id ?? 0, name: response.data.name || fileName, downloadUrl: response.data.browser_download_url || response.data.download_url || "", size: response.data.size ?? file.size, createdAt: response.data.created_at || "" }; } export async function createGiteaIssueComment( owner: string, repo: string, issueNumber: number, body: string ): Promise { const client = createGiteaClient(); await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { body }); } export async function deleteGiteaIssue(owner: string, repo: string, issueNumber: number): Promise { const client = createGiteaClient(); await client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}`); } async function getGiteaLabels(owner: string, repo: string): Promise { const client = createGiteaClient(); const response = await client.get(`/repos/${owner}/${repo}/labels`, { params: { limit: 100 } }); return response.data; } export async function ensureGiteaLabel(owner: string, repo: string, name: string, color = "0e8a16"): Promise { const existingLabels = await getGiteaLabels(owner, repo); const existing = existingLabels.find((label) => label.name.toLowerCase() === name.toLowerCase()); if (existing) { return existing; } const client = createGiteaClient(); try { const response = await client.post(`/repos/${owner}/${repo}/labels`, { name, color }); return response.data; } catch { const refreshedLabels = await getGiteaLabels(owner, repo); const refreshed = refreshedLabels.find((label) => label.name.toLowerCase() === name.toLowerCase()); if (refreshed) { return refreshed; } throw new Error(`Could not create or find the ${name} label.`); } } export async function addGiteaIssueLabels( owner: string, repo: string, issueNumber: number, labelIds: number[] ): Promise { const client = createGiteaClient(); await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, { labels: labelIds }); } export async function getReposOrderedByLastCommit(): Promise { const client = axios.create({ baseURL: getApiBaseUrl(giteaConfig.baseUrl), headers: { Authorization: `token ${giteaToken}` } }); assertGiteaConfigured(); const repos: GiteaRepoResponse[] = []; let page = 1; while (page <= 10) { const response = await client.get("/user/repos", { params: { limit: 50, page } }); repos.push(...response.data); if (response.data.length < 50) { break; } page += 1; } const repoDetails = await Promise.all( repos.map(async (repo): Promise => { const owner = repo.owner?.login || giteaConfig.repoOwner; const defaultBranch = repo.default_branch || "main"; const fullName = repo.full_name || `${owner}/${repo.name}`; let branch: GiteaBranchResponse | null = null; try { const branchResponse = await client.get( `/repos/${owner}/${repo.name}/branches/${defaultBranch}` ); branch = branchResponse.data; } catch { branch = null; } return { name: repo.name, fullName, description: repo.description || "", webUrl: repo.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${fullName}`, cloneUrl: repo.clone_url || "", defaultBranch, isPrivate: Boolean(repo.private), lastCommitAt: branch?.commit?.timestamp || repo.updated_at || "", lastCommitSha: branch?.commit?.id || "", lastCommitMessage: branch?.commit?.message?.split("\n")[0] || "No commit message available" }; }) ); return repoDetails.sort((a, b) => { const aTime = a.lastCommitAt ? Date.parse(a.lastCommitAt) : 0; const bTime = b.lastCommitAt ? Date.parse(b.lastCommitAt) : 0; return bTime - aTime || a.fullName.localeCompare(b.fullName); }); }