Move size to About section, add Git Trees fallback for 0KB repos, and enhance GH_PAT secret retrieval

This commit is contained in:
2026-09-08 14:19:24 -07:00
parent 41a728ce19
commit 7d36fda419
2 changed files with 94 additions and 49 deletions

View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Display Repo Size for GitHub // @name Display Repo Size for GitHub
// @version 1.0.0 // @version 1.1.0
// @description Displays the total repository size on GitHub repository pages (supports public & private repos via GH_PAT). // @description Displays repository size inside the About section on GitHub (supports public & private repos using GH_PAT).
// @author OpenScript // @author OpenScript
// @match https://github.com/*/* // @match https://github.com/*/*
// @grant none // @grant none
@@ -11,86 +11,132 @@
'use strict'; 'use strict';
const cache = new Map(); const cache = new Map();
const BADGE_ID = 'openscript-repo-size'; const ROW_ID = 'openscript-repo-size-about';
// Format KB to readable size // Format bytes to human readable format
const formatBytes = kb => { const formatSize = bytes => {
if (kb < 1024) return `${kb} KB`; const kb = bytes / 1024;
if (kb < 1024) return `${kb.toFixed(1)} KB`;
if (kb < 1024 * 1024) return `${(kb / 1024).toFixed(1)} MB`; if (kb < 1024 * 1024) return `${(kb / 1024).toFixed(1)} MB`;
return `${(kb / (1024 * 1024)).toFixed(2)} GB`; return `${(kb / (1024 * 1024)).toFixed(2)} GB`;
}; };
// Parse owner & repo from path // Extract owner & repo
const getRepoInfo = () => { const getRepoInfo = () => {
const [, owner, repo] = location.pathname.split('/'); const [, owner, repo] = location.pathname.split('/');
const reserved = new Set(['settings', 'orgs', 'organizations', 'notifications', 'search', 'features', 'pricing', 'explore']); const reserved = new Set(['settings', 'orgs', 'organizations', 'notifications', 'search', 'features', 'pricing', 'explore', 'marketplace']);
return (owner && repo && !reserved.has(owner)) ? { owner, repo } : null; return (owner && repo && !reserved.has(owner)) ? { owner, repo } : null;
}; };
// Fetch size from GitHub API with optional GH_PAT // 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 fetchRepoSize = async (owner, repo) => {
const key = `${owner}/${repo}`; const key = `${owner}/${repo}`;
if (cache.has(key)) return cache.get(key); if (cache.has(key)) return cache.get(key);
const token = window.OpenScript?.env?.GH_PAT || window.env?.GH_PAT; const pat = getPat();
const headers = { Accept: 'application/vnd.github.v3+json' }; const headers = { Accept: 'application/vnd.github.v3+json' };
if (token) headers.Authorization = `Bearer ${token}`; if (pat) headers.Authorization = `Bearer ${pat.trim()}`;
try { try {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers }); const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers });
if (!res.ok) return res.status === 404 ? 'private/missing GH_PAT' : 'error'; 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(); const data = await res.json();
const formatted = formatBytes(data.size); 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); cache.set(key, formatted);
return formatted; return formatted;
} catch { } catch {
return null; return 'failed to load';
} }
}; };
// Inject or update size badge // Find the GitHub "About" section in sidebar
const updateBadge = async () => { 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(); const info = getRepoInfo();
if (!info) return; if (!info) return;
// Anchor locations on GitHub repo pages // Clean up any old top badge if present
const anchor = document.querySelector('.file-navigation') || document.getElementById('openscript-repo-size')?.remove();
document.querySelector('[data-testid="latest-commit-details"]') ||
document.querySelector('.BorderGrid-cell .d-flex') ||
document.querySelector('#repository-container-header ul');
if (!anchor || document.getElementById(BADGE_ID)) return; const container = findAboutContainer();
if (!container || document.getElementById(ROW_ID)) return;
const badge = document.createElement('div'); const row = document.createElement('div');
badge.id = BADGE_ID; row.id = ROW_ID;
badge.className = 'd-inline-flex flex-items-center mr-2 px-2 py-1 text-bold text-small rounded-2 border color-border-default color-bg-subtle'; row.className = 'mt-2 text-small color-fg-muted d-flex flex-items-center';
badge.style.cssText = 'align-self: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;'; row.innerHTML = `
badge.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">
<svg aria-hidden="true" height="14" viewBox="0 0 16 16" width="14" class="octicon octicon-database mr-1 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> <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> </svg>
<span class="size-text color-fg-default">calculating...</span> <span><strong class="size-val color-fg-default font-semibold">calculating...</strong> repo size</span>
`; `;
anchor.prepend(badge); // 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 size = await fetchRepoSize(info.owner, info.repo);
const sizeSpan = badge.querySelector('.size-text'); const valEl = row.querySelector('.size-val');
if (sizeSpan) sizeSpan.textContent = size ? size : 'unknown'; if (valEl) valEl.textContent = size;
}; };
// Re-run on Turbo navigation & DOM mutations // Listen to GitHub SPA navigation events
['turbo:load', 'turbo:render', 'pjax:end', 'popstate'].forEach(ev => ['turbo:load', 'turbo:render', 'pjax:end', 'popstate'].forEach(ev =>
window.addEventListener(ev, () => { window.addEventListener(ev, () => {
document.getElementById(BADGE_ID)?.remove(); document.getElementById(ROW_ID)?.remove();
updateBadge(); updateAboutSize();
}) })
); );
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
if (!document.getElementById(BADGE_ID)) updateBadge(); if (!document.getElementById(ROW_ID)) updateAboutSize();
}); });
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
updateBadge(); updateAboutSize();
})(); })();

View File

@@ -1,24 +1,23 @@
# Display Repo Size for GitHub (OpenScript Userscript) # Display Repo Size for GitHub (OpenScript Userscript)
A lightweight OpenScript userscript that displays the total repository size directly on GitHub repository pages (for both public and private repositories). A lightweight OpenScript userscript that displays the total repository size directly in the **About** section on GitHub repository pages (for both public and private repositories).
## Features ## Features
- Displays total repo size (formatted in KB, MB, or GB) next to the file navigation header. - **Integrated in About Section**: Injects cleanly under repository details in the right sidebar (e.g. `📦 673.9 KB repo size`).
- Works seamlessly on public and private repositories. - **Accurate Size Detection**: Automatically detects when GitHub's API returns `0 KB` on newly created repositories and falls back to calculating the total content size via the Git Trees API.
- Integrates with OpenScript's synced secrets: uses `GH_PAT` (GitHub Personal Access Token) to authenticate API requests, unlock private repo access, and elevate the rate limit from 60 to 5,000 req/hr. - **Private & Public Repositories**: Works on public repositories out-of-the-box and uses `GH_PAT` from OpenScript secrets for private repositories.
- Supports GitHub's Turbo and SPA client-side navigations. - **Turbo / SPA Compatible**: Seamlessly persists across GitHub's Turbo and client-side page transitions.
## Installation in OpenScript ## Installation in OpenScript
1. Open the **OpenScript** extension popup. 1. Open the **OpenScript** extension popup.
2. Click **+ New** in the header. 2. Click **+ New** in the header.
3. Copy and paste the code from [`DisplayRepoSizeGitHub.user.js`](./DisplayRepoSizeGitHub.user.js) into the editor. 3. Paste the contents of [`DisplayRepoSizeGitHub.user.js`](./DisplayRepoSizeGitHub.user.js).
4. Click **save script**. 4. Click **save script**.
## Private Repos & GitHub PAT Configuration ## GitHub PAT Configuration (for Private Repos)
To view the size of private repositories:
1. Generate a GitHub Personal Access Token (`repo` scope for private repos) at [github.com/settings/tokens](https://github.com/settings/tokens). 1. Generate a GitHub Personal Access Token (`repo` scope for private repos) at [github.com/settings/tokens](https://github.com/settings/tokens).
2. Open **OpenScript** and switch to the **Secrets** tab. 2. Open **OpenScript** and switch to the **Secrets** tab.
3. Add a secret with: 3. Add a secret:
- **Key**: `GH_PAT` - **Key**: `GH_PAT`
- **Value**: `<your_personal_access_token>` - **Value**: `<your_token>`
4. Click **+ Add**. The secret will sync across your devices via `chrome.storage.sync` and will be automatically available to the script. 4. Click **+ Add**. OpenScript synchronizes the secret via `chrome.storage.sync` and securely provides it to your script as `OpenScript.env.GH_PAT`.