Update from Git Manager GUI
This commit is contained in:
315
src/git/apiHandler.js
Normal file
315
src/git/apiHandler.js
Normal 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
57
src/git/gitHandler.js
Normal 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 };
|
||||||
Reference in New Issue
Block a user