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-12 提交的版本,檢視 最新版本

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