Github PR Incremental Diffs

Provides you incremental diffs with the help of an extra server

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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!");
    };
})();