429 lines
11 KiB
TypeScript
429 lines
11 KiB
TypeScript
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<GiteaUser> {
|
|
const client = createGiteaClient();
|
|
const response = await client.get<GiteaUser>("/user");
|
|
return response.data;
|
|
}
|
|
|
|
export async function createGiteaIssue(
|
|
owner: string,
|
|
repo: string,
|
|
title: string,
|
|
body: string,
|
|
labelIds: number[] = []
|
|
): Promise<CreatedGiteaIssue> {
|
|
const client = createGiteaClient();
|
|
const response = await client.post<GiteaIssueResponse>(`/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<GiteaIssueSummary[]> {
|
|
const client = createGiteaClient();
|
|
const response = await client.get<GiteaIssueResponse[]>(`/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<GiteaIssueDetail> {
|
|
const client = createGiteaClient();
|
|
const [issueResponse, commentsResponse, attachmentsResponse] = await Promise.all([
|
|
client.get<GiteaIssueResponse>(`/repos/${owner}/${repo}/issues/${issueNumber}`),
|
|
client.get<GiteaIssueCommentResponse[]>(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
|
params: {
|
|
limit: 100,
|
|
page: 1
|
|
}
|
|
}),
|
|
client.get<GiteaAttachmentResponse[]>(`/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<GiteaIssueAttachment> {
|
|
const client = createGiteaClient();
|
|
const formData = new FormData();
|
|
formData.append("attachment", file, fileName);
|
|
|
|
const response = await client.post<GiteaAttachmentResponse>(
|
|
`/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<void> {
|
|
const client = createGiteaClient();
|
|
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
|
body
|
|
});
|
|
}
|
|
|
|
export async function deleteGiteaIssue(owner: string, repo: string, issueNumber: number): Promise<void> {
|
|
const client = createGiteaClient();
|
|
await client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}`);
|
|
}
|
|
|
|
async function getGiteaLabels(owner: string, repo: string): Promise<GiteaLabelResponse[]> {
|
|
const client = createGiteaClient();
|
|
const response = await client.get<GiteaLabelResponse[]>(`/repos/${owner}/${repo}/labels`, {
|
|
params: {
|
|
limit: 100
|
|
}
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
export async function ensureGiteaLabel(owner: string, repo: string, name: string, color = "0e8a16"): Promise<GiteaLabelResponse> {
|
|
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<GiteaLabelResponse>(`/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<void> {
|
|
const client = createGiteaClient();
|
|
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, {
|
|
labels: labelIds
|
|
});
|
|
}
|
|
|
|
export async function getReposOrderedByLastCommit(): Promise<GiteaRepoSummary[]> {
|
|
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<GiteaRepoResponse[]>("/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<GiteaRepoSummary> => {
|
|
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<GiteaBranchResponse>(
|
|
`/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);
|
|
});
|
|
}
|