Files
GitHub.openscript/DisplayRepoSizeGitHub.user.js

156 lines
5.6 KiB
JavaScript

// ==UserScript==
// @name Display Repo Size for GitHub
// @version 1.3.0
// @description Displays total repository size (including full git history) inside the About section on GitHub using GH_PAT.
// @author OpenScript
// @match https://github.com/*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const cache = new Map();
const ROW_ID = 'openscript-repo-size-about';
// Format KB to readable size
const formatKb = kb => {
if (kb <= 0) return '0 KB (calculating...)';
if (kb < 1024) return `${kb} KB`;
if (kb < 1024 * 1024) return `${(kb / 1024).toFixed(1)} MB`;
return `${(kb / (1024 * 1024)).toFixed(2)} GB`;
};
// Extract owner & repo from path
const getRepoInfo = () => {
const [, owner, repo] = location.pathname.split('/');
const reserved = new Set([
'settings', 'orgs', 'organizations', 'notifications', 'search',
'features', 'pricing', 'explore', 'marketplace', 'topics', 'trending'
]);
return (owner && repo && !reserved.has(owner)) ? { owner, repo } : null;
};
// Safely retrieve GH_PAT from OpenScript environment
const getPat = () => {
try {
const envObj = (typeof OpenScript !== 'undefined' && OpenScript?.env) ||
(typeof env !== 'undefined' && env) ||
window.OpenScript?.env ||
window.env ||
{};
for (const [k, v] of Object.entries(envObj)) {
if (/^(GH_PAT|GITHUB_PAT|GITHUB_TOKEN|PAT)$/i.test(k) && v) return String(v).trim();
}
if (typeof GM_getValue === 'function') {
const gm = GM_getValue('GH_PAT') || GM_getValue('gh_pat');
if (gm) return String(gm).trim();
}
} catch {
// ignore
}
return null;
};
// Fetch full repository size from GitHub API (no blobs API fallback)
const fetchRepoSize = async (owner, repo) => {
const key = `${owner}/${repo}`;
if (cache.has(key)) return cache.get(key);
const pat = getPat();
const headers = { Accept: 'application/vnd.github.v3+json' };
if (pat) headers.Authorization = `Bearer ${pat}`;
try {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers });
if (!res.ok) {
if (res.status === 404) return { text: 'private (missing GH_PAT)', title: 'Set GH_PAT secret in OpenScript for private repository access' };
if (res.status === 401) return { text: 'invalid GH_PAT', title: 'GH_PAT token was rejected by GitHub API' };
return { text: `API error (${res.status})`, title: `GitHub API returned ${res.status}` };
}
const data = await res.json();
const kb = data.size ?? 0;
const isZero = kb === 0;
const result = {
text: formatKb(kb),
title: isZero
? 'GitHub is still calculating disk usage for this new/recent repo. Check back in a few minutes.'
: `Total repository disk usage (including full git history): ${kb.toLocaleString()} KB`
};
if (!isZero) cache.set(key, result);
return result;
} catch {
return { text: 'failed to load', title: 'Network or fetch error' };
}
};
// Locate the GitHub "About" heading (works on React and classic GitHub pages)
const findAboutHeading = () => {
for (const h of document.querySelectorAll('h2')) {
if (/^\s*About\s*$/i.test(h.textContent.trim())) return h;
}
return null;
};
// Insert or update size row in About section
const updateAboutSize = async () => {
const info = getRepoInfo();
if (!info) return;
// Clean up any legacy badges
document.getElementById('openscript-repo-size')?.remove();
if (document.getElementById(ROW_ID)) return;
const heading = findAboutHeading();
if (!heading) return;
const row = document.createElement('div');
row.id = ROW_ID;
row.className = 'mt-2 text-small color-fg-muted d-flex flex-items-center';
row.innerHTML = `
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" width="16" class="octicon octicon-database mr-2 color-fg-muted" fill="currentColor">
<path d="M1 3.5c0-.83.67-1.5 1.5-1.5h11c.83 0 1.5.67 1.5 1.5v9c0 .83-.67 1.5-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9Zm1.5-.5a.5.5 0 0 0-.5.5V5h12V3.5a.5.5 0 0 0-.5-.5h-11ZM14 6H2v2h12V6Zm0 3H2v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9Z"></path>
</svg>
<span><strong class="size-val color-fg-default font-semibold">calculating...</strong> repo size</span>
`;
// Place directly after description / about heading
const sibling = heading.nextElementSibling;
if (sibling) sibling.insertAdjacentElement('afterend', row);
else heading.insertAdjacentElement('afterend', row);
const sizeInfo = await fetchRepoSize(info.owner, info.repo);
const valEl = row.querySelector('.size-val');
if (valEl) {
valEl.textContent = sizeInfo.text;
if (sizeInfo.title) row.setAttribute('title', sizeInfo.title);
}
};
// Navigation handlers & periodic check during dynamic React sidebar hydration
const run = () => {
document.getElementById(ROW_ID)?.remove();
updateAboutSize();
let count = 0;
const timer = setInterval(() => {
if (document.getElementById(ROW_ID) || ++count > 10) clearInterval(timer);
else updateAboutSize();
}, 250);
};
['turbo:load', 'turbo:render', 'pjax:end', 'popstate'].forEach(ev =>
window.addEventListener(ev, run)
);
const observer = new MutationObserver(() => {
if (!document.getElementById(ROW_ID)) updateAboutSize();
});
observer.observe(document.body, { childList: true, subtree: true });
run();
})();