Last active
November 8, 2018 16:50
-
-
Save victor-homyakov/39e1ad32984df37bcdbb28fc03bad349 to your computer and use it in GitHub Desktop.
GitHub code review helper
This file contains 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 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