diff --git a/main.js b/main.js index f9627b9..cf8a532 100644 --- a/main.js +++ b/main.js @@ -252,19 +252,13 @@ ipcMain.handle('push-project', async (event, data) => { try { if (!data.folder || !fs.existsSync(data.folder)) return { ok: false, error: 'folder-not-found' }; - // Prüfen, ob der lokale Branch 'master' heißt und in 'main' umbenennen + // Aktuellen Branch ermitteln (NICHT umbenennen!) + let currentBranch = data.branch || null; try { - const currentBranch = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim(); - console.log('Current local branch:', currentBranch); - - if (currentBranch === 'master') { - console.log('Attempting to rename master to main...'); - try { - execSync('git branch -m master main', { cwd: data.folder, stdio: 'inherit' }); - console.log('Successfully renamed local branch master to main'); - } catch (e) { - console.warn('Failed to rename branch (maybe main already exists)', e.message); - } + const detected = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim(); + if (detected) { + currentBranch = currentBranch || detected; + console.log('Current local branch:', detected); } } catch (e) { console.warn('Could not check local branch (maybe not a git repo yet)', e.message); @@ -310,9 +304,11 @@ ipcMain.handle('push-project', async (event, data) => { } } - // 3. Pushen (nutze 'main') + // 3. Pushen (nutze den tatsächlichen Branch - main ODER master) const progressCb = percent => { try { event.sender.send('push-progress', percent); } catch (_) {} }; - await commitAndPush(data.folder, 'main', 'Update from Git Manager GUI', progressCb); + const commitMsg = data.commitMessage || 'Update from Git Manager GUI'; + const pushBranch = currentBranch || 'main'; + await commitAndPush(data.folder, pushBranch, commitMsg, progressCb); return { ok: true }; } catch (e) { console.error('push-project error', e); @@ -323,8 +319,20 @@ ipcMain.handle('push-project', async (event, data) => { ipcMain.handle('getBranches', async (event, data) => { try { const branches = await getBranches(data.folder); - // Sortieren, damit 'main' oben steht - branches.sort((a, b) => (a === 'main' ? -1 : b === 'main' ?1 : 0)); + // Aktuellen Branch ermitteln und nach oben sortieren + let currentBranch = null; + try { + currentBranch = execSync('git branch --show-current', { cwd: data.folder, stdio: 'pipe', encoding: 'utf-8' }).trim(); + } catch (_) {} + branches.sort((a, b) => { + if (a === currentBranch) return -1; + if (b === currentBranch) return 1; + if (a === 'main') return -1; + if (b === 'main') return 1; + if (a === 'master') return -1; + if (b === 'master') return 1; + return 0; + }); return { ok: true, branches }; } catch (e) { console.error('getBranches error', e); @@ -463,6 +471,121 @@ ipcMain.handle('writeFile', async (event, data) => { ipcMain.handle('deleteFile', async (event, data) => { try { + // --- GITEA DELETION --- + if (data && data.isGitea) { + const credentials = readCredentials(); + const token = (data.token) || (credentials && credentials.giteaToken); + const giteaUrl = (data.url) || (credentials && credentials.giteaURL); + if (!token || !giteaUrl) return { ok: false, error: 'missing-token-or-url' }; + + const owner = data.owner; + const repo = data.repo; + const filePath = data.path; + if (!owner || !repo || !filePath) return { ok: false, error: 'missing-owner-repo-or-path' }; + + const urlObj = new URL(giteaUrl); + const protocol = urlObj.protocol === 'https:' ? https : http; + const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80); + + // Helper: GET contents from Gitea API + function giteaGet(path) { + return new Promise((resolve, reject) => { + const req = protocol.request({ + hostname: urlObj.hostname, port, + path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=${data.ref || 'HEAD'}`, + method: 'GET', + headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' } + }, (res) => { + let body = ''; + res.on('data', chunk => body += chunk); + res.on('end', () => { + try { resolve(JSON.parse(body)); } catch (e) { reject(e); } + }); + }); + req.on('error', reject); + req.end(); + }); + } + + // Helper: DELETE a single file by path + sha + function giteaDeleteFile(filePath, sha) { + return new Promise((resolve) => { + const body = JSON.stringify({ + message: `Delete ${filePath} via Git Manager GUI`, + sha, + branch: data.ref || 'HEAD' + }); + const req = protocol.request({ + hostname: urlObj.hostname, port, + path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}`, + method: 'DELETE', + headers: { + 'Authorization': `token ${token}`, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body) + } + }, (res) => { + let respBody = ''; + res.on('data', chunk => respBody += chunk); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) resolve({ ok: true }); + else resolve({ ok: false, error: `HTTP ${res.statusCode}: ${respBody}` }); + }); + }); + req.on('error', (e) => resolve({ ok: false, error: String(e) })); + req.write(body); + req.end(); + }); + } + + // Helper: Recursively collect all files in a folder + async function collectAllFiles(path) { + const contents = await giteaGet(path); + const files = []; + if (Array.isArray(contents)) { + // It's a folder — recurse into it + for (const item of contents) { + if (item.type === 'dir') { + const sub = await collectAllFiles(item.path); + files.push(...sub); + } else if (item.type === 'file') { + files.push({ path: item.path, sha: item.sha }); + } + } + } else if (contents && contents.sha) { + // It's a single file + files.push({ path: contents.path, sha: contents.sha }); + } else { + throw new Error(`Unbekannte Antwort: ${JSON.stringify(contents)}`); + } + return files; + } + + // Collect all files to delete (handles both file and folder) + const filesToDelete = await collectAllFiles(filePath); + + if (filesToDelete.length === 0) { + return { ok: false, error: 'Keine Dateien zum Löschen gefunden' }; + } + + // Delete all files sequentially + let failed = 0; + for (const f of filesToDelete) { + const res = await giteaDeleteFile(f.path, f.sha); + if (!res.ok) { + console.error(`Fehler beim Löschen von ${f.path}:`, res.error); + failed++; + } + } + + if (failed > 0) { + return { ok: false, error: `${failed} von ${filesToDelete.length} Dateien konnten nicht gelöscht werden` }; + } + + return { ok: true, deleted: filesToDelete.length }; + } + + // --- LOCAL DELETION --- if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' }; fs.rmSync(data.path, { recursive: true, force: true }); return { ok: true }; @@ -503,8 +626,8 @@ ipcMain.handle('get-gitea-repo-contents', async (event, data) => { // This allows apiHandler.js to try ['main', 'master'] if no ref is passed const ref = data.ref; - const items = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref }); - return { ok: true, items }; + const result = await getGiteaRepoContents({ token, url, owner, repo, path: p, ref }); + return { ok: true, items: result.items || result, empty: result.empty || false }; } catch (e) { console.error('get-gitea-repo-contents error', e); return { ok: false, error: String(e) }; @@ -546,7 +669,7 @@ ipcMain.handle('read-gitea-file', async (event, data) => { const owner = data.owner; const repo = data.repo; const p = data.path; - const ref = data.ref || 'main'; + const ref = data.ref || 'HEAD'; console.log(`read-gitea-file: ${owner}/${repo}/${p} (ref: ${ref})`); @@ -619,9 +742,8 @@ ipcMain.handle('upload-gitea-file', async (event, data) => { const repo = data.repo; // destPath is the target folder in the repo const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, ''); - // FIXED: Konvertiere 'master' zu 'main' (Upload should generally target main) - let branch = data.branch || 'main'; - if (branch === 'master') branch = 'main'; + // Branch wird unverändert übernommen (main UND master werden unterstützt) + let branch = data.branch || 'HEAD'; const message = data.message || 'Upload via Git Manager GUI'; const localFiles = Array.isArray(data.localPath) ? data.localPath : (data.localPath ? [data.localPath] : []); const results = []; @@ -678,7 +800,7 @@ ipcMain.handle('write-gitea-file', async (event, data) => { const repo = data.repo; const path = data.path; const content = data.content || ''; - const ref = data.ref || 'main'; + const ref = data.ref || 'HEAD'; // Konvertiere Content zu Base64 const base64 = Buffer.from(content, 'utf8').toString('base64'); @@ -712,9 +834,8 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => { const repo = data.repo; // destPath is the target directory in the repo const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, ''); - // FIXED: Konvertiere 'master' zu 'main' - let branch = data.branch || 'main'; - if (branch === 'master') branch = 'main'; + // Branch wird unverändert übernommen (main UND master werden unterstützt) + let branch = data.branch || 'HEAD'; const messagePrefix = data.messagePrefix || 'Upload folder via GUI'; const concurrency = data.concurrency || DEFAULT_CONCURRENCY; if (!localFolder || !fs.existsSync(localFolder)) return { ok: false, error: 'local-folder-not-found' }; @@ -792,7 +913,7 @@ ipcMain.handle('download-gitea-file', async (event, data) => { const repo = data.repo; const filePath = data.path; if (!owner || !repo || !filePath) return { ok: false, error: 'missing-owner-repo-or-path' }; - const content = await getGiteaFileContent({ token, url, owner, repo, path: filePath, ref: 'main' }); + const content = await getGiteaFileContent({ token, url, owner, repo, path: filePath, ref: data.ref || 'HEAD' }); const save = await dialog.showSaveDialog({ defaultPath: ppath.basename(filePath) }); if (save.canceled || !save.filePath) return { ok: false, error: 'save-canceled' }; fs.writeFileSync(save.filePath, content, 'utf8'); @@ -820,7 +941,8 @@ ipcMain.handle('download-gitea-folder', async (event, data) => { const allFiles = []; async function gather(pathInRepo) { - const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' }); + const _r = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'HEAD' }); + const items = _r.items || _r; for (const item of items) { if (item.type === 'dir') await gather(item.path); else if (item.type === 'file') allFiles.push(item.path); @@ -832,7 +954,7 @@ ipcMain.handle('download-gitea-folder', async (event, data) => { if (total === 0) return { ok: true, savedTo: destBase, files: [] }; const tasks = allFiles.map(remoteFile => async () => { - const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: 'main' }); + const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'HEAD' }); const localPath = ppath.join(destBase, remoteFile); fs.mkdirSync(ppath.dirname(localPath), { recursive: true }); fs.writeFileSync(localPath, content, 'utf8'); @@ -892,8 +1014,9 @@ ipcMain.handle('prepare-download-drag', async (event, data) => { // Gather list of files (recursive) const allFiles = []; async function gather(pathInRepo) { - const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'main' }); - for (const item of items || []) { + const _r2 = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'HEAD' }); + const items = (_r2.items || _r2) || []; + for (const item of items) { if (item.type === 'dir') await gather(item.path); else if (item.type === 'file') allFiles.push(item.path); } @@ -909,7 +1032,7 @@ ipcMain.handle('prepare-download-drag', async (event, data) => { // Download files sequentially or with limited concurrency: const tasks = allFiles.map(remoteFile => async () => { - const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'main' }); + const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'HEAD' }); const localPath = ppath.join(tmpBase, remoteFile); ensureDir(ppath.dirname(localPath)); @@ -1030,9 +1153,8 @@ ipcMain.handle('upload-and-push', async (event, data) => { const giteaUrl = (data && data.url) || (credentials && credentials.giteaURL); const owner = data.owner; const repo = data.repo; - // FIXED: Konvertiere 'master' zu 'main' - let branch = data.branch || 'main'; - if (branch === 'master') branch = 'main'; + // Branch wird unverändert übernommen (main UND master werden unterstützt) + let branch = data.branch || 'HEAD'; const cloneUrl = data.cloneUrl || null; const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, ''); if (!owner || !repo) return { ok: false, error: 'missing-owner-or-repo' }; @@ -1275,6 +1397,226 @@ ipcMain.handle('upload-and-push', async (event, data) => { return { ok: false, error: String(e) }; } }); +/* ================================ + RENAME / CREATE / MOVE HANDLERS + ================================ */ + +// Gitea: Datei/Ordner umbenennen (= alle Dateien kopieren + alte löschen) +ipcMain.handle('rename-gitea-item', async (event, data) => { + try { + const credentials = readCredentials(); + const token = credentials?.giteaToken; + const giteaUrl = credentials?.giteaURL; + if (!token || !giteaUrl) return { ok: false, error: 'missing-token-or-url' }; + + const { owner, repo, oldPath, newPath, isDir } = data; + const urlObj = new URL(giteaUrl); + const protocol = urlObj.protocol === 'https:' ? https : http; + const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80); + + function giteaRequest(method, apiPath, body) { + return new Promise((resolve, reject) => { + const bodyStr = body ? JSON.stringify(body) : null; + const req = protocol.request({ + hostname: urlObj.hostname, port, + path: apiPath, method, + headers: { + 'Authorization': `token ${token}`, + 'Content-Type': 'application/json', + ...(bodyStr ? { 'Content-Length': Buffer.byteLength(bodyStr) } : {}) + } + }, (res) => { + let b = ''; + res.on('data', c => b += c); + res.on('end', () => { + try { resolve({ status: res.statusCode, body: JSON.parse(b) }); } + catch (_) { resolve({ status: res.statusCode, body: b }); } + }); + }); + req.on('error', reject); + if (bodyStr) req.write(bodyStr); + req.end(); + }); + } + + async function collectFiles(path) { + const r = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${path.split('/').map(encodeURIComponent).join('/')}?ref=HEAD`, null); + const files = []; + if (Array.isArray(r.body)) { + for (const item of r.body) { + if (item.type === 'dir') files.push(...await collectFiles(item.path)); + else files.push({ path: item.path, sha: item.sha }); + } + } else if (r.body?.sha) { + files.push({ path: r.body.path, sha: r.body.sha }); + } + return files; + } + + async function readFileContent(filePath) { + const r = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}?ref=HEAD`, null); + return r.body?.content ? r.body.content.replace(/\n/g, '') : ''; + } + + async function uploadFile(targetPath, contentBase64, message) { + // Check if exists first (need SHA for update) + const check = await giteaRequest('GET', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}?ref=HEAD`, null); + const body = { message, content: contentBase64, branch: 'HEAD' }; + if (check.body?.sha) body.sha = check.body.sha; + return giteaRequest('POST', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}`, body); + } + + async function deleteFile(filePath, sha) { + return giteaRequest('DELETE', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath.split('/').map(encodeURIComponent).join('/')}`, { + message: `Delete ${filePath} (rename)`, sha, branch: 'HEAD' + }); + } + + // Collect all files under oldPath + const files = await collectFiles(oldPath); + + // For each file: read content, upload to newPath, delete from oldPath + for (const f of files) { + const content = await readFileContent(f.path); + const relPath = isDir ? f.path.slice(oldPath.length + 1) : ''; + const targetPath = isDir ? `${newPath}/${relPath}` : newPath; + await uploadFile(targetPath, content, `Rename: move ${f.path} to ${targetPath}`); + await deleteFile(f.path, f.sha); + } + + return { ok: true }; + } catch (e) { + console.error('rename-gitea-item error', e); + return { ok: false, error: String(e) }; + } +}); + +// Gitea: Neue Datei oder Ordner (Ordner = Datei mit .gitkeep) +ipcMain.handle('create-gitea-item', async (event, data) => { + try { + const credentials = readCredentials(); + const token = credentials?.giteaToken; + const giteaUrl = credentials?.giteaURL; + if (!token || !giteaUrl) return { ok: false, error: 'missing-token-or-url' }; + + const { owner, repo, path: itemPath, type } = data; + const urlObj = new URL(giteaUrl); + const protocol = urlObj.protocol === 'https:' ? https : http; + const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80); + + // Für Ordner: .gitkeep Datei anlegen + const targetPath = type === 'folder' ? `${itemPath}/.gitkeep` : itemPath; + const content = Buffer.from('').toString('base64'); + + return new Promise((resolve) => { + const body = JSON.stringify({ message: `Create ${itemPath}`, content, branch: data.branch || 'HEAD' }); + const req = protocol.request({ + hostname: urlObj.hostname, port, + path: `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${targetPath.split('/').map(encodeURIComponent).join('/')}`, + method: 'POST', + headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } + }, (res) => { + let b = ''; + res.on('data', c => b += c); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) resolve({ ok: true }); + else resolve({ ok: false, error: `HTTP ${res.statusCode}: ${b}` }); + }); + }); + req.on('error', e => resolve({ ok: false, error: String(e) })); + req.write(body); + req.end(); + }); + } catch (e) { + console.error('create-gitea-item error', e); + return { ok: false, error: String(e) }; + } +}); + +// Lokal: Umbenennen +ipcMain.handle('rename-local-item', async (event, data) => { + try { + const { oldPath, newName } = data; + if (!oldPath || !fs.existsSync(oldPath)) return { ok: false, error: 'path-not-found' }; + const dir = ppath.dirname(oldPath); + const newPath = ppath.join(dir, newName); + fs.renameSync(oldPath, newPath); + return { ok: true, newPath }; + } catch (e) { + console.error('rename-local-item error', e); + return { ok: false, error: String(e) }; + } +}); + +// Lokal: Neue Datei oder Ordner erstellen +ipcMain.handle('create-local-item', async (event, data) => { + try { + const { parentDir, name, type } = data; + const targetPath = ppath.join(parentDir, name); + if (type === 'folder') { + fs.mkdirSync(targetPath, { recursive: true }); + } else { + // Sicherstellen dass Elternordner existiert + fs.mkdirSync(ppath.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, '', 'utf8'); + } + return { ok: true, path: targetPath }; + } catch (e) { + console.error('create-local-item error', e); + return { ok: false, error: String(e) }; + } +}); + +// Lokal: Verschieben (Cut & Paste) +ipcMain.handle('move-local-item', async (event, data) => { + try { + const { srcPath, destDir } = data; + if (!srcPath || !fs.existsSync(srcPath)) return { ok: false, error: 'source-not-found' }; + const name = ppath.basename(srcPath); + const destPath = ppath.join(destDir, name); + fs.mkdirSync(destDir, { recursive: true }); + fs.renameSync(srcPath, destPath); + return { ok: true, destPath }; + } catch (e) { + // renameSync kann über Laufwerke nicht funktionieren — dann cpSync + rmSync + try { + const { srcPath, destDir } = data; + const name = ppath.basename(srcPath); + const destPath = ppath.join(destDir, name); + if (fs.statSync(srcPath).isDirectory()) { + fs.cpSync(srcPath, destPath, { recursive: true }); + } else { + fs.copyFileSync(srcPath, destPath); + } + fs.rmSync(srcPath, { recursive: true, force: true }); + return { ok: true, destPath }; + } catch (e2) { + console.error('move-local-item error', e2); + return { ok: false, error: String(e2) }; + } + } +}); + +// Lokal: Kopieren +ipcMain.handle('copy-local-item', async (event, data) => { + try { + const { src, destDir } = data; + if (!src || !fs.existsSync(src)) return { ok: false, error: 'source-not-found' }; + const name = ppath.basename(src); + const dest = ppath.join(destDir, name); + fs.mkdirSync(destDir, { recursive: true }); + if (fs.statSync(src).isDirectory()) { + fs.cpSync(src, dest, { recursive: true }); + } else { + fs.copyFileSync(src, dest); + } + return { ok: true, dest }; + } catch (e) { + console.error('copy-local-item error', e); + return { ok: false, error: String(e) }; + } +}); + /* ================================ RELEASE MANAGEMENT IPC HANDLERS ================================ */ @@ -1592,6 +1934,57 @@ ipcMain.handle('get-commit-files', async (event, data) => { +/* ============================================ + FAVORITEN & ZULETZT GEÖFFNET - Persistenz + ============================================ */ + +function getFavoritesFilePath() { + return ppath.join(app.getPath('userData'), 'favorites.json'); +} + +function getRecentFilePath() { + return ppath.join(app.getPath('userData'), 'recent.json'); +} + +ipcMain.handle('load-favorites', async () => { + try { + const p = getFavoritesFilePath(); + if (!fs.existsSync(p)) return { ok: true, favorites: [] }; + return { ok: true, favorites: JSON.parse(fs.readFileSync(p, 'utf8')) || [] }; + } catch (e) { + return { ok: true, favorites: [] }; + } +}); + +ipcMain.handle('save-favorites', async (event, favorites) => { + try { + fs.writeFileSync(getFavoritesFilePath(), JSON.stringify(favorites || [], null, 2), 'utf8'); + return { ok: true }; + } catch (e) { + return { ok: false, error: String(e) }; + } +}); + +ipcMain.handle('load-recent', async () => { + try { + const p = getRecentFilePath(); + if (!fs.existsSync(p)) return { ok: true, recent: [] }; + return { ok: true, recent: JSON.parse(fs.readFileSync(p, 'utf8')) || [] }; + } catch (e) { + return { ok: true, recent: [] }; + } +}); + +ipcMain.handle('save-recent', async (event, recent) => { + try { + const trimmed = (recent || []).slice(0, 20); + fs.writeFileSync(getRecentFilePath(), JSON.stringify(trimmed, null, 2), 'utf8'); + return { ok: true }; + } catch (e) { + return { ok: false, error: String(e) }; + } +}); + // main.js - Updater IPC Handlers // 1. Version abfragen