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