Update from Git Manager GUI

This commit is contained in:
2025-12-28 18:06:55 +01:00
parent c510f52bdd
commit f7bb79c391
2 changed files with 372 additions and 0 deletions

315
src/git/apiHandler.js Normal file
View File

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

57
src/git/gitHandler.js Normal file
View File

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