🏠 

GitHub Diff Files Filter

A userscript that adds filters that toggle diff & PR folders, and files by extension


Install this script?
// ==UserScript==
// @name        GitHub Diff Files Filter
// @version     2.1.5
// @description A userscript that adds filters that toggle diff & PR folders, and files by extension
// @license     MIT
// @author      Rob Garrison
// @namespace   https://github.com/Mottie
// @match       https://github.com/*
// @run-at      document-idle
// @grant       GM_addStyle
// @require     https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
// @icon        https://github.githubassets.com/pinned-octocat.svg
// @supportURL  https://github.com/Mottie/GitHub-userscripts/issues
// ==/UserScript==
(() => {
"use strict";
// Example page: https://github.com/julmot/mark.js/pull/250/files
GM_addStyle(".gdf-extension-hidden, .gdf-folder-hidden { display: none; }");
const allLabel = "\u00ABall\u00BB",
rootLabel = "\u00ABroot\u00BB",
noExtLabel = "\u00ABno-ext\u00BB",
dotExtLabel = "\u00ABdot-files\u00BB",
renameFileLabel = "\u00ABrenamed\u00BB",
minFileLabel = "\u00ABmin\u00BB";
let exts = {};
let folders = {};
function toggleBlocks({subgroup, type, show}) {
if (type === allLabel) {
// Toggle "all" blocks
$$("#files div[id*='diff']").forEach(el => {
el.classList.toggle(`gdf-${subgroup}-hidden`, !show);
});
// update filter buttons
$$(`#files .gdf-${subgroup}-filter a`).forEach(el => {
el.classList.toggle("selected", show);
});
} else if (subgroup === "folder") {
Object.keys(folders)
.reduce((acc, folder) => {
if (folders[folder].length && !folder.includes("→")) {
acc.push({
folder,
show: $(`.gdf-folder-filter a[data-item=${folder}]`).classList.contains("selected")
});
}
return acc;
}, [])
// sort show:true to the end; to fix hiding files that should be shown
.sort((a, b) => {
if (a.show && b.show) {
return 0;
}
return a.show && !b.show ? 1 : -1;
})
.forEach(({folder, show}) => {
toggleGroup({group: folders[folder], subgroup, show });
});
} else if (exts[type]) {
toggleGroup({group: exts[type], subgroup, show});
}
updateAllButton(subgroup);
}
function toggleGroup({group, subgroup, show}) {
const files = $("#files");
/* group contains an array of div ids used to target the
* hidden link added immediately above each file div container
* <a name="diff-xxxxx"></a>
* <div id="diff-#" class="file js-file js-details container">
*/
group.forEach(id => {
const file = $(`#${id}`, files);
if (file) {
file.classList.toggle(`gdf-${subgroup}-hidden`, !show);
}
});
}
function updateAllButton(subgroup) {
const buttons = $(`#files .gdf-${subgroup}-filter`),
filters = $$(`a:not(.gdf-${subgroup}-all)`, buttons),
selected = $$(`a:not(.gdf-${subgroup}-all).selected`, buttons);
// set "all" button
$(`.gdf-${subgroup}-all`, buttons).classList.toggle(
"selected",
filters.length === selected.length
);
}
function getSHA(file) {
return file.hash
// #toc points to "a"
? file.hash.slice(1)
// .pr-toolbar points to "a > div > div.filename"
: file.closest("a").hash.slice(1);
}
function buildList() {
exts = {};
folders = {};
// make noExtLabel the first element in the object
exts[noExtLabel] = [];
exts[dotExtLabel] = [];
exts[renameFileLabel] = [];
exts[minFileLabel] = [];
folders[rootLabel] = [];
// TOC in file diffs and pr-toolbar in Pull requests
$$(".file-header .file-info > a").forEach(file => {
let txt = (file.title || file.textContent || "").trim();
if (txt) {
const path = txt.split("/");
const filename = path.splice(-1)[0];
// test for no extension, then get extension name
// regexp from https://github.com/silverwind/file-extension
let ext = /\./.test(filename) ? /[^./\\]*$/.exec(filename)[0] : noExtLabel;
const min = /\.min\./.test(filename);
// Add filter for renamed files: {old path} → {new path}
if (txt.indexOf(" → ") > -1) {
ext = renameFileLabel;
} else if (ext === filename.slice(1)) {
ext = dotExtLabel;
}
const sha = getSHA(file);
if (ext) {
if (!exts[ext]) {
exts[ext] = [];
}
exts[ext].push(sha);
if (min) {
exts[minFileLabel].push(sha);
}
}
if (path.length > 0) {
path.forEach(folder => {
if (!folders[folder]) {
folders[folder] = [];
}
folders[folder].push(sha);
});
} else {
folders[rootLabel].push(sha);
}
}
});
}
function makeFilter({subgroup, label}) {
const files = $("#files");
let filters = 0;
const group = subgroup === "folder" ? folders : exts;
const keys = Object.keys(group);
let html = `${label}: <div class="BtnGroup gdf-${subgroup}-filter">`;
const btnClass = "btn btn-sm selected BtnGroup-item tooltipped tooltipped-n";
// get length, but don't count empty arrays
keys.forEach(item => {
filters += group[item].length > 0 ? 1 : 0;
});
// Don't bother showing the filter if only one extension is found
if (files && filters > 1) {
filters = $(`.gdf-${subgroup}-filter-wrapper`);
if (!filters) {
filters = document.createElement("p");
filters.className = `gdf-${subgroup}-filter-wrapper`;
files.insertBefore(filters, files.firstChild);
filters.addEventListener("click", event => {
if (event.target.nodeName === "A") {
event.preventDefault();
event.stopPropagation();
const el = event.target;
el.classList.toggle("selected");
toggleBlocks({
subgroup: el.dataset.subgroup,
type: el.textContent.trim(),
show: el.classList.contains("selected")
});
}
});
}
// add a filter "all" button to the beginning
html += `
<a class="${btnClass} gdf-${subgroup}-all" data-subgroup="${subgroup}" data-item="${allLabel}" aria-label="Toggle all files" href="#">
${allLabel}
</a>`;
keys.forEach(item => {
if (group[item].length) {
html += `
<a class="${btnClass}" aria-label="${group[item].length}" data-subgroup="${subgroup}" data-item="${item}" href="#">
${item}
</a>`;
}
});
// prepend filter buttons
filters.innerHTML = html + "</div>";
}
}
function init() {
if ($("#files.diff-view") || $(".pr-toolbar")) {
buildList();
makeFilter({subgroup: "folder", label: "Filter file folder"});
makeFilter({subgroup: "extension", label: "Filter file extension"});
}
}
function $(str, el) {
return (el || document).querySelector(str);
}
function $$(str, el) {
return [...(el || document).querySelectorAll(str)];
}
document.addEventListener("ghmo:container", init);
document.addEventListener("ghmo:diff", init);
init();
})();