From 0f758616cf913912d1821cc2abac4c86fc0c3478 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Tue, 24 Mar 2026 19:18:28 +0100 Subject: [PATCH] Update from Git Manager GUI --- src/git/apiHandler.js | 139 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/git/apiHandler.js b/src/git/apiHandler.js index d1486a6..1ff0d0e 100644 --- a/src/git/apiHandler.js +++ b/src/git/apiHandler.js @@ -260,6 +260,144 @@ async function listGiteaRepos({ token, url }) { return response.data; } +function toDateKey(value) { + if (value == null) return null; + + if (typeof value === 'string') { + const s = value.trim(); + if (!s) return null; + const match = s.match(/^(\d{4}-\d{2}-\d{2})/); + if (match) return match[1]; + if (/^\d+$/.test(s)) { + const n = Number(s); + if (!Number.isFinite(n)) return null; + const ms = n < 1e12 ? n * 1000 : n; + const d = new Date(ms); + if (Number.isNaN(d.getTime())) return null; + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + } + const d = new Date(s); + if (Number.isNaN(d.getTime())) return null; + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + } + + if (typeof value === 'number' && Number.isFinite(value)) { + const ms = value < 1e12 ? value * 1000 : value; + const d = new Date(ms); + if (Number.isNaN(d.getTime())) return null; + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + } + + if (value instanceof Date && !Number.isNaN(value.getTime())) { + return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; + } + + return null; +} + +function normalizeHeatmapEntries(payload) { + const acc = new Map(); + + const addEntry = (dateLike, countLike) => { + const key = toDateKey(dateLike); + if (!key) return; + const n = Number(countLike); + const count = Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 1; + acc.set(key, (acc.get(key) || 0) + count); + }; + + const walk = (node, depth = 0) => { + if (depth > 6 || node == null) return; + + if (Array.isArray(node)) { + node.forEach(item => walk(item, depth + 1)); + return; + } + + if (typeof node === 'number' || typeof node === 'string' || node instanceof Date) { + addEntry(node, 1); + return; + } + + if (typeof node !== 'object') return; + + const dateLike = + node.date ?? node.day ?? node.timestamp ?? node.time ?? node.ts ?? node.created_at ?? node.created ?? node.when; + const countLike = + node.count ?? node.contributions ?? node.value ?? node.total ?? node.commits ?? node.frequency; + + if (dateLike != null) { + addEntry(dateLike, countLike); + } + + if (node.data != null) walk(node.data, depth + 1); + if (node.values != null) walk(node.values, depth + 1); + if (node.heatmap != null) walk(node.heatmap, depth + 1); + if (node.contributions != null) walk(node.contributions, depth + 1); + if (node.days != null) walk(node.days, depth + 1); + + const keys = Object.keys(node); + for (const key of keys) { + if (/^\d{4}-\d{2}-\d{2}$/.test(key)) { + addEntry(key, node[key]); + } + } + }; + + walk(payload, 0); + + return Array.from(acc.entries()) + .map(([date, count]) => ({ date, count })) + .sort((a, b) => a.date.localeCompare(b.date)); +} + +async function getGiteaUserHeatmap({ token, url }) { + const base = normalizeBase(url); + if (!base) throw new Error('Invalid Gitea base URL'); + + const headers = { + Authorization: `token ${token}`, + 'User-Agent': 'Git-Manager-GUI' + }; + + const meRes = await axiosInstance.get(`${base}/api/v1/user`, { + headers, + timeout: 10000 + }); + + const username = meRes?.data?.login || meRes?.data?.username || meRes?.data?.name || null; + + const candidates = [ + `${base}/api/v1/user/heatmap`, + username ? `${base}/api/v1/users/${encodeURIComponent(username)}/heatmap` : null + ].filter(Boolean); + + let lastError = null; + for (const endpoint of candidates) { + try { + const response = await axiosInstance.get(endpoint, { + headers, + timeout: 12000, + validateStatus: () => true + }); + + if (response.status >= 200 && response.status < 300) { + return { + username, + endpoint, + entries: normalizeHeatmapEntries(response.data) + }; + } + + lastError = new Error(`Heatmap endpoint failed (${response.status}): ${endpoint}`); + } catch (e) { + lastError = e; + } + } + + throw lastError || new Error('No usable heatmap endpoint found'); +} + /** * Returns array of items for a directory or single item for file. * Each item includes name, path, type, size, download_url, sha (if present). @@ -926,6 +1064,7 @@ module.exports = { createRepoGitea, checkGiteaConnection, listGiteaRepos, + getGiteaUserHeatmap, getGiteaRepoContents, getGiteaFileContent, uploadGiteaFile,