Skip to content

Instantly share code, notes, and snippets.

@Far-Se
Created November 27, 2025 07:01
Show Gist options
  • Select an option

  • Save Far-Se/d1cbb6c9eca78ad2bd0138236edf6339 to your computer and use it in GitHub Desktop.

Select an option

Save Far-Se/d1cbb6c9eca78ad2bd0138236edf6339 to your computer and use it in GitHub Desktop.
GitHub Repo Lines of Code Analysis Tampermonkey Script
// ==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