329 lines
10 KiB
TypeScript
329 lines
10 KiB
TypeScript
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<any> {
|
|
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<void> {
|
|
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<Array<{
|
|
missionUser: any,
|
|
giteaUsername: string | null
|
|
}>> {
|
|
// 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<boolean> {
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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}`;
|
|
}
|
|
}
|