Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f2e1d1e30 | |||
| e7b3c9c447 | |||
| e8474957e8 | |||
| 94e8c21e9b | |||
| 7d18161141 | |||
| d88eade5a0 | |||
| 6895a042d1 | |||
| e379f6a096 | |||
| fca9d9c66f | |||
| fd868ea238 | |||
| 1a76dc4e64 | |||
| a0c964e05e | |||
| e109082d56 | |||
| 2c93a139d5 | |||
| 7bd8668e79 | |||
| 19be614284 | |||
| 0272756be7 |
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
679
main.js
679
main.js
@@ -7,6 +7,8 @@ const crypto = require('crypto');
|
||||
const { execSync } = require('child_process');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const Updater = require('./updater.js'); // Auto-Updater
|
||||
let updater = null;
|
||||
|
||||
const {
|
||||
createRepoGitHub,
|
||||
@@ -14,7 +16,20 @@ const {
|
||||
listGiteaRepos,
|
||||
getGiteaRepoContents,
|
||||
getGiteaFileContent,
|
||||
uploadGiteaFile
|
||||
uploadGiteaFile,
|
||||
getGiteaCommits,
|
||||
getGiteaCommit,
|
||||
getGiteaCommitDiff,
|
||||
getGiteaCommitFiles,
|
||||
searchGiteaCommits,
|
||||
getGiteaBranches,
|
||||
listGiteaReleases,
|
||||
getGiteaRelease,
|
||||
createGiteaRelease,
|
||||
editGiteaRelease,
|
||||
deleteGiteaRelease,
|
||||
uploadReleaseAsset,
|
||||
deleteReleaseAsset
|
||||
} = require('./src/git/apiHandler.js');
|
||||
|
||||
const { initRepo, commitAndPush, getBranches, getCommitLogs } = require('./src/git/gitHandler.js');
|
||||
@@ -69,19 +84,24 @@ function ensureDir(dirPath) {
|
||||
*/
|
||||
function getSafeTmpDir(baseName) {
|
||||
const safeBase = (baseName || 'tmp').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 64);
|
||||
const dir = ppath.join(os.tmpdir(), `${safeBase}-${Date.now()}`);
|
||||
try {
|
||||
if (fs.existsSync(dir)) {
|
||||
if (!fs.statSync(dir).isDirectory()) fs.unlinkSync(dir);
|
||||
else fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
} catch (e) {
|
||||
throw new Error(`getSafeTmpDir failed for ${dir}: ${e && e.message ? e.message : e}`);
|
||||
}
|
||||
|
||||
// Basis-Temp-Ordner für interne Verwaltung
|
||||
const internalBase = ppath.join(os.tmpdir(), 'gitea-drag');
|
||||
if (!fs.existsSync(internalBase)) fs.mkdirSync(internalBase, { recursive: true });
|
||||
|
||||
// Eindeutiger Unterordner, um Kollisionen zu vermeiden
|
||||
const uniqueSub = crypto.randomBytes(8).toString('hex');
|
||||
const internalDir = ppath.join(internalBase, uniqueSub);
|
||||
fs.mkdirSync(internalDir, { recursive: true });
|
||||
|
||||
// Sichtbarer Ordnername = safeBase
|
||||
const finalDir = ppath.join(internalDir, safeBase);
|
||||
fs.mkdirSync(finalDir, { recursive: true });
|
||||
|
||||
return finalDir;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------
|
||||
app / window
|
||||
----------------------------- */
|
||||
@@ -324,6 +344,52 @@ ipcMain.handle('getCommitLogs', async (event, data) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
Neue/kompatible Handler: 'get-commits' (Renderer verwendet ggf. diesen)
|
||||
- Unterstützt:
|
||||
1) Lokale Git-Logs via data.folder -> getCommitLogs
|
||||
2) Gitea Commits via owner+repo -> getGiteaCommits
|
||||
---------------------------------------------------------------- */
|
||||
ipcMain.handle('get-commits', async (event, data) => {
|
||||
try {
|
||||
// 1) Lokale Git-Logs (folder vorhanden)
|
||||
if (data && data.folder) {
|
||||
const logs = await getCommitLogs(data.folder, data.count || 50);
|
||||
return { ok: true, logs };
|
||||
}
|
||||
|
||||
// 2) Gitea-Commits (owner/repo vorhanden)
|
||||
if (data && data.owner && data.repo) {
|
||||
const credentials = readCredentials();
|
||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
||||
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
|
||||
|
||||
// map optional params
|
||||
const page = data.page || 1;
|
||||
const limit = data.limit || 50;
|
||||
const sha = data.sha; // optional branch/sha filter
|
||||
|
||||
const commits = await getGiteaCommits({
|
||||
token,
|
||||
url,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
page,
|
||||
limit,
|
||||
sha
|
||||
});
|
||||
|
||||
return { ok: true, commits };
|
||||
}
|
||||
|
||||
return { ok: false, error: 'invalid-parameters-for-get-commits' };
|
||||
} catch (e) {
|
||||
console.error('get-commits error', e);
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
Local file tree functions
|
||||
----------------------------- */
|
||||
@@ -382,6 +448,21 @@ ipcMain.handle('readFile', async (event, data) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('writeFile', async (event, data) => {
|
||||
try {
|
||||
if (!data || !data.path) return { ok: false, error: 'invalid-path' };
|
||||
const dir = ppath.dirname(data.path);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(data.path, data.content || '', 'utf8');
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
console.error('writeFile error', e);
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('deleteFile', async (event, data) => {
|
||||
try {
|
||||
if (!data || !data.path || !fs.existsSync(data.path)) return { ok: false, error: 'file-not-found' };
|
||||
@@ -453,6 +534,83 @@ ipcMain.handle('get-gitea-file-content', async (event, data) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Alias für Editor: read-gitea-file (für Text und Bilder)
|
||||
ipcMain.handle('read-gitea-file', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
||||
if (!token || !url) {
|
||||
console.error('Missing token or URL');
|
||||
return { ok: false, error: 'missing-token-or-url' };
|
||||
}
|
||||
|
||||
const owner = data.owner;
|
||||
const repo = data.repo;
|
||||
const p = data.path;
|
||||
const ref = data.ref || 'main';
|
||||
|
||||
console.log(`read-gitea-file: ${owner}/${repo}/${p} (ref: ${ref})`);
|
||||
|
||||
// Prüfe ob es eine Bilddatei ist
|
||||
const isImage = /\.(png|jpg|jpeg|gif|webp|svg)$/i.test(p);
|
||||
|
||||
if (isImage) {
|
||||
// Für Bilder: Lade als Base64
|
||||
console.log('Loading as image (Base64)');
|
||||
const apiUrl = `${url}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/raw/${encodeURIComponent(p)}?ref=${ref}`;
|
||||
console.log('Image URL:', apiUrl);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const protocol = url.startsWith('https') ? https : http;
|
||||
protocol.get(apiUrl, {
|
||||
headers: {
|
||||
'Authorization': `token ${token}`,
|
||||
'User-Agent': 'Git-Manager-GUI'
|
||||
}
|
||||
}, (res) => {
|
||||
console.log(`Image response status: ${res.statusCode}`);
|
||||
if (res.statusCode !== 200) {
|
||||
resolve({ ok: false, error: `HTTP ${res.statusCode}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
res.on('data', chunk => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
const base64 = buffer.toString('base64');
|
||||
console.log(`Image loaded: ${base64.length} bytes`);
|
||||
resolve({ ok: true, content: base64 });
|
||||
} catch (e) {
|
||||
console.error('Base64 conversion error:', e.message);
|
||||
resolve({ ok: false, error: String(e) });
|
||||
}
|
||||
});
|
||||
}).on('error', (e) => {
|
||||
console.error('Image HTTP error:', e.message);
|
||||
resolve({ ok: false, error: String(e) });
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Image load try error:', e.message);
|
||||
resolve({ ok: false, error: String(e) });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Für Text: Nutze normale Funktion
|
||||
console.log('Loading as text file');
|
||||
const content = await getGiteaFileContent({ token, url, owner, repo, path: p, ref });
|
||||
console.log(`Text file loaded: ${content.length} chars`);
|
||||
return { ok: true, content };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('read-gitea-file error:', e.message, e.stack);
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
@@ -510,6 +668,41 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Alias für Editor: write-gitea-file
|
||||
ipcMain.handle('write-gitea-file', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
||||
if (!token || !url) return { ok: false, error: 'missing-token-or-url' };
|
||||
|
||||
const owner = data.owner;
|
||||
const repo = data.repo;
|
||||
const path = data.path;
|
||||
const content = data.content || '';
|
||||
const ref = data.ref || 'main';
|
||||
|
||||
// Konvertiere Content zu Base64
|
||||
const base64 = Buffer.from(content, 'utf8').toString('base64');
|
||||
|
||||
const uploaded = await uploadGiteaFile({
|
||||
token,
|
||||
url,
|
||||
owner,
|
||||
repo,
|
||||
path,
|
||||
contentBase64: base64,
|
||||
message: `Edit ${path} via Git Manager GUI`,
|
||||
branch: ref
|
||||
});
|
||||
|
||||
return { ok: true, uploaded };
|
||||
} catch (e) {
|
||||
console.error('write-gitea-file error', e);
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
@@ -661,6 +854,30 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
prepare-download-drag (robust)
|
||||
- stellt sicher, dass alle Dateien komplett geschrieben sind
|
||||
- erkennt Base64 vs UTF-8 und schreibt als Buffer wenn nötig
|
||||
- nutzt getSafeTmpDir() (siehe oben in deiner main.js)
|
||||
----------------------------- */
|
||||
function isBase64Like(str) {
|
||||
if (typeof str !== 'string') return false;
|
||||
// Strip whitespace/newlines
|
||||
const s = str.replace(/\s+/g, '');
|
||||
if (s.length === 0) return false;
|
||||
// Base64 valid chars + padding
|
||||
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
|
||||
// length must be multiple of 4 (except maybe line breaks removed)
|
||||
if (s.length % 4 !== 0) return false;
|
||||
try {
|
||||
// Round-trip check (cheap and practical)
|
||||
const decoded = Buffer.from(s, 'base64');
|
||||
return decoded.toString('base64') === s;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle('prepare-download-drag', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
@@ -671,46 +888,105 @@ ipcMain.handle('prepare-download-drag', async (event, data) => {
|
||||
const repo = data.repo;
|
||||
const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
|
||||
// Use safe tmp dir (unique)
|
||||
// Create a unique temp directory (guarantees clean state)
|
||||
const tmpBase = getSafeTmpDir(repo || 'gitea-repo');
|
||||
|
||||
// Gather list of files (recursive)
|
||||
const allFiles = [];
|
||||
async function gather(pathInRepo) {
|
||||
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' });
|
||||
for (const item of items) {
|
||||
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'main' });
|
||||
for (const item of items || []) {
|
||||
if (item.type === 'dir') await gather(item.path);
|
||||
else if (item.type === 'file') allFiles.push(item.path);
|
||||
}
|
||||
}
|
||||
await gather(remotePath || '');
|
||||
|
||||
// If no files, return early (still provide empty dir)
|
||||
if (allFiles.length === 0) {
|
||||
// schedule cleanup
|
||||
setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS);
|
||||
return { ok: true, tempPath: tmpBase, files: [] };
|
||||
}
|
||||
|
||||
// Download files sequentially or with limited concurrency:
|
||||
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 || 'main' });
|
||||
const localPath = ppath.join(tmpBase, remoteFile);
|
||||
fs.mkdirSync(ppath.dirname(localPath), { recursive: true });
|
||||
ensureDir(ppath.dirname(localPath));
|
||||
|
||||
// Decide how to write: if Buffer already, write directly. If string, try base64 detection
|
||||
if (Buffer.isBuffer(content)) {
|
||||
fs.writeFileSync(localPath, content);
|
||||
} else if (typeof content === 'string') {
|
||||
if (isBase64Like(content)) {
|
||||
const buf = Buffer.from(content, 'base64');
|
||||
fs.writeFileSync(localPath, buf);
|
||||
} else {
|
||||
// treat as utf8 text
|
||||
fs.writeFileSync(localPath, content, 'utf8');
|
||||
}
|
||||
} else {
|
||||
// fallback: convert to string
|
||||
fs.writeFileSync(localPath, String(content), 'utf8');
|
||||
}
|
||||
return localPath;
|
||||
});
|
||||
|
||||
await runLimited(tasks, DEFAULT_CONCURRENCY);
|
||||
// runLimited ensures concurrency and waits for all writes to finish
|
||||
const results = await runLimited(tasks, data.concurrency || DEFAULT_CONCURRENCY);
|
||||
|
||||
// verify at least one successful file
|
||||
const successFiles = results.filter(r => r.ok).map(r => r.result);
|
||||
if (successFiles.length === 0) {
|
||||
// cleanup on complete failure
|
||||
try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {}
|
||||
return { ok: false, error: 'no-files-downloaded' };
|
||||
}
|
||||
|
||||
// give renderer the temp dir (renderer should then call 'ondragstart' with the folder path)
|
||||
// schedule cleanup after delay to keep files available for drag & drop
|
||||
setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS);
|
||||
return { ok: true, tempPath: tmpBase };
|
||||
|
||||
return { ok: true, tempPath: tmpBase, files: successFiles };
|
||||
} catch (e) {
|
||||
console.error('prepare-download-drag error', e);
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
});
|
||||
|
||||
/* -----------------------------
|
||||
ondragstart (no change to API but more defensive)
|
||||
- expects renderer to call window.electronAPI.ondragStart(tempPath)
|
||||
----------------------------- */
|
||||
ipcMain.on('ondragstart', async (event, filePath) => {
|
||||
try {
|
||||
if (!filePath || !fs.existsSync(filePath)) return;
|
||||
if (!filePath || !fs.existsSync(filePath)) {
|
||||
console.warn('ondragstart: path missing or not exists:', filePath);
|
||||
return;
|
||||
}
|
||||
// Prefer folder icon when dragging a directory
|
||||
let icon = nativeImage.createEmpty();
|
||||
try { icon = await app.getFileIcon(filePath); } catch (e) {}
|
||||
try { event.sender.startDrag({ file: filePath, icon: icon }); } catch (e) { console.error('startDrag failed', e); }
|
||||
} catch (e) { console.error('ondragstart error', e); }
|
||||
try {
|
||||
// ask platform for file icon; large size for clearer drag icon
|
||||
icon = await app.getFileIcon(filePath, { size: 'large' });
|
||||
} catch (e) {
|
||||
// ignore, keep empty icon
|
||||
}
|
||||
|
||||
// startDrag accepts { file } where file can be a directory
|
||||
try {
|
||||
event.sender.startDrag({ file: filePath, icon });
|
||||
} catch (e) {
|
||||
// some platforms may require a single file — if folder fails, try to drop a placeholder file
|
||||
console.error('startDrag failed for', filePath, e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('ondragstart error', e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle('delete-gitea-repo', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
@@ -1001,3 +1277,360 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
return { ok: false, error: String(e) };
|
||||
}
|
||||
});
|
||||
/* ================================
|
||||
RELEASE MANAGEMENT IPC HANDLERS
|
||||
================================ */
|
||||
|
||||
// List all releases for a repository
|
||||
ipcMain.handle('list-releases', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
||||
|
||||
const releases = await listGiteaReleases({
|
||||
token: credentials.giteaToken,
|
||||
url: credentials.giteaURL,
|
||||
owner: data.owner,
|
||||
repo: data.repo
|
||||
});
|
||||
|
||||
return { ok: true, releases };
|
||||
} catch (error) {
|
||||
console.error('list-releases error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// Get a specific release by tag
|
||||
ipcMain.handle('get-release', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
||||
|
||||
const release = await getGiteaRelease({
|
||||
token: credentials.giteaToken,
|
||||
url: credentials.giteaURL,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
tag: data.tag
|
||||
});
|
||||
|
||||
return { ok: true, release };
|
||||
} catch (error) {
|
||||
console.error('get-release error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new release
|
||||
ipcMain.handle('create-release', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
||||
|
||||
const releaseData = {
|
||||
tag_name: data.tag_name,
|
||||
name: data.name,
|
||||
body: data.body,
|
||||
draft: data.draft,
|
||||
prerelease: data.prerelease,
|
||||
target_commitish: data.target_commitish
|
||||
};
|
||||
|
||||
const release = await createGiteaRelease({
|
||||
token: credentials.giteaToken,
|
||||
url: credentials.giteaURL,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
data: releaseData
|
||||
});
|
||||
|
||||
return { ok: true, release };
|
||||
} catch (error) {
|
||||
console.error('create-release error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// Edit/update a release
|
||||
ipcMain.handle('edit-release', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
||||
|
||||
const updateData = {};
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.body !== undefined) updateData.body = data.body;
|
||||
if (data.draft !== undefined) updateData.draft = data.draft;
|
||||
if (data.prerelease !== undefined) updateData.prerelease = data.prerelease;
|
||||
|
||||
const release = await editGiteaRelease({
|
||||
token: credentials.giteaToken,
|
||||
url: credentials.giteaURL,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
releaseId: data.releaseId,
|
||||
data: updateData
|
||||
});
|
||||
|
||||
return { ok: true, release };
|
||||
} catch (error) {
|
||||
console.error('edit-release error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a release
|
||||
ipcMain.handle('delete-release', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
||||
|
||||
await deleteGiteaRelease({
|
||||
token: credentials.giteaToken,
|
||||
url: credentials.giteaURL,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
releaseId: data.releaseId
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
console.error('delete-release error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// Upload a release asset
|
||||
ipcMain.handle('upload-release-asset', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
||||
|
||||
const asset = await uploadReleaseAsset({
|
||||
token: credentials.giteaToken,
|
||||
url: credentials.giteaURL,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
releaseId: data.releaseId,
|
||||
filePath: data.filePath,
|
||||
fileName: data.fileName
|
||||
});
|
||||
|
||||
return { ok: true, asset };
|
||||
} catch (error) {
|
||||
console.error('upload-release-asset error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a release asset
|
||||
ipcMain.handle('delete-release-asset', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
||||
|
||||
await deleteReleaseAsset({
|
||||
token: credentials.giteaToken,
|
||||
url: credentials.giteaURL,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
assetId: data.assetId
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
console.error('delete-release-asset error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// Download release archive (ZIP/TAR)
|
||||
ipcMain.handle('download-release-archive', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials) return { ok: false, error: 'no-credentials' };
|
||||
|
||||
const base = credentials.giteaURL.replace(/\/$/, '');
|
||||
const archiveUrl = `${base}/${data.owner}/${data.repo}/archive/${data.tag}.zip`;
|
||||
|
||||
// Ask user where to save
|
||||
const result = await dialog.showSaveDialog({
|
||||
defaultPath: `${data.repo}-${data.tag}.zip`,
|
||||
filters: [{ name: 'ZIP Archive', extensions: ['zip'] }]
|
||||
});
|
||||
|
||||
if (result.canceled) return { ok: false, canceled: true };
|
||||
|
||||
const savePath = result.filePath;
|
||||
|
||||
// Download archive
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const writer = fs.createWriteStream(savePath);
|
||||
|
||||
const response = await axios({
|
||||
url: archiveUrl,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
headers: { Authorization: `token ${credentials.giteaToken}` }
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => resolve({ ok: true, savedTo: savePath }));
|
||||
writer.on('error', (err) => reject(err));
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('download-release-archive error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
/* ========================
|
||||
LOCAL COMMIT HANDLERS
|
||||
======================== */
|
||||
ipcMain.handle('get-local-commits', async (event, data) => {
|
||||
try {
|
||||
const { getCommitLogs } = require('./src/git/gitHandler.js');
|
||||
const commits = await getCommitLogs(data.folderPath, data.branch || 'HEAD');
|
||||
return { ok: true, commits };
|
||||
} catch (error) {
|
||||
console.error('get-local-commits error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-local-commit-details', async (event, data) => {
|
||||
try {
|
||||
const { getCommitDetails } = require('./src/git/gitHandler.js');
|
||||
const details = await getCommitDetails(data.folderPath, data.sha);
|
||||
return { ok: true, ...details };
|
||||
} catch (error) {
|
||||
console.error('get-local-commit-details error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-local-commit-diff', async (event, data) => {
|
||||
try {
|
||||
const { getCommitDiff } = require('./src/git/gitHandler.js');
|
||||
const diff = await getCommitDiff(data.folderPath, data.sha);
|
||||
return { ok: true, diff };
|
||||
} catch (error) {
|
||||
console.error('get-local-commit-diff error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-local-commit-files', async (event, data) => {
|
||||
try {
|
||||
const { getCommitDetails } = require('./src/git/gitHandler.js');
|
||||
const details = await getCommitDetails(data.folderPath, data.sha);
|
||||
return { ok: true, files: details.files || [], stats: { additions: 0, deletions: 0 } };
|
||||
} catch (error) {
|
||||
console.error('get-local-commit-files error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
/* ========================
|
||||
GITEA COMMIT HANDLERS
|
||||
======================== */
|
||||
ipcMain.handle('get-commit-diff', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
||||
|
||||
if (!token || !url) {
|
||||
return { ok: false, error: 'missing-token-or-url' };
|
||||
}
|
||||
|
||||
const diff = await getGiteaCommitDiff({
|
||||
token,
|
||||
url,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
sha: data.sha
|
||||
});
|
||||
|
||||
return { ok: true, diff };
|
||||
} catch (error) {
|
||||
console.error('get-commit-diff error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-commit-files', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
||||
|
||||
if (!token || !url) {
|
||||
return { ok: false, error: 'missing-token-or-url' };
|
||||
}
|
||||
|
||||
const result = await getGiteaCommitFiles({
|
||||
token,
|
||||
url,
|
||||
owner: data.owner,
|
||||
repo: data.repo,
|
||||
sha: data.sha
|
||||
});
|
||||
|
||||
return { ok: true, files: result.files, stats: result.stats };
|
||||
} catch (error) {
|
||||
console.error('get-commit-files error:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// main.js - Updater IPC Handlers
|
||||
|
||||
// 1. Version abfragen
|
||||
ipcMain.handle('get-app-version', async () => {
|
||||
return { ok: true, version: app.getVersion() };
|
||||
});
|
||||
|
||||
// 2. Suche nach Updates (Manuell oder Automatisch)
|
||||
ipcMain.handle('check-for-updates', async (event) => {
|
||||
console.log("[Main] Update-Check angefordert...");
|
||||
try {
|
||||
if (!updater) {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (win) updater = new Updater(win);
|
||||
}
|
||||
if (updater) await updater.checkForUpdates(false);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
console.error('[Main] Fehler beim Update-Check:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Download starten (wird vom "Jetzt installieren" Button gerufen)
|
||||
ipcMain.handle('start-update-download', async (event, asset) => {
|
||||
console.log("[Main] Download-Signal erhalten für:", asset ? asset.name : "Unbekannt");
|
||||
try {
|
||||
if (!updater) {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
updater = new Updater(win);
|
||||
}
|
||||
if (asset && asset.browser_download_url) {
|
||||
await updater.startDownload(asset);
|
||||
return { ok: true };
|
||||
}
|
||||
return { ok: false, error: 'Ungültiges Asset' };
|
||||
} catch (error) {
|
||||
console.error('[Main] Download-Fehler:', error);
|
||||
return { ok: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "git-manager-gui",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "git-manager-gui",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.5.0",
|
||||
"axios": "^1.13.4",
|
||||
"form-data": "^4.0.5",
|
||||
"simple-git": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1286,9 +1287,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
|
||||
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "git-manager-gui",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.0",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.5.0",
|
||||
"axios": "^1.13.4",
|
||||
"form-data": "^4.0.5",
|
||||
"simple-git": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
100
preload.js
100
preload.js
@@ -2,43 +2,99 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Lokale Datei-Operationen
|
||||
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
||||
selectFile: () => ipcRenderer.invoke('select-file'),
|
||||
getFileTree: (data) => ipcRenderer.invoke('getFileTree', data),
|
||||
readFile: (data) => ipcRenderer.invoke('readFile', data),
|
||||
writeFile: (data) => ipcRenderer.invoke('writeFile', data),
|
||||
deleteFile: (data) => ipcRenderer.invoke('deleteFile', data),
|
||||
|
||||
// Gitea Datei-Operationen
|
||||
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
|
||||
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
||||
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
||||
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
|
||||
writeGiteaFile: (data) => ipcRenderer.invoke('write-gitea-file', data),
|
||||
uploadGiteaFile: (data) => ipcRenderer.invoke('upload-gitea-file', data),
|
||||
downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data),
|
||||
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
||||
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
||||
|
||||
// Repository & Git Management
|
||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
|
||||
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
|
||||
pushProject: (data) => ipcRenderer.invoke('push-project', data),
|
||||
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
|
||||
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
|
||||
getFileTree: (data) => ipcRenderer.invoke('getFileTree', data),
|
||||
readFile: (data) => ipcRenderer.invoke('readFile', data),
|
||||
deleteFile: (data) => ipcRenderer.invoke('deleteFile', data),
|
||||
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
|
||||
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
||||
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
||||
uploadGiteaFile: (data) => ipcRenderer.invoke('upload-gitea-file', data),
|
||||
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
||||
downloadGiteaFolder: (data) => ipcRenderer.invoke('download-gitea-folder', data),
|
||||
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
||||
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
|
||||
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
|
||||
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
|
||||
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
|
||||
|
||||
// progress subscriptions
|
||||
// Drag & Drop
|
||||
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
|
||||
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
|
||||
|
||||
// Release Management
|
||||
listReleases: (data) => ipcRenderer.invoke('list-releases', data),
|
||||
getRelease: (data) => ipcRenderer.invoke('get-release', data),
|
||||
createRelease: (data) => ipcRenderer.invoke('create-release', data),
|
||||
editRelease: (data) => ipcRenderer.invoke('edit-release', data),
|
||||
deleteRelease: (data) => ipcRenderer.invoke('delete-release', data),
|
||||
uploadReleaseAsset: (data) => ipcRenderer.invoke('upload-release-asset', data),
|
||||
deleteReleaseAsset: (data) => ipcRenderer.invoke('delete-release-asset', data),
|
||||
downloadReleaseArchive: (data) => ipcRenderer.invoke('download-release-archive', data),
|
||||
|
||||
// Commit History & Visualization
|
||||
getCommits: (data) => ipcRenderer.invoke('get-commits', data),
|
||||
getCommitDetails: (data) => ipcRenderer.invoke('get-commit-details', data),
|
||||
getCommitDiff: (data) => ipcRenderer.invoke('get-commit-diff', data),
|
||||
getCommitFiles: (data) => ipcRenderer.invoke('get-commit-files', data),
|
||||
searchCommits: (data) => ipcRenderer.invoke('search-commits', data),
|
||||
getRepoBranches: (data) => ipcRenderer.invoke('get-repo-branches', data),
|
||||
|
||||
// Local Repository Commits
|
||||
getLocalCommits: (data) => ipcRenderer.invoke('get-local-commits', data),
|
||||
getLocalCommitDiff: (data) => ipcRenderer.invoke('get-local-commit-diff', data),
|
||||
getLocalCommitDetails: (data) => ipcRenderer.invoke('get-local-commit-details', data),
|
||||
searchLocalCommits: (data) => ipcRenderer.invoke('search-local-commits', data),
|
||||
|
||||
// === UPDATER APIs ===
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
|
||||
// Triggert den tatsächlichen Download des Assets
|
||||
// WICHTIG: Muss in renderer.js als window.electronAPI.startUpdateDownload(asset) aufgerufen werden
|
||||
startUpdateDownload: (asset) => ipcRenderer.invoke('start-update-download', asset),
|
||||
|
||||
// === Progress & Update Subscriptions ===
|
||||
onUpdateAvailable: (cb) => {
|
||||
const listener = (event, info) => cb(info);
|
||||
ipcRenderer.on('update-available', listener);
|
||||
return () => ipcRenderer.removeListener('update-available', listener);
|
||||
},
|
||||
|
||||
onUpdateProgress: (cb) => {
|
||||
const listener = (event, percent) => cb(percent);
|
||||
ipcRenderer.on('update-progress', listener);
|
||||
return () => ipcRenderer.removeListener('update-progress', listener);
|
||||
},
|
||||
|
||||
onPushProgress: (cb) => {
|
||||
ipcRenderer.on('push-progress', (event, percent) => {
|
||||
try { cb(percent); } catch (_) {}
|
||||
});
|
||||
const listener = (event, percent) => { try { cb(percent); } catch (_) {} };
|
||||
ipcRenderer.on('push-progress', listener);
|
||||
return () => ipcRenderer.removeListener('push-progress', listener);
|
||||
},
|
||||
|
||||
onFolderUploadProgress: (cb) => {
|
||||
ipcRenderer.on('folder-upload-progress', (event, payload) => {
|
||||
try { cb(payload); } catch (_) {}
|
||||
});
|
||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||
ipcRenderer.on('folder-upload-progress', listener);
|
||||
return () => ipcRenderer.removeListener('folder-upload-progress', listener);
|
||||
},
|
||||
|
||||
onFolderDownloadProgress: (cb) => {
|
||||
ipcRenderer.on('folder-download-progress', (event, payload) => {
|
||||
try { cb(payload); } catch (_) {}
|
||||
});
|
||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||
ipcRenderer.on('folder-download-progress', listener);
|
||||
return () => ipcRenderer.removeListener('folder-download-progress', listener);
|
||||
}
|
||||
});
|
||||
@@ -1,89 +1,208 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Git Manager Explorer</title>
|
||||
<title>Git Manager Explorer Pro</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="toolbar">
|
||||
<button id="btnSettings">Settings</button>
|
||||
<button id="btnSelectFolder">Select Folder</button>
|
||||
<button id="btnPush">Push / Update</button>
|
||||
<select id="platform"><option value="github">GitHub</option><option value="gitea">Gitea</option></select>
|
||||
<span id="status" class="status"></span>
|
||||
<div class="tool-group">
|
||||
<button id="btnSettings" title="Einstellungen">⚙️ Settings</button>
|
||||
<button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
|
||||
<button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
|
||||
<button id="btnLoadGiteaRepos" class="accent-btn" title="Gitea Repositories laden">🌐 Load Gitea</button>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<aside id="explorerPanel" class="card">
|
||||
<h3>Project Files</h3>
|
||||
<div id="fileTree" class="file-tree"></div>
|
||||
</aside>
|
||||
<div class="tool-group">
|
||||
<select id="platform" title="Plattform auswählen">
|
||||
<option value="gitea" selected>Gitea</option>
|
||||
<option value="github">GitHub</option>
|
||||
</select>
|
||||
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
||||
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
||||
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
|
||||
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
|
||||
</div>
|
||||
|
||||
<section id="detailsPanel">
|
||||
<div id="repoDetails" class="card">
|
||||
<div><label>Branch:</label><select id="branchSelect"></select></div>
|
||||
<div><label>Repository Name:</label><input id="repoName" type="text" placeholder="repo-name"></div>
|
||||
<span id="status" class="status">Bereit</span>
|
||||
</div>
|
||||
|
||||
<!-- NEU: License Auswahl -->
|
||||
<div style="margin-bottom: 5px;">
|
||||
<label for="licenseSelect">License:</label>
|
||||
<main id="main">
|
||||
<div id="explorerGrid" class="explorer-grid">
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="settingsModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<label>GitHub Token</label>
|
||||
<input id="githubToken" type="password" placeholder="ghp_...">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Gitea Token</label>
|
||||
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Gitea URL</label>
|
||||
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
|
||||
</div>
|
||||
|
||||
<div class="input-group" style="margin-top: 30px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<label>App Version</label>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<input id="appVersion" type="text" readonly style="flex: 1; background: rgba(255,255,255,0.05); cursor: not-allowed;">
|
||||
<button id="btnCheckUpdates" style="
|
||||
background: linear-gradient(135deg, #00d4ff, #8b5cf6);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
">🔄 Nach Updates suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnSaveSettings">Speichern</button>
|
||||
<button id="btnCloseSettings" class="secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="repoActionModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>🚀 Neues Repository erstellen</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Repository Name</label>
|
||||
<input id="repoName" type="text" placeholder="mein-projekt">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Target Branch</label>
|
||||
<input id="targetBranch" type="text" value="main" placeholder="main">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Lizenz</label>
|
||||
<select id="licenseSelect">
|
||||
<option value="">No License</option>
|
||||
<option value="MIT">MIT License</option>
|
||||
<option value="Apache-2.0">Apache License 2.0</option>
|
||||
<option value="GPL-3.0">GNU General Public License v3.0</option>
|
||||
<option value="BSD-3-Clause">BSD 3-Clause "New" or "Revised" License</option>
|
||||
<option value="BSD-2-Clause">BSD 2-Clause "Simplified" License</option>
|
||||
<option value="LGPL-3.0">GNU Lesser General Public License v3.0</option>
|
||||
<option value="MPL-2.0">Mozilla Public License 2.0</option>
|
||||
<option value="Unlicense">The Unlicense</option>
|
||||
<option value="">Keine Lizenz</option>
|
||||
<option value="mit">MIT License</option>
|
||||
<option value="apache-2.0">Apache 2.0</option>
|
||||
<option value="gpl-3.0">GPL v3</option>
|
||||
<option value="bsd-3-clause">BSD 3-Clause</option>
|
||||
<option value="agpl-3.0">AGPL v3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- NEU: README Checkbox -->
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label>
|
||||
<div class="input-group">
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||
<input type="checkbox" id="createReadme" checked>
|
||||
Initialize Repository with a README
|
||||
<span style="text-transform: none; letter-spacing: normal;">README.md initialisieren</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div><button id="btnCreateRepo">Create Repo</button></div>
|
||||
<div style="margin-top:10px;"><button id="btnLoadGiteaRepos">Load My Gitea Repos</button>
|
||||
<div id="giteaRepoContainer"></div>
|
||||
<div class="input-group">
|
||||
<label>Lokaler Push-Branch</label>
|
||||
<select id="branchSelect">
|
||||
<option value="main">main</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnCreateRepo" class="accent-btn">Erstellen</button>
|
||||
<button id="btnCloseRepoActions" class="secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="previewArea" class="card">
|
||||
<h3 id="previewTitle">Preview</h3>
|
||||
<pre id="previewContent" class="preview-content">Select a file to preview</pre>
|
||||
<div id="fileEditorModal" class="modal hidden">
|
||||
<div class="file-editor-card">
|
||||
<div class="file-editor-header">
|
||||
<div class="file-editor-title">
|
||||
<span id="fileEditorIcon">📄</span>
|
||||
<span id="fileEditorName">file.txt</span>
|
||||
</div>
|
||||
<div class="file-editor-toolbar">
|
||||
<button id="btnEditorSearch" class="editor-tool-btn" title="Suchen (Ctrl+F)">🔍</button>
|
||||
<button id="btnEditorSave" class="editor-tool-btn" title="Speichern">💾</button>
|
||||
<button id="btnCloseEditor" class="editor-tool-btn" title="Schließen">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="commitLogs" class="card">
|
||||
<h3>Commit Logs</h3>
|
||||
<div id="logs">No commits yet.</div>
|
||||
</div>
|
||||
</section>
|
||||
<div id="fileEditorTabs" class="file-editor-tabs"></div>
|
||||
|
||||
<div id="searchBar" class="search-bar hidden">
|
||||
<input id="searchInput" type="text" class="search-input" placeholder="Suchen...">
|
||||
<input id="replaceInput" type="text" class="search-input" placeholder="Ersetzen...">
|
||||
<button id="btnReplace" class="search-btn">Ersetzen</button>
|
||||
<button id="btnReplaceAll" class="search-btn">Alle</button>
|
||||
<button id="btnCloseSearch" class="search-btn">✕</button>
|
||||
<span id="searchInfo" class="search-info">0/0</span>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settingsModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>Settings</h2>
|
||||
<label>GitHub Token</label><input id="githubToken" type="password">
|
||||
<label>Gitea Token</label><input id="giteaToken" type="password">
|
||||
<label>Gitea URL</label><input id="giteaURL" type="text" placeholder="https://gitea.example.com">
|
||||
<div style="margin-top:10px;">
|
||||
<button id="btnSaveSettings">Save</button>
|
||||
<button id="btnCloseSettings">Close</button>
|
||||
<div class="file-editor-container">
|
||||
<div id="lineNumbers" class="line-numbers"></div>
|
||||
<textarea id="fileEditorContent" class="file-editor-textarea" placeholder="Dateiinhalt..."></textarea>
|
||||
<div id="imagePreview" class="image-preview hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="file-editor-footer">
|
||||
<div class="file-editor-info">
|
||||
<span id="fileEditorPath" class="file-path">Pfad: /path/to/file</span>
|
||||
<span id="fileEditorStats" class="file-stats"></span>
|
||||
<span id="fileEditorCursor" class="file-cursor">Zeile 1, Spalte 1</span>
|
||||
<span id="autoSaveStatus" class="auto-save-status" style="display:none;">✓ Gespeichert</span>
|
||||
</div>
|
||||
<div class="modal-buttons">
|
||||
<button id="btnDiscardEdit" class="secondary">Verwerfen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
<div id="updateModal" class="modal hidden">
|
||||
<div class="modalContent card" style="max-width: 450px; border: 1px solid var(--accent-primary); box-shadow: 0 0 30px rgba(0, 212, 255, 0.2);">
|
||||
<div style="display: flex; align-items: center; gap: 20px; margin-bottom: 20px;">
|
||||
<div style="font-size: 3rem; filter: drop-shadow(0 0 10px var(--accent-primary));">🚀</div>
|
||||
<div>
|
||||
<h2 style="margin: 0; background: var(--accent-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Update verfügbar!</h2>
|
||||
<p id="updateVersionInfo" style="color: var(--text-secondary); margin: 5px 0 0 0; font-family: monospace;"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label style="color: var(--accent-primary); font-size: 0.8rem; letter-spacing: 1px;">RELEASE NOTES</label>
|
||||
<div id="updateChangelog" style="
|
||||
background: rgba(0,0,0,0.25);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
white-space: pre-wrap;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons" style="margin-top: 25px; gap: 12px;">
|
||||
<button id="btnStartUpdate" class="accent-btn" style="flex: 2; height: 45px; font-weight: bold;">🚀 Jetzt installieren</button>
|
||||
<button id="btnIgnoreUpdate" class="secondary" style="flex: 1; height: 45px;">Später</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <script src="renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
3591
renderer/renderer.js
3591
renderer/renderer.js
File diff suppressed because it is too large
Load Diff
1428
renderer/style.css
1428
renderer/style.css
File diff suppressed because it is too large
Load Diff
@@ -305,11 +305,366 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
COMMIT HISTORY FUNCTIONS (GITEA)
|
||||
================================ */
|
||||
|
||||
/**
|
||||
* Get commit history from Gitea repository
|
||||
*/
|
||||
async function getGiteaCommits({ token, url, owner, repo, branch = 'main', page = 1, limit = 50 }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: { Authorization: `token ${token}` },
|
||||
params: {
|
||||
sha: branch,
|
||||
page,
|
||||
limit
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('getGiteaCommits error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific commit with diff
|
||||
*/
|
||||
async function getGiteaCommit({ token, url, owner, repo, sha }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: { Authorization: `token ${token}` }
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('getGiteaCommit error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit diff/patch
|
||||
*/
|
||||
async function getGiteaCommitDiff({ token, url, owner, repo, sha }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
// Gitea returns diff in the commit endpoint with .diff extension
|
||||
const endpoint = `${base}/${owner}/${repo}/commit/${sha}.diff`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: { Authorization: `token ${token}` }
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('getGiteaCommitDiff error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit file changes/stats
|
||||
*/
|
||||
async function getGiteaCommitFiles({ token, url, owner, repo, sha }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: { Authorization: `token ${token}` }
|
||||
});
|
||||
|
||||
// Gitea commit response includes stats
|
||||
const commit = response.data;
|
||||
|
||||
// Normalize files to match local git format
|
||||
const files = (commit.files || []).map(f => ({
|
||||
file: f.filename || f.file || '',
|
||||
changes: (f.additions || 0) + (f.deletions || 0),
|
||||
insertions: f.additions || 0,
|
||||
deletions: f.deletions || 0,
|
||||
binary: f.binary || false
|
||||
}));
|
||||
|
||||
return {
|
||||
files,
|
||||
stats: commit.stats || { additions: 0, deletions: 0, total: 0 }
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('getGiteaCommitFiles error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search commits in Gitea repository
|
||||
*/
|
||||
async function searchGiteaCommits({ token, url, owner, repo, query, branch = 'main' }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
// Gitea doesn't have direct commit search, so we get commits and filter
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: { Authorization: `token ${token}` },
|
||||
params: {
|
||||
sha: branch,
|
||||
limit: 100 // Get more for searching
|
||||
}
|
||||
});
|
||||
|
||||
// Filter commits by query
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const filtered = response.data.filter(commit => {
|
||||
const message = (commit.commit?.message || '').toLowerCase();
|
||||
const author = (commit.commit?.author?.name || '').toLowerCase();
|
||||
return message.includes(lowerQuery) || author.includes(lowerQuery);
|
||||
});
|
||||
|
||||
return filtered;
|
||||
} catch (err) {
|
||||
console.error('searchGiteaCommits error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get branches for branch graph visualization
|
||||
*/
|
||||
async function getGiteaBranches({ token, url, owner, repo }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: { Authorization: `token ${token}` }
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('getGiteaBranches error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
RELEASE MANAGEMENT FUNCTIONS
|
||||
================================ */
|
||||
|
||||
/**
|
||||
* List all releases for a repository
|
||||
*/
|
||||
async function listGiteaReleases({ token, url, owner, repo }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: { Authorization: `token ${token}` }
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('listGiteaReleases error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific release by tag
|
||||
*/
|
||||
async function getGiteaRelease({ token, url, owner, repo, tag }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint, {
|
||||
headers: { Authorization: `token ${token}` }
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('getGiteaRelease error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new release
|
||||
*/
|
||||
async function createGiteaRelease({ token, url, owner, repo, data }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
|
||||
|
||||
const body = {
|
||||
tag_name: data.tag_name,
|
||||
name: data.name || data.tag_name,
|
||||
body: data.body || '',
|
||||
draft: data.draft || false,
|
||||
prerelease: data.prerelease || false,
|
||||
target_commitish: data.target_commitish || 'main'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpoint, body, {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('createGiteaRelease error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit/update an existing release
|
||||
*/
|
||||
async function editGiteaRelease({ token, url, owner, repo, releaseId, data }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`;
|
||||
|
||||
const body = {};
|
||||
if (data.name !== undefined) body.name = data.name;
|
||||
if (data.body !== undefined) body.body = data.body;
|
||||
if (data.draft !== undefined) body.draft = data.draft;
|
||||
if (data.prerelease !== undefined) body.prerelease = data.prerelease;
|
||||
if (data.tag_name !== undefined) body.tag_name = data.tag_name;
|
||||
|
||||
try {
|
||||
const response = await axios.patch(endpoint, body, {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('editGiteaRelease error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a release
|
||||
*/
|
||||
async function deleteGiteaRelease({ token, url, owner, repo, releaseId }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`;
|
||||
|
||||
try {
|
||||
await axios.delete(endpoint, {
|
||||
headers: { Authorization: `token ${token}` }
|
||||
});
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
console.error('deleteGiteaRelease error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a release asset (attachment)
|
||||
* Note: Gitea uses multipart/form-data for asset uploads
|
||||
*/
|
||||
async function uploadReleaseAsset({ token, url, owner, repo, releaseId, filePath, fileName }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const FormData = require('form-data');
|
||||
const fs = require('fs');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}/assets`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', fs.createReadStream(filePath), {
|
||||
filename: fileName || require('path').basename(filePath)
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpoint, formData, {
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
...formData.getHeaders()
|
||||
},
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity
|
||||
});
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.error('uploadReleaseAsset error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a release asset
|
||||
*/
|
||||
async function deleteReleaseAsset({ token, url, owner, repo, assetId }) {
|
||||
const base = normalizeBase(url);
|
||||
if (!base) throw new Error('Invalid Gitea base URL');
|
||||
|
||||
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/assets/${assetId}`;
|
||||
|
||||
try {
|
||||
await axios.delete(endpoint, {
|
||||
headers: { Authorization: `token ${token}` }
|
||||
});
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
console.error('deleteReleaseAsset error:', err.response?.data || err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRepoGitHub,
|
||||
createRepoGitea,
|
||||
listGiteaRepos,
|
||||
getGiteaRepoContents,
|
||||
getGiteaFileContent,
|
||||
uploadGiteaFile
|
||||
uploadGiteaFile,
|
||||
// Commit History
|
||||
getGiteaCommits,
|
||||
getGiteaCommit,
|
||||
getGiteaCommitDiff,
|
||||
getGiteaCommitFiles,
|
||||
searchGiteaCommits,
|
||||
getGiteaBranches,
|
||||
// Release Management
|
||||
listGiteaReleases,
|
||||
getGiteaRelease,
|
||||
createGiteaRelease,
|
||||
editGiteaRelease,
|
||||
deleteGiteaRelease,
|
||||
uploadReleaseAsset,
|
||||
deleteReleaseAsset
|
||||
};
|
||||
@@ -54,4 +54,282 @@ async function getCommitLogs(folderPath, count = 50) {
|
||||
return log.all.map(c => `${c.hash.substring(0,7)} - ${c.message}`);
|
||||
}
|
||||
|
||||
module.exports = { initRepo, commitAndPush, getBranches, getCommitLogs };
|
||||
/* ================================
|
||||
EXTENDED COMMIT HISTORY FUNCTIONS
|
||||
================================ */
|
||||
|
||||
/**
|
||||
* Get detailed commit history with full information
|
||||
*/
|
||||
async function getDetailedCommitHistory(folderPath, options = {}) {
|
||||
const git = gitFor(folderPath);
|
||||
|
||||
const logOptions = {
|
||||
maxCount: options.limit || 100,
|
||||
file: options.file || undefined
|
||||
};
|
||||
|
||||
if (options.author) {
|
||||
logOptions['--author'] = options.author;
|
||||
}
|
||||
|
||||
if (options.since) {
|
||||
logOptions['--since'] = options.since;
|
||||
}
|
||||
|
||||
if (options.until) {
|
||||
logOptions['--until'] = options.until;
|
||||
}
|
||||
|
||||
if (options.grep) {
|
||||
logOptions['--grep'] = options.grep;
|
||||
}
|
||||
|
||||
const log = await git.log(logOptions);
|
||||
|
||||
return log.all.map(commit => ({
|
||||
hash: commit.hash,
|
||||
shortHash: commit.hash.substring(0, 7),
|
||||
author: commit.author_name,
|
||||
authorEmail: commit.author_email,
|
||||
date: commit.date,
|
||||
message: commit.message,
|
||||
body: commit.body,
|
||||
refs: commit.refs || '',
|
||||
parentHashes: commit.parent || []
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit diff for a specific commit
|
||||
*/
|
||||
async function getCommitDiff(folderPath, commitHash) {
|
||||
const git = gitFor(folderPath);
|
||||
|
||||
try {
|
||||
// Get diff from parent to this commit
|
||||
const diff = await git.diff([`${commitHash}~1`, commitHash]);
|
||||
return diff;
|
||||
} catch (error) {
|
||||
// If no parent (first commit), show diff from empty tree
|
||||
try {
|
||||
const diff = await git.diff(['--root', commitHash]);
|
||||
return diff;
|
||||
} catch (err) {
|
||||
console.error('Error getting diff:', err);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file changes for a specific commit
|
||||
*/
|
||||
async function getCommitFileChanges(folderPath, commitHash) {
|
||||
const git = gitFor(folderPath);
|
||||
|
||||
try {
|
||||
const diff = await git.diffSummary([`${commitHash}~1`, commitHash]);
|
||||
|
||||
return {
|
||||
files: diff.files.map(file => ({
|
||||
file: file.file,
|
||||
changes: file.changes,
|
||||
insertions: file.insertions,
|
||||
deletions: file.deletions,
|
||||
binary: file.binary || false
|
||||
})),
|
||||
insertions: diff.insertions,
|
||||
deletions: diff.deletions,
|
||||
changed: diff.changed
|
||||
};
|
||||
} catch (error) {
|
||||
// First commit case
|
||||
try {
|
||||
const diff = await git.diffSummary(['--root', commitHash]);
|
||||
return {
|
||||
files: diff.files.map(file => ({
|
||||
file: file.file,
|
||||
changes: file.changes,
|
||||
insertions: file.insertions,
|
||||
deletions: file.deletions,
|
||||
binary: file.binary || false
|
||||
})),
|
||||
insertions: diff.insertions,
|
||||
deletions: diff.deletions,
|
||||
changed: diff.changed
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error getting file changes:', err);
|
||||
return { files: [], insertions: 0, deletions: 0, changed: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed commit info including stats
|
||||
*/
|
||||
async function getCommitDetails(folderPath, commitHash) {
|
||||
const git = gitFor(folderPath);
|
||||
|
||||
try {
|
||||
const log = await git.log({ from: commitHash, to: commitHash, maxCount: 1 });
|
||||
|
||||
if (log.all.length === 0) {
|
||||
throw new Error('Commit not found');
|
||||
}
|
||||
|
||||
const commit = log.all[0];
|
||||
const fileChanges = await getCommitFileChanges(folderPath, commitHash);
|
||||
const diff = await getCommitDiff(folderPath, commitHash);
|
||||
|
||||
return {
|
||||
hash: commit.hash,
|
||||
shortHash: commit.hash.substring(0, 7),
|
||||
author: commit.author_name,
|
||||
authorEmail: commit.author_email,
|
||||
date: commit.date,
|
||||
message: commit.message,
|
||||
body: commit.body,
|
||||
refs: commit.refs || '',
|
||||
parentHashes: commit.parent || [],
|
||||
fileChanges,
|
||||
diff
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting commit details:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get branch graph data for visualization
|
||||
*/
|
||||
async function getBranchGraph(folderPath, limit = 50) {
|
||||
const git = gitFor(folderPath);
|
||||
|
||||
try {
|
||||
// Get all branches
|
||||
const branches = await git.branchLocal();
|
||||
const allBranches = branches.all;
|
||||
|
||||
// Get graph structure
|
||||
const log = await git.log({
|
||||
maxCount: limit,
|
||||
'--all': null,
|
||||
'--graph': null,
|
||||
'--decorate': null,
|
||||
'--oneline': null
|
||||
});
|
||||
|
||||
return {
|
||||
branches: allBranches,
|
||||
current: branches.current,
|
||||
commits: log.all
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting branch graph:', error);
|
||||
return { branches: [], current: '', commits: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search commits by message, author, or content
|
||||
*/
|
||||
async function searchCommits(folderPath, query, options = {}) {
|
||||
const git = gitFor(folderPath);
|
||||
|
||||
const logOptions = {
|
||||
maxCount: options.limit || 100,
|
||||
'--all': null
|
||||
};
|
||||
|
||||
// Search in commit message
|
||||
if (options.searchMessage !== false) {
|
||||
logOptions['--grep'] = query;
|
||||
}
|
||||
|
||||
// Search by author
|
||||
if (options.author) {
|
||||
logOptions['--author'] = options.author;
|
||||
}
|
||||
|
||||
// Case insensitive
|
||||
if (options.caseInsensitive !== false) {
|
||||
logOptions['--regexp-ignore-case'] = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const log = await git.log(logOptions);
|
||||
|
||||
return log.all.map(commit => ({
|
||||
hash: commit.hash,
|
||||
shortHash: commit.hash.substring(0, 7),
|
||||
author: commit.author_name,
|
||||
authorEmail: commit.author_email,
|
||||
date: commit.date,
|
||||
message: commit.message,
|
||||
body: commit.body,
|
||||
refs: commit.refs || ''
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error searching commits:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit statistics
|
||||
*/
|
||||
async function getCommitStats(folderPath, since = '1 month ago') {
|
||||
const git = gitFor(folderPath);
|
||||
|
||||
try {
|
||||
const log = await git.log({
|
||||
'--since': since,
|
||||
'--all': null
|
||||
});
|
||||
|
||||
const authorStats = {};
|
||||
const dailyStats = {};
|
||||
|
||||
log.all.forEach(commit => {
|
||||
// Author stats
|
||||
const author = commit.author_name;
|
||||
if (!authorStats[author]) {
|
||||
authorStats[author] = { commits: 0, email: commit.author_email };
|
||||
}
|
||||
authorStats[author].commits++;
|
||||
|
||||
// Daily stats
|
||||
const date = new Date(commit.date).toISOString().split('T')[0];
|
||||
if (!dailyStats[date]) {
|
||||
dailyStats[date] = 0;
|
||||
}
|
||||
dailyStats[date]++;
|
||||
});
|
||||
|
||||
return {
|
||||
totalCommits: log.all.length,
|
||||
authors: authorStats,
|
||||
daily: dailyStats
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting commit stats:', error);
|
||||
return { totalCommits: 0, authors: {}, daily: {} };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initRepo,
|
||||
commitAndPush,
|
||||
getBranches,
|
||||
getCommitLogs,
|
||||
getDetailedCommitHistory,
|
||||
getCommitDiff,
|
||||
getCommitFileChanges,
|
||||
getCommitDetails,
|
||||
getBranchGraph,
|
||||
searchCommits,
|
||||
getCommitStats
|
||||
};
|
||||
164
updater.js
Normal file
164
updater.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// updater.js - Moderner Auto-Updater für Git Manager GUI
|
||||
const { app, shell } = require('electron');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const GITEA_API_URL = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Git-Manager-Gui/releases';
|
||||
|
||||
class Updater {
|
||||
constructor(mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.checkingForUpdates = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hauptfunktion zur Prüfung auf Updates
|
||||
*/
|
||||
async checkForUpdates(silent = false) {
|
||||
if (this.checkingForUpdates) return;
|
||||
this.checkingForUpdates = true;
|
||||
|
||||
try {
|
||||
const latestRelease = await this.getLatestRelease();
|
||||
if (!latestRelease) return;
|
||||
|
||||
// Versionen säubern (nur Zahlen und Punkte)
|
||||
const serverVer = latestRelease.tag_name.replace(/[^\d.]/g, '');
|
||||
const localVer = app.getVersion().replace(/[^\d.]/g, '');
|
||||
|
||||
console.log(`[Updater] Version-Check: Server(${serverVer}) vs Lokal(${localVer})`);
|
||||
|
||||
if (this.compareVersions(serverVer, localVer) > 0) {
|
||||
console.log("[Updater] Update verfügbar. Sende Daten an Renderer...");
|
||||
this.mainWindow.webContents.send('update-available', {
|
||||
version: serverVer,
|
||||
body: latestRelease.body,
|
||||
url: latestRelease.html_url,
|
||||
asset: this.findAsset(latestRelease.assets)
|
||||
});
|
||||
} else {
|
||||
console.log("[Updater] Anwendung ist auf dem neuesten Stand.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Updater] Fehler beim Update-Check:', error);
|
||||
} finally {
|
||||
this.checkingForUpdates = false;
|
||||
}
|
||||
}
|
||||
|
||||
async getLatestRelease() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = { headers: { 'User-Agent': 'GitManager-GUI-Updater' } };
|
||||
https.get(GITEA_API_URL, options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const releases = JSON.parse(data);
|
||||
resolve(Array.isArray(releases) ? releases[0] : null);
|
||||
} catch (e) { reject(e); }
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
compareVersions(v1, v2) {
|
||||
const a = v1.split('.').map(Number);
|
||||
const b = v2.split('.').map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((a[i] || 0) > (b[i] || 0)) return 1;
|
||||
if ((a[i] || 0) < (b[i] || 0)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
findAsset(assets) {
|
||||
if (!assets) return null;
|
||||
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
||||
return assets.find(a => a.name.toLowerCase().endsWith(ext));
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet den Download und führt die Installation aus
|
||||
*/
|
||||
async startDownload(asset) {
|
||||
if (!asset || !asset.browser_download_url) {
|
||||
console.error("[Updater] Kein gültiges Asset gefunden!");
|
||||
return;
|
||||
}
|
||||
|
||||
const tempPath = path.join(app.getPath('temp'), asset.name);
|
||||
console.log(`[Updater] Download gestartet: ${asset.name} -> ${tempPath}`);
|
||||
|
||||
const file = fs.createWriteStream(tempPath);
|
||||
|
||||
const download = (url) => {
|
||||
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
|
||||
// Handle Redirects
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return download(res.headers.location);
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
console.error(`[Updater] Download-Fehler: Status ${res.statusCode}`);
|
||||
return;
|
||||
}
|
||||
|
||||
res.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
console.log("[Updater] Download abgeschlossen. Initialisiere entkoppelten Installer...");
|
||||
this.installAndQuit(tempPath);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
fs.unlink(tempPath, () => {});
|
||||
console.error("[Updater] Netzwerkfehler beim Download:", err);
|
||||
});
|
||||
};
|
||||
|
||||
download(asset.browser_download_url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt den Installer entkoppelt aus und schließt die App mit Verzögerung
|
||||
*/
|
||||
installAndQuit(filePath) {
|
||||
console.log(`[Updater] Bereite Installation vor: ${filePath}`);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Wir nutzen spawn mit detached: true, damit der Installer weiterläuft,
|
||||
// wenn der Hauptprozess (Electron) beendet wird.
|
||||
try {
|
||||
const child = spawn('cmd.exe', ['/c', 'start', '""', filePath], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
});
|
||||
|
||||
child.unref(); // Trennt die Referenz zum Installer
|
||||
|
||||
console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek...");
|
||||
|
||||
// WICHTIG: Wir geben Windows Zeit, den Installer stabil zu laden,
|
||||
// bevor wir die App schließen und die Dateisperren aufheben.
|
||||
setTimeout(() => {
|
||||
app.quit();
|
||||
}, 2000);
|
||||
|
||||
} catch (err) {
|
||||
console.error("[Updater] Kritischer Fehler beim Installer-Start:", err);
|
||||
// Notfall-Versuch über shell
|
||||
shell.openPath(filePath).then(() => app.quit());
|
||||
}
|
||||
} else {
|
||||
// Linux/AppImage
|
||||
shell.openPath(filePath).then(() => {
|
||||
setTimeout(() => app.quit(), 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Updater;
|
||||
Reference in New Issue
Block a user