import * as vscode from 'vscode'; import axios from 'axios'; import { AxiosError } from 'axios'; import { exec } from 'child_process'; import * as nls from 'vscode-nls'; import * as path from 'path'; import * as fs from 'fs'; // Interface für die Gitea-Fehlerantwort interface GiteaErrorResponse { message?: string; url?: string; documentation_url?: string; errors?: Array<{ resource?: string; field?: string; code?: string; message?: string; }>; } // Interface für Gitea-Instanzen interface GiteaInstance { name: string; url: string; token: string; } // Initialisiere die Lokalisierung const localize = nls.config({ messageFormat: nls.MessageFormat.file })(); // Globale Variablen für Statusleisten-Icons let statusBarItem: vscode.StatusBarItem | null = null; let prStatusBarItem: vscode.StatusBarItem | null = null; let branchStatusBarItem: vscode.StatusBarItem | null = null; let buildStatusBarItem: vscode.StatusBarItem | null = null; // Hilfsfunktion, um die Gitea-Instanzen zu laden function getGiteaInstances(): GiteaInstance[] { const instances = vscode.workspace.getConfiguration().get('gitea.instances'); if (!instances) { return []; } return instances.map((instance) => ({ name: instance.name, url: instance.url, token: instance.token, })); } // Hilfsfunktion, um die passende Gitea-Instanz basierend auf der Remote-URL zu ermitteln async function getMatchingGiteaInstance(folderPath: string): Promise { const instances = getGiteaInstances(); if (instances.length === 0) { return null; } return new Promise((resolve) => { exec(`git config --get remote.origin.url`, { cwd: folderPath }, (error, stdout) => { if (error || !stdout) { resolve(null); } else { let repoUrl = stdout.trim(); // URL normalisieren if (repoUrl.startsWith('git@')) { const parts = repoUrl.split(':'); const domain = parts[0].replace('git@', ''); const path = parts[1].replace('.git', ''); repoUrl = `https://${domain}/${path}`; } else { repoUrl = repoUrl.replace('.git', ''); } const matchingInstance = instances.find((instance) => repoUrl.startsWith(instance.url)); resolve(matchingInstance || null); } }); }); } // Hilfsfunktion, um zu prüfen, ob ein Ordner ein Git-Repository ist async function isGitRepository(folderPath: string): Promise { return new Promise((resolve) => { exec(`git rev-parse --is-inside-work-tree`, { cwd: folderPath }, (error) => { resolve(!error); }); }); } // Helper function to slugify the repository name function slugify(text: string): string { return text .toString() .toLowerCase() .trim() .replace(/[\s\W-]+/g, '-'); // Replace spaces and non-word characters with a dash } // UPDATED FUNCTION: Creating a Repository in Gitea async function createGiteaRepository() { const instances = getGiteaInstances(); if (instances.length === 0) { vscode.window.showErrorMessage(localize('giteaClone.configMissing', 'No Gitea instances are configured.')); return; } // User selects a Gitea instance const instanceNames = instances.map(instance => instance.name); const selectedInstanceName = await vscode.window.showQuickPick(instanceNames, { placeHolder: localize('giteaClone.selectInstance', 'Select a Gitea instance to create the repository in') }); if (!selectedInstanceName) { vscode.window.showInformationMessage(localize('giteaClone.noInstanceSelected', 'No Gitea instance selected.')); return; } const selectedInstance = instances.find(instance => instance.name === selectedInstanceName); if (!selectedInstance) { vscode.window.showErrorMessage(localize('giteaClone.invalidInstance', 'Invalid Gitea instance selected.')); return; } const instanceUrl = selectedInstance.url; const token = selectedInstance.token; try { // Fetch user organizations const organizations = await getUserOrganizations(instanceUrl, token); const ownerChoices = ['(Personal)'].concat(organizations.map(org => org.username)); // User selects the owner (personal account or organization) const selectedOwner = await vscode.window.showQuickPick(ownerChoices, { placeHolder: localize('giteaClone.selectOwner', 'Select the owner for the repository') }); if (selectedOwner === undefined) { vscode.window.showInformationMessage(localize('giteaClone.noOwnerSelected', 'No owner selected.')); return; } const owner = selectedOwner === '(Personal)' ? null : selectedOwner; // User inputs the repository name const repoNameInput = await vscode.window.showInputBox({ prompt: localize('giteaClone.enterRepoName', 'Enter the name of the new repository'), placeHolder: 'my-new-repo' }); if (!repoNameInput) { vscode.window.showInformationMessage(localize('giteaClone.noRepoName', 'No repository name provided.')); return; } // Sanitize and slugify the repository name const repoName = slugify(repoNameInput); if (!repoName) { vscode.window.showErrorMessage(localize('giteaClone.invalidRepoName', 'Invalid repository name.')); return; } // User selects the visibility const visibilityChoice = await vscode.window.showQuickPick(['Private', 'Public'], { placeHolder: localize('giteaClone.selectVisibility', 'Select the visibility of the repository'), canPickMany: false }); const isPrivate = visibilityChoice !== 'Public'; // Default to Private if undefined // Set the .gitignore template to 'VisualStudioCode' by default const gitignoreTemplate = 'VisualStudioCode'; // Repository creation data const repoData = { name: repoName, // Slugified name repo_name: repoNameInput, // Original name for display purposes private: isPrivate, auto_init: true, license: 'MIT', gitignores: gitignoreTemplate }; const apiUrl = owner ? `${instanceUrl}/api/v1/orgs/${owner}/repos` : `${instanceUrl}/api/v1/user/repos`; const response = await axios.post(apiUrl, repoData, { headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' } }); const repo = response.data; vscode.window.showInformationMessage(localize('giteaClone.repoCreated', `Repository '${repoNameInput}' created successfully.`)); // Clone the repository await cloneRepository(repo.ssh_url, repoName); } catch (err: any) { let errMessage = ''; if (axios.isAxiosError(err)) { const axiosError = err as AxiosError; if (axiosError.response && axiosError.response.data) { const data = axiosError.response.data as GiteaErrorResponse; if (typeof data === 'string') { errMessage = data; } else if (data.message) { errMessage = data.message; } } else if (axiosError.message) { errMessage = axiosError.message; } } else { errMessage = err.message; } let errorMessage = localize('giteaClone.createRepoError', `Error creating repository: ${errMessage}`); vscode.window.showErrorMessage(errorMessage); console.error('Error creating repository:', err); } } // Hilfsfunktion zum Abrufen der Benutzerorganisationen async function getUserOrganizations(instanceUrl: string, token: string): Promise { try { const response = await axios.get(`${instanceUrl}/api/v1/user/orgs`, { headers: { 'Authorization': `token ${token}` } }); if (response.status === 200) { return response.data; } else { return []; } } catch (err) { console.error('Error fetching user organizations:', err); return []; } } // Hilfsfunktion zum Abrufen der verfügbaren .gitignore-Templates async function getGitignoreTemplates(instanceUrl: string, token: string): Promise { try { const response = await axios.get(`${instanceUrl}/api/v1/gitignores`, { headers: { 'Authorization': `token ${token}` } }); if (response.status === 200) { return response.data; } else { return []; } } catch (err) { console.error('Error fetching gitignore templates:', err); return []; } } // Hilfsfunktion zum Klonen eines Repositories async function cloneRepository(sshUrl: string, repoName: string) { const folderUri = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, openLabel: localize('giteaClone.selectFolder', 'Select folder to clone into') }); if (folderUri && folderUri[0]) { const folderPath = folderUri[0].fsPath; const targetPath = path.join(folderPath, repoName); // Überprüfen, ob das Zielverzeichnis bereits existiert if (fs.existsSync(targetPath)) { vscode.window.showErrorMessage(localize('giteaClone.targetExists', `The target directory "${targetPath}" already exists.`)); return; } vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize('giteaClone.cloningRepo', `Cloning ${repoName}`), cancellable: false }, async (progress, token) => { progress.report({ message: localize('giteaClone.cloningProgress', 'Cloning in progress...'), increment: 0 }); return new Promise((resolve, reject) => { exec(`git clone ${sshUrl} "${targetPath}"`, (error, stdout, stderr) => { if (error) { vscode.window.showErrorMessage(localize('giteaClone.cloneError', 'Error cloning the repository.')); console.error(stderr); reject(error); } else { progress.report({ message: localize('giteaClone.cloneComplete', 'Repository cloned successfully.'), increment: 100 }); vscode.window.showInformationMessage(localize('giteaClone.cloneSuccess', `Repository ${repoName} cloned successfully.`)); // Öffne das geklonte Repository im VSCode try { vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(targetPath), true) .then(() => resolve()); } catch (err: unknown) { vscode.window.showErrorMessage(localize('giteaClone.openRepoError', 'Error opening the cloned repository.')); console.error(err); reject(err); } } }); }); }); } else { vscode.window.showInformationMessage(localize('giteaClone.noFolderSelected', 'No target folder selected.')); } } // Funktion zum Erstellen eines Pull Requests async function createGiteaPullRequest() { // Aktuellen Workspace-Ordner abrufen const currentWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; if (!currentWorkspaceFolder) { // Kein Arbeitsverzeichnis geöffnet return; } // Prüfen, ob aktueller Ordner ein Git-Repository ist const isGitRepo = await isGitRepository(currentWorkspaceFolder); if (!isGitRepo) { // Kein Git-Repository, keine Aktion erforderlich return; } // Passende Gitea-Instanz ermitteln const giteaInstance = await getMatchingGiteaInstance(currentWorkspaceFolder); if (!giteaInstance) { // Keine passende Gitea-Instanz gefunden return; } const instanceUrl = giteaInstance.url; const token = giteaInstance.token; // Git-Repository-Informationen abrufen exec(`git config --get remote.origin.url`, { cwd: currentWorkspaceFolder }, async (error, stdout, stderr) => { if (error || !stdout) { vscode.window.showErrorMessage(localize('giteaClone.noRemoteUrl', 'Could not retrieve Git remote URL.')); return; } let repoUrl = stdout.trim(); // URL umwandeln, wenn sie im SSH-Format vorliegt (git@gitea.com:owner/repo.git) if (repoUrl.startsWith('git@')) { const parts = repoUrl.split(':'); const domain = parts[0].replace('git@', ''); const path = parts[1].replace('.git', ''); repoUrl = `https://${domain}/${path}`; } else { repoUrl = repoUrl.replace('.git', ''); } // Repository-Besitzer und -Name aus der URL extrahieren const [owner, repo] = repoUrl.split('/').slice(-2); try { // Den letzten Commit und den Branch abrufen const { title, body } = await getLastCommit(currentWorkspaceFolder); const branch = await getCurrentBranch(currentWorkspaceFolder); if (!branch) { vscode.window.showErrorMessage(localize('giteaClone.noBranch', 'Could not determine the branch.')); return; } // Base-Branch über die Gitea API ermitteln const baseBranch = await getDefaultBranch(instanceUrl, owner, repo, token); // Titel und Body auf maximale Länge beschränken const maxTitleLength = 255; const maxBodyLength = 65535; // Beispielwert, kann je nach Gitea-Konfiguration variieren let truncatedTitle = title; let truncatedBody = body; if (title.length > maxTitleLength) { truncatedTitle = title.substring(0, maxTitleLength); vscode.window.showWarningMessage(localize('giteaClone.titleTruncated', 'The commit message was too long and has been truncated for the pull request title.')); } if (body.length > maxBodyLength) { truncatedBody = body.substring(0, maxBodyLength); vscode.window.showWarningMessage(localize('giteaClone.bodyTruncated', 'The commit message body was too long and has been truncated for the pull request description.')); } // API-Request-Daten vorbereiten const prData = { title: truncatedTitle, // Der getrunkierte Titel body: truncatedBody || '', // Der getrunkierte Body head: branch, // Der aktuelle Branch als "head" base: baseBranch // Der ermittelte base-Branch }; const config = { method: 'post', maxBodyLength: Infinity, url: `${instanceUrl}/api/v1/repos/${owner}/${repo}/pulls`, headers: { 'Content-Type': 'application/json', 'Authorization': `token ${token}` }, data: JSON.stringify(prData) }; // API-Request zum Erstellen des Pull Requests const response = await axios.request(config); const prUrl = response.data.html_url; // Öffne die URL des erstellten PRs im Browser vscode.env.openExternal(vscode.Uri.parse(prUrl)); vscode.window.showInformationMessage(localize('giteaClone.pullRequestCreated', 'Pull request created successfully and opened in the browser.')); } catch (err: any) { let errMessage = ''; if (axios.isAxiosError(err)) { const axiosError = err as AxiosError; if (axiosError.response && axiosError.response.data) { const data = axiosError.response.data as GiteaErrorResponse; if (typeof data === 'string') { errMessage = data; } else if (data.message) { errMessage = data.message; } // Optional: Weitere Details aus 'errors' extrahieren if (data.errors && Array.isArray(data.errors)) { data.errors.forEach((errorItem: any) => { if (errorItem.message) { errMessage += ` ${errorItem.message}`; } }); } } } else { errMessage = err.message; } let errorMessage = localize('giteaClone.pullRequestError', `Error creating pull request: ${errMessage}`); vscode.window.showErrorMessage(errorMessage); console.error('Error creating PR:', err); } }); } // Hilfsfunktion, um den letzten Commit zu ermitteln async function getLastCommit(folderPath: string): Promise<{ title: string, body: string }> { return new Promise((resolve, reject) => { exec(`git log -1 --pretty=format:"%s%n%b"`, { cwd: folderPath }, (error, stdout) => { if (error) { reject(localize('giteaClone.commitError', 'Error retrieving the last commit.')); } else { const output = stdout.split('\n'); const title = output[0]; // Commit-Message als Titel const body = output.slice(1).join('\n'); // Commit-Kommentar als Body resolve({ title: title.trim(), body: body.trim() }); } }); }); } // Hilfsfunktion, um den aktuellen Branch zu ermitteln async function getCurrentBranch(folderPath: string): Promise { return new Promise((resolve, reject) => { exec(`git rev-parse --abbrev-ref HEAD`, { cwd: folderPath }, (error, stdout) => { if (error) { reject(localize('giteaClone.branchError', 'Error retrieving the branch.')); } else { resolve(stdout.trim()); } }); }); } // Hilfsfunktion, um den base-Branch über die Gitea API zu ermitteln async function getDefaultBranch(instanceUrl: string, owner: string, repo: string, token: string): Promise { try { const response = await axios.get(`${instanceUrl}/api/v1/repos/${owner}/${repo}`, { headers: { 'Authorization': `token ${token}` } }); if (response.status === 200 && response.data.default_branch) { return response.data.default_branch; } else { vscode.window.showErrorMessage(localize('giteaClone.defaultBranchError', 'Could not determine the base branch. Defaulting to "main".')); return 'main'; } } catch (err: any) { let errMessage = ''; if (axios.isAxiosError(err)) { const axiosError = err as AxiosError; if (axiosError.response && axiosError.response.data) { const data = axiosError.response.data as GiteaErrorResponse; if (typeof data === 'string') { errMessage = data; } else if (data.message) { errMessage = data.message; } } } else { errMessage = err.message; } let errorMessage = localize('giteaClone.defaultBranchError', `Error retrieving the base branch: ${errMessage}`); vscode.window.showErrorMessage(errorMessage); return 'main'; } } // Funktion zum Abrufen der Repositories des Benutzers von Gitea async function getGiteaRepositories(): Promise { const instances = getGiteaInstances(); if (instances.length === 0) { vscode.window.showErrorMessage(localize('giteaClone.configMissing', 'No Gitea instances are configured.')); return []; } // Benutzer wählt eine Gitea-Instanz aus const instanceNames = instances.map(instance => instance.name); const selectedInstanceName = await vscode.window.showQuickPick(instanceNames, { placeHolder: localize('giteaClone.selectInstance', 'Select a Gitea instance') }); if (!selectedInstanceName) { vscode.window.showInformationMessage(localize('giteaClone.noInstanceSelected', 'No Gitea instance selected.')); return []; } const selectedInstance = instances.find(instance => instance.name === selectedInstanceName); if (!selectedInstance) { vscode.window.showErrorMessage(localize('giteaClone.invalidInstance', 'Invalid Gitea instance selected.')); return []; } const instanceUrl = selectedInstance.url; const token = selectedInstance.token; try { const response = await axios.get(`${instanceUrl}/api/v1/user/repos`, { headers: { 'Authorization': `token ${token}` } }); if (response.status === 200) { return response.data; } else { vscode.window.showErrorMessage(localize('giteaClone.repoError', 'Error retrieving repositories.')); return []; } } catch (err: any) { let errMessage = ''; if (axios.isAxiosError(err)) { const axiosError = err as AxiosError; if (axiosError.response && axiosError.response.data) { const data = axiosError.response.data as GiteaErrorResponse; if (typeof data === 'string') { errMessage = data; } else if (data.message) { errMessage = data.message; } } } else { errMessage = err.message; } let errorMessage = localize('giteaClone.repoError', `Error retrieving repositories: ${errMessage}`); vscode.window.showErrorMessage(errorMessage); console.error(err); return []; } } // Funktion zum Klonen eines Repositories via SSH async function cloneGiteaRepository() { const repos = await getGiteaRepositories(); if (repos.length === 0) { vscode.window.showInformationMessage(localize('giteaClone.noRepos', 'No repositories found.')); return; } const repoNames = repos.map(repo => repo.full_name); const selectedRepo = await vscode.window.showQuickPick(repoNames, { placeHolder: localize('giteaClone.selectRepo', 'Select a repository to clone') }); if (!selectedRepo) { vscode.window.showInformationMessage(localize('giteaClone.noRepoSelected', 'No repository selected.')); return; } const repo = repos.find(r => r.full_name === selectedRepo); if (repo && repo.ssh_url) { const folderUri = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, openLabel: localize('giteaClone.selectFolder', 'Select folder to clone into') }); if (folderUri && folderUri[0]) { const folderPath = folderUri[0].fsPath; const repoName = repo.name; const targetPath = path.join(folderPath, repoName); // Überprüfen, ob das Zielverzeichnis bereits existiert if (fs.existsSync(targetPath)) { vscode.window.showErrorMessage(localize('giteaClone.targetExists', `The target directory "${targetPath}" already exists.`)); return; } vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize('giteaClone.cloningRepo', `Cloning ${selectedRepo}`), cancellable: false }, async (progress, token) => { progress.report({ message: localize('giteaClone.cloningProgress', 'Cloning in progress...'), increment: 0 }); return new Promise((resolve, reject) => { exec(`git clone ${repo.ssh_url} "${targetPath}"`, (error, stdout, stderr) => { if (error) { vscode.window.showErrorMessage(localize('giteaClone.cloneError', 'Error cloning the repository.')); console.error(stderr); reject(error); } else { progress.report({ message: localize('giteaClone.cloneComplete', 'Repository cloned successfully.'), increment: 100 }); vscode.window.showInformationMessage(localize('giteaClone.cloneSuccess', `Repository ${selectedRepo} cloned successfully.`)); // Öffne das geklonte Repository im VSCode try { vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(targetPath), true) .then(() => resolve()); } catch (err: unknown) { vscode.window.showErrorMessage(localize('giteaClone.openRepoError', 'Error opening the cloned repository.')); console.error(err); reject(err); } } }); }); }); } else { vscode.window.showInformationMessage(localize('giteaClone.noFolderSelected', 'No target folder selected.')); } } else { vscode.window.showErrorMessage(localize('giteaClone.noSshUrl', 'Could not find SSH clone URL.')); } } // Funktion zum Hinzufügen des Statusleisten-Icons async function addStatusBarIcon() { const currentWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; if (!currentWorkspaceFolder) { // Kein Arbeitsverzeichnis geöffnet return null; } const isGitRepo = await isGitRepository(currentWorkspaceFolder); if (!isGitRepo) { // Kein Git-Repository return null; } // Passende Gitea-Instanz ermitteln const giteaInstance = await getMatchingGiteaInstance(currentWorkspaceFolder); if (!giteaInstance) { // Keine passende Gitea-Instanz gefunden return null; } const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); statusBar.text = `$(git-pull-request) ${localize('giteaClone.createPullRequest', 'Create Gitea PR')}`; statusBar.tooltip = localize('giteaClone.createPullRequestTooltip', 'Create a Pull Request in Gitea'); statusBar.command = 'gitea.createPullRequest'; statusBar.show(); // Sofortiges Anzeigen des Icons in der Statusleiste return statusBar; } // Funktion zur Authentifizierung bei Gitea mit dem Personal Access Token (PAT) async function authenticateGitea() { const instances = getGiteaInstances(); if (instances.length === 0) { vscode.window.showErrorMessage(localize('giteaClone.configMissing', 'No Gitea instances are configured.')); return; } // Benutzer wählt eine Gitea-Instanz aus const instanceNames = instances.map(instance => instance.name); const selectedInstanceName = await vscode.window.showQuickPick(instanceNames, { placeHolder: localize('giteaClone.selectInstance', 'Select a Gitea instance to authenticate') }); if (!selectedInstanceName) { vscode.window.showInformationMessage(localize('giteaClone.noInstanceSelected', 'No Gitea instance selected.')); return; } const selectedInstance = instances.find(instance => instance.name === selectedInstanceName); if (!selectedInstance) { vscode.window.showErrorMessage(localize('giteaClone.invalidInstance', 'Invalid Gitea instance selected.')); return; } const instanceUrl = selectedInstance.url; const token = selectedInstance.token; try { const response = await axios.get(`${instanceUrl}/api/v1/user`, { headers: { 'Authorization': `token ${token}` } }); if (response.status === 200) { vscode.window.showInformationMessage(localize('giteaClone.authSuccess', `Authentication successful: ${response.data.username}`)); } } catch (err: any) { let errMessage = ''; if (axios.isAxiosError(err)) { const axiosError = err as AxiosError; if (axiosError.response && axiosError.response.data) { const data = axiosError.response.data as GiteaErrorResponse; if (typeof data === 'string') { errMessage = data; } else if (data.message) { errMessage = data.message; } } } else { errMessage = err.message; } let errorMessage = localize('giteaClone.authFailed', `Authentication failed: ${errMessage}`); vscode.window.showErrorMessage(errorMessage); console.error(err); } } // Funktion zur Konfiguration der Gitea-Instanzen und Tokens über die Command Palette async function configureGitea() { const instances = getGiteaInstances(); // Benutzer fragt, ob er eine neue Instanz hinzufügen oder eine bestehende bearbeiten möchte const action = await vscode.window.showQuickPick(['Add New Instance', 'Edit Existing Instance'], { placeHolder: 'What would you like to do?' }); if (action === 'Add New Instance') { // Neue Instanz hinzufügen const name = await vscode.window.showInputBox({ prompt: 'Enter a name for the Gitea instance', placeHolder: 'e.g., Company Gitea' }); const instanceUrl = await vscode.window.showInputBox({ prompt: localize('giteaClone.enterInstanceUrl', 'Enter Gitea instance URL'), placeHolder: 'https://your-gitea-instance.com' }); const token = await vscode.window.showInputBox({ prompt: localize('giteaClone.enterToken', 'Enter Gitea Personal Access Token'), placeHolder: 'token', password: true }); if (name && instanceUrl && token) { instances.push({ name, url: instanceUrl, token }); await vscode.workspace.getConfiguration().update('gitea.instances', instances, vscode.ConfigurationTarget.Global); vscode.window.showInformationMessage(localize('giteaClone.configUpdated', 'Gitea configuration updated.')); } } else if (action === 'Edit Existing Instance') { // Existierende Instanz bearbeiten const instanceNames = instances.map(instance => instance.name); const selectedInstanceName = await vscode.window.showQuickPick(instanceNames, { placeHolder: 'Select a Gitea instance to edit' }); if (!selectedInstanceName) { vscode.window.showInformationMessage(localize('giteaClone.noInstanceSelected', 'No Gitea instance selected.')); return; } const selectedInstanceIndex = instances.findIndex(instance => instance.name === selectedInstanceName); const selectedInstance = instances[selectedInstanceIndex]; const name = await vscode.window.showInputBox({ prompt: 'Enter a name for the Gitea instance', placeHolder: 'e.g., Company Gitea', value: selectedInstance.name }); const instanceUrl = await vscode.window.showInputBox({ prompt: localize('giteaClone.enterInstanceUrl', 'Enter Gitea instance URL'), placeHolder: 'https://your-gitea-instance.com', value: selectedInstance.url }); const token = await vscode.window.showInputBox({ prompt: localize('giteaClone.enterToken', 'Enter Gitea Personal Access Token'), placeHolder: 'token', password: true, value: selectedInstance.token }); if (name && instanceUrl && token) { instances[selectedInstanceIndex] = { name, url: instanceUrl, token }; await vscode.workspace.getConfiguration().update('gitea.instances', instances, vscode.ConfigurationTarget.Global); vscode.window.showInformationMessage(localize('giteaClone.configUpdated', 'Gitea configuration updated.')); } } } // Funktion zum Abrufen offener Pull Requests async function getOpenPullRequests(): Promise { // Aktuellen Workspace-Ordner abrufen const currentWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; if (!currentWorkspaceFolder) { // Kein Arbeitsverzeichnis geöffnet return []; } const isGitRepo = await isGitRepository(currentWorkspaceFolder); if (!isGitRepo) { // Kein Git-Repository return []; } // Passende Gitea-Instanz ermitteln const giteaInstance = await getMatchingGiteaInstance(currentWorkspaceFolder); if (!giteaInstance) { // Keine passende Gitea-Instanz gefunden return []; } const instanceUrl = giteaInstance.url; const token = giteaInstance.token; // Git-Repository-Informationen abrufen const { owner, repo } = await getRepoInfo(currentWorkspaceFolder); if (!owner || !repo) { vscode.window.showErrorMessage(localize('giteaClone.repoInfoError', 'Could not retrieve repository information.')); return []; } try { const response = await axios.get(`${instanceUrl}/api/v1/repos/${owner}/${repo}/pulls`, { headers: { 'Authorization': `token ${token}` }, params: { state: 'open' } }); if (response.status === 200) { return response.data; } else { vscode.window.showErrorMessage(localize('giteaClone.pullRequestsError', 'Error retrieving pull requests.')); return []; } } catch (err: any) { let errMessage = ''; if (axios.isAxiosError(err)) { const axiosError = err as AxiosError; if (axiosError.response && axiosError.response.data) { const data = axiosError.response.data as GiteaErrorResponse; if (typeof data === 'string') { errMessage = data; } else if (data.message) { errMessage = data.message; } } } else { errMessage = err.message; } let errorMessage = localize('giteaClone.pullRequestsError', `Error retrieving pull requests: ${errMessage}`); vscode.window.showErrorMessage(errorMessage); console.error(err); return []; } } // Hilfsfunktion zum Abrufen von Repository-Informationen async function getRepoInfo(folderPath: string): Promise<{ owner: string, repo: string }> { return new Promise((resolve, reject) => { exec(`git config --get remote.origin.url`, { cwd: folderPath }, (error, stdout) => { if (error || !stdout) { reject(localize('giteaClone.noRemoteUrl', 'Could not retrieve Git remote URL.')); } else { let repoUrl = stdout.trim(); // URL umwandeln, wenn sie im SSH-Format vorliegt if (repoUrl.startsWith('git@')) { const parts = repoUrl.split(':'); const domain = parts[0].replace('git@', ''); const path = parts[1].replace('.git', ''); repoUrl = `https://${domain}/${path}`; } else { repoUrl = repoUrl.replace('.git', ''); } const [owner, repo] = repoUrl.split('/').slice(-2); resolve({ owner, repo }); } }); }); } // Funktion zum Anzeigen offener Pull Requests async function showOpenPullRequests() { const pullRequests = await getOpenPullRequests(); if (pullRequests.length === 0) { vscode.window.showInformationMessage(localize('giteaClone.noOpenPRs', 'No open pull requests available.')); return; } const prItems = pullRequests.map(pr => ({ label: `#${pr.number}: ${pr.title}`, description: `by ${pr.user.username}`, pr })); const selectedPr = await vscode.window.showQuickPick(prItems, { placeHolder: localize('giteaClone.selectPullRequest', 'Select a pull request to view') }); if (selectedPr) { // Öffne die URL des ausgewählten PRs im Browser vscode.env.openExternal(vscode.Uri.parse(selectedPr.pr.html_url)); } } // Funktion zum Aktualisieren des PR-Statusleisten-Icons async function updatePRStatusBarItem(context: vscode.ExtensionContext) { try { const currentWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; if (!currentWorkspaceFolder) { if (prStatusBarItem) prStatusBarItem.hide(); return; } const isGitRepo = await isGitRepository(currentWorkspaceFolder); if (!isGitRepo) { if (prStatusBarItem) prStatusBarItem.hide(); return; } // Passende Gitea-Instanz ermitteln const giteaInstance = await getMatchingGiteaInstance(currentWorkspaceFolder); if (!giteaInstance) { if (prStatusBarItem) prStatusBarItem.hide(); return; } const pullRequests = await getOpenPullRequests(); if (!prStatusBarItem) { prStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); prStatusBarItem.command = 'gitea.showOpenPullRequests'; prStatusBarItem.tooltip = localize('giteaClone.showOpenPullRequestsTooltip', 'Show open pull requests'); context.subscriptions.push(prStatusBarItem); } const prCount = pullRequests.length; prStatusBarItem.text = `$(git-pull-request) Gitea Open PRs: ${prCount}`; prStatusBarItem.show(); } catch (error) { console.error('Error updating PR status bar item:', error); if (prStatusBarItem) { prStatusBarItem.hide(); } } } // Funktion zum Aktualisieren des Branch-Statusleisten-Icons async function updateBranchStatusBarItem() { try { const currentWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; if (!currentWorkspaceFolder) { if (branchStatusBarItem) branchStatusBarItem.hide(); return; } const isGitRepo = await isGitRepository(currentWorkspaceFolder); if (!isGitRepo) { if (branchStatusBarItem) branchStatusBarItem.hide(); return; } const branch = await getCurrentBranch(currentWorkspaceFolder); if (!branchStatusBarItem) { branchStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); branchStatusBarItem.tooltip = localize('giteaClone.branchStatusTooltip', 'Current branch status'); branchStatusBarItem.command = 'git.checkout'; } // Überprüfen, ob der Branch up-to-date ist const branchStatus = await getBranchStatus(currentWorkspaceFolder, branch); branchStatusBarItem.text = `$(git-branch) ${branch} ${branchStatus}`; branchStatusBarItem.show(); } catch (error) { console.error('Error updating branch status bar item:', error); if (branchStatusBarItem) { branchStatusBarItem.hide(); } } } // Hilfsfunktion, um den Branch-Status zu ermitteln async function getBranchStatus(folderPath: string, branch: string): Promise { return new Promise((resolve) => { exec(`git status -sb`, { cwd: folderPath }, (error, stdout) => { if (error || !stdout) { resolve(''); } else { const statusLine = stdout.split('\n')[0]; const aheadMatch = /\[ahead (\d+)\]/.exec(statusLine); const behindMatch = /\[behind (\d+)\]/.exec(statusLine); let status = ''; if (aheadMatch) { status += `↑${aheadMatch[1]}`; } if (behindMatch) { status += `↓${behindMatch[1]}`; } resolve(status); } }); }); } // Funktion zum Aktualisieren des Build-Statusleisten-Icons async function updateBuildStatusBarItem() { try { const currentWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; if (!currentWorkspaceFolder) { if (buildStatusBarItem) buildStatusBarItem.hide(); return; } const isGitRepo = await isGitRepository(currentWorkspaceFolder); if (!isGitRepo) { if (buildStatusBarItem) buildStatusBarItem.hide(); return; } // Passende Gitea-Instanz ermitteln const giteaInstance = await getMatchingGiteaInstance(currentWorkspaceFolder); if (!giteaInstance) { if (buildStatusBarItem) buildStatusBarItem.hide(); return; } const instanceUrl = giteaInstance.url; const token = giteaInstance.token; // Git-Repository-Informationen abrufen const { owner, repo } = await getRepoInfo(currentWorkspaceFolder); if (!owner || !repo) { if (buildStatusBarItem) buildStatusBarItem.hide(); return; } // Aktuellen Commit-Hash abrufen const commitHash = await getCurrentCommitHash(currentWorkspaceFolder); // Build-Status von Gitea abrufen const buildStatus = await getBuildStatus(instanceUrl, owner, repo, commitHash, token); if (!buildStatusBarItem) { buildStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); buildStatusBarItem.tooltip = localize('giteaClone.buildStatusTooltip', 'Build status of the current commit'); } buildStatusBarItem.text = `$(gear) Build: ${buildStatus}`; buildStatusBarItem.show(); } catch (error) { console.error('Error updating build status bar item:', error); if (buildStatusBarItem) { buildStatusBarItem.hide(); } } } // Hilfsfunktion, um den aktuellen Commit-Hash zu ermitteln async function getCurrentCommitHash(folderPath: string): Promise { return new Promise((resolve) => { exec(`git rev-parse HEAD`, { cwd: folderPath }, (error, stdout) => { if (error || !stdout) { resolve(''); } else { resolve(stdout.trim()); } }); }); } // Hilfsfunktion, um den Build-Status von Gitea abzurufen async function getBuildStatus(instanceUrl: string, owner: string, repo: string, commitHash: string, token: string): Promise { try { const response = await axios.get(`${instanceUrl}/api/v1/repos/${owner}/${repo}/statuses/${commitHash}`, { headers: { 'Authorization': `token ${token}` } }); if (response.status === 200 && response.data.length > 0) { const latestStatus = response.data[0]; return latestStatus.state; } else { return 'unknown'; } } catch (error) { console.error('Error fetching build status:', error); return 'unknown'; } } // Funktion zum Starten des Build- und Branch-Status-Updaters function startStatusBarItemUpdater(context: vscode.ExtensionContext) { updateBranchStatusBarItem(); updateBuildStatusBarItem(); // Aktualisiere bei Fokuswechsel vscode.window.onDidChangeWindowState((windowState) => { if (windowState.focused) { updateBranchStatusBarItem(); updateBuildStatusBarItem(); } }, null, context.subscriptions); // Aktualisiere alle 2 Minuten setInterval(() => { updateBranchStatusBarItem(); updateBuildStatusBarItem(); }, 120000); // 2 Minuten } // Aktivierungsfunktion des Plugins export async function activate(context: vscode.ExtensionContext) { // Registriert den Befehl zur Authentifizierung let authCommand = vscode.commands.registerCommand('gitea.authenticate', authenticateGitea); context.subscriptions.push(authCommand); // Registriert den Befehl zur Konfiguration let configCommand = vscode.commands.registerCommand('gitea.configure', configureGitea); context.subscriptions.push(configCommand); // Registriere den Befehl zum Erstellen eines Pull Requests let pullRequestCommand = vscode.commands.registerCommand('gitea.createPullRequest', createGiteaPullRequest); context.subscriptions.push(pullRequestCommand); // Befehl zum Klonen eines Repositories let cloneCommand = vscode.commands.registerCommand('gitea.cloneRepository', cloneGiteaRepository); context.subscriptions.push(cloneCommand); // Befehl zum Anzeigen offener Pull Requests let showPRCommand = vscode.commands.registerCommand('gitea.showOpenPullRequests', showOpenPullRequests); context.subscriptions.push(showPRCommand); // NEUER BEFEHL zum Erstellen eines Repositories let createRepoCommand = vscode.commands.registerCommand('gitea.createRepository', createGiteaRepository); context.subscriptions.push(createRepoCommand); // Statusleisten-Icon erstellen statusBarItem = await addStatusBarIcon(); if (statusBarItem) { context.subscriptions.push(statusBarItem); } // Statusleisten-Icons aktualisieren bei Änderungen im Arbeitsbereich vscode.workspace.onDidChangeWorkspaceFolders(async () => { if (statusBarItem) { const currentWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; if (currentWorkspaceFolder && await isGitRepository(currentWorkspaceFolder) && await getMatchingGiteaInstance(currentWorkspaceFolder)) { statusBarItem.show(); } else { statusBarItem.hide(); } } await updatePRStatusBarItem(context); updateBranchStatusBarItem(); updateBuildStatusBarItem(); }); vscode.window.onDidChangeActiveTextEditor(async () => { if (statusBarItem) { const currentWorkspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath; if (currentWorkspaceFolder && await isGitRepository(currentWorkspaceFolder) && await getMatchingGiteaInstance(currentWorkspaceFolder)) { statusBarItem.show(); } else { statusBarItem.hide(); } } await updatePRStatusBarItem(context); updateBranchStatusBarItem(); updateBuildStatusBarItem(); }); // Starten Sie den PR-Statusleisten-Updater startStatusBarItemUpdater(context); // Starten Sie den Branch- und Build-Statusleisten-Updater startStatusBarItemUpdater(context); } // Deaktivierungsfunktion des Plugins export function deactivate() {}