Last active
January 25, 2022 14:17
-
-
Save josecanciani/2e6d1d0b9ce9889eb5ac480219f21e19 to your computer and use it in GitHub Desktop.
Gitlab Merge Request Changes file filtering
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 Gitlab Merge Request Changes file filtering | |
// @namespace https://github.com/josecanciani | |
// @include https://gitlab.com/*/-/merge_requests/* | |
// @include https://*gitlab*/-/merge_requests/* | |
// @version 10 | |
// @updateURL https://gist.githubusercontent.com/josecanciani/2e6d1d0b9ce9889eb5ac480219f21e19/raw/gitlab-merge-request-diff-file-filter.user.js | |
// @author Jose Luis Canciani | |
// @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html | |
// @description This script will provide a filter box to add Regexp filters for files in the list | |
// ==/UserScript== | |
(function() { | |
const scriptName = 'gitlab-merge-request-diff-file-filter.user.js'; | |
const enableDebug = false; | |
/** | |
* @typedef {Filter} Processed filter | |
* @property {Regexp} filter - The regular expression object to apply | |
* @property {String} filterText - The user entered text from where the filter was created | |
* @property {Boolean} positive - If any negative filter is matched, the file will be hidden, even if other positive filter is matched too | |
*/ | |
// simple debug method | |
const debug = function(what, name) { | |
if (!enableDebug) { | |
return; | |
} | |
if (typeof(what) === 'string' || typeof(what) === 'number' || typeof(what) === 'undefined' || what === null) { | |
console.log(scriptName + ': ' + (name ? name + ' is ' : '') + what); | |
} else { | |
console.log(scriptName + ': logging' + (name ? ' of ' + name : '') + ' starts here'); | |
console.log(what); | |
console.log(scriptName + ': logging' + (name ? ' of ' + name : '') + ' ends here'); | |
} | |
} | |
// assert we have a valid observer | |
const MyMutationObserver = this.MutationObserver || this.WebKitMutationObserver || (window ? window.MutationObserver || window.WebKitMutationObserver : null); | |
if (!MyMutationObserver) { | |
debug('MutationObserver not found, disabling plugin'); | |
return; | |
} | |
class App { | |
/** | |
* wait for the menu bar to appear before adding the button | |
*/ | |
start() { | |
this._waitForUrl(true); | |
} | |
_waitForUrl(init) { | |
if (window.location.href.match(/\/diffs/)) { | |
debug('/diffs URL detected'); | |
this._waitForMenu(); | |
} else { | |
init && debug('/diffs URL not found, waiting for it...'); | |
setTimeout(this._waitForUrl.bind(this), 1000); | |
} | |
} | |
_waitForMenu() { | |
this._buildAppIfMenuIsReady(); | |
debug('Starting observing DOM (waiting for menu)'); | |
this._observer = new MyMutationObserver(this._buildAppIfMenuIsReady.bind(this)); | |
this._observer.observe(document.body, {attributes:true, childList:true, subtree:true}); | |
} | |
_buildAppIfMenuIsReady() { | |
const menu = document.querySelector('div.diff-stats.is-compare-versions-header'); | |
if (!menu) { | |
return; | |
} | |
if (this._observer) { | |
debug('Stopping observing DOM'); | |
this._observer.disconnect(); | |
delete this._observer; | |
} | |
debug('Diffs menu found, starting the app'); | |
this._buildFilterForm(); | |
this._addButtonToMenuBar(menu); | |
} | |
_buildFilterForm() { | |
this._filterForm = document.createElement('div'); | |
this._filterForm.classList.add('btn'); | |
this._filterForm.style = 'text-align: left; width: 240px; height: 205px; position: absolute; z-index: 999999; background-color: white; cursor: unset; overflow: hidden; white-space: normal;'; | |
const filterTextArea = document.createElement('textarea'); | |
filterTextArea.style = 'width: 220px; height: 150px; margin-bottom: 6px;'; | |
filterTextArea.placeholder = '* use any text to match (* wildcards allowed).\n\n * for Regexps, start with re/ (don\'t end with /).\n\n* Start with - to negate.'; | |
filterTextArea.addEventListener('input', this._filterFiles.bind(this)); | |
this._filterForm.appendChild(filterTextArea); | |
const formButton = document.createElement('button'); | |
formButton.classList.add('btn'); | |
formButton.appendChild(document.createTextNode('Close')); | |
formButton.addEventListener('click', this._onFilterFormButtonClick.bind(this)); | |
this._filterForm.appendChild(formButton); | |
const orCheckbox = document.createElement('input'); | |
orCheckbox.type = 'checkbox'; | |
orCheckbox.value = 'OR'; | |
orCheckbox.style = 'vertical-align: middle; margin-left: 0.4em;'; | |
orCheckbox.addEventListener('click', this._filterFiles.bind(this)); | |
this._filterForm.appendChild(orCheckbox); | |
const orCheckboxLabel = document.createElement('label'); | |
orCheckboxLabel.appendChild(document.createTextNode('use OR')); | |
orCheckboxLabel.style = 'vertical-align: middle; margin-left: 0.4em; font-size: 0.9em; margin-bottom: 0; font-weight: normal;'; | |
this._filterForm.appendChild(orCheckboxLabel); | |
const resultSpan = document.createElement('span'); | |
resultSpan.classList.add('result'); | |
resultSpan.style = 'vertical-align: middle; margin-left: 1em; font-size: 0.9em; font-weight: bold;'; | |
this._filterForm.appendChild(resultSpan); | |
} | |
_onFilterFormButtonClick() { | |
debug('Form button clicked, closing popup'); | |
document.body.removeChild(this._filterForm); | |
} | |
_addButtonToMenuBar(menu) { | |
debug('Adding filter button'); | |
this._filterButton = document.createElement('img'); | |
this._filterButton.src = 'data:image/png;base64, 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'; | |
this._filterButton.classList.add('btn'); | |
this._filterButton.style = 'width: 34px; height: 34px; padding: 6px 6px;'; | |
this._filterButton.addEventListener('click', this._onFilterButtonClick.bind(this)); | |
const div = document.createElement('div'); | |
div.classList.add('diff-stats-group'); | |
div.appendChild(this._filterButton); | |
menu.appendChild(div); | |
} | |
_onFilterButtonClick(event) { | |
debug('Filter button clicked, opening popup'); | |
this._filterForm.style.left = event.clientX + 'px'; | |
this._filterForm.style.top = event.clientY + 'px'; | |
document.body.appendChild(this._filterForm); | |
this._filterForm.querySelector('textarea').focus(); | |
} | |
/** | |
* will hide files from gitlab diff window that do not match the given filters | |
*/ | |
_filterFiles() { | |
const filters = (this._filterForm.querySelector('textarea').value || '').trim().split(/\n/).map(this._buildRegexpFilter.bind(this)).filter(function(f) { return !!f}); | |
const useOr = this._filterForm.querySelector('input[type=checkbox]').checked; | |
let total = 0; | |
let filterCount = 0; | |
document.querySelectorAll('div.diff-file.file-holder').forEach(function (file) { | |
const filePath = file.getAttribute('data-path'); | |
if (!filePath) { | |
return; | |
} | |
let show = null; | |
total++; | |
filters.filter(function (f) { return !f.positive; }).forEach(function (f) { | |
if (filePath.match(f.filter)) { | |
debug('Matching negative filter for file ' + filePath + ' (' + f.filterText + ')'); | |
show = false; | |
} | |
}); | |
const positiveFilters = filters.filter(function (f) { return f.positive; }); | |
if (show === null) { | |
if (!positiveFilters.length) { | |
// only negative filters, without matches, so show it! | |
show = true; | |
} else { | |
// only process positive filters if it didn't match a negative one | |
let possitiveFilterMatched = 0; | |
positiveFilters.filter(function (f) { return f.positive; }).forEach(function (f) { | |
if (filePath.match(f.filter)) { | |
debug('Matching filter for file ' + filePath + ' (' + f.filterText + ')'); | |
possitiveFilterMatched++; | |
} | |
}); | |
show = useOr ? possitiveFilterMatched > 0 : possitiveFilterMatched === positiveFilters.length; | |
} | |
} | |
const hide = filters.length && !show; | |
if (hide) { | |
filterCount++; | |
} | |
debug((hide ? 'Hiding ' : 'Showing ') + ' file ' + filePath); | |
file.style.display = hide ? 'none' : 'inherit'; | |
}); | |
this._filterButton.style['background-color'] = filterCount ? '#1f75cb' : 'unset'; | |
const totalFiltered = total - filterCount; | |
this._filterForm.querySelector('span.result').innerHTML = totalFiltered ? totalFiltered + ' / ' + total : (filters.length ? 'No matches' : ''); | |
} | |
/** | |
* given a user entered filter, convert it into a proper regexp filter (if valid) | |
* @return {Filter}|null | |
*/ | |
_buildRegexpFilter(userTextFilter) { | |
let f = String(userTextFilter).trim(); | |
const positive = !f.startsWith('-'); | |
if (!positive) { | |
f = f.substring(1); | |
} | |
const isRegularExpression = f.startsWith('re/'); | |
if (isRegularExpression) { | |
f.substring(3); | |
} | |
if (!f) { | |
debug('Ignoring empty filter "' + userTextFilter + '"'); | |
return null; | |
} | |
let regex = ''; | |
if (isRegularExpression) { | |
try { | |
regex = f.substring(3); | |
return {filter: new RegExp(regex), positive: positive, filterText: regex}; | |
} catch (e) { | |
debug('Ignoring bad Regexp: ' + userTextFilter); | |
return null; | |
} | |
} | |
// now let's try normal filters | |
f.split('*').forEach(function (part) { | |
if (regex !== '') { | |
regex += '.*'; | |
} | |
// it seems this is not a builtin function yet, like php's preg_quote() | |
regex += part.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); | |
}); | |
return {filter: new RegExp(regex), positive: positive, filterText: regex}; | |
} | |
}; | |
(new App()).start(); | |
})(this); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@wences-dc-uba-ar fixed! Just checking if file-path has a value, not sure how it will behave with those without it, as I couldn't reproduce it yet.
Another bug I've seen, is that sometimes there's two filter icons added, if you have any corrections, just let me know!
Jose