// ==UserScript== // @name Display Repo Size for GitHub // @version 1.1.0 // @description Displays repository size inside the About section on GitHub (supports public & private repos 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 bytes to human readable format const formatSize = bytes => { const kb = bytes / 1024; if (kb < 1024) return `${kb.toFixed(1)} KB`; if (kb < 1024 * 1024) return `${(kb / 1024).toFixed(1)} MB`; return `${(kb / (1024 * 1024)).toFixed(2)} GB`; }; // Extract owner & repo const getRepoInfo = () => { const [, owner, repo] = location.pathname.split('/'); const reserved = new Set(['settings', 'orgs', 'organizations', 'notifications', 'search', 'features', 'pricing', 'explore', 'marketplace']); return (owner && repo && !reserved.has(owner)) ? { owner, repo } : null; }; // Safely retrieve GH_PAT from OpenScript environment const getPat = () => { const secrets = (typeof OpenScript !== 'undefined' ? OpenScript?.env : null) || (typeof env !== 'undefined' ? env : null) || window.OpenScript?.env || window.env || {}; return secrets.GH_PAT || secrets.gh_pat || secrets.GITHUB_PAT || secrets.GITHUB_TOKEN || null; }; // Fetch repository size from GitHub API (with fallback to recursive Git Trees API when size is 0) 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.trim()}`; try { const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers }); if (!res.ok) { if (res.status === 404) return pat ? 'repo not found' : 'private (set GH_PAT)'; if (res.status === 401) return 'invalid GH_PAT'; return `API error (${res.status})`; } const data = await res.json(); let bytes = (data.size || 0) * 1024; // GitHub async calculation fallback: if size is 0, sum blob sizes via Git Trees API if (bytes <= 0) { const branch = data.default_branch || 'main'; const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`, { headers }); if (treeRes.ok) { const treeData = await treeRes.json(); bytes = (treeData.tree || []).reduce((acc, item) => acc + (item.size || 0), 0); } } const formatted = formatSize(bytes); cache.set(key, formatted); return formatted; } catch { return 'failed to load'; } }; // Find the GitHub "About" section in sidebar const findAboutContainer = () => { // Priority 1: First BorderGrid-cell containing About header or in right sidebar const cells = document.querySelectorAll('.Layout-sidebar .BorderGrid-cell, .BorderGrid-cell'); for (const cell of cells) { const h2 = cell.querySelector('h2'); if (h2 && /About/i.test(h2.textContent)) return cell; } // Priority 2: Standard first cell in sidebar return document.querySelector('.Layout-sidebar .BorderGrid-row:first-child .BorderGrid-cell') || document.querySelector('[data-testid="about-section"]') || document.querySelector('.Layout-sidebar section'); }; // Render or update the size element inside the About section const updateAboutSize = async () => { const info = getRepoInfo(); if (!info) return; // Clean up any old top badge if present document.getElementById('openscript-repo-size')?.remove(); const container = findAboutContainer(); if (!container || document.getElementById(ROW_ID)) 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 `; // Insert after description / before bottom stats const heading = container.querySelector('h2'); if (heading && heading.nextElementSibling) { heading.parentNode.insertBefore(row, heading.nextElementSibling.nextElementSibling || heading.nextElementSibling); } else { container.appendChild(row); } const size = await fetchRepoSize(info.owner, info.repo); const valEl = row.querySelector('.size-val'); if (valEl) valEl.textContent = size; }; // Listen to GitHub SPA navigation events ['turbo:load', 'turbo:render', 'pjax:end', 'popstate'].forEach(ev => window.addEventListener(ev, () => { document.getElementById(ROW_ID)?.remove(); updateAboutSize(); }) ); const observer = new MutationObserver(() => { if (!document.getElementById(ROW_ID)) updateAboutSize(); }); observer.observe(document.body, { childList: true, subtree: true }); updateAboutSize(); })();