From f7bb79c391099cee13a1123c002a517a3dbd4e92 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sun, 28 Dec 2025 18:06:55 +0100 Subject: [PATCH] Update from Git Manager GUI --- src/git/apiHandler.js | 315 ++++++++++++++++++++++++++++++++++++++++++ src/git/gitHandler.js | 57 ++++++++ 2 files changed, 372 insertions(+) create mode 100644 src/git/apiHandler.js create mode 100644 src/git/gitHandler.js diff --git a/src/git/apiHandler.js b/src/git/apiHandler.js new file mode 100644 index 0000000..0cfdcde --- /dev/null +++ b/src/git/apiHandler.js @@ -0,0 +1,315 @@ +// src/git/apiHandler.js (CommonJS) +// enthält: createRepoGitHub, createRepoGitea, listGiteaRepos, +// getGiteaRepoContents, getGiteaFileContent, uploadGiteaFile + +const axios = require('axios'); + +function normalizeBase(url) { + if (!url) return null; + return url.replace(/\/$/, ''); +} + +function buildContentsUrl(base, owner, repo, p) { + if (!p) return `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents`; + const parts = p.split('/').map(seg => encodeURIComponent(seg)).join('/'); + return `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${parts}`; +} + +async function tryRequest(url, token, opts = {}) { + try { + const res = await axios.get(url, { + headers: token ? { Authorization: `token ${token}` } : {}, + timeout: opts.timeout || 10000 + }); + return { ok: true, data: res.data, status: res.status, url }; + } catch (err) { + return { ok: false, error: err, status: err.response ? err.response.status : null, url }; + } +} + +async function createRepoGitHub({ name, token, auto_init = true, license = '', private: isPrivate = false }) { + const body = { + name, + private: isPrivate, + auto_init: auto_init + }; + + // GitHub verwendet 'license_template' statt 'license' + if (license) { + body.license_template = license; + } + + const response = await axios.post('https://api.github.com/user/repos', body, { + headers: { Authorization: `token ${token}` } + }); + return response.data; +} + +async function createRepoGitea({ name, token, url, auto_init = true, license = '', private: isPrivate = false }) { + const endpoint = normalizeBase(url) + '/api/v1/user/repos'; + + console.log('=== createRepoGitea DEBUG ==='); + console.log('Endpoint:', endpoint); + console.log('Token present:', !!token); + console.log('Token length:', token ? token.length : 0); + console.log('Name:', name); + console.log('auto_init:', auto_init); + + const body = { + name, + private: isPrivate, + auto_init: auto_init, + default_branch: 'main' + }; + + if (license) { + body.license = license; + } + + console.log('Request body:', JSON.stringify(body, null, 2)); + + try { + const response = await axios.post(endpoint, body, { + headers: { Authorization: `token ${token}` } + }); + console.log('Success! Status:', response.status); + return response.data; + } catch (error) { + console.error('Error creating repo:', error.response?.status, error.response?.data); + throw error; + } +} + +async function listGiteaRepos({ token, url }) { + const endpoint = normalizeBase(url) + '/api/v1/user/repos'; + const response = await axios.get(endpoint, { + headers: { Authorization: `token ${token}` } + }); + return response.data; +} + +/** + * Returns array of items for a directory or single item for file. + * Each item includes name, path, type, size, download_url, sha (if present). + */ +async function getGiteaRepoContents({ token, url, owner, repo, path = '', ref = 'main' }) { + const base = normalizeBase(url); + if (!base) throw new Error('Invalid Gitea base URL'); + + if (!owner && repo && repo.includes('/')) { + const parts = repo.split('/'); + owner = parts[0]; + repo = parts[1]; + } + + // FIXED: Verwende den übergebenen ref Parameter statt hardcoded 'master' + // Falls ref explizit 'master' ist, konvertiere zu 'main' + let branchRef = ref || 'main'; + if (branchRef === 'master') branchRef = 'main'; + + // console.log('=== getGiteaRepoContents DEBUG ==='); // Optional: Stumm geschaltet + // console.log('Input ref:', ref, 'Final branchRef:', branchRef, 'Path:', path); + + const candidates = []; + candidates.push(buildContentsUrl(base, owner, repo, path) + `?ref=${branchRef}`); + candidates.push(buildContentsUrl(base, owner, repo, path)); + + if (path) { + candidates.push(`${base}/api/v1/repos/${owner}/${repo}/contents/${path}?ref=${branchRef}`); + candidates.push(`${base}/api/v1/repos/${owner}/${repo}/contents/${path}`); + } + + let lastErr = null; + for (const urlCandidate of candidates) { + const r = await tryRequest(urlCandidate, token); + if (r.ok) { + const payload = r.data; + if (Array.isArray(payload)) { + return payload.map(item => ({ + name: item.name, + path: item.path, + type: item.type, + size: item.size, + download_url: item.download_url || item.html_url || null, + sha: item.sha || item.commit_id || null + })); + } else { + return [{ + name: payload.name, + path: payload.path, + type: payload.type, + size: payload.size, + download_url: payload.download_url || payload.html_url || null, + sha: payload.sha || payload.commit_id || null + }]; + } + } else { + lastErr = r; + if (r.status && (r.status === 401 || r.status === 403)) { + throw new Error(`Auth error (${r.status}) when requesting ${r.url}`); + } + } + } + + const msg = lastErr ? `Failed (${lastErr.status || 'no-status'}) ${lastErr.url}` : 'Unknown error'; + const err = new Error('getGiteaRepoContents failed: ' + msg); + err.detail = lastErr; + throw err; +} + +/** + * Get file content (decoded) from Gitea repo. + */ +async function getGiteaFileContent({ token, url, owner, repo, path, ref = 'main' }) { + const base = normalizeBase(url); + if (!base) throw new Error('Invalid Gitea base URL'); + + if (!owner && repo && repo.includes('/')) { + const parts = repo.split('/'); + owner = parts[0]; + repo = parts[1]; + } + + let branchRef = ref || 'main'; + if (branchRef === 'master') branchRef = 'main'; + + const candidates = []; + candidates.push(buildContentsUrl(base, owner, repo, path) + `?ref=${branchRef}`); + candidates.push(buildContentsUrl(base, owner, repo, path)); + candidates.push(`${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/raw/${encodeURIComponent(path)}?ref=${branchRef}`); + candidates.push(`${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/raw/${encodeURIComponent(path)}`); + + let lastErr = null; + for (const c of candidates) { + const r = await tryRequest(c, token); + if (r.ok) { + if (r.data && typeof r.data === 'object' && r.data.content) { + try { + return Buffer.from(r.data.content, 'base64').toString('utf8'); + } catch (e) { + return r.data.content; + } + } + if (typeof r.data === 'string') return r.data; + if (r.data && r.data.download_url) { + const r2 = await tryRequest(r.data.download_url, token); + if (r2.ok) return r2.data; + } + return JSON.stringify(r.data, null, 2); + } else { + lastErr = r; + if (r.status && (r.status === 401 || r.status === 403)) { + throw new Error(`Auth error (${r.status}) when requesting ${r.url}`); + } + } + } + const msg = lastErr ? `Failed (${lastErr.status || 'no-status'}) ${lastErr.url}` : 'Unknown error'; + const err = new Error('getGiteaFileContent failed: ' + msg); + err.detail = lastErr; + throw err; +} + +/** + * Upload (create or update) a file to Gitea. + * Implementiert Retry-Logik für Server-Caching-Probleme (404 beim Lesen, 422 beim Schreiben). + */ +async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, message = 'Upload via Git Manager GUI', branch = 'main' }) { + const base = normalizeBase(url); + if (!base) throw new Error('Invalid Gitea base URL'); + if (!owner && repo && repo.includes('/')) { + const parts = repo.split('/'); + owner = parts[0]; + repo = parts[1]; + } + + let branchName = branch || 'main'; + if (branchName === 'master') branchName = 'main'; + + const fetchSha = async () => { + try { + const existing = await getGiteaRepoContents({ token, url: base, owner, repo, path, ref: branchName }); + if (existing && existing.length > 0 && existing[0].sha) { + return existing[0].sha; + } + return null; + } catch (e) { + return null; + } + }; + + const fetchShaFromDir = async () => { + try { + const pathParts = path.split('/'); + const fileName = pathParts.pop(); + const dirPath = pathParts.join('/'); + + const list = await getGiteaRepoContents({ token, url: base, owner, repo, path: dirPath, ref: branchName }); + if (Array.isArray(list)) { + const item = list.find(i => i.name === fileName); + if (item && item.sha) return item.sha; + } + return null; + } catch (e) { + return null; + } + }; + + const endpoint = buildContentsUrl(base, owner, repo, path); + + // Helper für Warten + const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + + let retryCount = 0; + const MAX_RETRIES = 3; + + while (retryCount <= MAX_RETRIES) { + let sha = await fetchSha(); + if (!sha) { + sha = await fetchShaFromDir(); + } + + const body = { + content: contentBase64, + message, + branch: branchName + }; + if (sha) body.sha = sha; + + try { + const res = await axios.put(endpoint, body, { + headers: { Authorization: `token ${token}` } + }); + return res.data; + } catch (err) { + console.error(`Upload Attempt ${retryCount + 1} for ${path}:`, err.response ? err.response.data : err.message); + + const isShaRequired = err.response && + err.response.status === 422 && + err.response.data && + err.response.data.message && + err.response.data.message.includes('[SHA]'); + + if (isShaRequired && retryCount < MAX_RETRIES) { + retryCount++; + console.warn(`-> 422 SHA Required. Waiting 2 seconds for server index update... (Retry ${retryCount}/${MAX_RETRIES})`); + await sleep(2000); // Wartezeit geben + // Schleife wird neu gestartet, SHA wird erneut gesucht + continue; + } else if (isShaRequired && retryCount >= MAX_RETRIES) { + throw new Error(`Upload failed after ${MAX_RETRIES} retries. Server insists file exists but we cannot find its SHA. Check the repository manually.`); + } + + // Andere Fehler sofort werfen + throw err; + } + } +} + +module.exports = { + createRepoGitHub, + createRepoGitea, + listGiteaRepos, + getGiteaRepoContents, + getGiteaFileContent, + uploadGiteaFile +}; \ No newline at end of file diff --git a/src/git/gitHandler.js b/src/git/gitHandler.js new file mode 100644 index 0000000..9f236ea --- /dev/null +++ b/src/git/gitHandler.js @@ -0,0 +1,57 @@ +// src/git/gitHandler.js (CommonJS) +const path = require('path'); +const fs = require('fs'); +const simpleGit = require('simple-git'); + +function gitFor(folderPath) { + return simpleGit(folderPath); +} + +async function initRepo(folderPath) { + const git = gitFor(folderPath); + if (!fs.existsSync(path.join(folderPath, '.git'))) { + await git.init(); + } + return true; +} + +async function commitAndPush(folderPath, branch = 'master', message = 'Update from Git Manager GUI', progressCb = null) { + const git = gitFor(folderPath); + + await git.add('./*'); + + try { + await git.commit(message); + } catch (e) { + if (!/nothing to commit/i.test(String(e))) throw e; + } + + const localBranches = (await git.branchLocal()).all; + if (!localBranches.includes(branch)) { + await git.checkoutLocalBranch(branch); + } else { + await git.checkout(branch); + } + + if (progressCb) progressCb(30); + + // push -u origin branch + await git.push(['-u', 'origin', branch]); + + if (progressCb) progressCb(100); + return true; +} + +async function getBranches(folderPath) { + const git = gitFor(folderPath); + const summary = await git.branchLocal(); + return summary.all; +} + +async function getCommitLogs(folderPath, count = 50) { + const git = gitFor(folderPath); + const log = await git.log({ n: count }); + return log.all.map(c => `${c.hash.substring(0,7)} - ${c.message}`); +} + +module.exports = { initRepo, commitAndPush, getBranches, getCommitLogs };