Skip to content

Instantly share code, notes, and snippets.

@victor-homyakov
Last active November 8, 2018 16:50
Show Gist options
  • Save victor-homyakov/39e1ad32984df37bcdbb28fc03bad349 to your computer and use it in GitHub Desktop.
Save victor-homyakov/39e1ad32984df37bcdbb28fc03bad349 to your computer and use it in GitHub Desktop.
GitHub code review helper
// ==UserScript==
// @name GitHub code review helper
// @namespace https://github.com/victor-homyakov/
// @version 0.6.0
// @description Open/hide GitHub diff when clicking on diff header. Open/hide all diffs with the same extension when ctrl-clicking on diff file name.
// @author Victor Homyakov
// @copyright 2013+, Victor Homyakov
// @include https://github.com/*/*/commit/*
// @include https://github.com/*/*/pull/*
// @include https://github.com/*/*/compare/*
// @grant none
// ==/UserScript==
// https://gist.github.com/victor-homyakov/39e1ad32984df37bcdbb28fc03bad349
var state;
var STORAGE_PREFIX = 'GH helper ';
function getStorageKey() {
return STORAGE_PREFIX + location.pathname.replace(/\/files$/, '');
}
function getState(key) {
var state = localStorage.getItem(key || getStorageKey()) || '{}';
return JSON.parse(state) || {};
}
function setState(state) {
try {
state.timestamp = Date.now();
localStorage.setItem(getStorageKey(), JSON.stringify(state));
} catch (e) {
console.error('Cannot save state to localStorage', e);
}
}
function isStateOlderThan(key, timestamp) {
var state = getState(key);
return (state.timestamp || 0) < timestamp;
}
function purgeOldState() {
var expirationTimestamp = Date.now() - 1000 * 60 * 60 * 24 * 365;
Object.keys(localStorage).filter(function (k) {
return k.startsWith(STORAGE_PREFIX) && isStateOlderThan(k, expirationTimestamp);
}).forEach(function (k) {
console.log('Remove from localStorage old state:', k);
localStorage.removeItem(k);
});
}
function hasClass(/*HTMLElement*/element, className) {
return element && element.classList && element.classList.contains(className);
}
function isDiffHeader(element) {
return hasClass(element, 'file-header');
}
function isDiffFileName(/*HTMLElement*/element) {
return element && element.tagName === 'A' &&
element.href && element.title &&
isDiffHeader(element.parentElement.parentElement);
}
function isDiffElement(element) {
return hasClass(element, 'file') && hasClass(element, 'js-details-container');
}
var HIDE_CLASS = 'hide-diff-content';
function showDiff(diffElement) {
diffElement.classList.remove(HIDE_CLASS);
diffElement.classList.add('Details--on');
}
function hideDiff(diffElement) {
diffElement.classList.add(HIDE_CLASS);
diffElement.classList.remove('Details--on');
}
function toggleDiffContent(diffElement, options) {
options = options || {};
var isCollapsed = hasClass(diffElement, HIDE_CLASS),
id = diffElement.id;
if (options.collapse === false || (isCollapsed && options.collapse !== true)) {
showDiff(diffElement);
delete state[id];
setState(state);
} else if (options.collapse === true || (!isCollapsed && options.collapse !== false)) {
hideDiff(diffElement);
if (id) {
state[id] = 1;
setState(state);
}
if (options.scrollIntoView) {
if (diffElement.scrollIntoViewIfNeeded) {
diffElement.scrollIntoViewIfNeeded();
} else {
diffElement.scrollIntoView();
}
}
}
}
function toggleDiff(target, options) {
// click on .file-header, .file-header *, .file.js-details-container
if (isDiffElement(target)) {
toggleDiffContent(target, options);
return;
}
while (target) {
if (hasClass(target, 'file-actions')) {
return;
}
var parent = target.parentElement;
if (isDiffHeader(target) && isDiffElement(parent)) {
toggleDiffContent(parent, options);
return;
}
target = parent;
}
}
function hideDiffWithName(element) {
element.closest('.Details').classList.add('hide-diff-with-name');
}
var insertRule = (function() {
var style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(''));
document.head.appendChild(style);
var i = 0;
return function(rule) {
style.sheet.insertRule(rule, i++);
};
})();
insertRule('.file.js-details-container.Details--on:after { ' +
'background-color: #f7f7f7; ' +
'border-top: 1px solid #d8d8d8; ' +
'color: #d8d8d8; ' +
'content: "click to collapse"; ' +
'display: block; ' +
'padding: 5px 10px;' +
' }');
insertRule('.hide-diff-content .image, .hide-diff-content .data, .hide-diff-content .render-wrapper, .hide-diff-content .js-diff-load-container { ' +
'display: none; ' +
' }');
insertRule('.hide-diff-with-name { ' +
'display: none; ' +
' }');
insertRule('.Popover-message { ' +
'width: 303px; ' +
' }');
state = getState();
setTimeout(purgeOldState, 0);
function applyDiffState() {
console.log('GH helper: apply diff state', state);
for (var key in state) {
if (state.hasOwnProperty(key)) {
var diffElement = document.getElementById(key);
if (diffElement) {
hideDiff(diffElement);
}
}
}
}
var COLLAPSE_DIFFS = true;
function createButton(id, text, tooltip) {
return '<button type="button" id="' + id + '"' +
' class="btn btn-sm btn-outline BtnGroup-item tooltipped tooltipped-s"' +
' aria-label="' + tooltip + '">' + text + '</button>';
}
function addButtonsToDiffBar() {
var cb = document.querySelector('.diffbar-item #whitespace-cb');
if (cb) {
var buttonsHtml =
'<div class="BtnGroup d-flex flex-content-stretch js-diff-style-toggle">' +
createButton('CollapseDeleted', '-del', 'Collapse all deleted files') +
createButton('HideGz', 'Hide .gz', 'Hide all *.json.gz files') +
createButton('HidePng', 'Hide .png', 'Hide all *.png images') +
createButton('HideI18n', 'Hide i18n', 'Hide all *.*-i18n/*.js files') +
'</div>';
cb.insertAdjacentHTML('beforebegin', buttonsHtml);
}
}
function initInterface() {
var filesElement = document.querySelector('#files:not(.gh-helper-applied)');
if (filesElement) {
console.log('GH helper: add buttons to panel', filesElement);
filesElement.classList.add('gh-helper-applied');
addButtonsToDiffBar();
}
var details = document.querySelector('#files .js-diff-progressive-container:not(.gh-helper-applied) .Details');
var container = details && details.closest('.js-diff-progressive-container');
// <img alt="" class="js-diff-progressive-spinner" height="64" src="https://github.yandex-team.ru/images/spinners/octocat-spinner-128.gif" width="64">
if (container) {
console.log('GH helper: process progressive container', container);
container.classList.add('gh-helper-applied');
applyDiffState();
}
}
initInterface();
setInterval(initInterface, 2000);
document.body.addEventListener('click', function (/*Event*/event) {
var /*HTMLElement*/element = event.target;
if ((event.ctrlKey || event.metaKey) && isDiffFileName(element)) {
event.preventDefault();
event.stopPropagation();
var fileExt = element.title.replace(/^.*(\.\w+)$/, '$1');
var similarFileNames = document.querySelectorAll('.file-header a[title$="' + fileExt + '"]');
for (var i = 0; i < similarFileNames.length; i++) {
toggleDiff(similarFileNames[i], {collapse: COLLAPSE_DIFFS, scrollIntoView: false});
}
COLLAPSE_DIFFS = !COLLAPSE_DIFFS;
} else if (element.id === 'CollapseDeleted') {
Array.from(document.querySelectorAll('.Details:not(.' + HIDE_CLASS + ') .diffstat[aria-label]'))
.filter(diffStat => diffStat.getAttribute('aria-label').startsWith('0 additions'))
.forEach(diffStat => toggleDiff(diffStat, {collapse: true}));
} else if (element.id === 'HideGz') {
document.querySelectorAll('a[title$=".json.gz"]').forEach(e => hideDiffWithName(e));
} else if (element.id === 'HidePng') {
document.querySelectorAll('a[title$=".png"]').forEach(e => hideDiffWithName(e));
} else if (element.id === 'HideI18n') {
document.querySelectorAll('a[title*=".priv-i18n/"], a[title*=".js-i18n/"]').forEach(e => hideDiffWithName(e));
} else {
toggleDiff(element, {scrollIntoView: true});
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment