Created
November 27, 2025 07:01
-
-
Save Far-Se/d1cbb6c9eca78ad2bd0138236edf6339 to your computer and use it in GitHub Desktop.
GitHub Repo Lines of Code Analysis Tampermonkey Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name GitHub Repo LOC Analysis (Detailed) | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2.0 | |
| // @description Shows detailed lines of code breakdown (Files, Blanks, Comments, Code) and filters small languages. | |
| // @author You | |
| // @match https://github.com/*/* | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM_addStyle | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // CSS for the table | |
| GM_addStyle(` | |
| .loc-table { width: 100%; border-collapse: collapse; font-size: 12px; } | |
| .loc-table th, .loc-table td { padding: 4px 2px; white-space: nowrap; } | |
| .loc-table th { font-weight: 600; border-bottom: 1px solid var(--color-border-muted); text-align:left;} | |
| /* User specified alignment */ | |
| .loc-table td:not(:first-child), .loc-table th:not(:first-child) { text-align: right; color: var(--color-fg-muted); } | |
| /* Highlight Total Row */ | |
| .loc-total { background-color: var(--color-canvas-subtle);} | |
| .loc-container { border-top: 1px solid var(--color-border-muted); } | |
| .loc-table tr:not(.loc-total) td { color: var(--fgColor-muted) !important;} | |
| `); | |
| async function init() { | |
| // Prevent duplicate injection | |
| if (document.getElementById('loc-analysis-container')) return; | |
| // Get Repo Info | |
| const info = getRepoInfo(); | |
| if (!info) return; | |
| // Identify insertion point | |
| const target = findInjectionPoint(); | |
| if (!target) return; | |
| // Create Container | |
| target.insertAdjacentHTML('afterEnd',` | |
| <div class="BorderGrid-row"> | |
| <div class="BorderGrid-cell"> | |
| <div id="loc-analysis-container" class="loc-container"> | |
| <h3 class="h4 mb-2">Lines of Code</h3> | |
| <div class="text-small color-fg-muted">Loading analysis...</div> | |
| </div> | |
| </div> | |
| </div> | |
| `); | |
| // Insert after the target element | |
| const container = document.querySelector('#loc-analysis-container'); | |
| try { | |
| const data = await fetchLocData(info.user, info.repo, info.branch); | |
| renderTable(container, data); | |
| } catch (e) { | |
| container.innerHTML = `<h3 class="h4 mb-2">Lines of Code</h3><div class="text-small color-fg-danger">Error: ${e.message || 'API Limit or Network Error'}</div>`; | |
| } | |
| } | |
| function getRepoInfo() { | |
| const parts = window.location.pathname.split('/').filter(p => p); | |
| if (parts.length < 2) return null; | |
| const user = parts[0]; | |
| const repo = parts[1]; | |
| // Try to find the branch selector | |
| let branch = 'main'; | |
| const branchSelector = document.querySelector('[data-hotkey="w"] span') || document.querySelector('.ref-selector-shim'); | |
| if (branchSelector) { | |
| branch = branchSelector.innerText.trim(); | |
| } | |
| return { user, repo, branch }; | |
| } | |
| function findInjectionPoint() { | |
| // 1. Specific requested selector | |
| let el = document.querySelector('.about-margin[data-pjax] >div:last-child'); | |
| // 2. Fallback: Standard GitHub Sidebar Bottom | |
| if (!el) { | |
| const sidebar = document.querySelector('.Layout-sidebar .BorderGrid'); | |
| if (sidebar) el = sidebar.lastElementChild; | |
| } | |
| return el; | |
| } | |
| function fetchLocData(user, repo, branch) { | |
| return new Promise((resolve, reject) => { | |
| const url = `https://api.codetabs.com/v1/loc?github=${user}/${repo}&branch=${branch}`; | |
| GM_xmlhttpRequest({ | |
| method: "GET", | |
| url: url, | |
| onload: function(response) { | |
| if (response.status === 200) { | |
| try { | |
| const json = JSON.parse(response.responseText); | |
| resolve(json); | |
| } catch (e) { | |
| reject(new Error("Invalid JSON")); | |
| } | |
| } else { | |
| reject(new Error(response.statusText)); | |
| } | |
| }, | |
| onerror: function(err) { | |
| reject(err); | |
| } | |
| }); | |
| }); | |
| } | |
| function renderTable(container, data) { | |
| if (!Array.isArray(data)) { | |
| container.innerHTML = '<div class="text-small color-fg-muted">No data returned.</div>'; | |
| return; | |
| } | |
| // 1. Separate "Total" from languages | |
| const totalObj = data.find(item => item.language === 'Total'); | |
| let languages = data.filter(item => item.language !== 'Total' && item.lines > 0); | |
| // 2. Sort by Lines descending | |
| languages.sort((a, b) => b.lines - a.lines); | |
| const grandTotalLines = totalObj ? totalObj.lines : 0; | |
| // 3. Filter out languages < 3% of global total | |
| languages = languages.filter(lang => { | |
| const percentageOfRepo = (lang.lines / grandTotalLines) * 100; | |
| return percentageOfRepo >= 3; | |
| }); | |
| // Helper to calculate composition percentages (Blank vs Comment vs Code) | |
| const getComposistion = (item) => { | |
| if (!item.lines) return { blnk: 0, comm: 0, code: 0 }; | |
| return { | |
| blnk: ((item.blanks / item.lines) * 100).toFixed(0), | |
| comm: ((item.comments / item.lines) * 100).toFixed(0), | |
| code: ((item.linesOfCode / item.lines) * 100).toFixed(0) | |
| }; | |
| }; | |
| let html = ` | |
| <h3 class="h4 mb-2">Lines of Code</h3> | |
| <table class="loc-table"> | |
| <thead> | |
| <tr> | |
| <th>Lang</th> | |
| <th title="Total Lines">Lines</th> | |
| <th title="% Actual Code">Code</th> | |
| <th title="% Blanks">Blank</th> | |
| <th title="% Comments">Comms</th> | |
| <th>Files</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| `; | |
| // Render Total Row at the Top | |
| if (totalObj) { | |
| const comp = getComposistion(totalObj); | |
| html += ` | |
| <tr class="loc-total"> | |
| <td>Total</td> | |
| <td>${formatNumber(totalObj.lines)}</td> | |
| <td>${comp.code}%</td> | |
| <td>${comp.blnk}%</td> | |
| <td>${comp.comm}%</td> | |
| <td>${formatNumber(totalObj.files)}</td> | |
| </tr> | |
| `; | |
| } | |
| // Render Languages | |
| languages.forEach(lang => { | |
| const comp = getComposistion(lang); | |
| html += ` | |
| <tr> | |
| <td>${lang.language}</td> | |
| <td>${formatNumber(lang.lines)}</td> | |
| <td>${comp.code}%</td> | |
| <td>${comp.blnk}%</td> | |
| <td>${comp.comm}%</td> | |
| <td>${formatNumber(lang.files)}</td> | |
| </tr> | |
| `; | |
| }); | |
| html += `</tbody></table> | |
| <div class="text-small color-fg-muted mt-2" style="font-size:9.5px !important;"> | |
| * Showing languages > 3% of repo. <br> | |
| </div>`; | |
| container.innerHTML = html; | |
| } | |
| function formatNumber(num) { | |
| if (!num) return "0"; | |
| if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; | |
| if (num >= 1000) return (num / 1000).toFixed(1) + 'k'; | |
| return num.toString(); | |
| } | |
| // GitHub Turbo/PJAX listeners | |
| const observeDOM = () => { | |
| // Small timeout to ensure DOM settles | |
| setTimeout(init, 500); | |
| }; | |
| document.addEventListener('turbo:load', observeDOM); | |
| document.addEventListener('pjax:end', observeDOM); | |
| window.addEventListener('load', observeDOM); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment