Peter e874cbefd6 feat(gitea-git-clone): add support for creating repositories in Gitea
* Added:
* - Users can now create new repositories in a selected Gitea instance directly from VSCode. Options include:
* - Selecting the Gitea instance (if multiple are configured).
* - Choosing the owner (personal account or organization).
* - Setting the repository visibility (private by default, or public).
* - Selecting a `.gitignore` template (if available via API).
* - The repository is initialized with an MIT license.
* - After creation, the new repository is automatically cloned and opened in VSCode.
* Changed:
* - Updated localization files to include new messages related to repository creation.
*
* Fixed:
* - Improved error handling and messages throughout the extension.
2024-10-31 14:58:36 +01:00

1245 lines
47 KiB
TypeScript

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<any[]>('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<GiteaInstance | null> {
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<boolean> {
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<any[]> {
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<string[]> {
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<void>((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<string> {
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<string> {
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<any[]> {
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<void>((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<any[]> {
// 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<string> {
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<string> {
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<string> {
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() {}