Github PR Incremental Diffs

Provides you incremental diffs with the help of an extra server

  1. // ==UserScript==
  2. // @name Github PR Incremental Diffs
  3. // @version 1.2
  4. // @namespace https://tampermonkey.net/
  5. // @homepage https://github.com/sociomantic-tsunami/kelpie
  6. // @supportURL https://github.com/sociomantic-tsunami/kelpie/issues
  7. // @description Provides you incremental diffs with the help of an extra server
  8. // @author Mathias L. Baumann
  9. // @copyright Copyright (c) 2017-2018 dunnhumby Germany GmbH. All rights reserved.
  10. // @license Boost Software License 1.0 (https://www.boost.org/LICENSE_1_0.txt)
  11. // @match *://github.com/*
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_addStyle
  15. // @grant GM_getResourceText
  16. // @grant GM_xmlhttpRequest
  17. // @require https://cdn.rawgit.com/cemerick/jsdifflib/da7da27640a30537cea9069622dc71600c5a1d61/difflib.js
  18. // @require https://cdn.rawgit.com/cemerick/jsdifflib/da7da27640a30537cea9069622dc71600c5a1d61/diffview.js
  19. // @resource CSSDIFF https://cdn.rawgit.com/cemerick/jsdifflib/da7da27640a30537cea9069622dc71600c5a1d61/diffview.css
  20. // ==/UserScript==
  21.  
  22. class FileTree
  23. {
  24. /* Params:
  25. sha = sha of the file tree
  26. url = api url of the file tree
  27. */
  28. constructor ( sha, url, root_path )
  29. {
  30. this.root_path = root_path;
  31. this.sha = sha;
  32. this.url = url;
  33. this.list = [];
  34. }
  35.  
  36. // Fetches the tree from using the API and calls callback with the result
  37. fetch ( cbthis, callback )
  38. {
  39. if (this.list && this.list.length > 0)
  40. {
  41. callback(this);
  42. return;
  43. }
  44.  
  45. var request = new XMLHttpRequest();
  46.  
  47. var receiveTree = function ( )
  48. {
  49. var response = JSON.parse(this.responseText);
  50.  
  51. for (var i=0; i < response.tree.length; i++)
  52. {
  53. var obj = { "path" : this.outside.root_path + response.tree[i].path,
  54. "sha" : response.tree[i].sha,
  55. "url" : response.tree[i].url,
  56. "type" : response.tree[i].type };
  57.  
  58. // Don't get the blob for tree's, get it as another tree
  59. if (response.tree[i].type == "tree")
  60. obj.url = obj.url.replace(/blobs/, "trees");
  61.  
  62. this.outside.list.push(obj);
  63. //console.log("entry info " + obj.path + ", " + obj.sha);
  64. }
  65.  
  66. this.userCb.call(this.cbthis, this.outside);
  67. };
  68.  
  69. request.outside = this;
  70. request.onload = receiveTree;
  71. request.userCb = callback;
  72. request.cbthis = cbthis;
  73.  
  74. // Initialize a request
  75. request.open('get', this.url);
  76.  
  77. var usertoken = GM_getValue("username") + ":" + GM_getValue("token");
  78. request.setRequestHeader("Authorization", "Basic " + btoa(usertoken));
  79. // Send it
  80. request.send();
  81. }
  82. }
  83.  
  84. class FileDiffer
  85. {
  86. constructor ( base, head, original )
  87. {
  88. // List of files that have changed
  89. this.changed = [];
  90.  
  91. this.base = base;
  92. this.head = head;
  93. this.original = original;
  94. }
  95.  
  96. fetch ( cbthis, callback )
  97. {
  98. this.cbthis = cbthis;
  99. this.callback = callback;
  100.  
  101. if (this.base === null)
  102. this.base = { "list" : [] };
  103. else
  104. {
  105. var tmp_base = this.base;
  106. this.base = null;
  107. tmp_base.fetch(this, this.assignBase);
  108. }
  109.  
  110. if (this.head === null)
  111. this.head = { "list" : [] };
  112. else
  113. {
  114. var tmp_head = this.head;
  115. this.head = null;
  116. tmp_head.fetch(this, this.assignHead);
  117. }
  118.  
  119. if (this.original === null)
  120. this.original = { "list" : [] };
  121. else
  122. {
  123. var tmp_orig = this.original;
  124. this.original = null;
  125. tmp_orig.fetch(this, this.assignOriginal);
  126. }
  127. }
  128.  
  129. assignBase ( base ) { this.base = base; this.checkComplete(); }
  130. assignHead ( head ) { this.head = head; this.checkComplete(); }
  131. assignOriginal ( original ) { this.original = original; this.checkComplete(); }
  132.  
  133. checkComplete ( )
  134. {
  135. if (!this.base || !this.head || !this.original)
  136. return;
  137.  
  138. console.log("Received all trees, extracting required files");
  139.  
  140. var diff = null;
  141. var i = 0;
  142. var head_el = null;
  143.  
  144. var matchPath = function (el) { return el.path==head_el.path; };
  145.  
  146. // Find all paths differing from base
  147. for (i=0; i < this.head.list.length; i++)
  148. {
  149. head_el = this.head.list[i];
  150.  
  151. var orig_path = this.original.list.find(matchPath);
  152.  
  153. // If this path exists in original with the same sha, it was added through a rebase
  154. if (orig_path !== undefined && orig_path.sha == head_el.sha)
  155. continue; // so ignore it
  156.  
  157. var base_path = this.base.list.find(matchPath);
  158.  
  159. if (orig_path === undefined)
  160. orig_path = null;
  161.  
  162. // base doesn't have that file?
  163. if (base_path === undefined)
  164. { // completely new file
  165. diff = { "base" : null, "head" : head_el,
  166. "orig" : null };
  167. console.log("File differs (no base): " + head_el.path);
  168. this.changed.push(diff);
  169. continue;
  170. }
  171.  
  172. // file exists in base and differs
  173. if (base_path.sha != head_el.sha)
  174. { // changes have been made
  175. diff = { "base" : base_path, "head" : head_el,
  176. "orig" : orig_path };
  177.  
  178. console.log("File differs: " + head_el.path);
  179. this.changed.push(diff);
  180. continue;
  181. }
  182. }
  183.  
  184. // Find any files not existing in head, but existing in base
  185. for (i=0; i < this.base.list.length; i++)
  186. {
  187. var base_el = this.base.list[i];
  188.  
  189. head_el = this.head.list.find(matchPath);
  190.  
  191. if (head_el !== undefined)
  192. continue;
  193.  
  194. diff = { "base" : base_el, "head" : null, "orig" : null };
  195. console.log("File differs (no head): " + base_el.path);
  196. this.changed.push(diff);
  197. }
  198.  
  199. this.recurseTree();
  200. }
  201.  
  202. // recurses into tree objects in our "changed" paths list and looks for diffs
  203. recurseTree ( )
  204. {
  205. var i = 0;
  206. var el = {};
  207. var base_tree = {};
  208. var head_tree = {};
  209. var orig_tree = {};
  210. var did_recurse = false;
  211. var path = "";
  212.  
  213. console.log("Recursing...");
  214.  
  215. // Prepare to recurse
  216. for (i=0; i < this.changed.length; i++)
  217. {
  218. el = this.changed[i];
  219.  
  220. // No need to recurse if one is null
  221. if (el.head === null || el.base === null)
  222. continue;
  223.  
  224. // we can only recurse into trees
  225. if (el.head.type != "tree" && el.base.type != "tree")
  226. continue;
  227.  
  228. if (el.base.type == "tree")
  229. base_tree = new FileTree(el.base.sha, el.base.url, el.base.path + "/");
  230. else
  231. base_tree = null;
  232.  
  233. if (el.head.type == "tree")
  234. head_tree = new FileTree(el.head.sha, el.head.url, el.head.path + "/");
  235. else
  236. head_tree = null;
  237.  
  238. if (el.orig !== null && el.orig.type == "tree")
  239. orig_tree = new FileTree(el.orig.sha, el.orig.url, el.orig.path + "/");
  240. else
  241. orig_tree = null;
  242.  
  243. console.log("Recurse task " + el.head.path);
  244.  
  245. el.pending = new FileDiffer(base_tree, head_tree, orig_tree);
  246. }
  247.  
  248. // Actually do the recursion
  249. for (i=0; i < this.changed.length; i++)
  250. {
  251. el = this.changed[i];
  252.  
  253. if ("pending" in el)
  254. {
  255. console.log("Starting task.. " + el.base.path);
  256. el.pending.fetch(this, this.recurseCallback);
  257. did_recurse = true;
  258. }
  259. }
  260.  
  261. if (did_recurse === false)
  262. this.fetchAllFiles();
  263. }
  264.  
  265. // called once for every recursion
  266. // * merges the recursed tree with ours
  267. // * if no more callbacks pending, calls user cb
  268. recurseCallback ( file_differ )
  269. {
  270. var still_waiting = false;
  271.  
  272. for (var i=0; i < this.changed.length; i++)
  273. {
  274. var el = this.changed[i];
  275.  
  276. if ("pending" in el && el.pending == file_differ)
  277. {
  278. console.log("recurseCb for " + el.base.path + " updated");
  279. this.changed = this.changed.concat(file_differ.changed);
  280. el.pending = null;
  281. continue;
  282. }
  283.  
  284. if ("pending" in el && el.pending !== null)
  285. {
  286. console.log("Still waiting for " + el.base.path);
  287. still_waiting = true;
  288. }
  289. }
  290.  
  291. if (still_waiting)
  292. {
  293. return;
  294. }
  295.  
  296. this.fetchAllFiles();
  297. }
  298.  
  299. fetchAllFiles ( )
  300. {
  301. console.log("Fetching files...");
  302. for (var i=0; i < this.changed.length; i++)
  303. {
  304. var el = this.changed[i];
  305.  
  306. if (el.base && el.base.url && !("content" in el.base))
  307. this.fetchFile(el.base.url);
  308.  
  309. if (el.head && el.head.url && !("content" in el.head))
  310. this.fetchFile(el.head.url);
  311. }
  312.  
  313. if (this.changed.length === 0)
  314. this.checkReceivedFiles();
  315. }
  316.  
  317. fetchFile ( url )
  318. {
  319. console.log("Fetching " + url);
  320. var request = new XMLHttpRequest();
  321.  
  322. var receiveBlob = function ( )
  323. {
  324. var response = JSON.parse(this.responseText);
  325.  
  326. function findMatch (elem)
  327. {
  328. if (elem.base && elem.base.sha == response.sha)
  329. return true;
  330. else if (elem.head && elem.head.sha == response.sha)
  331. return true;
  332.  
  333. return false;
  334. }
  335.  
  336. var el = this.outside.changed.find(findMatch);
  337.  
  338. if (el === undefined)
  339. {
  340. console.log("received unexpected sha " + response.sha);
  341. return;
  342. }
  343.  
  344. console.log("Received content for " + el.head.path);
  345.  
  346. var content = "content" in response && response.content.length > 0 ? atob(response.content) : "";
  347.  
  348. if (el.base !== null && el.base.sha == response.sha)
  349. el.base.content = content;
  350. else if (el.head !== null && el.head.sha == response.sha)
  351. el.head.content = content;
  352. else
  353. console.log("Unmatched sha?!");
  354.  
  355. this.outside.checkReceivedFiles();
  356. };
  357.  
  358. request.outside = this;
  359. request.onload = receiveBlob;
  360.  
  361. // Initialize a request
  362. request.open('get', url);
  363.  
  364. var usertoken = GM_getValue("username") + ":" + GM_getValue("token");
  365. request.setRequestHeader("Authorization", "Basic " + btoa(usertoken));
  366. // Send it
  367. request.send();
  368. }
  369.  
  370. checkReceivedFiles ( )
  371. {
  372. var all_content_received = true;
  373.  
  374. for (var i=0; i < this.changed.length; i++)
  375. {
  376. var el = this.changed[i];
  377.  
  378. if (el.base && !("content" in el.base) ||
  379. el.head && !("content" in el.head))
  380. {
  381. all_content_received = false;
  382. break;
  383. }
  384. }
  385.  
  386. if (all_content_received)
  387. {
  388. console.log("Received all content, calling cb");
  389. this.callback.call(this.cbthis, this);
  390. }
  391. }
  392. }
  393.  
  394.  
  395. class Fetcher
  396. {
  397. constructor ( )
  398. {
  399. this.files = [];
  400. }
  401.  
  402. start ( owner, repo, pr, commit1, commit2, element )
  403. {
  404. this.sha_base = commit1;
  405. this.sha_head = commit2;
  406. this.owner = owner;
  407. this.repo = repo;
  408. this.element = element;
  409. this.base_tree = null;
  410. this.head_tree = null;
  411. this.orig_tree = null;
  412.  
  413. this.usertoken = GM_getValue("username") + ":" + GM_getValue("token");
  414.  
  415. //this.fetchCommit(this.sha_update, "update");
  416. this.fetchPrBase(pr);
  417. }
  418.  
  419. // Fetches the base branch for the PR and extracts the latest commits sha
  420. fetchPrBase ( pr )
  421. {
  422. console.log("Fetching PR base");
  423. var receivePr = function ( )
  424. {
  425. var response = JSON.parse(this.responseText);
  426.  
  427. this.outside.fetchTreeShas(this.outside.sha_base, this.outside.sha_head, response.base.sha);
  428. };
  429.  
  430. var request = new XMLHttpRequest();
  431.  
  432. request.outside = this;
  433. request.onload = receivePr;
  434. // Initialize a request
  435. request.open('get', "https://api.github.com/repos/"+this.owner+"/"+this.repo+"/pulls/" + pr);
  436.  
  437. request.setRequestHeader("Authorization", "Basic " + btoa(this.usertoken));
  438. // Send it
  439. request.send();
  440. }
  441.  
  442. // Extracts the tree shas from base/head/orig commit
  443. fetchTreeShas ( base, head, orig )
  444. {
  445. console.log("Fetching trees");
  446. this.fetchTreeFromCommit(base, "base_tree", this.checkTreesDone);
  447. this.fetchTreeFromCommit(head, "head_tree", this.checkTreesDone);
  448. this.fetchTreeFromCommit(orig, "orig_tree", this.checkTreesDone);
  449. }
  450.  
  451. checkTreesDone ( )
  452. {
  453. console.log("checkTreesDone()");
  454.  
  455. if (!this.base_tree || !this.head_tree || !this.orig_tree)
  456. {
  457. console.log("Not all done: " + this.base_tree + " " + this.head_tree + " " + this.orig_tree);
  458. return;
  459. }
  460.  
  461. console.log("Received all trees-shas, fetching content..");
  462. var differ = new FileDiffer(this.base_tree, this.head_tree, this.orig_tree);
  463.  
  464. differ.fetch(this, this.render);
  465. }
  466.  
  467. printMe ( )
  468. {
  469. for (var key in this)
  470. console.log("key: " + key);
  471. }
  472.  
  473. fetchTreeFromCommit ( commit, name, usercb )
  474. {
  475. console.log("Fetching " + name + " " + commit);
  476. var receiveCommit = function ( )
  477. {
  478. var response = JSON.parse(this.responseText);
  479.  
  480. console.log("Received " + this.commit_name);
  481. this.outside[this.commit_name] = new FileTree(response.tree.sha, response.tree.url, "");
  482.  
  483. this.usercb.call(this.outside);
  484. };
  485.  
  486. var request = new XMLHttpRequest();
  487.  
  488. request.outside = this;
  489. request.onload = receiveCommit;
  490. request.commit_name = name;
  491. request.usercb = usercb;
  492.  
  493. // Initialize a request
  494. request.open('get', "https://api.github.com/repos/"+this.owner+"/"+this.repo+"/git/commits/" + commit);
  495.  
  496. request.setRequestHeader("Authorization", "Basic " + btoa(this.usertoken));
  497. // Send it
  498. request.send();
  499. }
  500.  
  501. // Generate the diff, append the elements to this.element
  502. render ( differ )
  503. {
  504. "use strict";
  505.  
  506. var contents = this.element.getElementsByClassName("file");
  507. var content = contents[0];
  508. content.innerHTML = "";
  509. content.style.backgroundColor = "white";
  510. content.style.textAlign = "center";
  511.  
  512. for (var i = 0; i < differ.changed.length; i++)
  513. {
  514. var el = differ.changed[i];
  515.  
  516. if ((el.head === null || el.head.type != "blob") &&
  517. (el.base === null || el.base.type != "blob"))
  518. continue;
  519.  
  520. var base_content = el.base ? el.base.content : "";
  521. var head_content = el.head ? el.head.content : "";
  522.  
  523. var fname = el.head ? el.head.path : el.base.path;
  524.  
  525. var base = difflib.stringAsLines(base_content),
  526. newtxt = difflib.stringAsLines(head_content),
  527. sm = new difflib.SequenceMatcher(base, newtxt),
  528. opcodes = sm.get_opcodes(),
  529. contextSize = 5; //byId("contextSize").value;
  530.  
  531. var filename = document.createElement("DIV");
  532. filename.className = "file-header";
  533. filename.innerText = fname;
  534.  
  535. content.appendChild(filename);
  536. contextSize = contextSize || null;
  537.  
  538. var diff = diffview.buildView({
  539. baseTextLines: base,
  540. newTextLines: newtxt,
  541. opcodes: opcodes,
  542. baseTextName: "Old",
  543. newTextName: "New",
  544. contextSize: contextSize,
  545. viewType: 0 // 0 for side-by-side
  546. });
  547.  
  548. diff.className = diff.className + " blob-wrapper";
  549. diff.style.margin = "auto";
  550. diff.style.textAlign = "left";
  551. content.appendChild(diff);
  552. }
  553.  
  554. var pos = content.getBoundingClientRect();
  555.  
  556. content.style.left = "" + (-pos.left + 15) + "px";
  557. content.style.width = "" + (document.documentElement.clientWidth - 30) + "px";
  558.  
  559. var close_link = document.createElement("A");
  560. close_link.href = "#" + this.element.id;
  561. close_link.onclick = function () { this.parentElement.parentElement.getElementsByClassName("btn")[0].onclick(); };
  562. close_link.innerText = "Close";
  563. content.appendChild(close_link);
  564. }
  565. }
  566.  
  567. var fetcher = new Fetcher();
  568.  
  569. var DefaultURLHelper = "Optional default hash data URL";
  570.  
  571.  
  572. function deleteYourself ( ) { this.outerHTML = ""; }
  573.  
  574. // Renders a box with user/token fields and button to ask for credentials
  575. function askCredentials ( )
  576. {
  577. if(document.getElementById("github-credentials-box"))
  578. return;
  579.  
  580. console.log("Asking credentials");
  581.  
  582. var box = document.createElement("DIV");
  583. box.style.backgroundColor = "white";
  584. box.style.position = "fixed";
  585. box.style.border = "solid black 2px";
  586. box.style.zIndex = 999999;
  587. box.style.left = "40%";
  588. box.style.top = "40%";
  589. box.style.padding = "20px";
  590. box.id = "github-credentials-box";
  591.  
  592. var textfield_user = document.createElement("INPUT");
  593. var textfield_token = document.createElement("INPUT");
  594. var textfield_hash_data_url = document.createElement("INPUT");
  595.  
  596. textfield_user.type = "text";
  597.  
  598. var user = GM_getValue("username");
  599. if (!user)
  600. user = "Username";
  601.  
  602. textfield_user.value = user;
  603. textfield_user.id = "github-user";
  604.  
  605. var token = GM_getValue("token");
  606. if (!token)
  607. token = "Github Token";
  608.  
  609. textfield_token.type = "text";
  610. textfield_token.value = token;
  611. textfield_token.id = "github-token";
  612.  
  613. var url = GM_getValue("hash_data_url");
  614. if (!url)
  615. url = DefaultURLHelper;
  616.  
  617. textfield_hash_data_url.type = "text";
  618. textfield_hash_data_url.value = url;
  619. textfield_hash_data_url.id = "hash-data-url";
  620.  
  621. var note = document.createElement("P");
  622. note.href = "https://github.com/settings/tokens";
  623. note.innerHTML = "The token required here can be created at <a href=\"https://github.com/settings/tokens\">your settings page</a>.<br>Required scope is 'repo'.";
  624.  
  625. var button = document.createElement("BUTTON");
  626. button.className = "btn";
  627. button.innerText = "Save";
  628. button.style.margin = "5px";
  629. button.onclick = saveCredentials;
  630.  
  631. box.appendChild(textfield_user);
  632. box.appendChild(textfield_token);
  633. box.appendChild(document.createElement("BR"));
  634. box.appendChild(textfield_hash_data_url);
  635. box.appendChild(button);
  636. box.appendChild(note);
  637.  
  638. document.body.appendChild(box);
  639. }
  640.  
  641. // saves the credentials and removes the box and the button
  642. function saveCredentials ( )
  643. {
  644. var user = document.getElementById("github-user");
  645. var token = document.getElementById("github-token");
  646. var hash_data_url = document.getElementById("hash-data-url");
  647.  
  648. if (hash_data_url.value != DefaultURLHelper)
  649. {
  650. hash_data_url = hash_data_url.value.trim();
  651.  
  652. if (hash_data_url.length > 0 && hash_data_url.substr(-1, 1) != "/")
  653. hash_data_url = hash_data_url + "/";
  654.  
  655. GM_setValue("hash_data_url", hash_data_url);
  656. }
  657.  
  658. GM_setValue("username", user.value.trim());
  659. GM_setValue("token", token.value.trim());
  660.  
  661. var box = document.getElementById("github-credentials-box");
  662. box.outerHTML = "";
  663.  
  664. fetchUpdates();
  665. }
  666.  
  667. function getTimeline ( )
  668. {
  669. var timeline;
  670. var timeline_content;
  671.  
  672. for (var i=0; i<discussion_bucket.children.length; i++)
  673. if (discussion_bucket.children[i].classname == "discussion-sidebar")
  674. continue;
  675. else
  676. timeline = discussion_bucket.children[i];
  677.  
  678. for (i=0; i < timeline.children.length; i++)
  679. if (timeline.children[i].className == "discussion-timeline-actions")
  680. continue;
  681. else
  682. timeline_content = timeline.children[0];
  683.  
  684. return timeline_content;
  685. }
  686.  
  687.  
  688. function getTimelineItems ( times_only, type )
  689. {
  690. var timeline_content = getTimeline();
  691.  
  692. // Walks up the parent chain until the direct parent is timeline_content
  693. var findTopMostChild = function ( child )
  694. {
  695. var my_child = child;
  696.  
  697. while (my_child.parentElement != timeline_content)
  698. my_child = my_child.parentElement;
  699.  
  700. return my_child;
  701. };
  702.  
  703. var times = timeline_content.getElementsByTagName("relative-time");
  704.  
  705. var return_array = [];
  706. var last;
  707. var last_was_review = false;
  708.  
  709. for (var o=0; o < times.length; o++)
  710. {
  711. var topmost = findTopMostChild(times[o]);
  712.  
  713. if (topmost == last)
  714. continue;
  715.  
  716. if (type == "review")
  717. {
  718. // Only review tags have this class
  719. var is_review = /discussion-item-review/g.test(topmost.className);
  720.  
  721.  
  722. if (!is_review)
  723. {
  724. last_was_review = false;
  725. continue;
  726. }
  727. }
  728. else if (type == "comment")
  729. {
  730. // Only comments have this class
  731. if (!/timeline-comment-wrapper/g.test(topmost.className))
  732. continue;
  733. }
  734.  
  735. if (times_only)
  736. {
  737. var date = times[o].getAttribute("datetime");
  738. var parsed_date = Date.parse(date);
  739.  
  740. // Collaps reviews that directly follow each other into one
  741. if (last_was_review)
  742. return_array[return_array.length-1] = parsed_date;
  743. else
  744. return_array.push(parsed_date);
  745. }
  746. else
  747. {
  748. // Collaps reviews that directly follow each other into one
  749. if (last_was_review)
  750. return_array[return_array.length-1] = topmost;
  751. else
  752. return_array.push(topmost);
  753. }
  754.  
  755. last = topmost;
  756. last_was_review = true;
  757. }
  758.  
  759. return return_array;
  760. }
  761.  
  762. function makeTimelineEntry ( time, text, action, id )
  763. {
  764. console.log("Creating entry " + text + " " + id);
  765.  
  766. var timeline_content = getTimeline();
  767.  
  768. // Walks up the parent chain until the direct parent is timeline_content
  769. var findTopMostChild = function ( child )
  770. {
  771. var my_child = child;
  772.  
  773. while (my_child.parentElement != timeline_content)
  774. my_child = my_child.parentElement;
  775.  
  776. return my_child;
  777. };
  778.  
  779. var times = timeline_content.getElementsByTagName("relative-time");
  780.  
  781. var insert_before;
  782.  
  783. // Find the right place in the timeline to insert
  784. for (var o=0; o < times.length; o++)
  785. {
  786. var date = times[o].getAttribute("datetime");
  787.  
  788. // Ignore review discussion timestamps
  789. if (/discussion/.test(times[o].parentElement.getAttribute("href")))
  790. continue;
  791.  
  792. if (Date.parse(date) > time)
  793. {
  794. insert_before = findTopMostChild(times[o]);
  795. break;
  796. }
  797. }
  798.  
  799. // Construct item to insert
  800. var timeline_item = document.createElement("DIV");
  801. timeline_item.className = "discussion-item-header discussion-item";
  802.  
  803. // Copied from github src code for push icon
  804. timeline_item.innerHTML = '<span class="discussion-item-icon"><svg aria-hidden="true" class="octicon octicon-repo-push" height="16" version="1.1" viewBox="0 0 12 16" width="12"><path fill-rule="evenodd" d="M4 3H3V2h1v1zM3 5h1V4H3v1zm4 0L4 9h2v7h2V9h2L7 5zm4-5H1C.45 0 0 .45 0 1v12c0 .55.45 1 1 1h4v-1H1v-2h4v-1H2V1h9.02L11 10H9v1h2v2H9v1h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1z"></path></svg></span>';
  805. timeline_item.id = id;
  806. timeline_item.appendChild(document.createTextNode(text));
  807.  
  808. var link = document.createElement("A");
  809.  
  810. link.className = "btn btn-sm btn-outline";
  811. link.innerText = "View changes";
  812. link.onclick = function () { action(this); return false; };
  813. link.href = "#";
  814.  
  815. timeline_item.appendChild(link);
  816.  
  817. timeline_content.insertBefore(timeline_item, insert_before);
  818. }
  819.  
  820. // Creates a button in the github sidebar in PRs
  821. function makeButton ( text, action, id )
  822. {
  823. var sidebar = document.getElementById("github-incremental-diffs-sidebar-item");
  824.  
  825. var buttondiv = document.createElement("DIV");
  826. buttondiv.id = id;
  827.  
  828. var button = document.createElement("A");
  829.  
  830. button.appendChild(document.createTextNode(text));
  831. button.onclick = function () { action(); return false; };
  832. button.href = "#";
  833.  
  834. buttondiv.appendChild(button);
  835. sidebar.appendChild(buttondiv);
  836. }
  837.  
  838. // Fetches the sha heads from hash_data_url
  839. function fetchUpdates ( base_url )
  840. {
  841. var urlsplit = document.URL.split("/");
  842. var owner = urlsplit[3];
  843. var repo = urlsplit[4];
  844. var prid_and_anker = urlsplit[6].split("#");
  845.  
  846. var prid = prid_and_anker[0];
  847.  
  848. var url = base_url+owner+'/'+repo+'/' + prid + "?cachebust=" + new Date().getTime();
  849.  
  850. console.log("Fetching updates from " + url);
  851.  
  852. // Create a new request object
  853. GM_xmlhttpRequest({
  854. method: "GET",
  855. url: url,
  856. onload: function (response) {
  857. if (response.status == 200)
  858. injectTimeline(response.responseText);
  859. else
  860. console.log("No pushes found at "+url+": " + response.status);
  861. }});
  862. }
  863.  
  864. /* Injects "Author pushed" events into the PR timeline
  865. *
  866. * Params:
  867. * shas = list of sha's and unix timestamp pairs. Sha and timestamp are separated by ";".
  868. * Each pair is separated by "\n"
  869. */
  870. function injectTimeline ( shas )
  871. {
  872. var sidebar = document.getElementsByClassName("discussion-sidebar")[0];
  873.  
  874. if (sidebar.removeEventListener)
  875. {
  876. sidebar.removeEventListener ('DOMSubtreeModified', fetchDelayed);
  877. }
  878.  
  879. var sha_list = shas.split("\n");
  880.  
  881. var base, head, update;
  882.  
  883. update = 1;
  884.  
  885. function makeShowDiffFunc ( inner_base, inner_head )
  886. {
  887. var func = function ( item )
  888. {
  889. if (item.innerText == "Hide changes")
  890. {
  891. var elem = item.parentElement.getElementsByClassName("file")[0];
  892. elem.outerHTML = "";
  893.  
  894. item.innerText = "View changes";
  895. return;
  896. }
  897.  
  898. var cont = document.createElement("DIV");
  899. cont.className = "file";
  900. cont.innerHTML = "Loading...";
  901. cont.style.backgroundColor = "yellow";
  902.  
  903. item.parentElement.appendChild(cont);
  904.  
  905. var urlsplit = document.URL.split("/");
  906. var owner = urlsplit[3];
  907. var repo = urlsplit[4];
  908.  
  909. item.innerText = "Hide changes";
  910.  
  911. console.log("pressed.. " + inner_base + " " + inner_head);
  912.  
  913. var prid_and_anker = document.URL.split("/")[6].split("#");
  914. var prid = prid_and_anker[0];
  915.  
  916. fetcher.start(owner, repo, prid, inner_base, inner_head, item.parentElement);
  917. };
  918. return func;
  919. }
  920.  
  921. var pairs = [];
  922.  
  923. // Build pairs of commits to create diff from
  924. for (var i = 0; i < sha_list.length; i++)
  925. {
  926. if (sha_list[i].length === 0)
  927. continue;
  928.  
  929. var sha_data = sha_list[i].split(";");
  930. var sha = sha_data[0];
  931. var time;
  932.  
  933. if (sha_data[1] !== undefined)
  934. time = new Date(parseInt(sha_data[1]) * 1000);
  935.  
  936. if (base === undefined)
  937. {
  938. base = sha;
  939. continue;
  940. }
  941.  
  942. head = sha;
  943.  
  944. var pair = {};
  945. pair.base = base;
  946. pair.head = head;
  947. pair.time = time;
  948.  
  949. pairs.push(pair);
  950.  
  951. base = head;
  952. }
  953.  
  954. console.log("Pairs: " + pairs.length + " last: " + head);
  955.  
  956. // Next, merge the pairs between reviews/comments
  957. var timeline_items = getTimelineItems(true, "review");
  958.  
  959. console.log("Found " + timeline_items.length + " items");
  960.  
  961. var base_pair = null;
  962.  
  963. var merged_pairs = [];
  964. var merged_pair = {};
  965.  
  966. var timeline_it = 0;
  967.  
  968. // Only try to merge pairs if more than one exists
  969. if (pairs.length > 1)
  970. {
  971. for (i=0; i < pairs.length; i++)
  972. {
  973. // Find the first review that is right before newer than our current
  974. while (pairs[i].time.getTime() > timeline_items[timeline_it] &&
  975. timeline_it+1 < timeline_items.length &&
  976. pairs[i].time.getTime() > timeline_items[timeline_it+1])
  977. timeline_it++;
  978.  
  979. //console.log("Comparing " + pairs[i].time + " > " + new Date(timeline_items[timeline_it]) + " " + i + " > " + timeline_it);
  980.  
  981. if (pairs[i].time.getTime() > timeline_items[timeline_it])
  982. {
  983. if (base_pair === null)
  984. {
  985. console.log("Set base at " + i);
  986. base_pair = pairs[i];
  987. timeline_it++;
  988. continue;
  989. }
  990.  
  991. console.log("Merging a pair");
  992.  
  993. // And use the pair one before that as head
  994. var head_pair = pairs[i-1];
  995.  
  996. merged_pair = {};
  997. merged_pair.base = base_pair.base;
  998. merged_pair.head = head_pair.head;
  999. merged_pair.time = head_pair.time;
  1000.  
  1001. merged_pairs.push(merged_pair);
  1002.  
  1003. base_pair = pairs[i];
  1004.  
  1005. timeline_it++;
  1006.  
  1007. if (timeline_it >= timeline_items.length)
  1008. break;
  1009.  
  1010. continue;
  1011. }
  1012. }
  1013.  
  1014. // Merge any remaining pairs
  1015. if (merged_pairs.length === 0 ||
  1016. merged_pairs[merged_pairs.length-1].head != pairs[pairs.length-1].head)
  1017. {
  1018. merged_pair = {};
  1019. merged_pair.base = base_pair.base;
  1020. merged_pair.head = pairs[pairs.length-1].head;
  1021. merged_pair.time = pairs[pairs.length-1].time;
  1022.  
  1023. merged_pairs.push(merged_pair);
  1024. }
  1025. }
  1026. else
  1027. {
  1028. merged_pairs = pairs;
  1029. }
  1030.  
  1031. console.log("Merged pairs: " + merged_pairs.length);
  1032.  
  1033. for (i=0; i < merged_pairs.length; i++)
  1034. {
  1035. var it = merged_pairs[i];
  1036.  
  1037. // Don't remake a button that already exists
  1038. if (!document.getElementById("diffbutton-" + update))
  1039. {
  1040. var formatted_time = update;
  1041.  
  1042. var addZero = function ( num )
  1043. {
  1044. if (num < 10)
  1045. num = "0" + num;
  1046.  
  1047. return num;
  1048. };
  1049.  
  1050. if (it.time !== undefined)
  1051. formatted_time = it.time.getDate() + "." +
  1052. addZero((it.time.getMonth()+1)) + "." +
  1053. it.time.getFullYear() + " " +
  1054. addZero(it.time.getHours()) + ":" +
  1055. addZero(it.time.getMinutes());
  1056.  
  1057. makeTimelineEntry(it.time.getTime(), "Author pushed code changes at " + formatted_time, makeShowDiffFunc(it.base, it.head), "diffbutton-" + update);
  1058. }
  1059.  
  1060. update++;
  1061. }
  1062.  
  1063. if (sidebar.addEventListener)
  1064. {
  1065. sidebar.addEventListener ('DOMSubtreeModified', fetchDelayed, false);
  1066. }
  1067. }
  1068.  
  1069. function fetchDelayed ( )
  1070. {
  1071. // Don't fetch again if there are still diff buttons
  1072. if (document.getElementById("diffbutton-1"))
  1073. {
  1074. return;
  1075. }
  1076.  
  1077. var sidebar = document.getElementsByClassName("discussion-sidebar")[0];
  1078. sidebar.removeEventListener ('DOMSubtreeModified', fetchDelayed);
  1079. setTimeout(fetchUpdates, 1000);
  1080. }
  1081.  
  1082. function render ( )
  1083. {
  1084. 'use strict';
  1085.  
  1086. var need_setup = !GM_getValue("username") || !GM_getValue("token");
  1087.  
  1088. var css_style = GM_getResourceText ("CSSDIFF");
  1089. GM_addStyle (css_style);
  1090.  
  1091. var sidebar = document.getElementById("partial-discussion-sidebar");
  1092.  
  1093. if (sidebar !== null)
  1094. {
  1095. var item = document.createElement("DIV");
  1096. item.className = "discussion-sidebar-item";
  1097. item.id = "github-incremental-diffs-sidebar-item";
  1098.  
  1099. var button = document.createElement("BUTTON");
  1100. button.className = "btn btn-sm";
  1101. button.type = "submit";
  1102.  
  1103. button.appendChild(document.createTextNode("Incremental Diffs Setup"));
  1104. button.onclick = askCredentials;
  1105.  
  1106. item.appendChild(button);
  1107.  
  1108. sidebar.appendChild(item);
  1109.  
  1110. fetchBaseUrl();
  1111. }
  1112. }
  1113.  
  1114. function fetchBaseUrl ( )
  1115. {
  1116. var baseUrlCb = function ( )
  1117. {
  1118. if (this.status == 404)
  1119. {
  1120. console.log("No project specific base URL, using global one: " + GM_getValue("hash_data_url"));
  1121. fetchUpdates(GM_getValue("hash_data_url"));
  1122. return;
  1123. }
  1124.  
  1125. var response = JSON.parse(this.responseText);
  1126.  
  1127. var blobCb = function ( )
  1128. {
  1129. var resp = JSON.parse(this.responseText);
  1130. var base_url = atob(resp.content);
  1131.  
  1132. console.log("Found project specific base url " + base_url);
  1133. fetchUpdates(base_url);
  1134. };
  1135.  
  1136. var request2 = new XMLHttpRequest();
  1137. request2.onload = blobCb;
  1138. request2.open('get', response.object.url);
  1139. var usertoken = GM_getValue("username") + ":" + GM_getValue("token");
  1140. request2.setRequestHeader("Authorization", "Basic " + btoa(usertoken));
  1141. request2.send();
  1142. };
  1143.  
  1144. var request = new XMLHttpRequest();
  1145.  
  1146. request.onload = baseUrlCb;
  1147.  
  1148. var urlsplit = document.URL.split("/");
  1149. var owner = urlsplit[3];
  1150. var repo = urlsplit[4];
  1151.  
  1152. // Initialize a request
  1153. request.open('get', "https://api.github.com/repos/" + owner + "/" + repo + "/git/refs/meta/incremental-diff-url");
  1154.  
  1155. var usertoken = GM_getValue("username") + ":" + GM_getValue("token");
  1156. request.setRequestHeader("Authorization", "Basic " + btoa(usertoken));
  1157. // Send it
  1158. request.send();
  1159. }
  1160.  
  1161.  
  1162. (function()
  1163. {
  1164. var parts = document.URL.split("/");
  1165.  
  1166. if (parts[5] == "pull")
  1167. render();
  1168. // This is required for this script to be run upon ajax load.. not sure why
  1169. window.onbeforeunload = function()
  1170. {
  1171. console.log("window changed!");
  1172. };
  1173. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址