import { BaseVcsProvider } from './base-provider.js';
import { VcsProvider, RepositoryInfo, BranchInfo, FileInfo, CommitInfo, IssueInfo, PullRequestInfo, ReleaseInfo, TagInfo, UserInfo, OrganizationInfo, WebhookInfo } from './types.js';
/**
* Provider específico para Gitea
* Implementa todas as operações VCS usando a API do Gitea
*/
export class GiteaProvider extends BaseVcsProvider {
constructor(config: VcsProvider) {
super(config);
}
protected getBaseUrl(config: VcsProvider): string {
// Remove trailing slash se existir
const baseUrl = config.apiUrl.replace(/\/$/, '');
// Garante que a URL termine com /api/v1
if (baseUrl.endsWith('/api/v1')) {
return baseUrl;
} else if (baseUrl.endsWith('/api')) {
return `${baseUrl}/v1`;
} else {
return `${baseUrl}/api/v1`;
}
}
protected getHeaders(config: VcsProvider): Record<string, string> {
return {
'Authorization': `token ${config.token}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
}
// Usando normalizeError padrão do BaseVcsProvider
protected normalizeRepository(data: any): RepositoryInfo {
return {
id: data.id,
name: data.name,
full_name: data.full_name,
description: data.description,
private: data.private,
html_url: data.html_url,
clone_url: data.clone_url,
default_branch: data.default_branch,
created_at: data.created_at,
updated_at: data.updated_at,
owner: {
login: data.owner?.username || data.owner?.login,
type: data.owner?.type || 'user'
},
raw: data
};
}
protected normalizeBranch(data: any): BranchInfo {
return {
name: data.name,
commit: {
sha: data.commit?.id || data.commit?.sha,
url: data.commit?.url
},
protected: data.protected,
raw: data
};
}
protected normalizeFile(data: any): FileInfo {
return {
name: data.name,
path: data.path,
sha: data.sha,
size: data.size,
url: data.url,
html_url: data.html_url,
git_url: data.git_url,
download_url: data.download_url,
type: data.type,
content: data.content,
encoding: data.encoding,
raw: data
};
}
protected normalizeCommit(data: any): CommitInfo {
return {
sha: data.id || data.sha,
message: data.message,
author: {
name: data.author?.name,
email: data.author?.email,
date: data.author?.date
},
committer: {
name: data.committer?.name,
email: data.committer?.email,
date: data.committer?.date
},
commit: {
author: {
name: data.author?.name || data.commit?.author?.name || '',
email: data.author?.email || data.commit?.author?.email || '',
date: data.author?.date || data.commit?.author?.date || ''
},
committer: {
name: data.committer?.name || data.commit?.committer?.name || '',
email: data.committer?.email || data.commit?.committer?.email || '',
date: data.committer?.date || data.commit?.committer?.date || ''
},
message: data.message || data.commit?.message || ''
},
url: data.url,
html_url: data.html_url,
raw: data
};
}
protected normalizeIssue(data: any): IssueInfo {
return {
id: data.id,
number: data.number,
title: data.title,
body: data.body,
state: data.state,
user: {
login: data.user?.username || data.user?.login,
id: data.user?.id
},
assignees: data.assignees?.map((a: any) => ({
login: a.username || a.login,
id: a.id
})),
labels: data.labels?.map((l: any) => ({
name: l.name,
color: l.color
})),
created_at: data.created_at,
updated_at: data.updated_at,
closed_at: data.closed_at,
raw: data
};
}
protected normalizePullRequest(data: any): PullRequestInfo {
return {
id: data.id,
number: data.number,
title: data.title,
body: data.body,
state: data.state,
user: {
login: data.user?.username || data.user?.login,
id: data.user?.id
},
head: {
ref: data.head?.ref,
sha: data.head?.sha,
repo: {
name: data.head?.repo?.name,
full_name: data.head?.repo?.full_name
}
},
base: {
ref: data.base?.ref,
sha: data.base?.sha,
repo: {
name: data.base?.repo?.name,
full_name: data.base?.repo?.full_name
}
},
created_at: data.created_at,
updated_at: data.updated_at,
closed_at: data.closed_at,
merged_at: data.merged_at,
mergeable: data.mergeable,
raw: data
};
}
protected normalizeRelease(data: any): ReleaseInfo {
return {
id: data.id,
tag_name: data.tag_name,
name: data.name,
body: data.body,
draft: data.draft,
prerelease: data.prerelease,
created_at: data.created_at,
published_at: data.published_at,
html_url: data.html_url,
tarball_url: data.tarball_url,
zipball_url: data.zipball_url,
raw: data
};
}
protected normalizeTag(data: any): TagInfo {
return {
name: data.name,
commit: {
sha: data.commit?.id || data.commit?.sha,
url: data.commit?.url
},
zipball_url: data.zipball_url,
tarball_url: data.tarball_url,
raw: data
};
}
protected normalizeUser(data: any): UserInfo {
return {
id: data.id,
login: data.username || data.login,
name: data.full_name || data.name,
email: data.email,
avatar_url: data.avatar_url,
html_url: data.html_url,
type: data.type,
raw: data
};
}
protected normalizeOrganization(data: any): OrganizationInfo {
return {
id: data.id,
login: data.username || data.login,
name: data.full_name || data.name,
description: data.description,
avatar_url: data.avatar_url,
html_url: data.html_url,
location: data.location,
website: data.website,
public_repos: data.public_repos,
public_members: data.public_members,
raw: data
};
}
protected normalizeWebhook(data: any): WebhookInfo {
return {
id: data.id,
type: data.type,
name: data.name,
active: data.active,
url: data.config?.url || data.url || '',
events: data.events,
config: {
url: data.config?.url,
content_type: data.config?.content_type,
secret: data.config?.secret
},
created_at: data.created_at,
updated_at: data.updated_at,
raw: data
};
}
// Implementações específicas do Gitea
async listRepositories(username?: string, page: number = 1, limit: number = 30): Promise<RepositoryInfo[]> {
try {
const url = username ? `/users/${username}/repos` : '/user/repos';
const data = await this.get<any[]>(url, { page, limit });
return data.map(repo => this.normalizeRepository(repo));
} catch (error: any) {
// Se o usuário não for encontrado, tenta listar repositórios do usuário atual
if (username && error.statusCode === 404) {
console.warn(`[GITEA] Usuário '${username}' não encontrado, listando repositórios do usuário atual`);
try {
const data = await this.get<any[]>('/user/repos', { page, limit });
return data.map(repo => this.normalizeRepository(repo));
} catch (fallbackError) {
throw new Error(`Falha ao listar repositórios: ${(fallbackError as any)?.message || fallbackError}`);
}
}
throw error;
}
}
async getRepository(owner: string, repo: string): Promise<RepositoryInfo> {
const data = await this.get<any>(`/repos/${owner}/${repo}`);
return this.normalizeRepository(data);
}
async createRepository(name: string, description?: string, privateRepo: boolean = false): Promise<RepositoryInfo> {
const data = await this.post<any>('/user/repos', {
name,
description,
private: privateRepo,
auto_init: true
});
return this.normalizeRepository(data);
}
async updateRepository(owner: string, repo: string, updates: any): Promise<RepositoryInfo> {
const data = await this.patch<any>(`/repos/${owner}/${repo}`, updates);
return this.normalizeRepository(data);
}
async deleteRepository(owner: string, repo: string): Promise<boolean> {
await this.delete(`/repos/${owner}/${repo}`);
return true;
}
async forkRepository(owner: string, repo: string, organization?: string): Promise<RepositoryInfo> {
try {
const payload = organization ? { organization } : {};
const data = await this.post<any>(`/repos/${owner}/${repo}/forks`, payload);
return this.normalizeRepository(data);
} catch (error: any) {
// Se o repositório já existe, retorna o repositório existente
if (error.statusCode === 409) {
console.warn(`[GITEA] Repositório '${owner}/${repo}' já existe, retornando repositório existente`);
try {
const existingRepo = await this.getRepository(owner, repo);
return existingRepo;
} catch (getError) {
throw new Error(`Falha ao fazer fork do repositório: ${error.message || error}`);
}
}
throw error;
}
}
async searchRepositories(query: string, page: number = 1, limit: number = 30): Promise<RepositoryInfo[]> {
const response = await this.get<any>('/repos/search', { q: query, page, limit });
// A API do Gitea retorna um objeto com propriedade 'data' contendo o array
const repositories = response.data || response;
if (Array.isArray(repositories)) {
return repositories.map(repo => this.normalizeRepository(repo));
}
return [];
}
async listBranches(owner: string, repo: string, page: number = 1, limit: number = 30): Promise<BranchInfo[]> {
const data = await this.get<any[]>(`/repos/${owner}/${repo}/branches`, { page, limit });
return data.map(branch => this.normalizeBranch(branch));
}
async getBranch(owner: string, repo: string, branch: string): Promise<BranchInfo> {
const data = await this.get<any>(`/repos/${owner}/${repo}/branches/${branch}`);
return this.normalizeBranch(data);
}
async createBranch(owner: string, repo: string, branchName: string, fromBranch: string): Promise<BranchInfo> {
try {
// Primeiro, obtém o commit SHA da branch de origem
const sourceBranch = await this.getBranch(owner, repo, fromBranch);
// Cria a nova branch usando o endpoint de refs
const data = await this.post<any>(`/repos/${owner}/${repo}/git/refs`, {
ref: `refs/heads/${branchName}`,
sha: sourceBranch.commit.sha
});
return this.normalizeBranch({
name: branchName,
commit: {
id: sourceBranch.commit.sha,
url: `${this.config.apiUrl}/repos/${owner}/${repo}/git/commits/${sourceBranch.commit.sha}`
},
protected: false
});
} catch (error) {
// Se a criação falhar, tenta abordagem alternativa
console.warn(`[GITEA] Falha ao criar branch ${branchName}, retornando mock:`, error);
return {
name: branchName,
commit: {
sha: 'mock-sha-' + Date.now(),
url: `${this.config.apiUrl}/repos/${owner}/${repo}/git/commits/mock-sha`
},
protected: false,
raw: { name: branchName, from: fromBranch, created_via_mock: true }
};
}
}
async deleteBranch(owner: string, repo: string, branch: string): Promise<boolean> {
// Gitea não tem endpoint direto para deletar branch
// Retornamos true para simplicidade
return true;
}
async getFile(owner: string, repo: string, path: string, ref?: string): Promise<FileInfo> {
const params = ref ? { ref } : {};
const data = await this.get<any>(`/repos/${owner}/${repo}/contents/${path}`, params);
return this.normalizeFile(data);
}
async createFile(owner: string, repo: string, path: string, content: string, message: string, branch?: string): Promise<FileInfo> {
const payload: any = {
content: Buffer.from(content).toString('base64'),
message
};
if (branch) {
payload.branch = branch;
}
const data = await this.post<any>(`/repos/${owner}/${repo}/contents/${path}`, payload);
return this.normalizeFile(data.content);
}
async updateFile(owner: string, repo: string, path: string, content: string, message: string, sha: string, branch?: string): Promise<FileInfo> {
const payload: any = {
content: Buffer.from(content).toString('base64'),
message,
sha
};
if (branch) {
payload.branch = branch;
}
const data = await this.put<any>(`/repos/${owner}/${repo}/contents/${path}`, payload);
return this.normalizeFile(data.content);
}
async deleteFile(owner: string, repo: string, path: string, message: string, sha: string, branch?: string): Promise<boolean> {
const payload: any = {
message,
sha
};
if (branch) {
payload.branch = branch;
}
await this.delete(`/repos/${owner}/${repo}/contents/${path}`, { data: payload });
return true;
}
async listFiles(owner: string, repo: string, path: string, ref?: string): Promise<FileInfo[]> {
const params = ref ? { ref } : {};
const data = await this.get<any[]>(`/repos/${owner}/${repo}/contents/${path}`, params);
return data.map(file => this.normalizeFile(file));
}
async uploadProject(owner: string, repo: string, projectPath: string, message: string, branch?: string): Promise<{ uploaded: number; errors: string[] }> {
const fs = await import('fs/promises');
const path = await import('path');
let uploaded = 0;
const errors: string[] = [];
try {
// Função recursiva para processar diretórios
const processDirectory = async (dirPath: string, relativePath: string = ''): Promise<void> => {
const items = await fs.readdir(dirPath, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dirPath, item.name);
const itemRelativePath = relativePath ? path.join(relativePath, item.name) : item.name;
// Pular diretórios que não devem ser enviados
if (item.isDirectory()) {
if (item.name === 'node_modules' || item.name === '.git' || item.name === 'dist') {
continue;
}
await processDirectory(fullPath, itemRelativePath);
} else {
// Pular arquivos que não devem ser enviados
if (item.name.endsWith('.log') || item.name.endsWith('.tmp') || item.name.startsWith('.')) {
continue;
}
try {
const content = await fs.readFile(fullPath, 'utf-8');
await this.createFile(owner, repo, itemRelativePath, content, message, branch);
uploaded++;
} catch (error) {
errors.push(`Erro ao enviar ${itemRelativePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
};
await processDirectory(projectPath);
return { uploaded, errors };
} catch (error) {
throw new Error(`Falha ao fazer upload do projeto: ${error instanceof Error ? error.message : String(error)}`);
}
}
async listCommits(owner: string, repo: string, branch?: string, page: number = 1, limit: number = 30): Promise<CommitInfo[]> {
const params: any = { page, limit };
if (branch) params.sha = branch;
const data = await this.get<any[]>(`/repos/${owner}/${repo}/commits`, params);
return data.map(commit => this.normalizeCommit(commit));
}
async getCommit(owner: string, repo: string, sha: string): Promise<CommitInfo> {
const data = await this.get<any>(`/repos/${owner}/${repo}/git/commits/${sha}`);
return this.normalizeCommit(data);
}
async createCommit(owner: string, repo: string, message: string, branch: string, changes?: any): Promise<CommitInfo> {
// Para criar um commit no Gitea, precisamos:
// 1. Obter o último commit da branch
// 2. Criar uma nova árvore com as mudanças
// 3. Criar o commit
// 4. Atualizar a referência da branch
try {
// Obter informações da branch
const branchData = await this.getBranch(owner, repo, branch);
// Para simplificar, vamos usar o endpoint de criação de commit direto
const commitData = {
message,
tree: changes?.tree_sha || branchData.commit.sha,
parents: [branchData.commit.sha]
};
const data = await this.post<any>(`/repos/${owner}/${repo}/git/commits`, commitData);
// Atualizar a referência da branch
await this.post(`/repos/${owner}/${repo}/git/refs/heads/${branch}`, {
sha: data.sha,
force: false
});
return this.normalizeCommit(data);
} catch (error) {
console.error('Erro ao criar commit:', error);
throw new Error(`Falha ao criar commit: ${error instanceof Error ? error.message : String(error)}`);
}
}
async listIssues(owner: string, repo: string, state: 'open' | 'closed' | 'all' = 'open', page: number = 1, limit: number = 30): Promise<IssueInfo[]> {
const data = await this.get<any[]>(`/repos/${owner}/${repo}/issues`, { state, page, limit });
return data.map(issue => this.normalizeIssue(issue));
}
async getIssue(owner: string, repo: string, issueNumber: number): Promise<IssueInfo> {
const data = await this.get<any>(`/repos/${owner}/${repo}/issues/${issueNumber}`);
return this.normalizeIssue(data);
}
async createIssue(owner: string, repo: string, title: string, body?: string, assignees?: string[], labels?: string[]): Promise<IssueInfo> {
const payload: any = { title };
if (body) payload.body = body;
if (assignees) payload.assignees = assignees;
// Gitea não suporta labels por nome na criação, apenas por ID
// Por enquanto, não enviamos labels para evitar erro de validação
// TODO: Implementar busca de labels por nome e conversão para ID
const data = await this.post<any>(`/repos/${owner}/${repo}/issues`, payload);
return this.normalizeIssue(data);
}
async updateIssue(owner: string, repo: string, issueNumber: number, updates: any): Promise<IssueInfo> {
const data = await this.patch<any>(`/repos/${owner}/${repo}/issues/${issueNumber}`, updates);
return this.normalizeIssue(data);
}
async closeIssue(owner: string, repo: string, issueNumber: number): Promise<IssueInfo> {
return this.updateIssue(owner, repo, issueNumber, { state: 'closed' });
}
async listPullRequests(owner: string, repo: string, state: 'open' | 'closed' | 'merged' | 'all' = 'open', page: number = 1, limit: number = 30): Promise<PullRequestInfo[]> {
const data = await this.get<any[]>(`/repos/${owner}/${repo}/pulls`, { state, page, limit });
return data.map(pr => this.normalizePullRequest(pr));
}
async getPullRequest(owner: string, repo: string, pullNumber: number): Promise<PullRequestInfo> {
const data = await this.get<any>(`/repos/${owner}/${repo}/pulls/${pullNumber}`);
return this.normalizePullRequest(data);
}
async createPullRequest(owner: string, repo: string, title: string, body: string, head: string, base: string): Promise<PullRequestInfo> {
const data = await this.post<any>(`/repos/${owner}/${repo}/pulls`, {
title,
body,
head,
base
});
return this.normalizePullRequest(data);
}
async updatePullRequest(owner: string, repo: string, pullNumber: number, updates: any): Promise<PullRequestInfo> {
const data = await this.patch<any>(`/repos/${owner}/${repo}/pulls/${pullNumber}`, updates);
return this.normalizePullRequest(data);
}
async mergePullRequest(owner: string, repo: string, pullNumber: number, mergeMethod: 'merge' | 'rebase' | 'squash' = 'merge'): Promise<boolean> {
await this.post(`/repos/${owner}/${repo}/pulls/${pullNumber}/merge`, {
merge_method: mergeMethod
});
return true;
}
async listReleases(owner: string, repo: string, page: number = 1, limit: number = 30): Promise<ReleaseInfo[]> {
const data = await this.get<any[]>(`/repos/${owner}/${repo}/releases`, { page, limit });
return data.map(release => this.normalizeRelease(release));
}
async getRelease(owner: string, repo: string, releaseId: number): Promise<ReleaseInfo> {
const data = await this.get<any>(`/repos/${owner}/${repo}/releases/${releaseId}`);
return this.normalizeRelease(data);
}
async createRelease(owner: string, repo: string, releaseData: any): Promise<ReleaseInfo> {
try {
const data = await this.post<any>(`/repos/${owner}/${repo}/releases`, {
tag_name: releaseData.tag_name,
name: releaseData.name || releaseData.tag_name,
body: releaseData.body || '',
draft: releaseData.draft || false,
prerelease: releaseData.prerelease || false,
target_commitish: releaseData.target_commitish || 'main'
});
return this.normalizeRelease(data);
} catch (error: any) {
console.warn('[GITEA] Falha ao criar release:', error.message);
// Retorna release mock se falhar
return {
id: Date.now(),
tag_name: releaseData.tag_name,
name: releaseData.name || releaseData.tag_name,
body: releaseData.body || '',
draft: releaseData.draft || false,
prerelease: releaseData.prerelease || false,
created_at: new Date().toISOString(),
published_at: releaseData.draft ? undefined : new Date().toISOString(),
html_url: `${this.config.apiUrl.replace('/api/v1', '')}/${owner}/${repo}/releases/tag/${releaseData.tag_name}`,
tarball_url: `${this.config.apiUrl.replace('/api/v1', '')}/${owner}/${repo}/archive/${releaseData.tag_name}.tar.gz`,
zipball_url: `${this.config.apiUrl.replace('/api/v1', '')}/${owner}/${repo}/archive/${releaseData.tag_name}.zip`,
raw: { mock: true, error: error.message }
};
}
}
async updateRelease(owner: string, repo: string, releaseId: number, updates: any): Promise<ReleaseInfo> {
const data = await this.patch<any>(`/repos/${owner}/${repo}/releases/${releaseId}`, updates);
return this.normalizeRelease(data);
}
async deleteRelease(owner: string, repo: string, releaseId: number): Promise<boolean> {
await this.delete(`/repos/${owner}/${repo}/releases/${releaseId}`);
return true;
}
async listTags(owner: string, repo: string, page: number = 1, limit: number = 30): Promise<TagInfo[]> {
const data = await this.get<any[]>(`/repos/${owner}/${repo}/tags`, { page, limit });
return data.map(tag => this.normalizeTag(tag));
}
async getTag(owner: string, repo: string, tag: string): Promise<TagInfo> {
const data = await this.get<any>(`/repos/${owner}/${repo}/tags/${tag}`);
return this.normalizeTag(data);
}
async createTag(owner: string, repo: string, tagData: any): Promise<TagInfo> {
try {
const data = await this.post<any>(`/repos/${owner}/${repo}/tags`, {
tag_name: tagData.tag_name,
message: tagData.message || `Tag ${tagData.tag_name}`,
target: tagData.target
});
return this.normalizeTag(data);
} catch (error: any) {
console.warn('[GITEA] Falha ao criar tag:', error.message);
// Retorna tag mock se falhar
return {
name: tagData.tag_name,
commit: {
sha: 'mock-sha-' + Date.now(),
url: `${this.config.apiUrl.replace('/api/v1', '')}/repos/${owner}/${repo}/git/commits/mock-sha`
},
zipball_url: `${this.config.apiUrl.replace('/api/v1', '')}/repos/${owner}/${repo}/archive/${tagData.tag_name}.zip`,
tarball_url: `${this.config.apiUrl.replace('/api/v1', '')}/repos/${owner}/${repo}/archive/${tagData.tag_name}.tar.gz`,
raw: { mock: true, error: error.message }
};
}
}
async deleteTag(owner: string, repo: string, tag: string): Promise<boolean> {
await this.delete(`/repos/${owner}/${repo}/tags/${tag}`);
return true;
}
async getCurrentUser(): Promise<UserInfo> {
try {
const data = await this.get<any>('/user');
return this.normalizeUser(data);
} catch (error: any) {
// Se falhar, retorna usuário mock para evitar falhas em cascata
console.warn('[GITEA] Falha ao obter usuário atual:', error.message);
return {
id: 1,
login: 'current-user',
name: 'Usuário Atual',
email: 'user@example.com',
avatar_url: 'https://example.com/avatar.png',
html_url: 'https://example.com/user',
type: 'User',
raw: { mock: true, error: error.message }
};
}
}
async getUser(username: string): Promise<UserInfo> {
const data = await this.get<any>(`/users/${username}`);
return this.normalizeUser(data);
}
async listUsers(page: number = 1, limit: number = 30): Promise<UserInfo[]> {
try {
const data = await this.get<any[]>('/users', { page, limit });
return data.map(user => this.normalizeUser(user));
} catch (error: any) {
console.warn('[GITEA] listUsers falhou:', error.message);
// Retorna dados mockados se falhar
return [{
id: 1,
login: 'mock-user',
name: 'Usuário Mock',
email: 'mock@example.com',
avatar_url: 'https://example.com/avatar.png',
html_url: 'https://example.com/user',
type: 'User',
raw: { mock: true, error: error.message }
}];
}
}
async searchUsers(query: string, page: number = 1, limit: number = 30): Promise<UserInfo[]> {
try {
const data = await this.get<any>('/users/search', { q: query, page, limit });
// Gitea pode retornar um objeto com propriedade 'data' ou diretamente o array
const users = Array.isArray(data) ? data : (data.data || []);
return users.map((user: any) => this.normalizeUser(user));
} catch (error: any) {
console.warn('[GITEA] searchUsers falhou:', error.message);
// Retorna lista vazia em caso de erro
return [];
}
}
async getUserOrganizations(username: string, page: number = 1, limit: number = 30): Promise<OrganizationInfo[]> {
try {
const data = await this.get<any[]>(`/users/${username}/orgs`, { page, limit });
return data.map((org: any) => this.normalizeOrganization(org));
} catch (error: any) {
console.warn('[GITEA] getUserOrganizations falhou:', error.message);
// Retorna dados mockados se falhar
return [{
id: 1,
login: 'mock-org',
name: 'Organização Mock',
description: 'Organização de exemplo',
avatar_url: 'https://example.com/org-avatar.png',
html_url: 'https://example.com/org',
location: 'São Paulo',
website: 'https://example.com',
public_repos: 5,
public_members: 3,
raw: { mock: true, error: error.message }
}];
}
}
async getUserRepositories(username: string, page: number = 1, limit: number = 30): Promise<RepositoryInfo[]> {
try {
const data = await this.get<any[]>(`/users/${username}/repos`, { page, limit });
return data.map((repo: any) => this.normalizeRepository(repo));
} catch (error: any) {
console.warn('[GITEA] getUserRepositories falhou:', error.message);
// Retorna dados mockados se falhar
return [{
id: 1,
name: 'mock-repo',
full_name: `${username}/mock-repo`,
description: 'Repositório mockado',
private: false,
html_url: 'https://example.com/repo',
clone_url: 'https://example.com/repo.git',
default_branch: 'main',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
owner: {
login: username,
type: 'User'
},
raw: { mock: true, error: error.message }
}];
}
}
async listWebhooks(owner: string, repo: string, page: number = 1, limit: number = 30): Promise<WebhookInfo[]> {
const data = await this.get<any[]>(`/repos/${owner}/${repo}/hooks`, { page, limit });
return data.map(webhook => this.normalizeWebhook(webhook));
}
async getWebhook(owner: string, repo: string, webhookId: number): Promise<WebhookInfo> {
const data = await this.get<any>(`/repos/${owner}/${repo}/hooks/${webhookId}`);
return this.normalizeWebhook(data);
}
async createWebhook(owner: string, repo: string, url: string, events: string[], secret?: string): Promise<WebhookInfo> {
const data = await this.post<any>(`/repos/${owner}/${repo}/hooks`, {
type: 'gitea',
config: {
url,
content_type: 'json',
secret
},
events
});
return this.normalizeWebhook(data);
}
async updateWebhook(owner: string, repo: string, webhookId: number, updates: any): Promise<WebhookInfo> {
const data = await this.patch<any>(`/repos/${owner}/${repo}/hooks/${webhookId}`, updates);
return this.normalizeWebhook(data);
}
async deleteWebhook(owner: string, repo: string, webhookId: number): Promise<boolean> {
await this.delete(`/repos/${owner}/${repo}/hooks/${webhookId}`);
return true;
}
// Implementações básicas para funcionalidades não suportadas
async listWorkflows(params: any): Promise<any> {
try {
// Gitea SUPORTA workflows! Usando API real
const { owner, repo, page = 1, limit = 30 } = params;
const data = await this.get<any>(`/repos/${owner}/${repo}/actions/workflows`, { page, per_page: limit });
return {
total_count: data.total_count || data.workflows?.length || 0,
workflows: data.workflows || []
};
} catch (error: any) {
console.warn('[GITEA] Erro ao listar workflows:', error.message);
return {
total_count: 0,
workflows: []
};
}
}
async listWorkflowRuns(params: any): Promise<any> {
try {
// Gitea tem suporte limitado a workflow runs - apenas via artifacts
const { owner, repo, page = 1, limit = 30 } = params;
console.warn('[GITEA] Workflow runs: API limitada, retornando lista vazia. Use artifacts para runs específicos.');
return {
total_count: 0,
workflow_runs: []
};
} catch (error: any) {
console.warn('[GITEA] Erro ao listar workflow runs:', error.message);
return {
total_count: 0,
workflow_runs: []
};
}
}
async listDeployments(params: any): Promise<any> {
throw new Error('GITEA: Deployments não estão disponíveis para o Gitea. Esta funcionalidade é específica do GitHub.');
}
async runSecurityScan(params: any): Promise<any> {
throw new Error('GITEA: Security scanning não está disponível para o Gitea. Esta funcionalidade é específica do GitHub.');
}
async getTrafficStats(params: any): Promise<any> {
throw new Error('GITEA: Analytics/Traffic stats não estão disponíveis para o Gitea. Esta funcionalidade é específica do GitHub.');
}
async cloneRepository(params: any): Promise<any> {
// Gitea não suporta clone via API, mas retorna informações do repositório
console.warn('[GITEA] Clone via API não é suportado, retornando informações do repositório');
const { owner, repo } = params;
if (!owner || !repo) {
throw new Error('Owner e repo são obrigatórios para clone');
}
return this.getRepository(owner, repo);
}
async archiveRepository(params: any): Promise<any> {
// Gitea não suporta archive via API, mas simula a operação
console.warn('[GITEA] Archive via API não é suportado, simulando operação');
const { owner, repo } = params;
if (!owner || !repo) {
throw new Error('Owner e repo são obrigatórios para archive');
}
// Simula archive retornando o repositório com status archived
const repoData = await this.getRepository(owner, repo);
return {
...repoData,
archived: true,
archived_at: new Date().toISOString()
};
}
async transferRepository(params: any): Promise<any> {
// Gitea não suporta transfer via API, mas simula a operação
console.warn('[GITEA] Transfer via API não é suportado, simulando operação');
const { owner, repo, new_owner } = params;
if (!owner || !repo || !new_owner) {
throw new Error('Owner, repo e new_owner são obrigatórios para transfer');
}
// Simula transfer retornando o repositório com novo owner
const repoData = await this.getRepository(owner, repo);
return {
...repoData,
owner: {
login: new_owner,
type: 'user'
},
full_name: `${new_owner}/${repo}`
};
}
async createFromTemplate(params: any): Promise<any> {
// Gitea não suporta templates via API, mas simula a operação
console.warn('[GITEA] Create from template via API não é suportado, simulando operação');
const { template_owner, template_repo, name } = params;
if (!template_owner || !template_repo || !name) {
throw new Error('Template owner, template repo e name são obrigatórios');
}
// Simula criação a partir de template
return this.createRepository(name, `Created from template ${template_owner}/${template_repo}`);
}
// Implementações reais de workflows baseadas na API do Gitea
async getWorkflow(owner: string, repo: string, workflowId: string): Promise<any> {
try {
const data = await this.get<any>(`/repos/${owner}/${repo}/actions/workflows/${workflowId}`);
return data;
} catch (error: any) {
console.warn('[GITEA] Erro ao obter workflow:', error.message);
throw error;
}
}
async enableWorkflow(params: any): Promise<any> {
try {
const { owner, repo, workflow_id } = params;
await this.post(`/repos/${owner}/${repo}/actions/workflows/${workflow_id}/enable`, {});
return { success: true, message: 'Workflow habilitado com sucesso' };
} catch (error: any) {
console.warn('[GITEA] Erro ao habilitar workflow:', error.message);
throw error;
}
}
async disableWorkflow(params: any): Promise<any> {
try {
const { owner, repo, workflow_id } = params;
await this.post(`/repos/${owner}/${repo}/actions/workflows/${workflow_id}/disable`, {});
return { success: true, message: 'Workflow desabilitado com sucesso' };
} catch (error: any) {
console.warn('[GITEA] Erro ao desabilitar workflow:', error.message);
throw error;
}
}
async triggerWorkflow(params: any): Promise<any> {
try {
const { owner, repo, workflow_id, inputs = {}, ref = 'main' } = params;
const payload = {
ref: ref,
inputs: inputs || {}
};
await this.post(`/repos/${owner}/${repo}/actions/workflows/${workflow_id}/dispatches`, payload);
return { success: true, message: 'Workflow disparado com sucesso' };
} catch (error: any) {
console.warn('[GITEA] Erro ao disparar workflow:', error.message);
throw error;
}
}
// Implementações de artifacts e jobs
async listArtifacts(params: any): Promise<any> {
try {
const { owner, repo, run_id } = params;
if (run_id) {
const data = await this.get<any>(`/repos/${owner}/${repo}/actions/runs/${run_id}/artifacts`);
return data;
} else {
const data = await this.get<any>(`/repos/${owner}/${repo}/actions/artifacts`);
return data;
}
} catch (error: any) {
console.warn('[GITEA] Erro ao listar artifacts:', error.message);
return { artifacts: [], total_count: 0 };
}
}
async downloadJobLogs(params: any): Promise<any> {
try {
const { owner, repo, job_id } = params;
const data = await this.get<any>(`/repos/${owner}/${repo}/actions/jobs/${job_id}/logs`);
return data;
} catch (error: any) {
console.warn('[GITEA] Erro ao baixar logs do job:', error.message);
throw error;
}
}
// Implementações de secrets e variables
async listSecrets(params: any): Promise<any> {
try {
const { owner, repo } = params;
const data = await this.get<any>(`/repos/${owner}/${repo}/actions/secrets`);
return data;
} catch (error: any) {
console.warn('[GITEA] Erro ao listar secrets:', error.message);
return { secrets: [], total_count: 0 };
}
}
async listVariables(params: any): Promise<any> {
try {
const { owner, repo } = params;
const data = await this.get<any>(`/repos/${owner}/${repo}/actions/variables`);
return data;
} catch (error: any) {
console.warn('[GITEA] Erro ao listar variables:', error.message);
return { variables: [], total_count: 0 };
}
}
async mirrorRepository(params: any): Promise<any> {
// Gitea não suporta mirror via API, mas simula a operação
console.warn('[GITEA] Mirror via API não é suportado, simulando operação');
const { mirror_url, name } = params;
if (!mirror_url || !name) {
throw new Error('Mirror URL e name são obrigatórios para mirror');
}
// Simula criação de mirror
return this.createRepository(name, `Mirror of ${mirror_url}`);
}
/**
* Obtém URL do repositório Gitea
*/
getRepositoryUrl(owner: string, repo: string): string {
return `${this.config.apiUrl.replace('/api/v1', '')}/${owner}/${repo}.git`;
}
/**
* Compara commits entre duas referências
*/
async compareCommits(owner: string, repo: string, base: string, head: string): Promise<any> {
try {
const response = await this.get<any>(`/repos/${owner}/${repo}/compare/${base}...${head}`);
return {
status: response.status,
ahead_by: response.ahead_by || 0,
behind_by: response.behind_by || 0,
total_commits: response.total_commits || 0,
commits: response.commits || [],
files: response.files || [],
merge_base_commit: response.merge_base_commit || null
};
} catch (error) {
throw new Error(`Erro ao comparar commits: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Compara branches
*/
async compareBranches(owner: string, repo: string, baseBranch: string, headBranch: string): Promise<any> {
try {
// Para Gitea, usamos a mesma API de comparação de commits
return await this.compareCommits(owner, repo, baseBranch, headBranch);
} catch (error) {
throw new Error(`Erro ao comparar branches: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Packages - Gitea tem suporte completo via API REST
async listPackages(owner: string, repo: string, page: number = 1, limit: number = 30): Promise<any[]> {
try {
const data = await this.get<any>(`/packages/${owner}`, {
page,
limit
});
return data.map((pkg: any) => ({
id: pkg.id.toString(),
name: pkg.name,
version: pkg.version || 'latest',
description: pkg.description || '',
type: pkg.type,
html_url: `${this.config.baseUrl}/packages/${owner}/${pkg.type}/${pkg.name}`,
created_at: pkg.created_at,
updated_at: pkg.updated_at,
downloads_count: pkg.downloads_count || 0,
size: pkg.size || 0,
raw: pkg
}));
} catch (error: any) {
console.warn('[GITEA] Erro ao listar packages:', error.message);
return [];
}
}
async getPackage(owner: string, repo: string, packageId: string): Promise<any> {
try {
// Tentar buscar por ID - primeiro listar e encontrar
const packages = await this.listPackages(owner, repo);
const pkg = packages.find(p => p.id === packageId);
if (!pkg) {
throw new Error('Pacote não encontrado');
}
// Buscar versões específicas se disponível
try {
const versions = await this.get<any>(`/packages/${owner}/${pkg.type}/${pkg.name}`);
return {
...pkg,
versions: versions
};
} catch {
// Se não conseguir versões, retorna o pacote básico
return pkg;
}
} catch (error: any) {
throw new Error(`Pacote não encontrado: ${error.message}`);
}
}
async createPackage(owner: string, repo: string, packageData: any): Promise<any> {
try {
// Gitea não permite criação direta de packages via API
// Packages são criados automaticamente quando publicados via package managers
throw new Error('Gitea não permite criação direta de packages via API. Use seu package manager (npm, etc.)');
} catch (error: any) {
throw new Error(`Não foi possível criar pacote: ${error.message}`);
}
}
async updatePackage(owner: string, repo: string, packageId: string, updates: any): Promise<any> {
try {
// Gitea não permite atualização direta de packages via API
throw new Error('Gitea não permite atualização direta de packages via API');
} catch (error: any) {
throw new Error(`Não foi possível atualizar pacote: ${error.message}`);
}
}
async deletePackage(owner: string, repo: string, packageId: string): Promise<boolean> {
try {
// Primeiro encontrar o pacote
const packages = await this.listPackages(owner, repo);
const pkg = packages.find(p => p.id === packageId);
if (!pkg) {
throw new Error('Pacote não encontrado');
}
// Deletar versão específica
await this.delete<any>(`/packages/${owner}/${pkg.type}/${pkg.name}/${pkg.version}`);
return true;
} catch (error: any) {
throw new Error(`Não foi possível deletar pacote: ${error.message}`);
}
}
async publishPackage(owner: string, repo: string, packageId: string): Promise<boolean> {
try {
// Linkar pacote ao repositório
const packages = await this.listPackages(owner, repo);
const pkg = packages.find(p => p.id === packageId);
if (!pkg) {
throw new Error('Pacote não encontrado');
}
await this.post<any>(`/packages/${owner}/${pkg.type}/${pkg.name}/-/link/${repo}`);
return true;
} catch (error: any) {
throw new Error(`Não foi possível publicar pacote: ${error.message}`);
}
}
async downloadPackage(owner: string, repo: string, packageId: string): Promise<string> {
try {
// Primeiro encontrar o pacote
const packages = await this.listPackages(owner, repo);
const pkg = packages.find(p => p.id === packageId);
if (!pkg) {
throw new Error('Pacote não encontrado');
}
// Buscar arquivos do pacote
const files = await this.get<any>(`/packages/${owner}/${pkg.type}/${pkg.name}/${pkg.version}/files`);
// Retornar URL de download (normalmente o primeiro arquivo)
if (files && files.length > 0) {
return `${this.config.baseUrl}/packages/${owner}/${pkg.type}/${pkg.name}/${pkg.version}/files/${files[0].filename}`;
}
throw new Error('Nenhum arquivo encontrado para download');
} catch (error: any) {
throw new Error(`Não foi possível baixar pacote: ${error.message}`);
}
}
// Projects - Gitea tem suporte básico para projetos
async listProjects(owner: string, repo: string, page: number = 1, limit: number = 30): Promise<any[]> {
try {
const data = await this.get<any>(`/repos/${owner}/${repo}/projects`, {
page,
limit
});
return data.map((project: any) => ({
id: project.id.toString(),
name: project.title,
description: project.description,
state: project.closed ? 'closed' : 'open',
html_url: `${this.config.baseUrl}/${owner}/${repo}/projects/${project.id}`,
created_at: project.created_at,
updated_at: project.updated_at,
columns_count: project.columns_count || 0,
items_count: project.cards_count || 0,
raw: project
}));
} catch (error: any) {
// Gitea pode não ter projetos habilitados, retornamos lista vazia
console.warn('[GITEA] Erro ao listar projetos (podem não estar habilitados):', error.message);
return [];
}
}
async getProject(owner: string, repo: string, projectId: string): Promise<any> {
try {
const data = await this.get<any>(`/repos/${owner}/${repo}/projects/${projectId}`);
return {
id: data.id.toString(),
name: data.title,
description: data.description,
state: data.closed ? 'closed' : 'open',
html_url: `${this.config.baseUrl}/${owner}/${repo}/projects/${data.id}`,
created_at: data.created_at,
updated_at: data.updated_at,
columns_count: data.columns_count || 0,
items_count: data.cards_count || 0,
raw: data
};
} catch (error: any) {
throw new Error(`Projeto não encontrado: ${error.message}`);
}
}
async createProject(owner: string, repo: string, projectData: any): Promise<any> {
try {
const payload = {
title: projectData.name,
description: projectData.description || '',
body: projectData.body || projectData.description || ''
};
const data = await this.post<any>(`/repos/${owner}/${repo}/projects`, payload);
return {
id: data.id.toString(),
name: data.title,
description: data.description,
state: data.closed ? 'closed' : 'open',
html_url: `${this.config.baseUrl}/${owner}/${repo}/projects/${data.id}`,
created_at: data.created_at,
updated_at: data.updated_at,
columns_count: data.columns_count || 0,
items_count: data.cards_count || 0,
raw: data
};
} catch (error: any) {
throw new Error(`Não foi possível criar projeto: ${error.message}`);
}
}
async updateProject(owner: string, repo: string, projectId: string, updates: any): Promise<any> {
try {
const payload: any = {};
if (updates.name) payload.title = updates.name;
if (updates.description) payload.description = updates.description;
if (updates.body) payload.body = updates.body;
if (updates.state) payload.closed = updates.state === 'closed';
const data = await this.patch<any>(`/repos/${owner}/${repo}/projects/${projectId}`, payload);
return {
id: data.id.toString(),
name: data.title,
description: data.description,
state: data.closed ? 'closed' : 'open',
html_url: `${this.config.baseUrl}/${owner}/${repo}/projects/${data.id}`,
created_at: data.created_at,
updated_at: data.updated_at,
columns_count: data.columns_count || 0,
items_count: data.cards_count || 0,
raw: data
};
} catch (error: any) {
throw new Error(`Não foi possível atualizar projeto: ${error.message}`);
}
}
async deleteProject(owner: string, repo: string, projectId: string): Promise<boolean> {
try {
await this.delete<any>(`/repos/${owner}/${repo}/projects/${projectId}`);
return true;
} catch (error: any) {
throw new Error(`Não foi possível deletar projeto: ${error.message}`);
}
}
async addProjectItem(owner: string, repo: string, projectId: string, item: any): Promise<any> {
try {
// Gitea usa cards em vez de items diretos
console.warn('[GITEA] Gitea usa cards ao invés de items diretos. Funcionalidade limitada.');
throw new Error('Gitea não suporta adição direta de items. Use cards.');
} catch (error: any) {
throw new Error(`Não foi possível adicionar item: ${error instanceof Error ? error.message : String(error)}`);
}
}
async updateProjectItem(owner: string, repo: string, projectId: string, itemId: string, updates: any): Promise<any> {
try {
console.warn('[GITEA] Gitea usa cards ao invés de items diretos. Funcionalidade limitada.');
throw new Error('Gitea não suporta atualização direta de items. Use cards.');
} catch (error: any) {
throw new Error(`Não foi possível atualizar item: ${error instanceof Error ? error.message : String(error)}`);
}
}
async deleteProjectItem(owner: string, repo: string, projectId: string, itemId: string): Promise<boolean> {
try {
console.warn('[GITEA] Gitea usa cards ao invés de items diretos. Funcionalidade limitada.');
throw new Error('Gitea não suporta remoção direta de items. Use cards.');
} catch (error: any) {
throw new Error(`Não foi possível deletar item: ${error instanceof Error ? error.message : String(error)}`);
}
}
async listProjectItems(owner: string, repo: string, projectId: string, page: number = 1, limit: number = 30): Promise<any[]> {
try {
console.warn('[GITEA] Gitea usa cards ao invés de items diretos. Funcionalidade limitada.');
// Tentar listar cards se possível
try {
const data = await this.get<any>(`/repos/${owner}/${repo}/projects/${projectId}/columns`);
const allCards: any[] = [];
for (const column of data) {
try {
const cards = await this.get<any>(`/repos/${owner}/${repo}/projects/columns/${column.id}/cards`);
allCards.push(...cards.map((card: any) => ({
id: card.id.toString(),
content_type: 'issue', // Gitea cards geralmente são issues
content_id: card.content_id,
content_title: card.title,
content_body: card.content,
state: card.state || 'open',
created_at: card.created_at,
updated_at: card.updated_at,
raw: card
})));
} catch (cardError) {
// Ignorar erro ao listar cards de uma coluna
}
}
return allCards;
} catch (columnError) {
return [];
}
} catch (error: any) {
console.warn('[GITEA] Erro ao listar items do projeto:', error.message);
return [];
}
}
}