173 lines
5.0 KiB
TypeScript
173 lines
5.0 KiB
TypeScript
|
|
import axios from 'axios';
|
||
|
|
import { Config, Issue, Comment } from './types.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Normalizes the Gitea/Forgejo URL by stripping trailing slashes
|
||
|
|
* and ensuring it has a protocol (defaults to https).
|
||
|
|
*/
|
||
|
|
export function normalizeUrl(url: string): string {
|
||
|
|
let cleaned = url.trim();
|
||
|
|
if (!cleaned) return '';
|
||
|
|
if (!/^https?:\/\//i.test(cleaned)) {
|
||
|
|
cleaned = 'https://' + cleaned;
|
||
|
|
}
|
||
|
|
return cleaned.replace(/\/+$/, '');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Creates an Axios instance with standard Gitea/Forgejo headers.
|
||
|
|
*/
|
||
|
|
function createAxiosInstance(config: Config) {
|
||
|
|
const headers: Record<string, string> = {
|
||
|
|
'Accept': 'application/json',
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
};
|
||
|
|
|
||
|
|
if (config.token) {
|
||
|
|
headers['Authorization'] = `token ${config.token.trim()}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
return axios.create({
|
||
|
|
baseURL: `${normalizeUrl(config.url)}/api/v1`,
|
||
|
|
headers,
|
||
|
|
timeout: 10000,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Tests the connection to the Gitea/Forgejo instance and validates that the repository exists.
|
||
|
|
* Returns the repository full name on success.
|
||
|
|
*/
|
||
|
|
export async function validateConnection(config: Config): Promise<string> {
|
||
|
|
const client = createAxiosInstance(config);
|
||
|
|
try {
|
||
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}`);
|
||
|
|
return response.data.full_name || `${config.owner}/${config.repo}`;
|
||
|
|
} catch (error: any) {
|
||
|
|
if (error.response) {
|
||
|
|
if (error.response.status === 401) {
|
||
|
|
throw new Error('Unauthorized: Invalid API Token or authentication failed.');
|
||
|
|
}
|
||
|
|
if (error.response.status === 404) {
|
||
|
|
throw new Error(`Repository "${config.owner}/${config.repo}" not found or is private.`);
|
||
|
|
}
|
||
|
|
throw new Error(`API Error: ${error.response.data?.message || error.message}`);
|
||
|
|
}
|
||
|
|
throw new Error(`Failed to connect to ${config.url}: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetches a page of issues for a specific repository.
|
||
|
|
*/
|
||
|
|
export async function fetchIssues(
|
||
|
|
config: Config,
|
||
|
|
options: {
|
||
|
|
page: number;
|
||
|
|
limit: number;
|
||
|
|
state: 'open' | 'closed' | 'all';
|
||
|
|
type: 'issues' | 'pulls' | 'all';
|
||
|
|
q: string;
|
||
|
|
sortField: 'created' | 'updated' | 'comments';
|
||
|
|
sortOrder: 'asc' | 'desc';
|
||
|
|
}
|
||
|
|
): Promise<{ issues: Issue[]; totalCount: number }> {
|
||
|
|
const client = createAxiosInstance(config);
|
||
|
|
|
||
|
|
// Map our internal sort fields to Gitea API sort parameters
|
||
|
|
let sort = 'newest';
|
||
|
|
if (options.sortField === 'created') {
|
||
|
|
sort = options.sortOrder === 'asc' ? 'oldest' : 'newest';
|
||
|
|
} else if (options.sortField === 'updated') {
|
||
|
|
sort = options.sortOrder === 'asc' ? 'leastupdate' : 'recentupdate';
|
||
|
|
} else if (options.sortField === 'comments') {
|
||
|
|
sort = options.sortOrder === 'asc' ? 'leastcomment' : 'mostcomment';
|
||
|
|
}
|
||
|
|
|
||
|
|
const params: Record<string, any> = {
|
||
|
|
page: options.page,
|
||
|
|
limit: options.limit,
|
||
|
|
state: options.state,
|
||
|
|
sort: sort,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Filter by type: pulls or issues
|
||
|
|
if (options.type !== 'all') {
|
||
|
|
params.type = options.type;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Filter by search query if present
|
||
|
|
if (options.q.trim()) {
|
||
|
|
params.q = options.q.trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}/issues`, { params });
|
||
|
|
|
||
|
|
// Parse x-total-count header
|
||
|
|
const totalCountHeader = response.headers['x-total-count'];
|
||
|
|
const totalCount = totalCountHeader ? parseInt(totalCountHeader, 10) : response.data.length;
|
||
|
|
|
||
|
|
const issues: Issue[] = response.data.map((item: any) => ({
|
||
|
|
id: item.id,
|
||
|
|
number: item.number,
|
||
|
|
title: item.title,
|
||
|
|
state: item.state,
|
||
|
|
body: item.body || '',
|
||
|
|
user: {
|
||
|
|
id: item.user?.id || 0,
|
||
|
|
login: item.user?.login || 'unknown',
|
||
|
|
full_name: item.user?.full_name || '',
|
||
|
|
},
|
||
|
|
created_at: item.created_at,
|
||
|
|
updated_at: item.updated_at,
|
||
|
|
comments: item.comments_count || item.comments || 0,
|
||
|
|
labels: (item.labels || []).map((l: any) => ({
|
||
|
|
id: l.id,
|
||
|
|
name: l.name,
|
||
|
|
color: l.color,
|
||
|
|
})),
|
||
|
|
pull_request: item.pull_request,
|
||
|
|
}));
|
||
|
|
|
||
|
|
return { issues, totalCount };
|
||
|
|
} catch (error: any) {
|
||
|
|
if (error.response) {
|
||
|
|
throw new Error(`API Error: ${error.response.data?.message || error.message}`);
|
||
|
|
}
|
||
|
|
throw new Error(`Network Error: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fetches comments for a specific issue.
|
||
|
|
*/
|
||
|
|
export async function fetchIssueComments(
|
||
|
|
config: Config,
|
||
|
|
issueNumber: number
|
||
|
|
): Promise<Comment[]> {
|
||
|
|
const client = createAxiosInstance(config);
|
||
|
|
try {
|
||
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}/comments`);
|
||
|
|
|
||
|
|
const comments: Comment[] = response.data.map((item: any) => ({
|
||
|
|
id: item.id,
|
||
|
|
user: {
|
||
|
|
id: item.user?.id || 0,
|
||
|
|
login: item.user?.login || 'unknown',
|
||
|
|
full_name: item.user?.full_name || '',
|
||
|
|
},
|
||
|
|
body: item.body || '',
|
||
|
|
created_at: item.created_at,
|
||
|
|
updated_at: item.updated_at,
|
||
|
|
}));
|
||
|
|
|
||
|
|
return comments;
|
||
|
|
} catch (error: any) {
|
||
|
|
if (error.response) {
|
||
|
|
throw new Error(`Failed to fetch comments: ${error.response.data?.message || error.message}`);
|
||
|
|
}
|
||
|
|
throw new Error(`Network Error: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|