Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save wences-dc-uba-ar/6a4304b3dca22f0416620387a49ef915 to your computer and use it in GitHub Desktop.
Save wences-dc-uba-ar/6a4304b3dca22f0416620387a49ef915 to your computer and use it in GitHub Desktop.
Gitlab Merge Request Changes file filtering
// ==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 9
// @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.value = 'crmsearch\ngeo\nmatching\nresume\nsemanticmapping\nskill';
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) {
// console.log('!data-path in', file);
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