🏠 

Github PR Incremental Diffs

Provides you incremental diffs with the help of an extra server


Install this script?
// ==UserScript==
// @name         Github PR Incremental Diffs
// @version      1.2
// @namespace    https://tampermonkey.net/
// @homepage     https://github.com/sociomantic-tsunami/kelpie
// @supportURL   https://github.com/sociomantic-tsunami/kelpie/issues
// @description  Provides you incremental diffs with the help of an extra server
// @author       Mathias L. Baumann
// @copyright    Copyright (c) 2017-2018 dunnhumby Germany GmbH. All rights reserved.
// @license      Boost Software License 1.0 (https://www.boost.org/LICENSE_1_0.txt)
// @match        *://github.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @grant       GM_getResourceText
// @grant       GM_xmlhttpRequest
// @require     https://cdn.rawgit.com/cemerick/jsdifflib/da7da27640a30537cea9069622dc71600c5a1d61/difflib.js
// @require     https://cdn.rawgit.com/cemerick/jsdifflib/da7da27640a30537cea9069622dc71600c5a1d61/diffview.js
// @resource    CSSDIFF https://cdn.rawgit.com/cemerick/jsdifflib/da7da27640a30537cea9069622dc71600c5a1d61/diffview.css
// ==/UserScript==
class FileTree
{
/* Params:
sha = sha of the file tree
url = api url of the file tree
*/
constructor ( sha, url, root_path )
{
this.root_path = root_path;
this.sha = sha;
this.url = url;
this.list = [];
}
// Fetches the tree from using the API and calls callback with the result
fetch ( cbthis, callback )
{
if (this.list && this.list.length > 0)
{
callback(this);
return;
}
var request = new XMLHttpRequest();
var receiveTree = function ( )
{
var response = JSON.parse(this.responseText);
for (var i=0; i < response.tree.length; i++)
{
var obj = { "path" : this.outside.root_path + response.tree[i].path,
"sha"  : response.tree[i].sha,
"url"  : response.tree[i].url,
"type" : response.tree[i].type };
// Don't get the blob for tree's, get it as another tree
if (response.tree[i].type == "tree")
obj.url = obj.url.replace(/blobs/, "trees");
this.outside.list.push(obj);
//console.log("entry info " + obj.path + ", " + obj.sha);
}
this.userCb.call(this.cbthis, this.outside);
};
request.outside = this;
request.onload = receiveTree;
request.userCb = callback;
request.cbthis = cbthis;
// Initialize a request
request.open('get', this.url);
var usertoken = GM_getValue("username") + ":" + GM_getValue("token");
request.setRequestHeader("Authorization", "Basic " + btoa(usertoken));
// Send it
request.send();
}
}
class FileDiffer
{
constructor ( base, head, original )
{
// List of files that have changed
this.changed = [];
this.base = base;
this.head = head;
this.original = original;
}
fetch ( cbthis, callback )
{
this.cbthis = cbthis;
this.callback = callback;
if (this.base === null)
this.base = { "list" : [] };
else
{
var tmp_base = this.base;
this.base = null;
tmp_base.fetch(this, this.assignBase);
}
if (this.head === null)
this.head = { "list" : [] };
else
{
var tmp_head = this.head;
this.head = null;
tmp_head.fetch(this, this.assignHead);
}
if (this.original === null)
this.original = { "list" : [] };
else
{
var tmp_orig = this.original;
this.original = null;
tmp_orig.fetch(this, this.assignOriginal);
}
}
assignBase ( base ) { this.base = base; this.checkComplete(); }
assignHead ( head ) { this.head = head; this.checkComplete(); }
assignOriginal ( original ) { this.original = original; this.checkComplete(); }
checkComplete ( )
{
if (!this.base || !this.head || !this.original)
return;
console.log("Received all trees, extracting required files");
var diff = null;
var i = 0;
var head_el = null;
var matchPath = function (el) { return el.path==head_el.path; };
// Find all paths differing from base
for (i=0; i < this.head.list.length; i++)
{
head_el = this.head.list[i];
var orig_path = this.original.list.find(matchPath);
// If this path exists in original with the same sha, it was added through a rebase
if (orig_path !== undefined && orig_path.sha == head_el.sha)
continue; // so ignore it
var base_path = this.base.list.find(matchPath);
if (orig_path === undefined)
orig_path = null;
// base doesn't have that file?
if (base_path === undefined)
{   // completely new file
diff = { "base" : null, "head" : head_el,
"orig" : null };
console.log("File differs (no base): " + head_el.path);
this.changed.push(diff);
continue;
}
// file exists in base and differs
if (base_path.sha != head_el.sha)
{   // changes have been made
diff = { "base" : base_path, "head" : head_el,
"orig" : orig_path };
console.log("File differs: " + head_el.path);
this.changed.push(diff);
continue;
}
}
// Find any files not existing in head, but existing in base
for (i=0; i < this.base.list.length; i++)
{
var base_el = this.base.list[i];
head_el = this.head.list.find(matchPath);
if (head_el !== undefined)
continue;
diff = { "base" : base_el, "head" : null, "orig" : null };
console.log("File differs (no head): " + base_el.path);
this.changed.push(diff);
}
this.recurseTree();
}
// recurses into tree objects in our "changed" paths list and looks for diffs
recurseTree ( )
{
var i = 0;
var el = {};
var base_tree = {};
var head_tree = {};
var orig_tree = {};
var did_recurse = false;
var path = "";
console.log("Recursing...");
// Prepare to recurse
for (i=0; i < this.changed.length; i++)
{
el = this.changed[i];
// No need to recurse if one is null
if (el.head === null || el.base === null)
continue;
// we can only recurse into trees
if (el.head.type != "tree" && el.base.type != "tree")
continue;
if (el.base.type == "tree")
base_tree = new FileTree(el.base.sha, el.base.url, el.base.path + "/");
else
base_tree = null;
if (el.head.type == "tree")
head_tree = new FileTree(el.head.sha, el.head.url, el.head.path + "/");
else
head_tree = null;
if (el.orig !== null && el.orig.type == "tree")
orig_tree = new FileTree(el.orig.sha, el.orig.url, el.orig.path + "/");
else
orig_tree = null;
console.log("Recurse task " + el.head.path);
el.pending = new FileDiffer(base_tree, head_tree, orig_tree);
}
// Actually do the recursion
for (i=0; i < this.changed.length; i++)
{
el = this.changed[i];
if ("pending" in el)
{
console.log("Starting task.. " + el.base.path);
el.pending.fetch(this, this.recurseCallback);
did_recurse = true;
}
}
if (did_recurse === false)
this.fetchAllFiles();
}
// called once for every recursion
// * merges the recursed tree with ours
// * if no more callbacks pending, calls user cb
recurseCallback ( file_differ )
{
var still_waiting = false;
for (var i=0; i < this.changed.length; i++)
{
var el = this.changed[i];
if ("pending" in el && el.pending == file_differ)
{
console.log("recurseCb for " + el.base.path + " updated");
this.changed = this.changed.concat(file_differ.changed);
el.pending = null;
continue;
}
if ("pending" in el && el.pending !== null)
{
console.log("Still waiting for " + el.base.path);
still_waiting = true;
}
}
if (still_waiting)
{
return;
}
this.fetchAllFiles();
}
fetchAllFiles ( )
{
console.log("Fetching files...");
for (var i=0; i < this.changed.length; i++)
{
var el = this.changed[i];
if (el.base && el.base.url && !("content" in el.base))
this.fetchFile(el.base.url);
if (el.head && el.head.url && !("content" in el.head))
this.fetchFile(el.head.url);
}
if (this.changed.length === 0)
this.checkReceivedFiles();
}
fetchFile ( url )
{
console.log("Fetching " + url);
var request = new XMLHttpRequest();
var receiveBlob = function ( )
{
var response = JSON.parse(this.responseText);
function findMatch (elem)
{
if (elem.base && elem.base.sha == response.sha)
return true;
else if (elem.head && elem.head.sha == response.sha)
return true;
return false;
}
var el = this.outside.changed.find(findMatch);
if (el === undefined)
{
console.log("received unexpected sha " + response.sha);
return;
}
console.log("Received content for " + el.head.path);
var content = "content" in response && response.content.length > 0 ? atob(response.content) : "";
if (el.base !== null && el.base.sha == response.sha)
el.base.content = content;
else if (el.head !== null && el.head.sha == response.sha)
el.head.content = content;
else
console.log("Unmatched sha?!");
this.outside.checkReceivedFiles();
};
request.outside = this;
request.onload = receiveBlob;
// Initialize a request
request.open('get', url);
var usertoken = GM_getValue("username") + ":" + GM_getValue("token");
request.setRequestHeader("Authorization", "Basic " + btoa(usertoken));
// Send it
request.send();
}
checkReceivedFiles ( )
{
var all_content_received = true;
for (var i=0; i < this.changed.length; i++)
{
var el = this.changed[i];
if (el.base && !("content" in el.base) ||
el.head && !("content" in el.head))
{
all_content_received = false;
break;
}
}
if (all_content_received)
{
console.log("Received all content, calling cb");
this.callback.call(this.cbthis, this);
}
}
}
class Fetcher
{
constructor ( )
{
this.files = [];
}
start ( owner, repo, pr, commit1, commit2, element )
{
this.sha_base = commit1;
this.sha_head = commit2;
this.owner = owner;
this.repo = repo;
this.element = element;
this.base_tree = null;
this.head_tree = null;
this.orig_tree = null;
this.usertoken = GM_getValue("username") + ":" + GM_getValue("token");
//this.fetchCommit(this.sha_update, "update");
this.fetchPrBase(pr);
}
// Fetches the base branch for the PR and extracts the latest commits sha
fetchPrBase ( pr )
{
console.log("Fetching PR base");
var receivePr = function ( )
{
var response = JSON.parse(this.responseText);
this.outside.fetchTreeShas(this.outside.sha_base, this.outside.sha_head, response.base.sha);
};
var request = new XMLHttpRequest();
request.outside = this;
request.onload = receivePr;
// Initialize a request
request.open('get', "https://api.github.com/repos/"+this.owner+"/"+this.repo+"/pulls/" + pr);
request.setRequestHeader("Authorization", "Basic " + btoa(this.usertoken));
// Send it
request.send();
}
// Extracts the tree shas from base/head/orig commit
fetchTreeShas ( base, head, orig )
{
console.log("Fetching trees");
this.fetchTreeFromCommit(base, "base_tree", this.checkTreesDone);
this.fetchTreeFromCommit(head, "head_tree", this.checkTreesDone);
this.fetchTreeFromCommit(orig, "orig_tree", this.checkTreesDone);
}
checkTreesDone ( )
{
console.log("checkTreesDone()");
if (!this.base_tree || !this.head_tree || !this.orig_tree)
{
console.log("Not all done: " + this.base_tree + " " + this.head_tree + " " + this.orig_tree);
return;
}
console.log("Received all trees-shas, fetching content..");
var differ = new FileDiffer(this.base_tree, this.head_tree, this.orig_tree);
differ.fetch(this, this.render);
}
printMe ( )
{
for (var key in this)
console.log("key: " + key);
}
fetchTreeFromCommit ( commit, name, usercb )
{
console.log("Fetching " + name + " " + commit);
var receiveCommit = function ( )
{
var response = JSON.parse(this.responseText);
console.log("Received " + this.commit_name);
this.outside[this.commit_name] = new FileTree(response.tree.sha, response.tree.url, "");
this.usercb.call(this.outside);
};
var request = new XMLHttpRequest();
request.outside = this;
request.onload = receiveCommit;
request.commit_name = name;
request.usercb = usercb;
// Initialize a request
request.open('get', "https://api.github.com/repos/"+this.owner+"/"+this.repo+"/git/commits/" + commit);
request.setRequestHeader("Authorization", "Basic " + btoa(this.usertoken));
// Send it
request.send();
}
// Generate the diff, append the elements to this.element
render ( differ )
{
"use strict";
var contents = this.element.getElementsByClassName("file");
var content = contents[0];
content.innerHTML = "";
content.style.backgroundColor = "white";
content.style.textAlign = "center";
for (var i = 0; i < differ.changed.length; i++)
{
var el = differ.changed[i];
if ((el.head === null || el.head.type != "blob") &&
(el.base === null || el.base.type != "blob"))
continue;
var base_content = el.base ? el.base.content : "";
var head_content = el.head ? el.head.content : "";
var fname = el.head ? el.head.path : el.base.path;
var base = difflib.stringAsLines(base_content),
newtxt = difflib.stringAsLines(head_content),
sm = new difflib.SequenceMatcher(base, newtxt),
opcodes = sm.get_opcodes(),
contextSize = 5; //byId("contextSize").value;
var filename = document.createElement("DIV");
filename.className = "file-header";
filename.innerText = fname;
content.appendChild(filename);
contextSize = contextSize || null;
var diff = diffview.buildView({
baseTextLines: base,
newTextLines: newtxt,
opcodes: opcodes,
baseTextName: "Old",
newTextName: "New",
contextSize: contextSize,
viewType: 0 // 0 for side-by-side
});
diff.className = diff.className + " blob-wrapper";
diff.style.margin = "auto";
diff.style.textAlign = "left";
content.appendChild(diff);
}
var pos = content.getBoundingClientRect();
content.style.left = "" + (-pos.left + 15) + "px";
content.style.width = "" + (document.documentElement.clientWidth - 30) + "px";
var close_link = document.createElement("A");
close_link.href = "#" + this.element.id;
close_link.onclick = function () { this.parentElement.parentElement.getElementsByClassName("btn")[0].onclick(); };
close_link.innerText = "Close";
content.appendChild(close_link);
}
}
var fetcher = new Fetcher();
var DefaultURLHelper = "Optional default hash data URL";
function deleteYourself ( ) { this.outerHTML = ""; }
// Renders a box with user/token fields and button to ask for credentials
function askCredentials ( )
{
if(document.getElementById("github-credentials-box"))
return;
console.log("Asking credentials");
var box = document.createElement("DIV");
box.style.backgroundColor = "white";
box.style.position = "fixed";
box.style.border = "solid black 2px";
box.style.zIndex = 999999;
box.style.left = "40%";
box.style.top = "40%";
box.style.padding = "20px";
box.id = "github-credentials-box";
var textfield_user = document.createElement("INPUT");
var textfield_token = document.createElement("INPUT");
var textfield_hash_data_url = document.createElement("INPUT");
textfield_user.type = "text";
var user = GM_getValue("username");
if (!user)
user = "Username";
textfield_user.value = user;
textfield_user.id = "github-user";
var token = GM_getValue("token");
if (!token)
token = "Github Token";
textfield_token.type = "text";
textfield_token.value = token;
textfield_token.id = "github-token";
var url = GM_getValue("hash_data_url");
if (!url)
url = DefaultURLHelper;
textfield_hash_data_url.type = "text";
textfield_hash_data_url.value = url;
textfield_hash_data_url.id = "hash-data-url";
var note = document.createElement("P");
note.href = "https://github.com/settings/tokens";
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'.";
var button = document.createElement("BUTTON");
button.className = "btn";
button.innerText = "Save";
button.style.margin = "5px";
button.onclick = saveCredentials;
box.appendChild(textfield_user);
box.appendChild(textfield_token);
box.appendChild(document.createElement("BR"));
box.appendChild(textfield_hash_data_url);
box.appendChild(button);
box.appendChild(note);
document.body.appendChild(box);
}
// saves the credentials and removes the box and the button
function saveCredentials ( )
{
var user = document.getElementById("github-user");
var token = document.getElementById("github-token");
var hash_data_url = document.getElementById("hash-data-url");
if (hash_data_url.value != DefaultURLHelper)
{
hash_data_url = hash_data_url.value.trim();
if (hash_data_url.length > 0 && hash_data_url.substr(-1, 1) != "/")
hash_data_url = hash_data_url + "/";
GM_setValue("hash_data_url", hash_data_url);
}
GM_setValue("username", user.value.trim());
GM_setValue("token", token.value.trim());
var box = document.getElementById("github-credentials-box");
box.outerHTML = "";
fetchUpdates();
}
function getTimeline ( )
{
var timeline;
var timeline_content;
for (var i=0; i<discussion_bucket.children.length; i++)
if (discussion_bucket.children[i].classname == "discussion-sidebar")
continue;
else
timeline = discussion_bucket.children[i];
for (i=0; i < timeline.children.length; i++)
if (timeline.children[i].className == "discussion-timeline-actions")
continue;
else
timeline_content = timeline.children[0];
return timeline_content;
}
function getTimelineItems ( times_only, type )
{
var timeline_content = getTimeline();
// Walks up the parent chain until the direct parent is timeline_content
var findTopMostChild = function ( child )
{
var my_child = child;
while (my_child.parentElement != timeline_content)
my_child = my_child.parentElement;
return my_child;
};
var times = timeline_content.getElementsByTagName("relative-time");
var return_array = [];
var last;
var last_was_review = false;
for (var o=0; o < times.length; o++)
{
var topmost = findTopMostChild(times[o]);
if (topmost == last)
continue;
if (type == "review")
{
// Only review tags have this class
var is_review = /discussion-item-review/g.test(topmost.className);
if (!is_review)
{
last_was_review = false;
continue;
}
}
else if (type == "comment")
{
// Only comments have this class
if (!/timeline-comment-wrapper/g.test(topmost.className))
continue;
}
if (times_only)
{
var date = times[o].getAttribute("datetime");
var parsed_date = Date.parse(date);
// Collaps reviews that directly follow each other into one
if (last_was_review)
return_array[return_array.length-1] = parsed_date;
else
return_array.push(parsed_date);
}
else
{
// Collaps reviews that directly follow each other into one
if (last_was_review)
return_array[return_array.length-1] = topmost;
else
return_array.push(topmost);
}
last = topmost;
last_was_review = true;
}
return return_array;
}
function makeTimelineEntry ( time, text, action, id )
{
console.log("Creating entry " + text + " " + id);
var timeline_content = getTimeline();
// Walks up the parent chain until the direct parent is timeline_content
var findTopMostChild = function ( child )
{
var my_child = child;
while (my_child.parentElement != timeline_content)
my_child = my_child.parentElement;
return my_child;
};
var times = timeline_content.getElementsByTagName("relative-time");
var insert_before;
// Find the right place in the timeline to insert
for (var o=0; o < times.length; o++)
{
var date = times[o].getAttribute("datetime");
// Ignore review discussion timestamps
if (/discussion/.test(times[o].parentElement.getAttribute("href")))
continue;
if (Date.parse(date) > time)
{
insert_before = findTopMostChild(times[o]);
break;
}
}
// Construct item to insert
var timeline_item = document.createElement("DIV");
timeline_item.className = "discussion-item-header discussion-item";
// Copied from github src code for push icon
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>';
timeline_item.id = id;
timeline_item.appendChild(document.createTextNode(text));
var link = document.createElement("A");
link.className = "btn btn-sm btn-outline";
link.innerText = "View changes";
link.onclick = function () { action(this); return false; };
link.href = "#";
timeline_item.appendChild(link);
timeline_content.insertBefore(timeline_item, insert_before);
}
// Creates a button in the github sidebar in PRs
function makeButton ( text, action, id )
{
var sidebar = document.getElementById("github-incremental-diffs-sidebar-item");
var buttondiv = document.createElement("DIV");
buttondiv.id = id;
var button = document.createElement("A");
button.appendChild(document.createTextNode(text));
button.onclick = function () { action(); return false; };
button.href = "#";
buttondiv.appendChild(button);
sidebar.appendChild(buttondiv);
}
// Fetches the sha heads from hash_data_url
function fetchUpdates ( base_url )
{
var urlsplit = document.URL.split("/");
var owner = urlsplit[3];
var repo  = urlsplit[4];
var prid_and_anker = urlsplit[6].split("#");
var prid = prid_and_anker[0];
var url = base_url+owner+'/'+repo+'/' + prid + "?cachebust=" + new Date().getTime();
console.log("Fetching updates from " + url);
// Create a new request object
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function (response) {
if (response.status == 200)
injectTimeline(response.responseText);
else
console.log("No pushes found at "+url+": " + response.status);
}});
}
/* Injects "Author pushed" events into the PR timeline
*
* Params:
*     shas = list of sha's and unix timestamp pairs. Sha and timestamp are separated by ";".
*            Each pair is separated by "\n"
*/
function injectTimeline ( shas )
{
var sidebar = document.getElementsByClassName("discussion-sidebar")[0];
if (sidebar.removeEventListener)
{
sidebar.removeEventListener ('DOMSubtreeModified', fetchDelayed);
}
var sha_list = shas.split("\n");
var base, head, update;
update = 1;
function makeShowDiffFunc ( inner_base, inner_head )
{
var func = function ( item )
{
if (item.innerText == "Hide changes")
{
var elem = item.parentElement.getElementsByClassName("file")[0];
elem.outerHTML = "";
item.innerText = "View changes";
return;
}
var cont = document.createElement("DIV");
cont.className = "file";
cont.innerHTML = "Loading...";
cont.style.backgroundColor = "yellow";
item.parentElement.appendChild(cont);
var urlsplit = document.URL.split("/");
var owner = urlsplit[3];
var repo  = urlsplit[4];
item.innerText = "Hide changes";
console.log("pressed.. " + inner_base + " " + inner_head);
var prid_and_anker = document.URL.split("/")[6].split("#");
var prid = prid_and_anker[0];
fetcher.start(owner, repo, prid, inner_base, inner_head, item.parentElement);
};
return func;
}
var pairs = [];
// Build pairs of commits to create diff from
for (var i = 0; i < sha_list.length; i++)
{
if (sha_list[i].length === 0)
continue;
var sha_data = sha_list[i].split(";");
var sha = sha_data[0];
var time;
if (sha_data[1] !== undefined)
time = new Date(parseInt(sha_data[1]) * 1000);
if (base === undefined)
{
base = sha;
continue;
}
head = sha;
var pair = {};
pair.base = base;
pair.head = head;
pair.time = time;
pairs.push(pair);
base = head;
}
console.log("Pairs: " + pairs.length + " last: " + head);
// Next, merge the pairs between reviews/comments
var timeline_items = getTimelineItems(true, "review");
console.log("Found " + timeline_items.length + " items");
var base_pair = null;
var merged_pairs = [];
var merged_pair = {};
var timeline_it = 0;
// Only try to merge pairs if more than one exists
if (pairs.length > 1)
{
for (i=0; i < pairs.length; i++)
{
// Find the first review that is right before newer than our current
while (pairs[i].time.getTime() > timeline_items[timeline_it] &&
timeline_it+1 < timeline_items.length &&
pairs[i].time.getTime() > timeline_items[timeline_it+1])
timeline_it++;
//console.log("Comparing " + pairs[i].time + " > " + new Date(timeline_items[timeline_it]) + " " + i + " > " + timeline_it);
if (pairs[i].time.getTime() > timeline_items[timeline_it])
{
if (base_pair === null)
{
console.log("Set base at " + i);
base_pair = pairs[i];
timeline_it++;
continue;
}
console.log("Merging a pair");
// And use the pair one before that as head
var head_pair = pairs[i-1];
merged_pair = {};
merged_pair.base = base_pair.base;
merged_pair.head = head_pair.head;
merged_pair.time = head_pair.time;
merged_pairs.push(merged_pair);
base_pair = pairs[i];
timeline_it++;
if (timeline_it >= timeline_items.length)
break;
continue;
}
}
// Merge any remaining pairs
if (merged_pairs.length === 0 ||
merged_pairs[merged_pairs.length-1].head != pairs[pairs.length-1].head)
{
merged_pair = {};
merged_pair.base = base_pair.base;
merged_pair.head = pairs[pairs.length-1].head;
merged_pair.time = pairs[pairs.length-1].time;
merged_pairs.push(merged_pair);
}
}
else
{
merged_pairs = pairs;
}
console.log("Merged pairs: " + merged_pairs.length);
for (i=0; i < merged_pairs.length; i++)
{
var it = merged_pairs[i];
// Don't remake a button that already exists
if (!document.getElementById("diffbutton-" + update))
{
var formatted_time = update;
var addZero = function ( num )
{
if (num < 10)
num = "0" + num;
return num;
};
if (it.time !== undefined)
formatted_time = it.time.getDate() + "." +
addZero((it.time.getMonth()+1)) + "." +
it.time.getFullYear() + " " +
addZero(it.time.getHours()) + ":" +
addZero(it.time.getMinutes());
makeTimelineEntry(it.time.getTime(), "Author pushed code changes at " + formatted_time, makeShowDiffFunc(it.base, it.head), "diffbutton-" + update);
}
update++;
}
if (sidebar.addEventListener)
{
sidebar.addEventListener ('DOMSubtreeModified', fetchDelayed, false);
}
}
function fetchDelayed ( )
{
// Don't fetch again if there are still diff buttons
if (document.getElementById("diffbutton-1"))
{
return;
}
var sidebar = document.getElementsByClassName("discussion-sidebar")[0];
sidebar.removeEventListener ('DOMSubtreeModified', fetchDelayed);
setTimeout(fetchUpdates, 1000);
}
function render ( )
{
'use strict';
var need_setup = !GM_getValue("username") || !GM_getValue("token");
var css_style = GM_getResourceText ("CSSDIFF");
GM_addStyle (css_style);
var sidebar = document.getElementById("partial-discussion-sidebar");
if (sidebar !== null)
{
var item = document.createElement("DIV");
item.className = "discussion-sidebar-item";
item.id = "github-incremental-diffs-sidebar-item";
var button = document.createElement("BUTTON");
button.className = "btn btn-sm";
button.type = "submit";
button.appendChild(document.createTextNode("Incremental Diffs Setup"));
button.onclick = askCredentials;
item.appendChild(button);
sidebar.appendChild(item);
fetchBaseUrl();
}
}
function fetchBaseUrl ( )
{
var baseUrlCb = function ( )
{
if (this.status == 404)
{
console.log("No project specific base URL, using global one: " + GM_getValue("hash_data_url"));
fetchUpdates(GM_getValue("hash_data_url"));
return;
}
var response = JSON.parse(this.responseText);
var blobCb = function ( )
{
var resp = JSON.parse(this.responseText);
var base_url = atob(resp.content);
console.log("Found project specific base url " + base_url);
fetchUpdates(base_url);
};
var request2 = new XMLHttpRequest();
request2.onload = blobCb;
request2.open('get', response.object.url);
var usertoken = GM_getValue("username") + ":" + GM_getValue("token");
request2.setRequestHeader("Authorization", "Basic " + btoa(usertoken));
request2.send();
};
var request = new XMLHttpRequest();
request.onload = baseUrlCb;
var urlsplit = document.URL.split("/");
var owner = urlsplit[3];
var repo  = urlsplit[4];
// Initialize a request
request.open('get', "https://api.github.com/repos/" + owner + "/" + repo + "/git/refs/meta/incremental-diff-url");
var usertoken = GM_getValue("username") + ":" + GM_getValue("token");
request.setRequestHeader("Authorization", "Basic " + btoa(usertoken));
// Send it
request.send();
}
(function()
{
var parts = document.URL.split("/");
if (parts[5] == "pull")
render();
// This is required for this script to be run upon ajax load.. not sure why
window.onbeforeunload = function()
{
console.log("window changed!");
};
})();