diff --git a/src/git/apiHandler.js b/src/git/apiHandler.js index 0ef827a..a04b8b9 100644 --- a/src/git/apiHandler.js +++ b/src/git/apiHandler.js @@ -682,8 +682,28 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m repo = parts[1]; } - // Behalte den branch so wie übergeben - keine Konvertierung + // Behalte den branch so wie übergeben - aber 'HEAD' muss zum echten Branch aufgelöst werden let branchName = branch || 'HEAD'; + + // HEAD-Auflösung: Wenn branch === 'HEAD', den Default-Branch des Repos abrufen + if (branchName === 'HEAD') { + try { + const repoInfoUrl = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`; + const repoInfo = await tryRequest(repoInfoUrl, token); + if (repoInfo.ok && repoInfo.data.default_branch) { + branchName = repoInfo.data.default_branch; + console.log(`[Upload Debug] HEAD aufgelöst zu: ${branchName}`); + } else { + // Fallback auf 'main' wenn Auflösung fehlschlägt + branchName = 'main'; + console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen, verwende Fallback: ${branchName}`); + } + } catch (e) { + // Fallback auf 'main' wenn Fehler + branchName = 'main'; + console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen (${e.message}), verwende Fallback: ${branchName}`); + } + } const fetchSha = async () => { try { diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 0000000..06b70d0 --- /dev/null +++ b/src/utils/helpers.js @@ -0,0 +1,293 @@ +/** + * Gemeinsame Utility-Funktionen für Git Manager GUI + * - Branch Handling + * - API Error Handling + * - Standardisiertes Logging + * - Caching + */ + +const fs = require('fs'); +const ppath = require('path'); + +// ===== LOGGING SYSTEM ===== +const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 }; +let currentLogLevel = process.env.NODE_ENV === 'production' ? LOG_LEVELS.INFO : LOG_LEVELS.DEBUG; +let logQueue = []; +const MAX_LOG_BUFFER = 100; + +function formatLog(level, context, message, details = null) { + const timestamp = new Date().toISOString(); + const levelStr = Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === level); + return { + timestamp, + level: levelStr, + context, + message, + details, + pid: process.pid + }; +} + +function writeLog(logEntry) { + logQueue.push(logEntry); + if (logQueue.length > MAX_LOG_BUFFER) { + logQueue.shift(); + } + + // Auch in Console schreiben + const { level, timestamp, context, message, details } = logEntry; + const prefix = `[${timestamp}] [${level}] [${context}]`; + + if (level === 'ERROR' && details?.error) { + console.error(prefix, message, details.error); + } else if (level === 'WARN') { + console.warn(prefix, message, details ? JSON.stringify(details) : ''); + } else if (level !== 'DEBUG' || process.env.DEBUG) { + console.log(prefix, message, details ? JSON.stringify(details) : ''); + } +} + +const logger = { + debug: (context, message, details) => writeLog(formatLog(LOG_LEVELS.DEBUG, context, message, details)), + info: (context, message, details) => writeLog(formatLog(LOG_LEVELS.INFO, context, message, details)), + warn: (context, message, details) => writeLog(formatLog(LOG_LEVELS.WARN, context, message, details)), + error: (context, message, details) => writeLog(formatLog(LOG_LEVELS.ERROR, context, message, details)), + getRecent: (count = 20) => logQueue.slice(-count), + setLevel: (level) => { currentLogLevel = LOG_LEVELS[level] || LOG_LEVELS.INFO; } +}; + +// ===== BRANCH HANDLING ===== +const BRANCH_DEFAULTS = { + gitea: 'main', + github: 'main' +}; + +function normalizeBranch(branch = 'HEAD', platform = 'gitea') { + const value = String(branch || '').trim(); + + // HEAD sollte immer zu Standard konvertiert werden + if (value.toLowerCase() === 'head') { + return BRANCH_DEFAULTS[platform] || 'main'; + } + + // Validierung: nur sichere Git-Referenzen + if (/^[a-zA-Z0-9._\-/]+$/.test(value)) { + return value; + } + + return BRANCH_DEFAULTS[platform] || 'main'; +} + +function isSafeBranch(branch) { + return /^[a-zA-Z0-9._\-/]+$/.test(String(branch || '')); +} + +// ===== ERROR HANDLING ===== +const ERROR_CODES = { + NETWORK: 'NETWORK_ERROR', + AUTH_FAILED: 'AUTH_FAILED', + NOT_FOUND: 'NOT_FOUND', + VALIDATION: 'VALIDATION_ERROR', + RATE_LIMIT: 'RATE_LIMIT', + SERVER_ERROR: 'SERVER_ERROR', + UNKNOWN: 'UNKNOWN_ERROR' +}; + +function parseApiError(error, defaultCode = ERROR_CODES.UNKNOWN) { + if (!error) { + return { code: defaultCode, message: 'Unknown error', statusCode: null }; + } + + // Axios-style error + if (error.response) { + const status = error.response.status; + const data = error.response.data; + let code = defaultCode; + + if (status === 401 || status === 403) { + code = ERROR_CODES.AUTH_FAILED; + } else if (status === 404) { + code = ERROR_CODES.NOT_FOUND; + } else if (status === 429) { + code = ERROR_CODES.RATE_LIMIT; + } else if (status >= 500) { + code = ERROR_CODES.SERVER_ERROR; + } + + return { + code, + message: data?.message || error.message || `HTTP ${status}`, + statusCode: status, + rawMessage: data?.message + }; + } + + // Network error + if (error.message?.includes('timeout') || error.code?.includes('TIMEOUT')) { + return { code: ERROR_CODES.NETWORK, message: 'Request timeout', statusCode: null }; + } + + if (error.code?.includes('ECONNREFUSED') || error.message?.includes('ECONNREFUSED')) { + return { code: ERROR_CODES.NETWORK, message: 'Connection refused', statusCode: null }; + } + + return { + code: ERROR_CODES.UNKNOWN, + message: error.message || String(error), + statusCode: null + }; +} + +function formatErrorForUser(error, context = 'Operation') { + const parsed = parseApiError(error); + const messages = { + [ERROR_CODES.AUTH_FAILED]: `Authentifizierung fehlgeschlagen. Bitte Token überprüfen.`, + [ERROR_CODES.NOT_FOUND]: `Ressource nicht gefunden.`, + [ERROR_CODES.NETWORK]: `Netzwerkfehler. Bitte Verbindung überprüfen.`, + [ERROR_CODES.RATE_LIMIT]: `Zu viele Anfragen. Bitte später versuchen.`, + [ERROR_CODES.SERVER_ERROR]: `Server-Fehler. Bitte später versuchen.`, + [ERROR_CODES.UNKNOWN]: `${context} fehlgeschlagen.` + }; + + return { + userMessage: messages[parsed.code], + technicalMessage: parsed.message, + code: parsed.code, + details: parsed + }; +} + +// ===== CACHING SYSTEM ===== +class Cache { + constructor(ttl = 300000) { // 5 min default + this.store = new Map(); + this.ttl = ttl; + } + + set(key, value, customTtl = null) { + const expiry = Date.now() + (customTtl || this.ttl); + this.store.set(key, { value, expiry }); + } + + get(key) { + const item = this.store.get(key); + if (!item) return null; + if (Date.now() > item.expiry) { + this.store.delete(key); + return null; + } + return item.value; + } + + invalidate(keyPattern) { + for (const [key] of this.store) { + if (key.includes(keyPattern)) { + this.store.delete(key); + } + } + } + + clear() { + this.store.clear(); + } + + size() { + return this.store.size; + } +} + +// Standard Caches +const caches = { + repos: new Cache(600000), // 10 min + fileTree: new Cache(300000), // 5 min + api: new Cache(120000) // 2 min +}; + +// ===== PARALLEL OPERATIONS ===== +async function runParallel(operations, concurrency = 4, onProgress = null) { + const results = new Array(operations.length); + let completed = 0; + let index = 0; + + async function worker() { + while (index < operations.length) { + const i = index++; + try { + results[i] = { ok: true, result: await operations[i]() }; + } catch (e) { + results[i] = { ok: false, error: e }; + } + completed++; + if (onProgress) { + try { onProgress(completed, operations.length); } catch (_) {} + } + } + } + + const workers = Array.from({ length: Math.min(concurrency, operations.length) }, () => worker()); + await Promise.all(workers); + return results; +} + +// ===== RETRY LOGIC ===== +async function retryWithBackoff(fn, maxAttempts = 3, baseDelay = 1000) { + let lastError; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + return await fn(); + } catch (e) { + lastError = e; + if (attempt < maxAttempts - 1) { + const delay = baseDelay * Math.pow(2, attempt); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + throw lastError; +} + +// ===== FILE OPERATIONS ===== +function ensureDirectory(dirPath) { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +} + +function safeReadFile(filePath, defaultValue = null) { + try { + if (!fs.existsSync(filePath)) return defaultValue; + return fs.readFileSync(filePath, 'utf8'); + } catch (e) { + logger.warn('safeReadFile', `Failed to read ${filePath}`, { error: e.message }); + return defaultValue; + } +} + +function safeWriteFile(filePath, content) { + try { + ensureDirectory(ppath.dirname(filePath)); + fs.writeFileSync(filePath, content, 'utf8'); + return true; + } catch (e) { + logger.error('safeWriteFile', `Failed to write ${filePath}`, { error: e.message }); + return false; + } +} + +// ===== EXPORTS ===== +module.exports = { + logger, + normalizeBranch, + isSafeBranch, + parseApiError, + formatErrorForUser, + ERROR_CODES, + Cache, + caches, + runParallel, + retryWithBackoff, + ensureDirectory, + safeReadFile, + safeWriteFile, + LOG_LEVELS +};