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

It generates a sortable table list of bugs related to Firefox for desktop for which patches have landed in Mozilla Mercurial pushlogs

目前為 2018-05-08 提交的版本,檢視 最新版本

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