Firefox for desktop - list modified bugs in Mercurial as sortable table

Lists (as sortable table) bugs related to Firefox for desktop for which patches have landed in Mozilla Mercurial pushlogs

  1. // ==UserScript==
  2. // @name Firefox for desktop - list modified bugs in Mercurial as sortable table
  3. // @namespace darkred
  4. // @version 5.5.9.2
  5. // @date 2020.8.25
  6. // @description Lists (as sortable table) bugs related to Firefox for desktop for which patches have landed in Mozilla Mercurial pushlogs
  7. // @author darkred, johnp
  8. // @license MIT
  9. // @include /^https?:\/\/hg\.mozilla\.org.*pushloghtml.*/
  10. // @grant GM_getResourceURL
  11. // @grant GM_getResourceText
  12. // @grant GM_addStyle
  13. // @grant GM_xmlhttpRequest
  14. // @require https://code.jquery.com/jquery-2.1.4.min.js
  15. // @require https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
  16. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.24.3/js/jquery.tablesorter.min.js
  17. // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment-with-locales.min.js
  18. // @require https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.4.1/moment-timezone-with-data.min.js
  19. // @require https://cdnjs.cloudflare.com/ajax/libs/jstimezonedetect/1.0.6/jstz.min.js
  20. // @require https://cdnjs.cloudflare.com/ajax/libs/datejs/1.0/date.min.js
  21. // @require https://cdnjs.cloudflare.com/ajax/libs/keypress/2.1.3/keypress.min.js
  22. // @resource jqUI_CSS http://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css
  23. // @resource IconSet1 https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-bg_glass_75_d0e5f5_1x400.png
  24. // @resource IconSet2 https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-bg_glass_85_dfeffc_1x400.png
  25. // @resource IconSet3 https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png
  26. // @resource IconSet4 https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-bg_inset-hard_100_fcfdfd_1x100.png
  27. // @resource IconSet5 https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-icons_217bc0_256x240.png
  28. // @resource IconSet6 https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-icons_469bdd_256x240.png
  29. // @resource IconSet7 https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/redmond/images/ui-icons_6da8d5_256x240.png
  30. // Thanks a lot to: johnp (your contribution is most appreciated!), wOxxOm and Brock Adams.
  31. // @supportURL https://github.com/darkred/Userscripts/issues
  32. // ==/UserScript==
  33.  
  34.  
  35. /* eslint-disable no-console, complexity */
  36. /* global jstz, moment */
  37.  
  38.  
  39. var silent = false;
  40. var debug = false;
  41.  
  42. time('MozillaMercurial');
  43.  
  44.  
  45.  
  46.  
  47.  
  48. // CSS rules in order to show 'up' and 'down' arrows in each table header
  49. var stylesheet = `
  50. <style>
  51. thead th {
  52. background-repeat: no-repeat;
  53. background-position: right center;
  54. }
  55. thead th.up {
  56. padding-right: 20px;
  57. background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7);
  58. }
  59. thead th.down {
  60. padding-right: 20px;
  61. background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7);
  62. }
  63. </style>`;
  64.  
  65. $('head').append(stylesheet);
  66.  
  67.  
  68. var stylesheet2 =
  69. `<style>
  70.  
  71. /* in order to highlight hovered table row */
  72. #tbl tr:hover{ background:#F6E6C6 !important;}
  73.  
  74. /* in order the table headers to be larger and bold */
  75. #tbl th {text-align: -moz-center !important; font-size: larger; font-weight: bold; }
  76.  
  77. /* in order to remove unnecessairy space between rows */
  78. #dialog > div > table > tbody {line-height: 14px;}
  79.  
  80.  
  81. #tbl > thead > tr > th {border-bottom: solid 1px};}
  82.  
  83.  
  84. #tbl td:nth-child(1) {text-align: -moz-right;}
  85.  
  86. /* in order the 'product/component' to be aligned to the right */
  87. #tbl td:nth-child(2) {text-align: -moz-right;}
  88.  
  89. /* in order the bug list to have width 1500px // it was 1500 and then 1600 */
  90. .ui-dialog {
  91. width:1700px !important;
  92. }
  93.  
  94. </style>`;
  95. $('head').append(stylesheet2);
  96.  
  97.  
  98.  
  99.  
  100.  
  101.  
  102.  
  103. // the dialog will only be opened after all these promises have finished
  104. var requests = [];
  105.  
  106.  
  107. // theme for the jQuery dialog
  108. if (typeof(GM_getResourceText) !== 'undefined' && typeof(GM_addStyle) !== 'undefined') {
  109.  
  110.  
  111. // https://stackoverflow.com/a/11532646/ , i.e. https://stackoverflow.com/a/11532646/3231411 (By Brock Adams)
  112. // Themes files URLs: https://cdnjs.com/libraries/jqueryui
  113. let iconSet1 = GM_getResourceURL ('IconSet1');
  114. let iconSet2 = GM_getResourceURL ('IconSet2');
  115. let iconSet3 = GM_getResourceURL ('IconSet3');
  116. let iconSet4 = GM_getResourceURL ('IconSet4');
  117. let iconSet5 = GM_getResourceURL ('IconSet5');
  118. let iconSet6 = GM_getResourceURL ('IconSet6');
  119. let iconSet7 = GM_getResourceURL ('IconSet7');
  120. let jqUI_CssSrc = GM_getResourceText ('jqUI_CSS');
  121. // jqUI_CssSrc = jqUI_CssSrc.replace (/url\(images\/ui\-bg_.*00\.png\)/g, '');
  122. jqUI_CssSrc = jqUI_CssSrc.replace (/images\/ui-bg_glass_75_d0e5f5_1x400\.png/g, iconSet1);
  123. jqUI_CssSrc = jqUI_CssSrc.replace (/images\/ui-bg_glass_85_dfeffc_1x400\.png/g, iconSet2);
  124. jqUI_CssSrc = jqUI_CssSrc.replace (/images\/ui-bg_gloss-wave_55_5c9ccc_500x100\.png/g, iconSet3);
  125. jqUI_CssSrc = jqUI_CssSrc.replace (/images\/ui-bg_inset-hard_100_fcfdfd_1x100\.png/g, iconSet4);
  126. jqUI_CssSrc = jqUI_CssSrc.replace (/images\/ui-icons_217bc0_256x240\.png/g, iconSet5);
  127. jqUI_CssSrc = jqUI_CssSrc.replace (/images\/ui-icons_469bdd_256x240\.png/g, iconSet6);
  128. jqUI_CssSrc = jqUI_CssSrc.replace (/images\/ui-icons_6da8d5_256x240\.png/g, iconSet7);
  129.  
  130. GM_addStyle (jqUI_CssSrc);
  131.  
  132.  
  133. } else { // e.g. Greasemonkey: https://github.com/greasemonkey/greasemonkey/issues/2548
  134. // load jquery-ui css dynamically to bypass Content-Security-Policy restrictions
  135. let loadCss = $.get('https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/redmond/jquery-ui.min.css', function(css) {
  136. $('head').append('<style>' + css + '</style>');
  137. });
  138. requests.push(loadCss); // prevent a possible race condition where the dialog is opened before the css is loaded
  139. }
  140.  
  141.  
  142.  
  143.  
  144.  
  145. var regex = /^https:\/\/bugzilla\.mozilla\.org\/show_bug\.cgi\?id=(.*)$/;
  146. var base_url = 'https://bugzilla.mozilla.org/rest/bug?include_fields=id,summary,status,resolution,product,component,op_sys,platform,whiteboard,last_change_time&id=';
  147. var bugIds = [];
  148. var bugsComplete = [];
  149.  
  150. var table = document.getElementsByTagName('table')[0];
  151. var links = table.getElementsByTagName('a');
  152. var len = links.length;
  153. for (let i = 0; i < len; i++) {
  154. let n = links[i].href.match(regex);
  155. if (n !== null && n.length > 0) {
  156. let id = parseInt(n[1]);
  157. if (bugIds.indexOf(id) === -1) {
  158. bugIds.push(id);
  159. }
  160. }
  161. }
  162.  
  163. var numBugs = bugIds.length;
  164. var counter = 0;
  165.  
  166. var rest_url = base_url + bugIds.join();
  167.  
  168.  
  169. String.prototype.escapeHTML = function() {
  170. var tagsToReplace = {
  171. '&': '&amp;',
  172. '<': '&lt;',
  173. '>': '&gt;'
  174. };
  175. return this.replace(/[&<>]/g, function(tag) {
  176. return tagsToReplace[tag] || tag;
  177. });
  178. };
  179.  
  180.  
  181.  
  182. time('MozillaMercurial-REST');
  183.  
  184.  
  185.  
  186. GM_xmlhttpRequest({
  187. method: 'GET',
  188. url: rest_url,
  189. onload: function(response) {
  190.  
  191. var data = JSON.parse(response.responseText);
  192.  
  193. timeEnd('MozillaMercurial-REST');
  194. $.each(data.bugs, function(index) {
  195. let bug = data.bugs[index];
  196. // process bug (let "shorthands" just to simplify things during refactoring)
  197. let status = bug.status;
  198. if (bug.resolution !== '') {status += ' ' + bug.resolution;}
  199. let product = bug.product;
  200. let component = bug.component;
  201. let platform = bug.platform;
  202. if (platform === 'Unspecified') {
  203. platform = 'Uns';
  204. }
  205. if (bug.op_sys !== '' && bug.op_sys !== 'Unspecified') {
  206. platform += '/' + bug.op_sys;
  207. }
  208. let whiteboard = bug.whiteboard === '' ? '[]' : bug.whiteboard;
  209. // todo: message???
  210.  
  211.  
  212.  
  213.  
  214.  
  215. // 2015-11-09T14:40:41Z
  216. function toRelativeTime(time, zone) {
  217. var format2 = ('YYYY-MM-DD HH:mm:ss Z');
  218. return moment(time, format2).tz(zone).fromNow();
  219. }
  220.  
  221.  
  222. function getLocalTimezone(){
  223. var tz = jstz.determine(); // Determines the time zone of the browser client
  224. return tz.name(); // Returns the name of the time zone eg "Europe/Berlin"
  225. }
  226.  
  227.  
  228.  
  229.  
  230. var changetime;
  231. var localTimezone = getLocalTimezone();
  232.  
  233. if (bug.last_change_time !== '') {
  234. var temp = toRelativeTime(bug.last_change_time, localTimezone);
  235. if (temp.match(/(an?) .*/)) {
  236. changetime = temp.replace(/an?/, 1);
  237. } else {
  238. changetime = temp;
  239. }
  240. // changetime
  241. } else {
  242. changetime = '';
  243. }
  244.  
  245.  
  246.  
  247.  
  248.  
  249.  
  250.  
  251.  
  252. log('----------------------------------------------------------------------------------------------------------------------------------');
  253. log((index + 1) + '/' + numBugs); // Progression counter
  254. log('BugNo: ' + bug.id + '\nTitle: ' + bug.summary + '\nStatus: ' + status + '\nProduct: ' + product + '\nComponent: ' + component + '\nPlatform: ' + platform + '\nWhiteboard: ' + whiteboard);
  255.  
  256. if (isRelevant(bug)) {
  257. // add html code for this bug
  258. bugsComplete.push('<tr><td><a href="'
  259. // + 'https://bugzilla.mozilla.org/show_bug.cgi?id='+ bug.id + '">'
  260. + 'https://bugzilla.mozilla.org/show_bug.cgi?id='+ bug.id + '"' + ' title="' + bug.id + ' - ' + bug.summary + '">#'
  261. + bug.id
  262. + '</a></td>'
  263. + '<td nowrap>(' + product + ': ' + component + ') </td>'
  264. + '<td>'+bug.summary.escapeHTML() + ' [' + platform + ']' + whiteboard.escapeHTML() + '</td>'
  265. + '<td>' + changetime + '</td>'
  266. + '<td>' + status + '</td></tr>'); // previously had a <br> at the end;
  267. }
  268. counter++; // increase counter
  269. // remove processed bug from bugIds
  270. let i = bugIds.indexOf(bug.id);
  271. if (i !== -1) {bugIds[i] = null;}
  272. });
  273. log('==============\nReceived ' + counter + ' of ' + numBugs + ' bugs.');
  274.  
  275.  
  276.  
  277.  
  278. // process remaining bugs one-by-one
  279. time('MozillaMercurial-missing');
  280. $.each(bugIds, function(index) {
  281. let id = bugIds[index];
  282. if (id !== null) {
  283. time('Requesting missing bug ' + id);
  284. let promise = $.getJSON('https://bugzilla.mozilla.org/rest/bug/' + id,
  285. function(json) {
  286. // I've not end up here yet, so cry if we do
  287. console.error('Request for bug ' + id + ' succeeded unexpectedly!');
  288. timeEnd('Requesting missing bug ' + id);
  289. console.error(json);
  290. });
  291. // Actually, we usually get an '401 Authorization Required' error
  292. promise.fail(function(req, status, error) {
  293. timeEnd('Requesting missing bug ' + id);
  294. if (error === 'Authorization Required') {
  295.  
  296. // log("Bug " + id + " requires authorization!");
  297. log('https://bugzilla.mozilla.org/show_bug.cgi?id=' + id + ' requires authorization!');
  298. let text = ' requires authorization!<br>';
  299.  
  300. bugsComplete.push('<a href="'
  301. + 'https://bugzilla.mozilla.org/show_bug.cgi?id='+ id + '">#'
  302. + id + '</a>' + text);
  303. } else {
  304. console.error('Unexpected error encountered (Bug' + id + '): ' + status + ' ' + error);
  305. }
  306. });
  307. requests.push(promise);
  308. }
  309. });
  310. // wait for all requests to be settled, then join them together
  311. // Source: https://stackoverflow.com/questions/19177087/deferred-how-to-detect-when-every-promise-has-been-executed
  312. $.when.apply($, $.map(requests, function(p) {
  313. return p.then(null, function() {
  314. return $.Deferred().resolveWith(this, arguments);
  315. });
  316. })).always(function() {
  317. timeEnd('MozillaMercurial-missing');
  318. // Variable that will contain all values of the bugsComplete array, and will be displayed in the 'dialog' below
  319. var docu = '';
  320. docu = bugsComplete.join('');
  321. docu = '<table id="tbl" style="width:100%">' +
  322. '<thead>' +
  323. '<tr><th>BugNo</th>' +
  324. '<th>Product/Component</th>' +
  325. '<th>Summary</th>' +
  326. '<th>Modified___</th>' +
  327. '<th>Status____________</th></tr>' +
  328. '</thead>' +
  329. '<tbody>' + docu + '</tbody></table>';
  330.  
  331.  
  332.  
  333.  
  334. var div = document.createElement('div');
  335. $('div.page_footer').append(div);
  336. div.id = 'dialog';
  337. docu = '<div id="dialog_content">' + docu + '</div>';
  338. div.innerHTML = docu;
  339. $('#dialog').hide();
  340.  
  341. $(function() {
  342. $('#dialog').dialog({
  343. title: 'List of modified bugs of Firefox for desktop (' + bugsComplete.length + ')',
  344. width: '1350px'
  345. });
  346. });
  347.  
  348.  
  349.  
  350.  
  351.  
  352.  
  353. // THE CUSTOM PARSER MUST BE PUT BEFORE '$('#tbl').tablesorter ( {'' or else it wont work !!!!
  354. // add parser through the tablesorter addParser method (for the "Last modified" column)
  355. $.tablesorter.addParser({
  356. // set a unique id
  357. id: 'dates',
  358. is: function(s) {
  359. return false; // return false so this parser is not auto detected
  360. },
  361. format: function(s) {
  362. // format your data for normalization
  363. if (s !== ''){
  364. var number1, number2;
  365.  
  366. // format your data for normalization
  367. number1 = Number((/(.{1,2}) .*/).exec(s)[1]);
  368.  
  369.  
  370. if (s.match(/A few seconds ago/)) { number2 = 0;}
  371. else if (s.match(/(.*)seconds?.*/)) { number2 = 1;}
  372. else if (s.match(/(.*)minutes?.*/)) {number2 = 60;}
  373. else if (s.match(/(.*)hours?.*/)) { number2 = 3600;}
  374. else if (s.match(/(.*)days?.*/)) { number2 = 86400;}
  375. else if (s.match(/(.*)months?.*/)) { number2 = 30 * 86400;}
  376. else if (s.match(/(.*)years?.*/)) {number2 = 365 * 30 * 86400;}
  377. return number1 * number2;
  378.  
  379. }
  380. },
  381. // set type, either numeric or text
  382. type: 'numeric'
  383. });
  384.  
  385.  
  386.  
  387. // make table sortable
  388. $('#tbl').tablesorter({
  389. cssAsc: 'up',
  390. cssDesc: 'down',
  391. sortList: [[3, 0],[1, 0],[2, 0]], // in order the table to be sorted by default by column 3 'Modified', then by column 1 'Product/Component' and then by column 2 'Summary'
  392. headers: {3: {sorter: 'dates'}},
  393. initialized: function() {
  394. var mytable = document.getElementById('tbl');
  395. for (var i = 2, j = mytable.rows.length + 1; i < j; i++) {
  396. if (mytable.rows[i].cells[3].innerHTML !== mytable.rows[i - 1].cells[3].innerHTML) {
  397. for (var k = 0; k < 5; k++) {
  398. mytable.rows[i - 1].cells[k].style.borderBottom = '1px black dotted';
  399. }
  400. }
  401. }
  402. }
  403. });
  404.  
  405.  
  406.  
  407.  
  408.  
  409.  
  410. log('ALL IS DONE');
  411. timeEnd('MozillaMercurial');
  412.  
  413.  
  414.  
  415.  
  416.  
  417. });
  418.  
  419. }
  420. });
  421.  
  422.  
  423. var flag = 1;
  424.  
  425. // bind keypress of ` so that when pressed, the separators between groups of the same timestamps to be removed, in order to sort manually
  426. var listener = new window.keypress.Listener();
  427. listener.simple_combo('`', function() {
  428. // console.log('You pressed `');
  429. if (flag === 1) {
  430. flag = 0;
  431. // remove seperators
  432. var mytable = document.getElementById('tbl');
  433. for (let i = 2, j = mytable.rows.length + 1; i < j; i++) {
  434. for (let k = 0; k < 5; k++) {
  435. mytable.rows[i - 1].cells[k].style.borderBottom = 'none';
  436. }
  437. }
  438. var sorting = [[1, 0], [2, 0]]; // sort by column 1 'Product/Component' and then by column 2 'Summary'
  439. $('#tbl').trigger('sorton', [sorting]);
  440. } else {
  441. if (flag === 0) {
  442. flag = 1;
  443. // console.log('You pressed ~');
  444. sorting = [[3, 0], [1, 0], [2, 0]]; // sort by column 3 'Modified Date, then by '1 'Product/Component' and then by column 2 'Summary'
  445. $('#tbl').trigger('sorton', [sorting]);
  446. mytable = document.getElementById('tbl');
  447. for (let i = 2, j = mytable.rows.length + 1; i < j; i++) {
  448. if (mytable.rows[i].cells[3].innerHTML !== mytable.rows[i - 1].cells[3].innerHTML) {
  449. for (let k = 0; k < 5; k++) {
  450. mytable.rows[i - 1].cells[k].style.borderBottom = '1px black dotted';
  451. }
  452. }
  453. }
  454. }
  455. }
  456. });
  457.  
  458.  
  459.  
  460.  
  461.  
  462.  
  463. function isRelevant(bug) {
  464. if (!bug.id) {return false;}
  465. // if (bug.status && bug.status !== 'RESOLVED' && bug.status !== 'VERIFIED') {
  466. // log(' IRRELEVANT because of it\'s Status --> ' + bug.status);
  467. // return false;
  468. // }
  469. if (bug.component && bug.product && bug.component === 'Build Config' && (bug.product === 'Toolkit' || bug.product === 'Firefox')) {
  470. log(' IRRELEVANT because of it\'s Product --> ' + bug.product + 'having component --> ' + bug.component);
  471. return false;
  472. }
  473. if (bug.product &&
  474. bug.product !== 'Add-on SDK' &&
  475. bug.product !== 'Cloud Services' &&
  476. bug.product !== 'Core' &&
  477. bug.product !== 'Firefox' &&
  478. bug.product !== 'Hello (Loop)' &&
  479. bug.product !== 'Toolkit') {
  480. log(' IRRELEVANT because of it\'s Product --> ' + bug.product);
  481. return false;
  482. }
  483. if (bug.component &&
  484. bug.component === 'AutoConfig' ||
  485. bug.component === 'Build Config' ||
  486. bug.component === 'DMD' ||
  487. bug.component === 'Embedding: GRE Core' ||
  488. bug.component === 'Embedding: Mac' ||
  489. bug.component === 'Embedding: MFC Embed' ||
  490. bug.component === 'Embedding: Packaging' ||
  491. bug.component === 'Hardware Abstraction Layer' ||
  492. bug.component === 'mach' ||
  493. bug.component === 'Nanojit' ||
  494. bug.component === 'QuickLaunch' ||
  495. bug.component === 'Widget: Gonk') {
  496. log(' IRRELEVANT because of it\'s Component --> ' + bug.component);
  497. return false;
  498. }
  499.  
  500. log(' OK ' + 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + bug.id);
  501. return true;
  502. }
  503.  
  504.  
  505.  
  506.  
  507. function log(str) {
  508. if (!silent) {
  509. console.log(str);
  510. }
  511. }
  512.  
  513. function time(str) {
  514. if (debug) {
  515. console.time(str);
  516. }
  517. }
  518.  
  519. function timeEnd(str) {
  520. if (debug) {
  521. console.timeEnd(str);
  522. }
  523. }
  524.  
  525. $('#dialog').dialog({
  526. modal: false,
  527. title: 'Draggable, sizeable dialog',
  528. position: {
  529. my: 'top',
  530. at: 'top',
  531. of: document,
  532. collision: 'none'
  533. },
  534. // width: 1500, // not working
  535. zIndex: 3666
  536. })
  537. .dialog('widget').draggable('option', 'containment', 'none');
  538.  
  539. //-- Fix crazy bug in FF! ...
  540. $('#dialog').parent().css({
  541. position: 'fixed',
  542. top: 0,
  543. left: '4em',
  544. width: '75ex'
  545. });