Github PR Incremental Diffs

Provides you incremental diffs with the help of an extra server

目前為 2017-04-25 提交的版本,檢視 最新版本

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

QingJ © 2025

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