Update from Git Manager GUI

This commit is contained in:
2026-04-01 21:30:29 +02:00
parent da47343b2e
commit 464d15464a
2 changed files with 314 additions and 1 deletions

View File

@@ -682,9 +682,29 @@ 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 {
const existing = await getGiteaRepoContents({ token, url: base, owner, repo, path, ref: branchName });

293
src/utils/helpers.js Normal file
View File

@@ -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
};