10 Commits
2.0.0 ... 2.0.3

Author SHA1 Message Date
eece161e58 Upload README.md via GUI 2026-02-03 21:52:41 +00:00
a35d6cf0cf Upload preload.js via GUI 2026-02-03 21:52:40 +00:00
594f12e927 Upload package-lock.json via GUI 2026-02-03 21:52:40 +00:00
8d70b71d66 Upload package.json via GUI 2026-02-03 21:52:39 +00:00
2c39ae4651 Upload main.js via GUI 2026-02-03 21:52:38 +00:00
9c14a1a7e0 Upload LICENSE via GUI 2026-02-03 21:52:37 +00:00
677b983706 Update from Git Manager GUI 2026-02-03 22:52:37 +01:00
20a9d601ff Update from Git Manager GUI 2026-02-03 22:52:35 +01:00
df9790177d Upload updater.js via GUI 2026-02-03 21:52:33 +00:00
1621a100af Update from Git Manager GUI 2026-02-03 22:51:03 +01:00
5 changed files with 343 additions and 27 deletions

BIN
assets/Thumbs.db Normal file

Binary file not shown.

12
main.js
View File

@@ -188,7 +188,14 @@ ipcMain.handle('select-folder', async () => {
ipcMain.handle('select-file', async () => {
const result = await dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] });
if (result.canceled) return { ok: false, files: [] };
return { ok: true, files: result.filePaths };
// Rückgabe mit path und name für jede Datei
const files = result.filePaths.map(filePath => ({
path: filePath,
name: ppath.basename(filePath)
}));
return { ok: true, files };
});
ipcMain.handle('save-credentials', async (event, data) => {
@@ -1348,7 +1355,8 @@ ipcMain.handle('create-release', async (event, data) => {
return { ok: true, release };
} catch (error) {
console.error('create-release error:', error);
return { ok: false, error: String(error) };
const errorMsg = error.message || String(error);
return { ok: false, error: errorMsg };
}
});

View File

@@ -1,6 +1,6 @@
{
"name": "git-manager-gui",
"version": "2.0.0",
"version": "2.0.1",
"main": "main.js",
"scripts": {
"start": "electron .",

View File

@@ -1827,11 +1827,23 @@ async function loadRepoReleases(owner, repo) {
</div>
`;
// Event-Listener MUSS VOR innerHTML += gesetzt werden
const newBtn = grid.querySelector('.btn-new-release');
newBtn.onclick = () => showCreateReleaseModal(owner, repo);
if (newBtn) {
newBtn.onclick = () => {
console.log('New Release button clicked');
showCreateReleaseModal(owner, repo);
};
} else {
console.error('New Release button not found in DOM');
}
if (!res.releases || res.releases.length === 0) {
grid.innerHTML += '<div style="grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-muted); font-size: 16px;">📭 Noch keine Releases veröffentlicht</div>';
// WICHTIG: appendChild statt innerHTML +=, um Event-Listener zu erhalten
const emptyMsg = document.createElement('div');
emptyMsg.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 60px; color: var(--text-muted); font-size: 16px;';
emptyMsg.textContent = '📭 Noch keine Releases veröffentlicht';
grid.appendChild(emptyMsg);
setStatus('No releases');
return;
}
@@ -2134,10 +2146,12 @@ function createReleaseCard(release, isLatest) {
CREATE RELEASE MODAL
------------------------- */
function showCreateReleaseModal(owner, repo) {
let selectedFiles = [];
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="card" style="width: 600px; max-width: 90vw;">
<div class="card" style="width: 650px; max-width: 90vw;">
<h2>🚀 Neues Release erstellen</h2>
<div class="input-group">
@@ -2173,6 +2187,23 @@ function showCreateReleaseModal(owner, repo) {
<input id="releaseTarget" type="text" value="main" placeholder="main">
</div>
<div class="input-group">
<label>📎 Release Assets (optional)</label>
<button id="btnSelectAssets" style="
width: 100%;
padding: 12px;
background: var(--bg-tertiary);
border: 2px dashed rgba(255, 255, 255, 0.2);
border-radius: var(--radius-md);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
">
📁 Dateien auswählen (z.B. .exe, .zip, .tar.gz)
</button>
<div id="assetsList" style="margin-top: 10px;"></div>
</div>
<div class="input-group" style="display: flex; gap: 20px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="releasePrerelease"> Pre-Release
@@ -2191,6 +2222,52 @@ function showCreateReleaseModal(owner, repo) {
document.body.appendChild(modal);
// Asset-Auswahl Handler
$('btnSelectAssets').onclick = async () => {
const res = await window.electronAPI.selectFile();
if (res.ok && res.files && res.files.length > 0) {
selectedFiles = res.files;
updateAssetsList();
}
};
function updateAssetsList() {
const list = $('assetsList');
if (selectedFiles.length === 0) {
list.innerHTML = '';
return;
}
list.innerHTML = selectedFiles.map((file, idx) => `
<div style="
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
margin-bottom: 6px;
">
<span style="flex: 1; color: var(--text-primary); font-size: 13px;">📄 ${file.name}</span>
<button onclick="removeAsset(${idx})" style="
background: rgba(255, 59, 48, 0.2);
color: #ff3b30;
border: none;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
">✕</button>
</div>
`).join('');
}
// Asset entfernen (global für onclick)
window.removeAsset = (idx) => {
selectedFiles.splice(idx, 1);
updateAssetsList();
};
$('btnCreateRelease').onclick = async () => {
const tag = $('releaseTag').value.trim();
const name = $('releaseName').value.trim() || tag;
@@ -2204,6 +2281,11 @@ function showCreateReleaseModal(owner, repo) {
return;
}
const btnCreate = $('btnCreateRelease');
const originalText = btnCreate.textContent;
btnCreate.disabled = true;
btnCreate.textContent = 'Erstelle Release...';
setStatus('Creating release...');
try {
@@ -2219,15 +2301,43 @@ function showCreateReleaseModal(owner, repo) {
});
if (res.ok) {
// Assets hochladen, falls vorhanden
if (selectedFiles.length > 0) {
btnCreate.textContent = `Lade Assets (0/${selectedFiles.length})...`;
setStatus(`Uploading ${selectedFiles.length} asset(s)...`);
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i];
btnCreate.textContent = `Lade Assets (${i + 1}/${selectedFiles.length})...`;
try {
await window.electronAPI.uploadReleaseAsset({
owner,
repo,
releaseId: res.release.id,
filePath: file.path,
fileName: file.name
});
} catch (err) {
console.error('Asset upload error:', err);
// Weiter mit nächster Datei
}
}
}
modal.remove();
setStatus('Release created!');
setStatus('Release created successfully!');
loadRepoReleases(owner, repo); // Reload
} else {
setStatus('Failed: ' + res.error);
btnCreate.disabled = false;
btnCreate.textContent = originalText;
}
} catch (error) {
console.error('Create release error:', error);
setStatus('Create failed');
btnCreate.disabled = false;
btnCreate.textContent = originalText;
}
};
@@ -2250,8 +2360,9 @@ async function showUploadAssetDialog(release) {
return;
}
const filePath = res.files[0];
const fileName = filePath.split(/[\\/]/).pop();
const file = res.files[0];
const filePath = file.path;
const fileName = file.name;
setStatus(`Uploading ${fileName}...`);
showProgress(0, `Uploading ${fileName}...`);

View File

@@ -39,10 +39,36 @@ async function createRepoGitHub({ name, token, auto_init = true, license = '', p
body.license_template = license;
}
try {
const response = await axios.post('https://api.github.com/user/repos', body, {
headers: { Authorization: `token ${token}` }
});
return response.data;
} catch (error) {
// Benutzerfreundliche Fehlerbehandlung
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihren GitHub-Token.');
} else if (status === 422) {
const msg = data?.message || 'Repository konnte nicht erstellt werden';
if (msg.includes('name already exists')) {
throw new Error(`Ein Repository mit dem Namen "${name}" existiert bereits.`);
}
throw new Error(`GitHub-Fehler: ${msg}`);
} else if (status === 403) {
throw new Error('Zugriff verweigert. Bitte überprüfen Sie Ihre Token-Berechtigungen.');
} else {
throw new Error(`GitHub-Fehler (${status}): ${data?.message || error.message}`);
}
} else if (error.request) {
throw new Error('Keine Antwort von GitHub. Bitte überprüfen Sie Ihre Internetverbindung.');
} else {
throw new Error(`Fehler beim Erstellen des Repositories: ${error.message}`);
}
}
}
async function createRepoGitea({ name, token, url, auto_init = true, license = '', private: isPrivate = false }) {
@@ -54,6 +80,10 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
console.log('Token length:', token ? token.length : 0);
console.log('Name:', name);
console.log('auto_init:', auto_init);
console.log('License:', license);
// Normalisiere Lizenz zu Großbuchstaben, wenn vorhanden
const normalizedLicense = license ? license.toUpperCase() : '';
const body = {
name,
@@ -62,21 +92,77 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
default_branch: 'main'
};
if (license) {
body.license = license;
if (normalizedLicense) {
body.license = normalizedLicense;
}
console.log('Request body:', JSON.stringify(body, null, 2));
try {
const response = await axios.post(endpoint, body, {
headers: { Authorization: `token ${token}` }
headers: { Authorization: `token ${token}` },
timeout: 15000 // 15 Sekunden Timeout
});
console.log('Success! Status:', response.status);
return response.data;
} catch (error) {
console.error('Error creating repo:', error.response?.status, error.response?.data);
throw error;
// Wenn Lizenz-Fehler auftritt (500 mit Lizenz-Meldung), versuche ohne Lizenz
if (error.response?.status === 500 &&
error.response?.data?.message?.includes('getLicense') &&
normalizedLicense) {
console.warn(`Lizenz "${normalizedLicense}" wird vom Server nicht unterstützt. Versuche ohne Lizenz...`);
const bodyWithoutLicense = {
name,
private: isPrivate,
auto_init: auto_init,
default_branch: 'main'
};
try {
const retryResponse = await axios.post(endpoint, bodyWithoutLicense, {
headers: { Authorization: `token ${token}` },
timeout: 15000
});
console.log('Success without license! Status:', retryResponse.status);
console.warn(`Hinweis: Repository wurde ohne Lizenz erstellt, da "${normalizedLicense}" nicht verfügbar ist.`);
return retryResponse.data;
} catch (retryError) {
// Falls auch ohne Lizenz fehlschlägt, behandle den neuen Fehler
error = retryError;
}
}
// Benutzerfreundliche Fehlerbehandlung
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihren Gitea-Token.');
} else if (status === 409 || (status === 422 && data?.message?.includes('already exists'))) {
throw new Error(`Ein Repository mit dem Namen "${name}" existiert bereits auf Gitea.`);
} else if (status === 403) {
throw new Error('Zugriff verweigert. Bitte überprüfen Sie Ihre Token-Berechtigungen.');
} else if (status === 404) {
throw new Error('Gitea-Server nicht gefunden. Bitte überprüfen Sie die URL.');
} else if (status === 422) {
const msg = data?.message || 'Repository konnte nicht erstellt werden';
throw new Error(`Gitea-Fehler: ${msg}`);
} else if (status === 500 && data?.message?.includes('getLicense')) {
throw new Error(`Die Lizenz "${normalizedLicense}" wird von Ihrem Gitea-Server nicht unterstützt. Bitte wählen Sie eine andere Lizenz oder lassen Sie das Feld leer.`);
} else {
throw new Error(`Gitea-Fehler (${status}): ${data?.message || error.message}`);
}
} else if (error.code === 'ECONNABORTED') {
throw new Error('Zeitüberschreitung beim Verbinden mit Gitea. Bitte überprüfen Sie Ihre Internetverbindung oder Server-URL.');
} else if (error.request) {
throw new Error('Keine Antwort von Gitea-Server. Bitte überprüfen Sie die URL und Ihre Internetverbindung.');
} else {
throw new Error(`Fehler beim Erstellen des Repositories: ${error.message}`);
}
}
}
@@ -260,7 +346,7 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
let retryCount = 0;
const MAX_RETRIES = 3;
const MAX_RETRIES = 2; // Reduziert auf 2 Retries für schnelleren Fallback
while (retryCount <= MAX_RETRIES) {
let sha = await fetchSha();
@@ -275,14 +361,48 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
};
if (sha) body.sha = sha;
console.log(`[Upload Debug] Datei: ${path}, Branch: ${branchName}, SHA: ${sha ? sha.substring(0, 8) : 'keine'}`);
try {
const res = await axios.put(endpoint, body, {
headers: { Authorization: `token ${token}` }
headers: { Authorization: `token ${token}` },
timeout: 30000 // 30 Sekunden Timeout für größere Dateien
});
console.log(`[Upload Success] ${path} erfolgreich gespeichert`);
return res.data;
} catch (err) {
console.error(`Upload Attempt ${retryCount + 1} for ${path}:`, err.response ? err.response.data : err.message);
// Behandle 500 Server-Fehler speziell
if (err.response && err.response.status === 500) {
const errorMsg = err.response.data?.message || err.message;
// Git-Referenz-Konflikt: Branch hat sich geändert, SHA ist veraltet
if (errorMsg.includes('cannot lock ref') || errorMsg.includes('failed to update ref') || errorMsg.includes('but expected')) {
if (retryCount < MAX_RETRIES) {
retryCount++;
console.warn(`-> Git-Referenz-Konflikt erkannt. Branch hat sich geändert. Aktualisiere SHA und versuche erneut... (Retry ${retryCount}/${MAX_RETRIES})`);
await sleep(500); // Kurze Pause
continue; // SHA wird in der nächsten Iteration neu geholt
} else {
throw new Error(`Git-Referenz-Konflikt: Der Branch "${branchName}" wurde während des Uploads geändert. Bitte versuchen Sie es erneut.`);
}
}
// Wenn es ein Lizenz-Fehler ist (sollte nicht passieren, aber als Fallback)
if (errorMsg.includes('getLicense')) {
throw new Error(`Server-Fehler beim Speichern: Lizenz-Problem. Versuchen Sie es erneut.`);
}
// Wenn es ein anderes Branch-Problem ist
if (errorMsg.includes('branch') || errorMsg.includes('ref')) {
throw new Error(`Server-Fehler: Problem mit Branch "${branchName}". Details: ${errorMsg}`);
}
// Allgemeiner 500-Fehler
throw new Error(`Server-Fehler (500) beim Speichern der Datei. Details: ${errorMsg}`);
}
const isShaRequired = err.response &&
err.response.status === 422 &&
err.response.data &&
@@ -291,15 +411,33 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
if (isShaRequired && retryCount < MAX_RETRIES) {
retryCount++;
console.warn(`-> 422 SHA Required. Waiting 2 seconds for server index update... (Retry ${retryCount}/${MAX_RETRIES})`);
await sleep(2000); // Wartezeit geben
console.warn(`-> 422 SHA Required. Waiting 1.5 seconds for server index update... (Retry ${retryCount}/${MAX_RETRIES})`);
await sleep(1500); // Reduzierte Wartezeit für schnelleren Fallback
// Schleife wird neu gestartet, SHA wird erneut gesucht
continue;
} else if (isShaRequired && retryCount >= MAX_RETRIES) {
throw new Error(`Upload failed after ${MAX_RETRIES} retries. Server insists file exists but we cannot find its SHA. Check the repository manually.`);
// Verbesserte Fehlermeldung mit Hinweis auf Git-Fallback
const error = new Error(`API-Upload fehlgeschlagen: Repository wurde gerade erstellt, Index noch nicht bereit. Verwende Git-Fallback.`);
error.code = 'SHA_NOT_FOUND';
throw error;
}
// Andere Fehler mit besserer Meldung werfen
if (err.response) {
const status = err.response.status;
const data = err.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihren Token.');
} else if (status === 403) {
throw new Error('Zugriff verweigert. Keine Berechtigung zum Schreiben in dieses Repository.');
} else if (status === 404) {
throw new Error(`Datei oder Repository nicht gefunden. Bitte überprüfen Sie den Pfad: ${path}`);
} else {
throw new Error(`Fehler beim Speichern (${status}): ${data?.message || err.message}`);
}
}
// Andere Fehler sofort werfen
throw err;
}
}
@@ -532,12 +670,42 @@ async function createGiteaRelease({ token, url, owner, repo, data }) {
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json'
}
},
timeout: 15000
});
return response.data;
} catch (err) {
console.error('createGiteaRelease error:', err.response?.data || err.message);
throw err;
// Benutzerfreundliche Fehlerbehandlung
if (err.response) {
const status = err.response.status;
const data = err.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen. Bitte überprüfen Sie Ihren Token.');
} else if (status === 403) {
throw new Error('Zugriff verweigert. Keine Berechtigung zum Erstellen von Releases.');
} else if (status === 404) {
throw new Error(`Repository "${owner}/${repo}" nicht gefunden.`);
} else if (status === 409 || (status === 422 && data?.message?.includes('already exists'))) {
throw new Error(`Ein Release mit dem Tag "${data.tag_name}" existiert bereits.`);
} else if (status === 422) {
const msg = data?.message || 'Release konnte nicht erstellt werden';
throw new Error(`Gitea-Fehler: ${msg}`);
} else if (status === 500) {
const msg = data?.message || err.message;
throw new Error(`Server-Fehler (500) beim Erstellen des Release. Details: ${msg}`);
} else {
throw new Error(`Fehler beim Erstellen des Release (${status}): ${data?.message || err.message}`);
}
} else if (err.code === 'ECONNABORTED') {
throw new Error('Zeitüberschreitung. Bitte versuchen Sie es erneut.');
} else if (err.request) {
throw new Error('Keine Antwort vom Server. Bitte überprüfen Sie Ihre Internetverbindung.');
} else {
throw new Error(`Fehler beim Erstellen des Release: ${err.message}`);
}
}
}
@@ -616,12 +784,41 @@ async function uploadReleaseAsset({ token, url, owner, repo, releaseId, filePath
...formData.getHeaders()
},
maxContentLength: Infinity,
maxBodyLength: Infinity
maxBodyLength: Infinity,
timeout: 300000 // 5 Minuten für große Dateien
});
return response.data;
} catch (err) {
console.error('uploadReleaseAsset error:', err.response?.data || err.message);
throw err;
// Benutzerfreundliche Fehlerbehandlung
if (err.response) {
const status = err.response.status;
const data = err.response.data;
if (status === 401) {
throw new Error('Authentifizierung fehlgeschlagen beim Upload.');
} else if (status === 403) {
throw new Error('Zugriff verweigert. Keine Berechtigung zum Hochladen von Assets.');
} else if (status === 404) {
throw new Error(`Release mit ID ${releaseId} nicht gefunden.`);
} else if (status === 413) {
throw new Error('Datei ist zu groß. Maximale Größe überschritten.');
} else if (status === 500) {
const msg = data?.message || err.message;
throw new Error(`Server-Fehler beim Upload: ${msg}`);
} else {
throw new Error(`Fehler beim Upload (${status}): ${data?.message || err.message}`);
}
} else if (err.code === 'ECONNABORTED') {
throw new Error('Upload-Zeitüberschreitung. Datei ist möglicherweise zu groß.');
} else if (err.code === 'ENOENT') {
throw new Error(`Datei nicht gefunden: ${filePath}`);
} else if (err.request) {
throw new Error('Keine Antwort vom Server beim Upload.');
} else {
throw new Error(`Upload fehlgeschlagen: ${err.message}`);
}
}
}