Initial commit
Dieser Commit ist enthalten in:
851
frontend/js/gitea.js
Normale Datei
851
frontend/js/gitea.js
Normale Datei
@ -0,0 +1,851 @@
|
||||
/**
|
||||
* 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() {
|
||||
this.application = null;
|
||||
this.gitStatus = null;
|
||||
this.branches = [];
|
||||
this.commits = [];
|
||||
this.giteaRepos = [];
|
||||
this.giteaConnected = false;
|
||||
this.initialized = false;
|
||||
this.refreshInterval = null;
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// DOM Elements
|
||||
this.giteaView = $('#view-gitea');
|
||||
this.noProjectSection = $('#gitea-no-project');
|
||||
this.configSection = $('#gitea-config-section');
|
||||
this.mainSection = $('#gitea-main-section');
|
||||
this.connectionStatus = $('#gitea-connection-status');
|
||||
|
||||
this.bindEvents();
|
||||
this.subscribeToStore();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 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));
|
||||
}
|
||||
|
||||
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 = '<option value="">-- Repository wählen --</option>';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async handlePush() {
|
||||
const projectId = store.get('currentProjectId');
|
||||
if (!projectId) return;
|
||||
|
||||
this.setOperationLoading('push', true);
|
||||
|
||||
try {
|
||||
const branch = $('#branch-select')?.value || 'main';
|
||||
let result = await api.gitPush(projectId, branch);
|
||||
|
||||
// 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, branch);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
this.showToast('Push erfolgreich', '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);
|
||||
}
|
||||
}
|
||||
|
||||
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 projectId = store.get('currentProjectId');
|
||||
if (!projectId) return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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');
|
||||
const aheadBehind = $('#git-ahead-behind');
|
||||
|
||||
if (!this.gitStatus) {
|
||||
statusBadge.textContent = 'Fehler';
|
||||
statusBadge.className = 'status-badge error';
|
||||
changesCount.textContent = '-';
|
||||
aheadBehind.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 if (this.gitStatus.ahead > 0) {
|
||||
statusBadge.textContent = 'Voraus';
|
||||
statusBadge.className = 'status-badge ahead';
|
||||
} else {
|
||||
statusBadge.textContent = 'OK';
|
||||
statusBadge.className = 'status-badge clean';
|
||||
}
|
||||
|
||||
// Änderungen
|
||||
const changes = this.gitStatus.changes || [];
|
||||
changesCount.textContent = changes.length;
|
||||
|
||||
// Ahead/Behind
|
||||
const ahead = this.gitStatus.ahead || 0;
|
||||
const behind = this.gitStatus.behind || 0;
|
||||
aheadBehind.textContent = `${ahead} / ${behind}`;
|
||||
}
|
||||
|
||||
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');
|
||||
if (!listEl) return;
|
||||
|
||||
if (this.commits.length === 0) {
|
||||
listEl.innerHTML = '<div class="gitea-empty-state" style="padding: var(--spacing-4);"><p>Keine Commits gefunden</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = this.commits.map(commit => `
|
||||
<div class="commit-item">
|
||||
<span class="commit-hash">${escapeHtml(commit.shortHash || commit.sha?.substring(0, 7))}</span>
|
||||
<div class="commit-info">
|
||||
<div class="commit-message">${escapeHtml(commit.message?.split('\n')[0] || '')}</div>
|
||||
<div class="commit-meta">
|
||||
<span class="author">${escapeHtml(commit.author)}</span> · ${this.formatDate(commit.date)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="change-item">
|
||||
<span class="change-status ${statusClass}" title="${statusLabel}">${change.status}</span>
|
||||
<span class="change-file">${escapeHtml(change.file)}</span>
|
||||
</div>
|
||||
`;
|
||||
}).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.loadApplication();
|
||||
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.application?.configured && !this.isLoading) {
|
||||
this.loadGitData();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const giteaManager = new GiteaManager();
|
||||
export { giteaManager };
|
||||
export default giteaManager;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren