Push für Serveranwendung in Gitea implementiert
Dieser Commit ist enthalten in:
HG
2025-12-30 17:25:14 +00:00
committet von Server Deploy
Ursprung 87c391d2e6
Commit c8707d6cf4
30 geänderte Dateien mit 11907 neuen und 170 gelöschten Zeilen

Datei anzeigen

@ -10,16 +10,27 @@ 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.initialized = false;
this.refreshInterval = null;
this.isLoading = false;
this.hiddenCommits = new Set(); // Ausgeblendete Commits
this.hiddenCommits = new Set();
// Server-Modus Eigenschaften
this.serverInfo = null;
this.serverStatus = null;
this.serverBranches = [];
this.serverCommits = [];
this.hiddenServerCommits = new Set();
}
async init() {
@ -27,19 +38,31 @@ class GiteaManager {
return;
}
// DOM Elements
// 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));
@ -86,6 +109,425 @@ class GiteaManager {
$('#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 = `<div class="gitea-empty-state" style="padding: var(--spacing-4);"><p>${message}</p></div>`;
return;
}
listEl.innerHTML = visibleCommits.map(commit => {
const hash = commit.hash || commit.sha || '';
const shortHash = commit.shortHash || hash.substring(0, 7);
return `
<div class="commit-item" data-hash="${escapeHtml(hash)}">
<span class="commit-hash">${escapeHtml(shortHash)}</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>
<button class="commit-delete" title="Ausblenden" data-hash="${escapeHtml(hash)}">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</button>
</div>
`}).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 `
<div class="change-item">
<span class="change-status ${statusClass}" title="${statusLabel}">${change.status}</span>
<span class="change-file">${escapeHtml(change.file)}</span>
</div>
`;
}).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) => {
@ -444,58 +886,68 @@ class GiteaManager {
async executePush(e) {
e.preventDefault();
const projectId = store.get('currentProjectId');
if (!projectId) return;
const modal = $('#git-push-modal');
const isServerMode = modal?.dataset.serverMode === 'true';
// Modal schließen
const modal = $('#git-push-modal');
modal?.classList.add('hidden');
modal?.classList.remove('visible');
$('#modal-overlay')?.classList.add('hidden');
store.closeModal();
this.setOperationLoading('push', true);
try {
const targetBranch = $('#push-target-branch')?.value || null;
if (isServerMode) {
// Server-Modus: Push für Server-Dateien
delete modal.dataset.serverMode;
const forcePush = $('#push-force')?.checked || false;
const localBranch = $('#branch-select')?.value || this.gitStatus?.branch || 'master';
await this.executeServerPush(forcePush);
} else {
// Projekt-Modus: Push für Projekt-Repository
const projectId = store.get('currentProjectId');
if (!projectId) return;
let result;
this.setOperationLoading('push', true);
// 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);
try {
const targetBranch = $('#push-target-branch')?.value || null;
const forcePush = $('#push-force')?.checked || false;
const localBranch = $('#branch-select')?.value || this.gitStatus?.branch || 'master';
// 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);
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');
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);
}
} catch (error) {
this.showToast(error.message || 'Push fehlgeschlagen', 'error');
} finally {
this.setOperationLoading('push', false);
}
}
@ -574,8 +1026,8 @@ class GiteaManager {
async handleCommit(e) {
e.preventDefault();
const projectId = store.get('currentProjectId');
if (!projectId) return;
const modal = $('#git-commit-modal');
const isServerMode = modal?.dataset.serverMode === 'true';
const message = $('#commit-message')?.value.trim();
const stageAll = $('#commit-stage-all')?.checked ?? true;
@ -585,18 +1037,31 @@ class GiteaManager {
return;
}
try {
const result = await api.gitCommit(projectId, message, stageAll);
if (result.success) {
this.showToast('Commit erstellt', 'success');
if (isServerMode) {
// Server-Modus: Commit für Server-Dateien
const success = await this.executeServerCommit(message, stageAll);
if (success) {
this.closeModal('git-commit-modal');
await this.loadGitData();
} else {
this.showToast(result.error || 'Commit fehlgeschlagen', 'error');
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');
}
} catch (error) {
this.showToast(error.message || 'Commit fehlgeschlagen', 'error');
}
}
@ -976,7 +1441,7 @@ class GiteaManager {
show() {
this.giteaView?.classList.remove('hidden');
this.giteaView?.classList.add('active');
this.loadApplication();
this.updateModeView();
this.startAutoRefresh();
}
@ -990,7 +1455,13 @@ class GiteaManager {
this.stopAutoRefresh();
// Status alle 30 Sekunden aktualisieren
this.refreshInterval = setInterval(() => {
if (this.application?.configured && !this.isLoading) {
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);