// ==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 = ` calculating... repo size `; // 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(); })();