Bugzilla - Merge "Updated" changes

Merge auxiliary changes by the same author

当前为 2025-01-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Bugzilla - Merge "Updated" changes
  3. // @description Merge auxiliary changes by the same author
  4. // @namespace RainSlide
  5. // @author RainSlide
  6. // @license AGPL-3.0-or-later
  7. // @version 1.0
  8. // @icon https://bugzilla.mozilla.org/extensions/BMO/web/images/favicon.ico
  9. // @match https://bugzilla.mozilla.org/show_bug.cgi?*
  10. // @grant none
  11. // @inject-into content
  12. // @run-at document-end
  13. // ==/UserScript==
  14.  
  15. "use strict";
  16.  
  17. const $ = (tagName, ...props) => Object.assign(
  18. document.createElement(tagName), ...props
  19. );
  20.  
  21. const css = `.activity .changes-container {
  22. display: flex;
  23. align-items: center;
  24. }
  25. .activity .changes-separator {
  26. display: inline-block;
  27. transform: scaleY(2.5);
  28. white-space: pre;
  29. }
  30. .activity .change-name,
  31. .activity .change-time {
  32. font-size: var(--font-size-medium);
  33. }
  34. .changes-container:target,
  35. .change:target {
  36. outline: 2px solid var(--focused-control-border-color);
  37. }`;
  38.  
  39. document.head.append($("style", { textContent: css }));
  40.  
  41. // Continuous groups of:
  42. // 1. auxiliary .change-set (.change-set with no comment text, id starts with "a")
  43. // 2. by the same author
  44. const aGroups = [];
  45.  
  46. let currentAuthor = null;
  47. let newGroup = true;
  48. document.querySelectorAll("#main-inner > .change-set").forEach(changeSet => {
  49.  
  50. // check if is auxiliary change set
  51. if (changeSet.id[0] !== "a") {
  52. // no, no longer continuous, add a new group for next auxiliary change set
  53. newGroup = true;
  54. return;
  55. }
  56.  
  57. // get & check author vcard
  58. const author = changeSet.querySelector(":scope .change-author > .vcard");
  59. if (author === null) {
  60. throw new TypeError('Element ".change-set .change-author > .vcard" not found!');
  61. }
  62.  
  63. // check if is the same author
  64. if (author.textContent !== currentAuthor) {
  65. // different author, set currentAuthor, add a new group directly
  66. currentAuthor = author.textContent;
  67. aGroups.push([changeSet]);
  68. newGroup = false;
  69. } else if (!newGroup) {
  70. // same author, push to current group
  71. aGroups.at(-1).push(changeSet);
  72. } else {
  73. // same author, add a new group
  74. aGroups.push([changeSet]);
  75. newGroup = false;
  76. }
  77.  
  78. });
  79.  
  80. // "move" an id, from an element, to another element
  81. const moveId = (from, to) => {
  82. const id = from.id;
  83. from.removeAttribute("id");
  84. to.id = id;
  85. };
  86.  
  87. // append .change to .activity, create container if needed
  88. const appendChanges = (changeSet, activity, isFirst) => {
  89.  
  90. // get & check .change element(s)
  91. const changes = changeSet.querySelectorAll(":scope > .activity > .change");
  92. if (changes.length === 0) {
  93. throw new TypeError('Element(s) ".change-set > .activity > .change" not found!');
  94. }
  95.  
  96. // get name & time
  97. const tr = changeSet.querySelector(
  98. ':scope > .change > .change-head > tbody > tr[id^="ar-a"]:nth-of-type(2)'
  99. );
  100. const td = tr?.querySelector(":scope > td:only-child");
  101.  
  102. // move name & time into .change or .changes-container, append .changes-container
  103. if (tr && td) {
  104. if (changes.length > 1) {
  105. // a group of .change, create container for nameTime & themselves
  106. const container = $("div", { className: "changes-container" });
  107. const group = $("div", { className: "changes" });
  108. const nameTime = $("div", { id: tr.id });
  109. const separator = $("span", { className: "changes-separator", textContent: "| " });
  110. nameTime.append(...td.childNodes, separator);
  111. group.append(...changes);
  112. container.append(nameTime, group);
  113. tr.remove();
  114.  
  115. // appending .changes-container
  116.  
  117. // "move" an id onto another existing element might mess up the :target highlight,
  118. // so skip that for the first
  119. if (!isFirst) {
  120. moveId(changeSet, container);
  121. }
  122. // but, first .changes-container needs append!
  123. activity.append(container);
  124.  
  125. return;
  126.  
  127. } else {
  128. // only one .change, don't create container, just move nameTime to changes[0]
  129. const nameTime = $("span", { id: tr.id });
  130. nameTime.append(...td.childNodes, "| ");
  131. changes[0].prepend(nameTime);
  132. tr.remove();
  133.  
  134. // no return here, append in if (!isFirst) ... below
  135. }
  136. }
  137.  
  138. // appending .change / a group of .change
  139.  
  140. // first doesn't need move id, see before;
  141. // first .change is already in .activity, doesn't need append either.
  142. if (!isFirst) {
  143. moveId(changeSet, changes[0]);
  144. activity.append(...changes);
  145. }
  146.  
  147. };
  148.  
  149. // merge the .change of each aGroup into the first .change-set with appendChanges()
  150. aGroups.forEach(group => {
  151. if (group.length < 2) return;
  152.  
  153. const first = group[0];
  154. const activity = first.querySelector(":scope > .activity");
  155. appendChanges(first, activity, true);
  156.  
  157. // starts from 1 to skip the first change set
  158. for (let i = 1; i < group.length; i++) {
  159. appendChanges(group[i], activity);
  160. group[i].remove();
  161. }
  162. });