import axios from 'axios'; export class GiteaService { private apiUrl: string; private apiToken: string; private owner: string; constructor() { this.apiUrl = process.env.GITEA_API_URL || ''; this.apiToken = process.env.GITEA_API_TOKEN || ''; this.owner = process.env.GITEA_OWNER || ''; console.log('GiteaService initialized with URL:', this.apiUrl); } /** * Get the properly formatted API endpoint */ private getApiEndpoint(path: string): string { const baseUrl = this.apiUrl.endsWith('/') ? this.apiUrl.slice(0, -1) : this.apiUrl; const pathWithoutLeadingSlash = path.startsWith('/') ? path.slice(1) : path; return `${baseUrl}/api/v1/${pathWithoutLeadingSlash}`; } /** * Create a new repository for a mission * @param mission The mission data * @returns Repository info or throws error */ async createRepository(mission: any): Promise { try { // Create a repo name from the mission name // Replace spaces with dashes and remove special characters const repoName = mission.name .toLowerCase() .replace(/\s+/g, '-') .replace(/[^a-z0-9-]/g, ''); console.log(`Creating repository: ${repoName}`); // Determine if we're creating under a user or an organization const endpoint = this.owner.startsWith('@') ? this.getApiEndpoint(`org/${this.owner.substring(1)}/repos`) : this.getApiEndpoint('user/repos'); // Create the repository with minimal parameters const response = await axios.post( endpoint, { name: repoName, private: true, auto_init: true }, { headers: { 'Content-Type': 'application/json', 'Authorization': `token ${this.apiToken}` } } ); console.log(`Repository created: ${response.data.full_name}`); // If the mission has users, add them as collaborators if (mission.missionUsers && mission.missionUsers.length > 0) { await this.addCollaborators(response.data.owner.login, repoName, mission.missionUsers); } return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response) { console.error('Gitea API Error:', { status: error.response.status, data: error.response.data }); } console.error('Error creating Gitea repository:', error); throw new Error(`Gitea integration failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Add collaborators to a repository * @param owner Repository owner * @param repo Repository name * @param missionUsers The mission users to add as collaborators */ async addCollaborators(owner: string, repo: string, missionUsers: any[]): Promise { console.log(`Adding ${missionUsers.length} collaborators to ${owner}/${repo}`); // First try to look up users if needed const usersWithInfo = await this.enrichUsersWithGiteaInfo(missionUsers); const addCollaboratorPromises = usersWithInfo.map(async (userInfo) => { try { // Skip if no username is found if (!userInfo.giteaUsername) { console.log(`No Gitea username for user ${userInfo.missionUser.user.email}, skipping`); return; } // Determine permission level based on role // For Gitea: 'read', 'write', 'admin' let permission = 'write'; if (userInfo.missionUser.role === 'gardien-temps') { permission = 'admin'; } else if (userInfo.missionUser.role === 'observateur') { permission = 'read'; } console.log(`Adding ${userInfo.giteaUsername} as ${permission}`); await axios.put( this.getApiEndpoint(`repos/${owner}/${repo}/collaborators/${userInfo.giteaUsername}`), { permission }, { headers: { 'Content-Type': 'application/json', 'Authorization': `token ${this.apiToken}` } } ); console.log(`Added ${userInfo.giteaUsername} as collaborator`); } catch (error) { console.error(`Error adding collaborator ${userInfo.missionUser.user.email}:`, error); // Continue with other collaborators even if one fails } }); // Wait for all collaborator additions to complete await Promise.all(addCollaboratorPromises); console.log('Finished adding collaborators'); } /** * Try to enrich users with Gitea username information * @param missionUsers The mission users * @returns Users with additional Gitea username info if available */ private async enrichUsersWithGiteaInfo(missionUsers: any[]): Promise> { // For each user, try to find their Gitea username return await Promise.all(missionUsers.map(async (missionUser) => { let giteaUsername = null; // Try to search for the user by email directly try { if (!missionUser.user.email) { console.log('User has no email address, skipping'); return { missionUser, giteaUsername }; } // Look up user by email in Gitea const searchResponse = await axios.get( this.getApiEndpoint('users/search'), { params: { q: missionUser.user.email }, headers: { 'Authorization': `token ${this.apiToken}` } } ); if (searchResponse.data && searchResponse.data.data && searchResponse.data.data.length > 0) { // Look for exact email match first const exactMatch = searchResponse.data.data.find((user: any) => user.email === missionUser.user.email ); if (exactMatch) { giteaUsername = exactMatch.username; console.log(`Found exact match Gitea username "${giteaUsername}" for ${missionUser.user.email}`); } else { // If no exact match, use the first result giteaUsername = searchResponse.data.data[0].username; console.log(`Found closest match Gitea username "${giteaUsername}" for ${missionUser.user.email}`); } } else { console.log(`No Gitea user found for email ${missionUser.user.email}`); } } catch (error) { console.log(`Error searching for Gitea user with email ${missionUser.user.email}:`, error); } return { missionUser, giteaUsername }; })); } /** * Delete a repository * @param owner Repository owner * @param repo Repository name * @returns True if successful, false otherwise */ async deleteRepository(owner: string, repo: string): Promise { try { await axios.delete( this.getApiEndpoint(`repos/${owner}/${repo}`), { headers: { 'Content-Type': 'application/json', 'Authorization': `token ${this.apiToken}` } } ); console.log(`Repository ${owner}/${repo} deleted successfully`); return true; } catch (error) { console.error(`Error deleting repository ${owner}/${repo}:`, error); return false; } } /** * Create a new branch in a repository * @param owner Repository owner * @param repo Repository name * @param branchName New branch name * @param baseBranchName Base branch name (default: 'main') * @returns True if successful, false otherwise */ async createBranch( owner: string, repo: string, branchName: string, baseBranchName: string = 'main' ): Promise { try { // First, get the SHA of the base branch const refResponse = await axios.get( this.getApiEndpoint(`repos/${owner}/${repo}/git/refs/heads/${baseBranchName}`), { headers: { 'Authorization': `token ${this.apiToken}` } } ); const sha = refResponse.data.object.sha; // Create the new branch await axios.post( this.getApiEndpoint(`repos/${owner}/${repo}/git/refs`), { ref: `refs/heads/${branchName}`, sha: sha }, { headers: { 'Content-Type': 'application/json', 'Authorization': `token ${this.apiToken}` } } ); console.log(`Branch ${branchName} created in ${owner}/${repo}`); return true; } catch (error) { console.error(`Error creating branch ${branchName} in ${owner}/${repo}:`, error); return false; } } /** * Create a file in a repository * @param owner Repository owner * @param repo Repository name * @param path File path * @param content File content (will be base64 encoded) * @param message Commit message * @param branch Branch name (default: 'main') * @returns True if successful, false otherwise */ async createFile( owner: string, repo: string, path: string, content: string, message: string, branch: string = 'main' ): Promise { try { // Convert content to base64 const contentBase64 = Buffer.from(content).toString('base64'); await axios.post( this.getApiEndpoint(`repos/${owner}/${repo}/contents/${path}`), { message, content: contentBase64, branch }, { headers: { 'Content-Type': 'application/json', 'Authorization': `token ${this.apiToken}` } } ); console.log(`File ${path} created in ${owner}/${repo}`); return true; } catch (error) { console.error(`Error creating file ${path} in ${owner}/${repo}:`, error); return false; } } /** * Get repository URL * @param repoName Repository name * @returns Full repository URL */ getRepositoryUrl(repoName: string): string { const baseUrl = this.apiUrl.replace('/api/v1', ''); return `${baseUrl}/${this.owner}/${repoName}`; } }