// ==UserScript==
// @name GitHub PullRequests Plus
// @author AZ
// @description display diff stats next to each pull request in the list view
// @downloadURL https://raw.githubusercontent.com/hamidzr/user-scripts/refs/heads/master/userscripts/github.com/github-plus.user.js
// @grant none
// @match https://github.com/*/*/pulls
// @match https://github.com/*/*/pulls/*
// @namespace https://latentbyte.com/products
// @run-at document-idle
// @updateURL https://raw.githubusercontent.com/hamidzr/user-scripts/refs/heads/master/userscripts/github.com/github-plus.user.js
// @version 0.4.0
// ==/UserScript==
'use strict';
(() => {
// src/lib/dom.ts
var waitForEl = (selector, timeoutMs = 5000) => {
return new Promise((resolve, reject) => {
const el = document.querySelector(selector);
if (el)
return resolve(el);
const endTime = Date.now() + timeoutMs;
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
resolve(el);
} else if (Date.now() > endTime) {
observer.disconnect();
reject(new Error(`waitForEl("${selector}") timed out after ${timeoutMs}ms`));
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
const el = document.querySelector(selector);
if (el)
resolve(el);
else
reject(new Error(`waitForEl("${selector}") timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
};
var injectCSS = (id, css) => {
let style = document.getElementById(id);
if (style) {
style.textContent = css;
return style;
}
style = document.createElement("style");
style.id = id;
style.textContent = css;
(document.head ?? document.documentElement).appendChild(style);
return style;
};
// src/github.com/github-plus.user.ts
var STYLE_ID = "gh-plus-styles";
var CSS = `
.gh-plus-diffstat {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 6px;
font-size: 12px;
font-weight: 600;
vertical-align: middle;
}
.gh-plus-additions { color: var(--fgColor-success, #1a7f37); }
.gh-plus-deletions { color: var(--fgColor-danger, #d1242f); }
`;
var MARKER = "data-gh-plus-diffstat";
var sels = {
prLinks: 'div.js-issue-row a[href*="/pull/"].js-navigation-open'
};
var fetchDiffStats = async (prUrl) => {
const resp = await fetch(`${prUrl}/files`, {
headers: { Accept: "application/json", "X-Requested-With": "XMLHttpRequest" }
});
if (!resp.ok)
throw new Error(`failed to fetch ${prUrl}/files: ${resp.status}`);
const json = await resp.json();
const summaries = json.payload.pullRequestsChangesRoute.diffSummaries;
let additions = 0;
let deletions = 0;
for (const f of summaries) {
additions += f.linesAdded;
deletions += f.linesDeleted;
}
return { additions, deletions };
};
var formatNum = (n) => n.toLocaleString();
var createBadge = (additions, deletions) => {
const badge = document.createElement("span");
badge.className = "gh-plus-diffstat";
badge.innerHTML = `+${formatNum(additions)}` + `-${formatNum(deletions)}`;
return badge;
};
var setupPrPage = () => {
document.querySelectorAll(sels.prLinks).forEach((a) => {
if (a.hasAttribute(MARKER))
return;
a.setAttribute(MARKER, "");
fetchDiffStats(a.href).then(({ additions, deletions }) => {
a.append(createBadge(additions, deletions));
}).catch(console.error);
});
};
var main = async () => {
injectCSS(STYLE_ID, CSS);
await waitForEl(sels.prLinks, 6000).catch(() => null);
setupPrPage();
};
main();
})();