Update from Git Manager GUI
This commit is contained in:
@@ -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
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user