/** * TASKMATE - Gitea Manager * ======================== * Git-Repository-Verwaltung pro Projekt */ import api from './api.js'; import { $, $$, escapeHtml } from './utils.js'; import store from './store.js'; class GiteaManager { constructor() { // Gemeinsame Eigenschaften this.initialized = false; this.refreshInterval = null; this.isLoading = false; this.currentMode = 'server'; // 'server' oder 'project' // Projekt-Modus Eigenschaften this.application = null; this.gitStatus = null; this.branches = []; this.commits = []; this.giteaRepos = []; this.giteaConnected = false; this.hiddenCommits = new Set(); // Server-Modus Eigenschaften this.serverInfo = null; this.serverStatus = null; this.serverBranches = []; this.serverCommits = []; this.hiddenServerCommits = new Set(); } async init() { if (this.initialized) { return; } // DOM Elements - Gemeinsam this.giteaView = $('#view-gitea'); this.modeSwitch = $('#gitea-mode-switch'); // DOM Elements - Server-Modus this.serverModeSection = $('#gitea-server-mode'); // DOM Elements - Projekt-Modus this.noProjectSection = $('#gitea-no-project'); this.configSection = $('#gitea-config-section'); this.mainSection = $('#gitea-main-section'); this.connectionStatus = $('#gitea-connection-status'); this.bindEvents(); this.bindServerEvents(); this.subscribeToStore(); this.initialized = true; } bindEvents() { // Modus-Schalter $$('.gitea-mode-btn').forEach(btn => { btn.addEventListener('click', (e) => this.handleModeSwitch(e)); }); // Konfiguration speichern $('#gitea-config-form')?.addEventListener('submit', (e) => this.handleConfigSave(e)); // Repository auswählen $('#gitea-repo-select')?.addEventListener('change', (e) => this.handleRepoSelect(e)); // Repositories aktualisieren $('#btn-refresh-repos')?.addEventListener('click', () => this.loadGiteaRepos()); // Neues Repository erstellen $('#btn-create-repo')?.addEventListener('click', () => this.openCreateRepoModal()); // Git-Operationen $('#btn-git-fetch')?.addEventListener('click', () => this.handleFetch()); $('#btn-git-pull')?.addEventListener('click', () => this.handlePull()); $('#btn-git-push')?.addEventListener('click', () => this.handlePush()); $('#btn-git-commit')?.addEventListener('click', () => this.openCommitModal()); // Branch wechseln $('#branch-select')?.addEventListener('change', (e) => this.handleBranchChange(e)); // Pfad validieren $('#local-path-input')?.addEventListener('blur', (e) => this.validateLocalPath(e.target.value)); // Konfiguration bearbeiten/entfernen $('#btn-edit-config')?.addEventListener('click', () => this.showConfigSection()); $('#btn-remove-config')?.addEventListener('click', () => this.handleRemoveConfig()); // Create Repo Modal $('#create-repo-form')?.addEventListener('submit', (e) => this.handleCreateRepo(e)); // Commit Modal $('#git-commit-form')?.addEventListener('submit', (e) => this.handleCommit(e)); // Push Modal $('#git-push-form')?.addEventListener('submit', (e) => this.executePush(e)); // Branch umbenennen $('#btn-rename-branch')?.addEventListener('click', () => this.openRenameBranchModal()); $('#git-rename-branch-form')?.addEventListener('submit', (e) => this.executeRenameBranch(e)); // Commits ausblenden $('#btn-clear-commits')?.addEventListener('click', () => this.clearAllCommits()); $('#git-commits-list')?.addEventListener('click', (e) => this.handleCommitListClick(e)); } bindServerEvents() { // Server Git-Operationen $('#btn-server-fetch')?.addEventListener('click', () => this.handleServerFetch()); $('#btn-server-pull')?.addEventListener('click', () => this.handleServerPull()); $('#btn-server-push')?.addEventListener('click', () => this.openServerPushModal()); $('#btn-server-commit')?.addEventListener('click', () => this.openServerCommitModal()); // Server Branch wechseln $('#server-branch-select')?.addEventListener('change', (e) => this.handleServerBranchChange(e)); // Server Commits ausblenden $('#btn-server-clear-commits')?.addEventListener('click', () => this.clearAllServerCommits()); $('#server-commits-list')?.addEventListener('click', (e) => this.handleServerCommitListClick(e)); } // Modus-Wechsel Handler handleModeSwitch(e) { const btn = e.currentTarget; const newMode = btn.dataset.mode; if (newMode === this.currentMode) return; // Button-Status aktualisieren $$('.gitea-mode-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); this.currentMode = newMode; this.updateModeView(); } updateModeView() { if (this.currentMode === 'server') { // Server-Modus this.serverModeSection?.classList.remove('hidden'); this.noProjectSection?.classList.add('hidden'); this.configSection?.classList.add('hidden'); this.mainSection?.classList.add('hidden'); this.loadServerData(); } else { // Projekt-Modus this.serverModeSection?.classList.add('hidden'); this.loadApplication(); } } // ===================== // SERVER-MODUS METHODEN // ===================== async loadServerData() { this.showLoading(); try { // Server-Info laden const infoResult = await api.getServerGitInfo(); this.serverInfo = infoResult; if (infoResult.accessible && infoResult.isRepository) { // Remote-URL anzeigen if (infoResult.remoteUrl) { const repoUrl = $('#server-repo-url'); if (repoUrl) { // Gitea HTML URL generieren (ohne .git) const htmlUrl = infoResult.remoteUrl.replace(/\.git$/, ''); repoUrl.href = htmlUrl; repoUrl.textContent = htmlUrl; repoUrl.classList.remove('hidden'); } } // Git-Daten laden await this.loadServerGitData(); } else { this.showToast('Server-Verzeichnis ist kein Git-Repository', 'error'); } } catch (error) { console.error('[Gitea Server] Fehler beim Laden:', error); this.showToast('Fehler beim Laden der Server-Daten', 'error'); } } async loadServerGitData() { try { const [statusResult, branchesResult, commitsResult] = await Promise.allSettled([ api.getServerGitStatus(), api.getServerGitBranches(), api.getServerGitCommits(10) ]); if (statusResult.status === 'fulfilled') { this.serverStatus = statusResult.value; } else { this.serverStatus = null; console.error('[Gitea Server] Status-Fehler:', statusResult.reason); } if (branchesResult.status === 'fulfilled') { this.serverBranches = branchesResult.value.branches || []; } else { this.serverBranches = []; } if (commitsResult.status === 'fulfilled') { this.serverCommits = commitsResult.value.commits || []; } else { this.serverCommits = []; } this.renderServerStatus(); this.renderServerBranches(); this.renderServerCommits(); this.renderServerChanges(); } catch (error) { console.error('[Gitea Server] Git-Daten laden fehlgeschlagen:', error); } } renderServerStatus() { const statusBadge = $('#server-status-indicator'); const changesCount = $('#server-changes-count'); if (!this.serverStatus) { if (statusBadge) { statusBadge.textContent = 'Fehler'; statusBadge.className = 'status-badge error'; } if (changesCount) changesCount.textContent = '-'; return; } if (statusBadge) { if (!this.serverStatus.success) { statusBadge.textContent = 'Fehler'; statusBadge.className = 'status-badge error'; } else if (this.serverStatus.isClean) { statusBadge.textContent = 'Sauber'; statusBadge.className = 'status-badge clean'; } else if (this.serverStatus.hasChanges) { statusBadge.textContent = 'Geändert'; statusBadge.className = 'status-badge dirty'; } else { statusBadge.textContent = 'OK'; statusBadge.className = 'status-badge clean'; } } const changes = this.serverStatus.changes || []; if (changesCount) changesCount.textContent = changes.length; } renderServerBranches() { const select = $('#server-branch-select'); if (!select) return; const currentBranch = this.serverStatus?.branch || 'main'; select.innerHTML = ''; const localBranches = this.serverBranches.filter(b => !b.isRemote); localBranches.forEach(branch => { const option = document.createElement('option'); option.value = branch.name; option.textContent = branch.name; if (branch.name === currentBranch) { option.selected = true; } select.appendChild(option); }); } renderServerCommits() { const listEl = $('#server-commits-list'); const clearBtn = $('#btn-server-clear-commits'); if (!listEl) return; const visibleCommits = this.serverCommits.filter(commit => { const hash = commit.hash || commit.sha || commit.shortHash; return !this.hiddenServerCommits.has(hash); }); if (clearBtn) { clearBtn.style.display = visibleCommits.length > 0 ? '' : 'none'; } if (visibleCommits.length === 0) { const message = this.serverCommits.length > 0 ? 'Alle Commits ausgeblendet' : 'Keine Commits gefunden'; listEl.innerHTML = `

${message}

`; return; } listEl.innerHTML = visibleCommits.map(commit => { const hash = commit.hash || commit.sha || ''; const shortHash = commit.shortHash || hash.substring(0, 7); return `
${escapeHtml(shortHash)}
${escapeHtml(commit.message?.split('\n')[0] || '')}
${escapeHtml(commit.author)} · ${this.formatDate(commit.date)}
`}).join(''); } renderServerChanges() { const changesSection = $('#server-changes-section'); const listEl = $('#server-changes-list'); if (!changesSection || !listEl) return; const changes = this.serverStatus?.changes || []; if (changes.length === 0) { changesSection.classList.add('hidden'); return; } changesSection.classList.remove('hidden'); listEl.innerHTML = changes.map(change => { const statusClass = this.getChangeStatusClass(change.status); const statusLabel = this.getChangeStatusLabel(change.status); return `
${change.status} ${escapeHtml(change.file)}
`; }).join(''); } // Server Git-Operationen async handleServerFetch() { this.setServerOperationLoading('fetch', true); try { const result = await api.serverGitFetch(); if (result.success) { this.showToast('Fetch erfolgreich', 'success'); await this.loadServerGitData(); } else { this.showToast(result.error || 'Fetch fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Fetch fehlgeschlagen', 'error'); } finally { this.setServerOperationLoading('fetch', false); } } async handleServerPull() { this.setServerOperationLoading('pull', true); try { const branch = $('#server-branch-select')?.value || null; const result = await api.serverGitPull(branch); if (result.success) { this.showToast('Pull erfolgreich', 'success'); await this.loadServerGitData(); } else { this.showToast(result.error || 'Pull fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Pull fehlgeschlagen', 'error'); } finally { this.setServerOperationLoading('pull', false); } } openServerPushModal() { const modal = $('#git-push-modal'); if (!modal) return; const currentBranch = $('#server-branch-select')?.value || this.serverStatus?.branch || 'main'; $('#push-local-branch').textContent = currentBranch; $('#push-target-branch').value = ''; $('#push-force').checked = false; // Temporär Attribut setzen um Server-Modus zu markieren modal.dataset.serverMode = 'true'; modal.classList.remove('hidden'); modal.classList.add('visible'); $('#modal-overlay')?.classList.remove('hidden'); store.openModal('git-push-modal'); } async executeServerPush(force = false) { this.setServerOperationLoading('push', true); try { const targetBranch = $('#push-target-branch')?.value || null; const localBranch = $('#server-branch-select')?.value || this.serverStatus?.branch || 'main'; const result = await api.serverGitPush(targetBranch || localBranch, force); if (result.success) { const pushedBranch = result.branch || targetBranch || localBranch; this.showToast(`Push erfolgreich nach Branch "${pushedBranch}"`, 'success'); await this.loadServerGitData(); } else { this.showToast(result.error || 'Push fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Push fehlgeschlagen', 'error'); } finally { this.setServerOperationLoading('push', false); } } openServerCommitModal() { const modal = $('#git-commit-modal'); if (!modal) return; $('#commit-message').value = ''; $('#commit-stage-all').checked = true; // Temporär Attribut setzen um Server-Modus zu markieren modal.dataset.serverMode = 'true'; modal.classList.remove('hidden'); modal.classList.add('visible'); $('#modal-overlay')?.classList.remove('hidden'); store.openModal('git-commit-modal'); } async executeServerCommit(message, stageAll = true) { try { const result = await api.serverGitCommit(message, stageAll); if (result.success) { this.showToast('Commit erstellt', 'success'); await this.loadServerGitData(); return true; } else { this.showToast(result.error || 'Commit fehlgeschlagen', 'error'); return false; } } catch (error) { this.showToast(error.message || 'Commit fehlgeschlagen', 'error'); return false; } } async handleServerBranchChange(e) { const branch = e.target.value; if (!branch) return; try { const result = await api.serverGitCheckout(branch); if (result.success) { this.showToast(`Gewechselt zu ${branch}`, 'success'); await this.loadServerGitData(); } else { this.showToast(result.error || 'Branch-Wechsel fehlgeschlagen', 'error'); await this.loadServerGitData(); } } catch (error) { this.showToast(error.message || 'Branch-Wechsel fehlgeschlagen', 'error'); await this.loadServerGitData(); } } handleServerCommitListClick(e) { const deleteBtn = e.target.closest('.commit-delete'); if (deleteBtn) { const hash = deleteBtn.dataset.hash; if (hash) { this.hideServerCommit(hash); } } } hideServerCommit(hash) { this.hiddenServerCommits.add(hash); this.renderServerCommits(); this.showToast('Commit ausgeblendet', 'info'); } clearAllServerCommits() { this.serverCommits.forEach(commit => { const hash = commit.hash || commit.sha || commit.shortHash; if (hash) { this.hiddenServerCommits.add(hash); } }); this.renderServerCommits(); this.showToast('Alle Commits ausgeblendet', 'info'); } setServerOperationLoading(operation, loading) { const buttonId = `btn-server-${operation}`; const button = $(`#${buttonId}`); if (button) { button.disabled = loading; if (loading) { button.classList.add('loading'); } else { button.classList.remove('loading'); } } } // ===================== // PROJEKT-MODUS METHODEN // ===================== subscribeToStore() { // Bei Projektwechsel neu laden store.subscribe('currentProjectId', async (projectId) => { if (projectId && store.get('currentView') === 'gitea') { await this.loadApplication(); } }); } async loadApplication() { const projectId = store.get('currentProjectId'); if (!projectId) { this.showNoProjectMessage(); return; } this.showLoading(); try { const result = await api.getProjectApplication(projectId); this.application = result; if (result.configured) { await this.loadGitData(); this.renderConfiguredView(); } else { await this.loadGiteaRepos(); this.renderConfigurationView(); } } catch (error) { console.error('[Gitea] Fehler beim Laden:', error); this.showError('Fehler beim Laden der Konfiguration'); } } async loadGitData() { const projectId = store.get('currentProjectId'); try { const [statusResult, branchesResult, commitsResult] = await Promise.allSettled([ api.getGitStatus(projectId), api.getGitBranches(projectId), api.getGitCommits(projectId, 10) ]); if (statusResult.status === 'fulfilled') { this.gitStatus = statusResult.value; } else { this.gitStatus = null; console.error('[Gitea] Status-Fehler:', statusResult.reason); } if (branchesResult.status === 'fulfilled') { this.branches = branchesResult.value.branches || []; } else { this.branches = []; } if (commitsResult.status === 'fulfilled') { this.commits = commitsResult.value.commits || []; } else { this.commits = []; } this.renderStatus(); this.renderBranches(); this.renderCommits(); this.renderChanges(); } catch (error) { console.error('[Gitea] Git-Daten laden fehlgeschlagen:', error); } } async loadGiteaRepos() { try { const result = await api.testGiteaConnection(); this.giteaConnected = result.connected; this.updateConnectionStatus(result); if (this.giteaConnected) { const reposResult = await api.getGiteaRepositories(); this.giteaRepos = reposResult.repositories || []; this.populateRepoSelect(); } } catch (error) { this.giteaConnected = false; this.updateConnectionStatus({ connected: false, error: error.message }); console.error('[Gitea] Verbindung fehlgeschlagen:', error); } } updateConnectionStatus(result) { const statusEl = this.connectionStatus; if (!statusEl) return; statusEl.classList.remove('connected', 'disconnected'); if (result.connected) { statusEl.classList.add('connected'); statusEl.querySelector('.status-text').textContent = `Verbunden als ${result.user?.login || 'Benutzer'}`; } else { statusEl.classList.add('disconnected'); statusEl.querySelector('.status-text').textContent = result.error || 'Verbindung fehlgeschlagen'; } } populateRepoSelect() { const select = $('#gitea-repo-select'); if (!select) return; select.innerHTML = ''; this.giteaRepos.forEach(repo => { const option = document.createElement('option'); option.value = JSON.stringify({ url: repo.cloneUrl, owner: repo.owner, name: repo.name, fullName: repo.fullName, htmlUrl: repo.htmlUrl, defaultBranch: repo.defaultBranch }); option.textContent = repo.fullName; select.appendChild(option); }); } handleRepoSelect(e) { const value = e.target.value; if (!value) return; try { const repo = JSON.parse(value); $('#default-branch-input').value = repo.defaultBranch || 'main'; } catch (error) { console.error('[Gitea] Fehler beim Parsen des Repository:', error); } } async validateLocalPath(path) { const resultEl = $('#path-validation-result'); if (!resultEl || !path) { if (resultEl) { resultEl.textContent = ''; resultEl.className = 'form-hint'; } return; } try { const result = await api.validatePath(path); if (result.valid) { if (result.isRepository) { resultEl.textContent = 'Pfad ist ein Git-Repository'; resultEl.className = 'form-hint success'; } else { resultEl.textContent = 'Pfad ist erreichbar (kein Git-Repository)'; resultEl.className = 'form-hint'; } } else { resultEl.textContent = 'Pfad nicht erreichbar'; resultEl.className = 'form-hint error'; } } catch (error) { resultEl.textContent = 'Fehler bei der Validierung'; resultEl.className = 'form-hint error'; } } async handleConfigSave(e) { e.preventDefault(); const projectId = store.get('currentProjectId'); if (!projectId) return; const repoSelectValue = $('#gitea-repo-select').value; const localPath = $('#local-path-input').value.trim(); const defaultBranch = $('#default-branch-input').value.trim() || 'main'; if (!localPath) { this.showToast('Bitte geben Sie einen lokalen Pfad an', 'error'); return; } let giteaRepoUrl = null; let giteaRepoOwner = null; let giteaRepoName = null; let cloneUrl = null; if (repoSelectValue) { try { const repo = JSON.parse(repoSelectValue); giteaRepoUrl = repo.htmlUrl; giteaRepoOwner = repo.owner; giteaRepoName = repo.name; cloneUrl = repo.url; // Clone URL für git remote } catch (error) { console.error('[Gitea] Fehler beim Parsen des Repository:', error); } } try { // 1. Konfiguration speichern const result = await api.saveProjectApplication({ projectId, localPath, giteaRepoUrl, giteaRepoOwner, giteaRepoName, defaultBranch }); if (result.success) { // 2. Repository für Gitea vorbereiten (init, remote setzen) if (cloneUrl) { this.showToast('Repository wird vorbereitet...', 'info'); const prepareResult = await api.prepareRepository(projectId, cloneUrl, defaultBranch); if (prepareResult.success) { this.showToast('Konfiguration gespeichert und Repository vorbereitet', 'success'); } else { this.showToast('Konfiguration gespeichert, aber Repository-Vorbereitung fehlgeschlagen: ' + (prepareResult.error || 'Unbekannter Fehler'), 'warning'); } } else { this.showToast('Konfiguration gespeichert', 'success'); } await this.loadApplication(); } else { this.showToast(result.error || 'Fehler beim Speichern', 'error'); } } catch (error) { this.showToast(error.message || 'Fehler beim Speichern', 'error'); } } async handleRemoveConfig() { const projectId = store.get('currentProjectId'); if (!projectId) return; if (!confirm('Möchten Sie die Repository-Konfiguration wirklich entfernen?')) { return; } try { await api.deleteProjectApplication(projectId); this.showToast('Konfiguration entfernt', 'success'); await this.loadApplication(); } catch (error) { this.showToast(error.message || 'Fehler beim Entfernen', 'error'); } } showConfigSection() { this.hideAllSections(); this.configSection?.classList.remove('hidden'); // Formular mit aktuellen Werten füllen if (this.application?.configured) { $('#local-path-input').value = this.application.local_path || ''; $('#default-branch-input').value = this.application.default_branch || 'main'; // Repository im Dropdown auswählen falls vorhanden if (this.application.gitea_repo_url) { const select = $('#gitea-repo-select'); const options = select?.querySelectorAll('option'); options?.forEach(option => { if (option.value) { try { const repo = JSON.parse(option.value); if (repo.owner === this.application.gitea_repo_owner && repo.name === this.application.gitea_repo_name) { select.value = option.value; } } catch (e) {} } }); } } this.loadGiteaRepos(); } // Git Operations async handleFetch() { const projectId = store.get('currentProjectId'); if (!projectId) return; this.setOperationLoading('fetch', true); try { const result = await api.gitFetch(projectId); if (result.success) { this.showToast('Fetch erfolgreich', 'success'); await this.loadGitData(); } else { this.showToast(result.error || 'Fetch fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Fetch fehlgeschlagen', 'error'); } finally { this.setOperationLoading('fetch', false); } } async handlePull() { const projectId = store.get('currentProjectId'); if (!projectId) return; this.setOperationLoading('pull', true); try { const branch = $('#branch-select')?.value || null; const result = await api.gitPull(projectId, branch); if (result.success) { this.showToast('Pull erfolgreich', 'success'); await this.loadGitData(); } else { this.showToast(result.error || 'Pull fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Pull fehlgeschlagen', 'error'); } finally { this.setOperationLoading('pull', false); } } handlePush() { // Öffne das Push-Modal zur Branch-Auswahl this.openPushModal(); } openPushModal() { const modal = $('#git-push-modal'); if (!modal) return; // Aktuellen lokalen Branch anzeigen const currentBranch = $('#branch-select')?.value || this.gitStatus?.branch || 'unbekannt'; $('#push-local-branch').textContent = currentBranch; // Ziel-Branch zurücksetzen (Standard: gleicher Name wie lokal) $('#push-target-branch').value = ''; modal.classList.remove('hidden'); modal.classList.add('visible'); $('#modal-overlay')?.classList.remove('hidden'); store.openModal('git-push-modal'); } async executePush(e) { e.preventDefault(); const modal = $('#git-push-modal'); const isServerMode = modal?.dataset.serverMode === 'true'; // Modal schließen modal?.classList.add('hidden'); modal?.classList.remove('visible'); $('#modal-overlay')?.classList.add('hidden'); store.closeModal(); if (isServerMode) { // Server-Modus: Push für Server-Dateien delete modal.dataset.serverMode; const forcePush = $('#push-force')?.checked || false; await this.executeServerPush(forcePush); } else { // Projekt-Modus: Push für Projekt-Repository const projectId = store.get('currentProjectId'); if (!projectId) return; this.setOperationLoading('push', true); try { const targetBranch = $('#push-target-branch')?.value || null; const forcePush = $('#push-force')?.checked || false; const localBranch = $('#branch-select')?.value || this.gitStatus?.branch || 'master'; let result; // Wenn ein anderer Target-Branch ausgewählt wurde oder Force-Push, verwende init-push if ((targetBranch && targetBranch !== localBranch) || forcePush) { const forceText = forcePush ? ' (Force)' : ''; const branchText = targetBranch && targetBranch !== localBranch ? `Push von "${localBranch}" nach "${targetBranch}"${forceText}...` : `Push nach "${localBranch}"${forceText}...`; this.showToast(branchText, 'info'); result = await api.gitInitPush(projectId, targetBranch, forcePush); } else { // Normaler Push (gleicher Branch-Name) result = await api.gitPush(projectId, localBranch); // Falls Push wegen fehlendem Upstream/Remote fehlschlägt, versuche init-push if (!result.success && result.error && (result.error.includes('No configured push destination') || result.error.includes('no upstream') || result.error.includes('Kein Remote'))) { this.showToast('Kein Upstream konfiguriert, führe initialen Push durch...', 'info'); result = await api.gitInitPush(projectId, targetBranch, false); } } if (result.success) { const pushedBranch = result.branch || targetBranch || localBranch; this.showToast(`Push erfolgreich nach Branch "${pushedBranch}"`, 'success'); await this.loadGitData(); } else { this.showToast(result.error || 'Push fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Push fehlgeschlagen', 'error'); } finally { this.setOperationLoading('push', false); } } } openRenameBranchModal() { const modal = $('#git-rename-branch-modal'); if (!modal) return; const currentBranch = $('#branch-select')?.value || this.gitStatus?.branch || 'master'; $('#rename-current-branch').textContent = currentBranch; $('#rename-new-branch').value = ''; modal.classList.remove('hidden'); modal.classList.add('visible'); $('#modal-overlay')?.classList.remove('hidden'); store.openModal('git-rename-branch-modal'); // Focus auf das Eingabefeld setTimeout(() => $('#rename-new-branch')?.focus(), 100); } async executeRenameBranch(e) { e.preventDefault(); const projectId = store.get('currentProjectId'); if (!projectId) return; const oldName = $('#rename-current-branch')?.textContent; const newName = $('#rename-new-branch')?.value.trim(); if (!newName) { this.showToast('Bitte geben Sie einen neuen Branch-Namen ein', 'error'); return; } if (oldName === newName) { this.showToast('Der neue Name ist identisch mit dem aktuellen', 'error'); return; } // Modal schließen const modal = $('#git-rename-branch-modal'); modal?.classList.add('hidden'); modal?.classList.remove('visible'); $('#modal-overlay')?.classList.add('hidden'); store.closeModal(); try { const result = await api.gitRenameBranch(projectId, oldName, newName); if (result.success) { this.showToast(`Branch umbenannt: "${oldName}" → "${newName}"`, 'success'); await this.loadGitData(); } else { this.showToast(result.error || 'Umbenennen fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Umbenennen fehlgeschlagen', 'error'); } } openCommitModal() { const modal = $('#git-commit-modal'); if (!modal) return; $('#commit-message').value = ''; $('#commit-stage-all').checked = true; modal.classList.remove('hidden'); modal.classList.add('visible'); $('#modal-overlay')?.classList.remove('hidden'); store.openModal('git-commit-modal'); } async handleCommit(e) { e.preventDefault(); const modal = $('#git-commit-modal'); const isServerMode = modal?.dataset.serverMode === 'true'; const message = $('#commit-message')?.value.trim(); const stageAll = $('#commit-stage-all')?.checked ?? true; if (!message) { this.showToast('Bitte geben Sie eine Commit-Nachricht ein', 'error'); return; } if (isServerMode) { // Server-Modus: Commit für Server-Dateien const success = await this.executeServerCommit(message, stageAll); if (success) { this.closeModal('git-commit-modal'); delete modal.dataset.serverMode; } } else { // Projekt-Modus: Commit für Projekt-Repository const projectId = store.get('currentProjectId'); if (!projectId) return; try { const result = await api.gitCommit(projectId, message, stageAll); if (result.success) { this.showToast('Commit erstellt', 'success'); this.closeModal('git-commit-modal'); await this.loadGitData(); } else { this.showToast(result.error || 'Commit fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Commit fehlgeschlagen', 'error'); } } } async handleBranchChange(e) { const projectId = store.get('currentProjectId'); const branch = e.target.value; if (!projectId || !branch) return; try { const result = await api.gitCheckout(projectId, branch); if (result.success) { this.showToast(`Gewechselt zu ${branch}`, 'success'); await this.loadGitData(); } else { this.showToast(result.error || 'Branch-Wechsel fehlgeschlagen', 'error'); // Zurück zum vorherigen Branch await this.loadGitData(); } } catch (error) { this.showToast(error.message || 'Branch-Wechsel fehlgeschlagen', 'error'); await this.loadGitData(); } } openCreateRepoModal() { const modal = $('#create-repo-modal'); if (!modal) return; $('#new-repo-name').value = ''; $('#new-repo-description').value = ''; $('#new-repo-private').checked = true; modal.classList.remove('hidden'); modal.classList.add('visible'); $('#modal-overlay')?.classList.remove('hidden'); store.openModal('create-repo-modal'); } async handleCreateRepo(e) { e.preventDefault(); const name = $('#new-repo-name')?.value.trim(); const description = $('#new-repo-description')?.value.trim(); const isPrivate = $('#new-repo-private')?.checked ?? true; if (!name) { this.showToast('Bitte geben Sie einen Repository-Namen ein', 'error'); return; } try { const result = await api.createGiteaRepository({ name, description, private: isPrivate }); if (result.success) { this.showToast('Repository erstellt', 'success'); this.closeModal('create-repo-modal'); await this.loadGiteaRepos(); // Neues Repository im Dropdown auswählen const select = $('#gitea-repo-select'); if (select && result.repository) { const option = document.createElement('option'); option.value = JSON.stringify({ url: result.repository.cloneUrl, owner: result.repository.owner, name: result.repository.name, fullName: result.repository.fullName, htmlUrl: result.repository.htmlUrl, defaultBranch: result.repository.defaultBranch }); option.textContent = result.repository.fullName; select.appendChild(option); select.value = option.value; } } else { this.showToast(result.error || 'Repository-Erstellung fehlgeschlagen', 'error'); } } catch (error) { this.showToast(error.message || 'Repository-Erstellung fehlgeschlagen', 'error'); } } // Rendering renderConfigurationView() { this.hideAllSections(); this.configSection?.classList.remove('hidden'); // Formular zurücksetzen $('#gitea-repo-select').value = ''; $('#local-path-input').value = ''; $('#default-branch-input').value = 'main'; $('#path-validation-result').textContent = ''; } renderConfiguredView() { this.hideAllSections(); this.mainSection?.classList.remove('hidden'); // Repository-Info const repoNameEl = $('#gitea-repo-name span'); const repoUrlEl = $('#gitea-repo-url'); const localPathEl = $('#gitea-local-path-display'); if (this.application) { if (this.application.gitea_repo_owner && this.application.gitea_repo_name) { repoNameEl.textContent = `${this.application.gitea_repo_owner}/${this.application.gitea_repo_name}`; } else { repoNameEl.textContent = 'Lokales Repository'; } if (this.application.gitea_repo_url) { repoUrlEl.href = this.application.gitea_repo_url; repoUrlEl.textContent = this.application.gitea_repo_url; repoUrlEl.classList.remove('hidden'); } else { repoUrlEl.classList.add('hidden'); } localPathEl.textContent = this.application.local_path || '-'; } } renderStatus() { const statusBadge = $('#git-status-indicator'); const changesCount = $('#git-changes-count'); if (!this.gitStatus) { statusBadge.textContent = 'Fehler'; statusBadge.className = 'status-badge error'; changesCount.textContent = '-'; return; } // Status Badge if (!this.gitStatus.success) { statusBadge.textContent = 'Fehler'; statusBadge.className = 'status-badge error'; } else if (this.gitStatus.isClean) { statusBadge.textContent = 'Sauber'; statusBadge.className = 'status-badge clean'; } else if (this.gitStatus.hasChanges) { statusBadge.textContent = 'Geändert'; statusBadge.className = 'status-badge dirty'; } else { statusBadge.textContent = 'OK'; statusBadge.className = 'status-badge clean'; } // Änderungen const changes = this.gitStatus.changes || []; changesCount.textContent = changes.length; } renderBranches() { const select = $('#branch-select'); if (!select) return; const currentBranch = this.gitStatus?.branch || 'main'; select.innerHTML = ''; // Lokale Branches zuerst const localBranches = this.branches.filter(b => !b.isRemote); localBranches.forEach(branch => { const option = document.createElement('option'); option.value = branch.name; option.textContent = branch.name; if (branch.name === currentBranch) { option.selected = true; } select.appendChild(option); }); } renderCommits() { const listEl = $('#git-commits-list'); const clearBtn = $('#btn-clear-commits'); if (!listEl) return; // Filtern: Ausgeblendete Commits nicht anzeigen const visibleCommits = this.commits.filter(commit => { const hash = commit.hash || commit.sha || commit.shortHash; return !this.hiddenCommits.has(hash); }); // Button-Text anpassen if (clearBtn) { clearBtn.style.display = visibleCommits.length > 0 ? '' : 'none'; } if (visibleCommits.length === 0) { const message = this.commits.length > 0 ? 'Alle Commits ausgeblendet' : 'Keine Commits gefunden'; listEl.innerHTML = `

${message}

`; return; } listEl.innerHTML = visibleCommits.map(commit => { const hash = commit.hash || commit.sha || ''; const shortHash = commit.shortHash || hash.substring(0, 7); return `
${escapeHtml(shortHash)}
${escapeHtml(commit.message?.split('\n')[0] || '')}
${escapeHtml(commit.author)} · ${this.formatDate(commit.date)}
`}).join(''); } handleCommitListClick(e) { const deleteBtn = e.target.closest('.commit-delete'); if (deleteBtn) { const hash = deleteBtn.dataset.hash; if (hash) { this.hideCommit(hash); } } } hideCommit(hash) { this.hiddenCommits.add(hash); this.renderCommits(); this.showToast('Commit ausgeblendet', 'info'); } clearAllCommits() { // Alle sichtbaren Commits ausblenden this.commits.forEach(commit => { const hash = commit.hash || commit.sha || commit.shortHash; if (hash) { this.hiddenCommits.add(hash); } }); this.renderCommits(); this.showToast('Alle Commits ausgeblendet', 'info'); } renderChanges() { const changesSection = $('#gitea-changes-section'); const listEl = $('#git-changes-list'); if (!changesSection || !listEl) return; const changes = this.gitStatus?.changes || []; if (changes.length === 0) { changesSection.classList.add('hidden'); return; } changesSection.classList.remove('hidden'); listEl.innerHTML = changes.map(change => { const statusClass = this.getChangeStatusClass(change.status); const statusLabel = this.getChangeStatusLabel(change.status); return `
${change.status} ${escapeHtml(change.file)}
`; }).join(''); } getChangeStatusClass(status) { const first = status.charAt(0); const second = status.charAt(1); if (first === 'M' || second === 'M') return 'modified'; if (first === 'A' || second === 'A') return 'added'; if (first === 'D' || second === 'D') return 'deleted'; if (first === 'R' || second === 'R') return 'renamed'; if (first === '?' || second === '?') return 'untracked'; return ''; } getChangeStatusLabel(status) { const first = status.charAt(0); const second = status.charAt(1); if (first === 'M' || second === 'M') return 'Geändert'; if (first === 'A' || second === 'A') return 'Hinzugefügt'; if (first === 'D' || second === 'D') return 'Gelöscht'; if (first === 'R' || second === 'R') return 'Umbenannt'; if (first === '?' || second === '?') return 'Nicht verfolgt'; return status; } formatDate(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'gerade eben'; if (diffMins < 60) return `vor ${diffMins} Minute${diffMins !== 1 ? 'n' : ''}`; if (diffHours < 24) return `vor ${diffHours} Stunde${diffHours !== 1 ? 'n' : ''}`; if (diffDays < 7) return `vor ${diffDays} Tag${diffDays !== 1 ? 'en' : ''}`; // Lokale Formatierung const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); return `${day}.${month}.${year}`; } // UI Helpers hideAllSections() { this.noProjectSection?.classList.add('hidden'); this.configSection?.classList.add('hidden'); this.mainSection?.classList.add('hidden'); } showNoProjectMessage() { this.hideAllSections(); this.noProjectSection?.classList.remove('hidden'); } showLoading() { this.isLoading = true; } showError(message) { this.showToast(message, 'error'); } showToast(message, type = 'info') { window.dispatchEvent(new CustomEvent('toast:show', { detail: { message, type } })); } closeModal(modalId) { const modal = $(`#${modalId}`); if (modal) { modal.classList.add('hidden'); modal.classList.remove('visible'); } $('#modal-overlay')?.classList.add('hidden'); store.closeModal(modalId); } setOperationLoading(operation, loading) { const buttonId = `btn-git-${operation}`; const button = $(`#${buttonId}`); if (button) { button.disabled = loading; if (loading) { button.classList.add('loading'); } else { button.classList.remove('loading'); } } } // View Control show() { this.giteaView?.classList.remove('hidden'); this.giteaView?.classList.add('active'); this.updateModeView(); this.startAutoRefresh(); } hide() { this.giteaView?.classList.add('hidden'); this.giteaView?.classList.remove('active'); this.stopAutoRefresh(); } startAutoRefresh() { this.stopAutoRefresh(); // Status alle 30 Sekunden aktualisieren this.refreshInterval = setInterval(() => { if (this.isLoading) return; if (this.currentMode === 'server') { // Server-Modus: Server-Daten aktualisieren this.loadServerGitData(); } else if (this.application?.configured) { // Projekt-Modus: Projekt-Daten aktualisieren this.loadGitData(); } }, 30000); } stopAutoRefresh() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } } } const giteaManager = new GiteaManager(); export { giteaManager }; export default giteaManager;