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