Update from Git Manager GUI

This commit is contained in:
2026-03-25 23:07:09 +01:00
parent 2a9812575c
commit 6965503bb3

View File

@@ -113,13 +113,24 @@ async function tryRequest(url, token, opts = {}) {
} }
} }
async function createRepoGitHub({ name, token, auto_init = true, license = '', private: isPrivate = false }) { async function createRepoGitHub({
name,
token,
auto_init = true,
license = '',
private: isPrivate = false,
description = '',
homepage = ''
}) {
const body = { const body = {
name, name,
private: isPrivate, private: isPrivate,
auto_init: auto_init auto_init: auto_init
}; };
if (description) body.description = description;
if (homepage) body.homepage = homepage;
// GitHub verwendet 'license_template' statt 'license' // GitHub verwendet 'license_template' statt 'license'
if (license) { if (license) {
body.license_template = license; body.license_template = license;
@@ -160,14 +171,6 @@ async function createRepoGitHub({ name, token, auto_init = true, license = '', p
async function createRepoGitea({ name, token, url, auto_init = true, license = '', private: isPrivate = false }) { async function createRepoGitea({ name, token, url, auto_init = true, license = '', private: isPrivate = false }) {
const endpoint = normalizeBase(url) + '/api/v1/user/repos'; 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);
console.log('License:', license);
// Normalisiere Lizenz zu Großbuchstaben, wenn vorhanden // Normalisiere Lizenz zu Großbuchstaben, wenn vorhanden
const normalizedLicense = license ? license.toUpperCase() : ''; const normalizedLicense = license ? license.toUpperCase() : '';
@@ -182,14 +185,12 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
body.license = normalizedLicense; body.license = normalizedLicense;
} }
console.log('Request body:', JSON.stringify(body, null, 2));
try { try {
const response = await axiosInstance.post(endpoint, body, { const response = await axiosInstance.post(endpoint, body, {
headers: { Authorization: `token ${token}` }, headers: { Authorization: `token ${token}` },
timeout: 15000 // 15 Sekunden Timeout timeout: 15000 // 15 Sekunden Timeout
}); });
console.log('Success! Status:', response.status);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Error creating repo:', error.response?.status, error.response?.data); console.error('Error creating repo:', error.response?.status, error.response?.data);
@@ -212,7 +213,6 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
headers: { Authorization: `token ${token}` }, headers: { Authorization: `token ${token}` },
timeout: 15000 timeout: 15000
}); });
console.log('Success without license! Status:', retryResponse.status);
console.warn(`Hinweis: Repository wurde ohne Lizenz erstellt, da "${normalizedLicense}" nicht verfügbar ist.`); console.warn(`Hinweis: Repository wurde ohne Lizenz erstellt, da "${normalizedLicense}" nicht verfügbar ist.`);
return retryResponse.data; return retryResponse.data;
} catch (retryError) { } catch (retryError) {
@@ -253,13 +253,119 @@ async function createRepoGitea({ name, token, url, auto_init = true, license = '
} }
async function listGiteaRepos({ token, url }) { async function listGiteaRepos({ token, url }) {
const endpoint = normalizeBase(url) + '/api/v1/user/repos'; const base = normalizeBase(url);
const endpoint = `${base}/api/v1/user/repos`;
const repoKey = (repo) => (
repo?.full_name || `${repo?.owner?.login || repo?.owner?.username || ''}/${repo?.name || ''}`
);
const fetchAllPages = async (extraParams = {}) => {
const perPage = 100;
const maxPages = 50;
const all = [];
for (let page = 1; page <= maxPages; page += 1) {
const response = await axiosInstance.get(endpoint, {
headers: {
Authorization: `token ${token}`,
'User-Agent': 'Git-Manager-GUI'
},
params: {
page,
limit: perPage,
...extraParams
},
timeout: 15000
});
const repos = Array.isArray(response.data) ? response.data : [];
all.push(...repos);
if (repos.length < perPage) break;
}
return all;
};
// Manche Gitea-Versionen liefern mit mode=all explizit auch private/collab Repos.
let repos = [];
try {
repos = await fetchAllPages({ mode: 'all', affiliation: 'owner,collaborator,organization_member' });
} catch (_) {
repos = await fetchAllPages({ affiliation: 'owner,collaborator,organization_member' });
}
// Fallback: private separat abfragen und mergen, falls der erste Abruf sie nicht enthielt.
if (!repos.some(r => !!r?.private)) {
try {
const privateRepos = await fetchAllPages({ mode: 'private' });
const byKey = new Map();
for (const repo of repos) {
const key = repoKey(repo);
byKey.set(key, repo);
}
for (const repo of privateRepos) {
const key = repoKey(repo);
if (!byKey.has(key)) byKey.set(key, repo);
}
repos = Array.from(byKey.values());
} catch (_) {
// ignorieren: nicht jede Gitea-Version unterstützt mode=private
}
}
return repos;
}
async function getGiteaCurrentUser({ token, url }) {
const endpoint = normalizeBase(url) + '/api/v1/user';
const response = await axiosInstance.get(endpoint, { const response = await axiosInstance.get(endpoint, {
headers: { Authorization: `token ${token}` } headers: {
Authorization: `token ${token}`,
'User-Agent': 'Git-Manager-GUI'
},
timeout: 10000
}); });
return response.data; return response.data;
} }
async function listGiteaTopicsCatalog({ token, url, maxPages = 20, perPage = 100 }) {
const base = normalizeBase(url);
if (!base) throw new Error('Invalid Gitea base URL');
const topics = new Set();
let page = 1;
while (page <= Math.max(1, maxPages)) {
const response = await axiosInstance.get(`${base}/api/v1/user/repos`, {
headers: {
Authorization: `token ${token}`,
'User-Agent': 'Git-Manager-GUI'
},
params: {
page,
limit: perPage
},
timeout: 15000
});
const repos = Array.isArray(response.data) ? response.data : [];
for (const repo of repos) {
const repoTopics = Array.isArray(repo?.topics) ? repo.topics : [];
for (const t of repoTopics) {
const s = String(t || '').trim();
if (s) topics.add(s);
}
}
if (repos.length < perPage) break;
page += 1;
}
return Array.from(topics).sort((a, b) => a.localeCompare(b, 'de'));
}
function toDateKey(value) { function toDateKey(value) {
if (value == null) return null; if (value == null) return null;
@@ -1058,11 +1164,766 @@ async function deleteReleaseAsset({ token, url, owner, repo, assetId }) {
} }
} }
async function migrateRepoToGitea({ token, url, cloneUrl, repoName, description, isPrivate, authToken, authUsername }) {
const base = normalizeAndValidateBaseUrl(url);
const body = {
clone_addr: cloneUrl,
repo_name: repoName,
description: description || '',
private: isPrivate || false,
issues: true,
labels: true,
milestones: true,
releases: true,
wiki: true,
pull_requests: true
};
// Optionale Auth für private Source-Repos (z.B. GitHub-Token)
if (authUsername) body.auth_username = authUsername;
if (authToken) body.auth_token = authToken;
const res = await axiosInstance.post(
`${base}/api/v1/repos/migrate`,
body,
{
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Git-Manager-GUI'
},
timeout: 60000
}
);
return res.data;
}
async function updateGiteaRepoAvatar({ token, url, owner, repo, imageBase64 }) {
const base = normalizeAndValidateBaseUrl(url);
const pureBase64 = imageBase64.replace(/^data:[^;]+;base64,/, '');
await axiosInstance.post(
`${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/avatar`,
{ image: pureBase64 },
{
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Git-Manager-GUI'
},
timeout: 15000
}
);
}
async function updateGiteaRepoVisibility({ token, url, owner, repo, isPrivate }) {
const base = normalizeAndValidateBaseUrl(url);
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
const res = await axiosInstance.patch(
endpoint,
{ private: !!isPrivate },
{
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Git-Manager-GUI'
},
timeout: 15000
}
);
return res.data;
}
async function updateGiteaRepoTopics({ token, url, owner, repo, topics }) {
const base = normalizeAndValidateBaseUrl(url);
const repoPath = `${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
const safeTopics = Array.isArray(topics) ? topics : [];
try {
const res = await axiosInstance.put(
`${base}/api/v1/repos/${repoPath}/topics`,
{ topics: safeTopics },
{
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Git-Manager-GUI'
},
timeout: 15000
}
);
return res.data;
} catch (err) {
// Fallback fuer aeltere/abweichende Gitea-Versionen
const res = await axiosInstance.patch(
`${base}/api/v1/repos/${repoPath}`,
{ topics: safeTopics },
{
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Git-Manager-GUI'
},
timeout: 15000
}
);
return res.data;
}
}
/* ====================================================
GITHUB API FUNCTIONS
==================================================== */
const GITHUB_API = 'https://api.github.com';
function githubHeaders(token) {
return {
Authorization: `token ${token}`,
'User-Agent': 'Git-Manager-GUI',
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
};
}
function parseGithubLinkHeader(header) {
if (!header) return {};
const links = {};
header.split(',').forEach(part => {
const match = part.match(/<([^>]+)>;\s*rel="([^"]+)"/);
if (match) links[match[2]] = match[1];
});
return links;
}
async function listGithubRepos({ token }) {
if (!token) throw new Error('GitHub Token fehlt. Bitte Token in Settings eintragen.');
const fetchPaged = async (startUrl) => {
const allRepos = [];
let nextUrl = startUrl;
while (nextUrl) {
const response = await axiosInstance.get(nextUrl, {
headers: githubHeaders(token),
timeout: 15000
});
const repos = Array.isArray(response.data) ? response.data : [];
allRepos.push(...repos);
const links = parseGithubLinkHeader(response.headers['link']);
nextUrl = links.next || null;
}
return allRepos;
};
const primaryUrl = `${GITHUB_API}/user/repos?affiliation=owner,collaborator,organization_member&per_page=100&page=1`;
const fallbackUrl = `${GITHUB_API}/user/repos?type=all&per_page=100&page=1`;
try {
return await fetchPaged(primaryUrl);
} catch (error) {
if (error?.response?.status === 422) {
return await fetchPaged(fallbackUrl);
}
throw error;
}
}
async function getGithubCurrentUser({ token }) {
if (!token) throw new Error('GitHub Token fehlt.');
const response = await axiosInstance.get(`${GITHUB_API}/user`, {
headers: githubHeaders(token),
timeout: 10000
});
return response.data;
}
async function getGithubUserHeatmap({ token, monthsBack = 20 }) {
if (!token) throw new Error('GitHub Token fehlt.');
const me = await getGithubCurrentUser({ token });
const username = me?.login || null;
if (!username) throw new Error('GitHub Benutzer konnte nicht ermittelt werden.');
const to = new Date();
const from = new Date(to.getFullYear(), to.getMonth(), to.getDate());
from.setMonth(from.getMonth() - Math.max(1, Number(monthsBack) || 20));
const formatDate = (d) => {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
};
const query = `
query($login: String!, $from: DateTime!, $to: DateTime!) {
user(login: $login) {
contributionsCollection(from: $from, to: $to) {
contributionCalendar {
weeks {
contributionDays {
date
contributionCount
}
}
}
}
}
}
`;
// Primaere Quelle: derselbe Contribution-Calendar-Endpunkt wie im GitHub-Profil.
// Das liefert die sichtbar "echten" Daten der Profilansicht.
try {
const contributionsUrl = `https://github.com/users/${encodeURIComponent(username)}/contributions` +
`?from=${encodeURIComponent(formatDate(from))}&to=${encodeURIComponent(formatDate(to))}`;
const response = await axiosInstance.get(contributionsUrl, {
headers: {
'User-Agent': 'Git-Manager-GUI',
'Accept': 'text/html,application/xhtml+xml,image/svg+xml'
},
timeout: 20000
});
const html = typeof response.data === 'string' ? response.data : '';
const acc = new Map();
const dayTags = html.match(/<[^>]*ContributionCalendar-day[^>]*>/gi) || [];
const readAttr = (tag, attr) => {
const rx = new RegExp(`${attr}\\s*=\\s*(["'])(.*?)\\1`, 'i');
const m = tag.match(rx);
return m ? m[2] : '';
};
const tooltipCountById = new Map();
const tooltipRegex = /<tool-tip\b[^>]*for=(["'])(.*?)\1[^>]*>([\s\S]*?)<\/tool-tip>/gi;
let tooltipMatch;
while ((tooltipMatch = tooltipRegex.exec(html)) !== null) {
const targetId = tooltipMatch[2] || '';
const body = String(tooltipMatch[3] || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
if (!targetId || !body) continue;
if (/^No contributions?/i.test(body)) {
tooltipCountById.set(targetId, 0);
continue;
}
const m = body.match(/(\d+)\s+contributions?/i);
if (m) {
tooltipCountById.set(targetId, Number(m[1]));
}
}
for (const tag of dayTags) {
const date = normalizeHeatmapEntryDate(readAttr(tag, 'data-date'));
if (!date) continue;
const id = readAttr(tag, 'id');
const level = Number(readAttr(tag, 'data-level'));
const rawCount = readAttr(tag, 'data-count');
const parsedDataCount = rawCount === '' ? Number.NaN : Number(rawCount);
let count = Number.NaN;
// 1) Tooltips enthalten i.d.R. den echten Wert aus dem GitHub-Graphen.
if (id && tooltipCountById.has(id)) {
count = Number(tooltipCountById.get(id));
}
// 2) Fallback auf data-count.
if (!Number.isFinite(count) && Number.isFinite(parsedDataCount)) {
count = parsedDataCount;
}
if (!Number.isFinite(count)) {
const aria = readAttr(tag, 'aria-label');
const ariaMatch = aria.match(/(\d+)\s+contributions?/i);
if (ariaMatch) {
count = Number(ariaMatch[1]);
}
}
if (!Number.isFinite(count)) {
count = Number.isFinite(level) && level > 0 ? level : 0;
}
// Sicherheitsnetz: manche GitHub-Varianten liefern data-count=0,
// obwohl data-level > 0 ist. Dann zumindest Intensitaet als Aktivitaet nutzen.
if (Number.isFinite(level) && level > 0 && Number.isFinite(count) && count <= 0) {
count = level;
}
const safeCount = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0;
acc.set(date, safeCount);
}
if (acc.size > 0) {
const entries = Array.from(acc.entries())
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
return {
username,
endpoint: contributionsUrl,
entries
};
}
} catch (_) {
// Fallback auf API-basierte Quellen unten.
}
// GitHub GraphQL liefert in der Praxis nur Bereiche bis max. ca. 1 Jahr stabil,
// deshalb teilen wir den gewünschten Zeitraum in mehrere Segmente.
const ranges = [];
let cursorTo = new Date(to);
while (cursorTo >= from) {
const cursorFrom = new Date(cursorTo.getFullYear() - 1, cursorTo.getMonth(), cursorTo.getDate() + 1);
const segmentFrom = cursorFrom < from ? new Date(from) : cursorFrom;
ranges.push({ from: segmentFrom, to: new Date(cursorTo) });
cursorTo = new Date(segmentFrom);
cursorTo.setDate(cursorTo.getDate() - 1);
}
const graphAcc = new Map();
let graphOk = false;
for (const range of ranges) {
try {
const response = await axiosInstance.post(
`${GITHUB_API}/graphql`,
{
query,
variables: {
login: username,
from: range.from.toISOString(),
to: range.to.toISOString()
}
},
{
headers: githubHeaders(token),
timeout: 20000,
validateStatus: () => true
}
);
if (!(response.status >= 200 && response.status < 300) || response.data?.errors) {
continue;
}
const weeks = response.data?.data?.user?.contributionsCollection?.contributionCalendar?.weeks || [];
for (const week of weeks) {
const days = week?.contributionDays || [];
for (const day of days) {
const date = normalizeHeatmapEntryDate(day?.date);
if (!date) continue;
const count = Number(day?.contributionCount || 0);
const n = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0;
graphAcc.set(date, (graphAcc.get(date) || 0) + n);
}
}
graphOk = true;
} catch (_) {
// probiere naechstes Segment
}
}
if (graphOk && graphAcc.size > 0) {
const entries = Array.from(graphAcc.entries())
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
return {
username,
endpoint: `${GITHUB_API}/graphql (chunked)`,
entries
};
}
try {
let nextUrl = `${GITHUB_API}/users/${encodeURIComponent(username)}/events?per_page=100&page=1`;
let pageCount = 0;
const acc = new Map();
while (nextUrl && pageCount < 10) {
const response = await axiosInstance.get(nextUrl, {
headers: githubHeaders(token),
timeout: 15000
});
const events = Array.isArray(response.data) ? response.data : [];
for (const ev of events) {
const date = normalizeHeatmapEntryDate(ev?.created_at);
if (!date) continue;
acc.set(date, (acc.get(date) || 0) + 1);
}
const links = parseGithubLinkHeader(response.headers?.link);
nextUrl = links.next || null;
pageCount += 1;
}
if (acc.size > 0) {
const entries = Array.from(acc.entries())
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
return {
username,
endpoint: `${GITHUB_API}/users/${encodeURIComponent(username)}/events`,
entries
};
}
} catch (_) {
// letztes Fallback unten
}
return {
username,
endpoint: 'none',
entries: []
};
}
async function getGithubRepoContents({ token, owner, repo, path = '', ref = 'HEAD' }) {
const refParam = (ref && ref !== 'HEAD') ? `?ref=${encodeURIComponent(ref)}` : '';
const safePath = path ? path.split('/').map(encodeURIComponent).join('/') : '';
const endpoint = `${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${safePath}${refParam}`;
try {
const response = await axiosInstance.get(endpoint, {
headers: githubHeaders(token),
timeout: 15000
});
const payload = response.data;
if (Array.isArray(payload)) {
return {
ok: true,
items: payload.map(item => ({
name: item.name,
path: item.path,
type: item.type === 'dir' ? 'dir' : 'file',
size: item.size || 0,
download_url: item.download_url || null,
sha: item.sha || null
}))
};
}
return {
ok: true,
items: [{
name: payload.name,
path: payload.path,
type: payload.type === 'dir' ? 'dir' : 'file',
size: payload.size || 0,
download_url: payload.download_url || null,
sha: payload.sha || null
}]
};
} catch (err) {
if (err.response?.status === 404) {
return { ok: true, items: [], empty: true };
}
throw err;
}
}
async function getGithubFileSha({ token, owner, repo, path, ref = 'HEAD' }) {
const refParam = (ref && ref !== 'HEAD') ? `?ref=${encodeURIComponent(ref)}` : '';
const safePath = path.split('/').map(encodeURIComponent).join('/');
const endpoint = `${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${safePath}${refParam}`;
try {
const response = await axiosInstance.get(endpoint, {
headers: githubHeaders(token),
timeout: 10000
});
return response.data?.sha || null;
} catch (_) {
return null;
}
}
async function getGithubFileContent({ token, owner, repo, path, ref = 'HEAD' }) {
const refParam = (ref && ref !== 'HEAD') ? `?ref=${encodeURIComponent(ref)}` : '';
const safePath = path.split('/').map(encodeURIComponent).join('/');
const endpoint = `${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${safePath}${refParam}`;
const response = await axiosInstance.get(endpoint, {
headers: githubHeaders(token),
timeout: 15000
});
const data = response.data;
if (data.content) {
return Buffer.from(data.content.replace(/\n/g, ''), 'base64').toString('utf8');
}
if (data.download_url) {
const rawRes = await axiosInstance.get(data.download_url, {
headers: { Authorization: `token ${token}`, 'User-Agent': 'Git-Manager-GUI' },
timeout: 15000
});
return typeof rawRes.data === 'string' ? rawRes.data : JSON.stringify(rawRes.data, null, 2);
}
throw new Error('GitHub Dateiinhalt nicht verfügbar.');
}
async function uploadGithubFile({ token, owner, repo, path, contentBase64, message = 'Upload via Git Manager GUI', branch = 'main' }) {
const safePath = path.split('/').map(encodeURIComponent).join('/');
const endpoint = `${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${safePath}`;
const sha = await getGithubFileSha({ token, owner, repo, path, ref: branch });
const body = { message, content: contentBase64, branch };
if (sha) body.sha = sha;
const response = await axiosInstance.put(endpoint, body, {
headers: githubHeaders(token),
timeout: 30000
});
return response.data;
}
async function deleteGithubFile({ token, owner, repo, path, message = 'Delete via Git Manager GUI', sha, branch = 'main' }) {
let fileSha = sha;
if (!fileSha) {
fileSha = await getGithubFileSha({ token, owner, repo, path, ref: branch });
}
if (!fileSha) throw new Error(`SHA für Datei nicht gefunden: ${path}`);
const safePath = path.split('/').map(encodeURIComponent).join('/');
const endpoint = `${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${safePath}`;
await axiosInstance.delete(endpoint, {
headers: githubHeaders(token),
data: { message, sha: fileSha, branch },
timeout: 15000
});
return { ok: true };
}
async function getGithubCommits({ token, owner, repo, branch = '', page = 1, limit = 50 }) {
const params = { per_page: limit, page };
if (branch && branch !== 'HEAD') params.sha = branch;
const response = await axiosInstance.get(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`,
{ headers: githubHeaders(token), params, timeout: 15000 }
);
return (response.data || []).map(c => ({
sha: c.sha,
commit: {
message: c.commit?.message || '',
author: {
name: c.commit?.author?.name || '',
email: c.commit?.author?.email || '',
date: c.commit?.author?.date || ''
},
committer: {
name: c.commit?.committer?.name || '',
date: c.commit?.committer?.date || ''
}
},
author: c.author ? { login: c.author.login, avatar_url: c.author.avatar_url } : null,
html_url: c.html_url || ''
}));
}
async function getGithubCommitDiff({ token, owner, repo, sha }) {
const response = await axiosInstance.get(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${sha}`,
{
headers: { ...githubHeaders(token), 'Accept': 'application/vnd.github.diff' },
timeout: 15000
}
);
return response.data;
}
async function getGithubCommitFiles({ token, owner, repo, sha }) {
const response = await axiosInstance.get(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${sha}`,
{ headers: githubHeaders(token), timeout: 15000 }
);
const commit = response.data;
const files = (commit.files || []).map(f => ({
file: f.filename || '',
changes: f.changes || 0,
insertions: f.additions || 0,
deletions: f.deletions || 0,
binary: f.status === 'binary' || false,
status: f.status || ''
}));
return {
files,
stats: {
additions: commit.stats?.additions || 0,
deletions: commit.stats?.deletions || 0,
total: commit.stats?.total || 0
}
};
}
async function searchGithubCommits({ token, owner, repo, query, branch = '' }) {
const params = { per_page: 100 };
if (branch && branch !== 'HEAD') params.sha = branch;
const response = await axiosInstance.get(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`,
{ headers: githubHeaders(token), params, timeout: 15000 }
);
const lowerQuery = query.toLowerCase();
return (response.data || []).filter(c => {
const message = (c.commit?.message || '').toLowerCase();
const author = (c.commit?.author?.name || '').toLowerCase();
return message.includes(lowerQuery) || author.includes(lowerQuery);
}).map(c => ({
sha: c.sha,
commit: {
message: c.commit?.message || '',
author: { name: c.commit?.author?.name || '', email: c.commit?.author?.email || '', date: c.commit?.author?.date || '' }
},
author: c.author ? { login: c.author.login, avatar_url: c.author.avatar_url } : null,
html_url: c.html_url || ''
}));
}
async function getGithubBranches({ token, owner, repo }) {
const response = await axiosInstance.get(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`,
{ headers: githubHeaders(token), params: { per_page: 100 }, timeout: 15000 }
);
return (response.data || []).map(b => ({
name: b.name,
commit: { id: b.commit?.sha || '', sha: b.commit?.sha || '' },
protected: b.protected || false
}));
}
async function listGithubReleases({ token, owner, repo }) {
const response = await axiosInstance.get(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`,
{ headers: githubHeaders(token), params: { per_page: 100 }, timeout: 15000 }
);
return (response.data || []).map(r => ({
id: r.id,
tag_name: r.tag_name,
name: r.name || r.tag_name,
body: r.body || '',
draft: r.draft || false,
prerelease: r.prerelease || false,
created_at: r.created_at,
published_at: r.published_at,
html_url: r.html_url || '',
assets: (r.assets || []).map(a => ({
id: a.id,
name: a.name,
size: a.size,
download_count: a.download_count,
browser_download_url: a.browser_download_url
}))
}));
}
async function createGithubRelease({ token, owner, repo, data }) {
const body = {
tag_name: data.tag_name,
name: data.name || data.tag_name,
body: data.body || '',
draft: data.draft || false,
prerelease: data.prerelease || false,
target_commitish: data.target_commitish || 'main'
};
const response = await axiosInstance.post(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`,
body,
{ headers: githubHeaders(token), timeout: 15000 }
);
return response.data;
}
async function editGithubRelease({ token, owner, repo, releaseId, data }) {
const body = {};
if (data.name !== undefined) body.name = data.name;
if (data.body !== undefined) body.body = data.body;
if (data.draft !== undefined) body.draft = data.draft;
if (data.prerelease !== undefined) body.prerelease = data.prerelease;
if (data.tag_name !== undefined) body.tag_name = data.tag_name;
const response = await axiosInstance.patch(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`,
body,
{ headers: githubHeaders(token), timeout: 15000 }
);
return response.data;
}
async function deleteGithubRelease({ token, owner, repo, releaseId }) {
await axiosInstance.delete(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`,
{ headers: githubHeaders(token), timeout: 15000 }
);
return { ok: true };
}
async function updateGithubRepoVisibility({ token, owner, repo, isPrivate }) {
const response = await axiosInstance.patch(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
{ private: !!isPrivate },
{ headers: githubHeaders(token), timeout: 15000 }
);
return response.data;
}
async function updateGithubRepoTopics({ token, owner, repo, topics }) {
const response = await axiosInstance.put(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/topics`,
{ names: Array.isArray(topics) ? topics : [] },
{
headers: { ...githubHeaders(token), 'Accept': 'application/vnd.github.mercy-preview+json' },
timeout: 15000
}
);
return response.data;
}
async function deleteGithubRepo({ token, owner, repo }) {
await axiosInstance.delete(
`${GITHUB_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
{ headers: githubHeaders(token), timeout: 15000 }
);
return { ok: true };
}
/* ====================================================
END GITHUB API FUNCTIONS
==================================================== */
async function updateGiteaAvatar({ token, url, imageBase64 }) {
const base = normalizeAndValidateBaseUrl(url);
// Strip data-URL prefix if present (e.g. "data:image/png;base64,")
const pureBase64 = imageBase64.replace(/^data:[^;]+;base64,/, '');
await axiosInstance.post(
`${base}/api/v1/user/avatar`,
{ image: pureBase64 },
{
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Git-Manager-GUI'
},
timeout: 15000
}
);
}
module.exports = { module.exports = {
normalizeAndValidateBaseUrl, normalizeAndValidateBaseUrl,
createRepoGitHub, createRepoGitHub,
createRepoGitea, createRepoGitea,
checkGiteaConnection, checkGiteaConnection,
updateGiteaAvatar,
updateGiteaRepoAvatar,
updateGiteaRepoVisibility,
updateGiteaRepoTopics,
migrateRepoToGitea,
listGiteaTopicsCatalog,
getGiteaCurrentUser,
listGiteaRepos, listGiteaRepos,
getGiteaUserHeatmap, getGiteaUserHeatmap,
getGiteaRepoContents, getGiteaRepoContents,
@@ -1082,5 +1943,25 @@ module.exports = {
editGiteaRelease, editGiteaRelease,
deleteGiteaRelease, deleteGiteaRelease,
uploadReleaseAsset, uploadReleaseAsset,
deleteReleaseAsset deleteReleaseAsset,
// GitHub API
listGithubRepos,
getGithubCurrentUser,
getGithubUserHeatmap,
getGithubRepoContents,
getGithubFileContent,
uploadGithubFile,
deleteGithubFile,
getGithubCommits,
getGithubCommitDiff,
getGithubCommitFiles,
searchGithubCommits,
getGithubBranches,
listGithubReleases,
createGithubRelease,
editGithubRelease,
deleteGithubRelease,
updateGithubRepoVisibility,
updateGithubRepoTopics,
deleteGithubRepo
}; };