GitHub Network Ninja

Full-viewport graph with searchable commit list on "GitHub repo network" pages.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           GitHub Network Ninja
// @version        2.0
// @namespace      https://github.com/maliayas
// @author         Ali Ayas <[email protected]>
// @description    Full-viewport graph with searchable commit list on "GitHub repo network" pages.
// @license        MIT
// @include        https://github.com/*/*/network
// @icon           data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAAB8hJR8iJiAiJiAjJyIkKCMlKSMmKSQmKiUnKyUoKyYoLCcqLSgqLSgrLiksLyosMCsuMCsuMSwuMCwuMS4wMy8xNC8xNTEyNjI1NzM1ODU4Oj0%2FQj5AQz9AREFCRUJDR0JER0dITEhJTElLTk9QU09RVFBRVFFSVVJUVlJUV1RWWFtcXltcX1xdYF1eYF5eYV5fYmFiZWNkZmRlZ2dpa2prbWprbmtsbmtsb2xucG5vcW9wc3JydXt7fnx8fnx9f3x%2Bf35%2BgH5%2FgYCAg4GChIKDhYOEhoaGiIaHiYeIioiJi4mKi4qLjY6OkI6PkZCQkpGRk5GSk5GSlJWVl5aWmJqanJycnp6dn5%2Ben56foKGhoqOjpKOjpaOkpaeoqaurrK2sra2trq2ur66vsLCwsba0tbW0tra2t7i4uby7vLy8vcHBwsTCw8TDxMXExcXFxsbGx8rKysvKy83MzM7Mzc3NztTS0tXV1dXV1tfW19jX19zb293c3N7d3t%2Fe39%2Ff4ODe3uLg4OLh4ePi4uTi4uXj4%2BXk5Obl5efm5ujl5ejm5unn5%2Bro6Ovp6evq6uzq6uzr6%2B3s7O7t7e%2Fu7u%2Fu7%2FDu7vHv7%2FHw8PLx8fLy8vPz8%2FTz8%2FX09PX19fb29vf39%2Fj39%2Fj4%2BPn5%2Bfr6%2Bvv7%2B%2Fz8%2FP39%2Ff7%2B%2Fv%2F%2F%2FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC0HIHQAAAEAdFJOU%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwBT9wclAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4xLjFjKpxLAAACEElEQVQ4T4WQCXPSQBiGiyQVL4g3nvVurVLvq94K3lpq61lvUcFirfdttbWF2kQJVIUIQbLBxl%2B5ft8mDEnGGZ%2BZZHff99nsThrof7ALetX4Axi6bgVIXdCNiVfXTx883BH7YBh1pSYQI9u1fI7F6mvqb80qLEGvXl4000ZTYoKYjSmQb1sEF2HNNJhAci1WbKNdY6egUP61IRAQhICN2fCEq2VL0M%2FC8mghvg6GBUF4besvh2BI4CdAUN%2FP9fv9twmp9L5UCBl%2F%2FogQchyiJkVlgnYAFv6Y5uAEZlfgjAb6czw4A%2BiuONiNWXMFBSUxHVgslx28noXpgAKCemoa0KW62InpnQIIpR04TZZcnMe0swRCMTQVeFx0cRXTSBHvEJoC3FdcdGMawTsUtvuAcwUXezDtzIOQPzkZWPsj70BaiGlMBiETb0RufndwDDPfWxS%2BSvN5QLiRqyOfacRsTY796uw%2BXri1medbLzwRs8CnZMcyrHm%2BJ8ME6U2AW%2FFwL8dxwXco9PEwRZaIEhOoHOW4XQ9aOW6%2FzFhl9vw96JkwKrV5ff0veuNPM4xNXkY4UxPo2GCzd1402ffsC2Mj69ulNHZMoOnBrZOAIxKjDaZcRGS9JdAR8WLQ4wmLjPUez9K7YspsLIGmxI%2FRlZdM4VBLTyo9ahU1gdKhsTqfh63QLuBNRrAdHqrtRuzCP6D0L3BZI7iJ8A14AAAAAElFTkSuQmCC
// @require        https://code.jquery.com/jquery-2.0.3.min.js
// @run-at         document-end
// ==/UserScript==

var $canvasContainer = $(".js-network-graph-container");

if ($canvasContainer.length) {
    // If there is a scrollbar, we should hide it in order to get the correct window width and height.
    $("body").css({"overflow": "hidden"});

    // Make the container fixed to 0x0 point.
    $canvasContainer.removeClass("position-relative").css({
        "position": "fixed",
        "left": 0,
        "right": 0,
        "bottom": "-16px",
        "top": 0,
        "z-index": "1000",
        "background-color": "white"
    });

    var $canvas = $canvasContainer.find("canvas");

    var $win = $(window);
    var winWidth = $win.width();
    var winHeight = $win.height();

    $canvas.attr("width", winWidth);
    $canvas.attr("height", winHeight);
}

(function initCommitFilterBox() {
    var config = {
        /**
         * Number of commits to fetch in each API request.
         */
        "number_of_commits_per_request": 500,

        /**
         * Number of milliseconds to wait between API requests.
         */
        "api_request_interval": 1000,

        /**
         * Number of maximum commits to fetch from the API.
         */
        "number_of_max_commits": 10000
    };

    insertCss(
        ".commit-filter-box-mask {"
            + "width: 100%;"
            + "height: 100%;"

            + "position: fixed;"
            + "top: 0;"
            + "left: 0;"
            + "z-index: 1001;"

            + "background-color: #000000;"
            + "opacity: 0.5;"

            + "cursor: pointer;"
        + "}"
        + ".commit-filter-button {"
            + "position: fixed;"
            + "top: 5px;"
            + "right: 5px;"
            + "z-index: 1000;"
        + "}"
        + ".commit-filter-box {"
            + "padding: 20px;"
            + "border-radius: 10px;"

            + "position: fixed;"
            + "left: 10%;"
            + "right: 10%;"
            + "top: 10%;"
            + "bottom: 10%;"
            + "overflow: hidden;"
            + "z-index: 1002;"

            + "background-color: #eaf5ff;"
        + "}"
        + ".commit-filter-box input {"
            + "margin-bottom: 10px;"
        + "}"
        + ".commit-filter-box .table-container {"
            + "overflow: auto;"
        + "}"
        + ".commit-filter-box .loading {"
            + "position: relative;"
            + "top: 25%;"

            + "color: #96a3ae;"

            + "text-align: center;"
        + "}"
        + ".commit-filter-box .loading .flash {"
            + "margin-top: 20px;"

            + "display: inline-block;"
        + "}"
        + ".commit-filter-box table {"
            + "width: 100%;"

            + "display: none;"
        + "}"
        + ".commit-filter-box .author {"
            + "white-space: nowrap;"
        + "}"
        + ".commit-filter-box .message {"
            + "padding-left: 10px;"
        + "}"
        + ".commit-filter-box table tr.non-merged .message {"
            + "font-weight: bold;"
        + "}"
        + ".commit-filter-box .date {"
            + "padding-right: 10px;"

            + "white-space: nowrap;"
            + "text-align: right;"
        + "}"
    );

    var $commitFilterBoxMask, $commitFilterButton, $commitFilterBox, $commitFilterInput, $commitFilterLoading, $commitFilterTable;

    var numberOfTotalCommits = 0;

    var repo = getCurrentRepo();

    /**
     * This is a hash string that's specific to the repo. It's required in order to
     * make API request for list of commits. However, -to my knowledge- it's not
     * possible to get it directly from somewhere. GitHub somehow creates/calculates
     * it. So we'll simply steal it from GitHub's runtime. See below for more
     * details.
     */
    var nethash;

    /*
     * In order to find "nethash" value, we need to hook into GitHub's API requests.
     * So we're creating a proxy "fetch()" method below. Whenever we find a request
     * that includes the "nethash" in its URL we'll save it for future usage.
     */
    var originalFetch = fetch;
    fetch = function (input, init) {
        var match;

        if (typeof nethash === "undefined" && (match = /[?&]nethash=([^&]+)/.exec(input.url))) {
            nethash = match[1];
        }

        // Just do what regular fetch() does.
        return originalFetch(input, init);
    };

    /**
     * Meta data related to axises of the network graph. See `fetchNetworkMetaData()`.
     */
    var networkMetaData;

    $commitFilterButton = $(
        "<button class='commit-filter-button btn btn-sm btn-primary'>"
            + "List of commits"
        + "</button>").appendTo("body");

    $commitFilterButton.click(showCommitFilterBox);

    function showCommitFilterBox() {
        if (typeof $commitFilterBox !== "undefined") {
            // It's already created before.

            $commitFilterBoxMask.show();
            $commitFilterBox.show();
            return;
        }

        $commitFilterBoxMask = $("<div class='commit-filter-box-mask'></div>").appendTo("body");
        $commitFilterBoxMask.click(hideCommitFilterBox);

        $commitFilterBox = $(
            "<div class='commit-filter-box'>"
                + "<input type='text' class='form-control input-block' />"
                + "<div class='table-container'>"
                    + "<div class='loading'>"
                        + "<p class='h1'><span class='number'>0</span> commits fetched...</p>"
                        + "<p class='h3'>Please wait until all the commits are fetched.</p>"
                        + "<p class='h5'>(Limit is " + config.number_of_max_commits + " commits. You can configure it in the source.)</p>"
                    + "</div>"
                    + "<table></table>"
                + "</div>"
            + "</div>").appendTo("body");

        // Adjust height dynamically based on the viewport height. 44 is the total
        // height of the input box.
        $commitFilterBox.find(".table-container").height($commitFilterBox.height() - 44);

        $commitFilterInput      = $commitFilterBox.find("input");
        $commitFilterLoading    = $commitFilterBox.find(".loading");
        $commitFilterTable      = $commitFilterBox.find("table");

        bindFilterFunctionOnInput();

        fetchNetworkMetaData();

        // Close commit filter box on escape.
        $(document).bind("keyup", function (e) {
            if (e.which == 27) {
                hideCommitFilterBox();
                return false;
            }

            return true;
        });
    }

    function hideCommitFilterBox() {
        $commitFilterBox.hide();
        $commitFilterBoxMask.hide();
        $commitFilterInput.val("").trigger("input");
    }

    /**
     * Returns "user/repo".
     */
    function getCurrentRepo() {
        return document.location.href.replace(/^https:\/\/(www\.)?github\.com\/([^/]+\/[^/]+)(\/.*)?$/, "$2");
    }

    function insertCss(css) {
        var style = document.createElement("style");
        style.type = "text/css";
        style.innerHTML = css;
        document.head.appendChild(style);
    }

    var timeoutId;
    function bindFilterFunctionOnInput() {
        $commitFilterInput.on("input", function () {
            window.clearTimeout(timeoutId);

            timeoutId = window.setTimeout(function () {
                var query = $commitFilterInput.val().toLowerCase();

                $commitFilterTable.find("tr").each(function () {
                    if (query === "" || this.getAttribute("search-text").includes(query)) {
                        this.removeAttribute("hidden");

                    } else {
                        this.setAttribute("hidden", "");
                    }
                });
            }, 1000);
        });
    }

    function fetchNetworkMetaData(retry) {
        if (typeof retry === "undefined") {
            // Number of retries for failed requests.
            retry = 3;
        }

        $.getJSON("https://github.com/" + repo + "/network/meta")
            .done(function (response) {
                networkMetaData = response;

                fetchCommits();

            }).fail(function () {
                retry--;

                if (! retry) {
                    $commitFilterLoading.append("<div class='flash flash-error'>Network meta data could not be fetched. You can refresh the page to retry.</div>");
                    return;
                }

                // Retry.
                window.setTimeout(function () {
                    fetchNetworkMetaData(retry);
                }, config.api_request_interval);
            });
    }

    function fetchCommits(offset, retry) {
        if (typeof offset === "undefined") {
            offset = 0;
        }

        if (typeof retry === "undefined") {
            // Number of retries for failed requests.
            retry = 3;
        }

        /*
         * "start" and "end" URL parameters works this way: "start=0&end=2" returns
         * 3 items whose indexes are 0, 1 and 2. Hence the "-1" in the calculation
         * of "end".
         */
        $.getJSON("https://github.com/" + repo + "/network/chunk", {
            "nethash"   : nethash,
            "start"     : offset,
            "end"       : offset + config.number_of_commits_per_request - 1
        }).done(function (response) {
            numberOfTotalCommits += response.commits.length;

            $commitFilterLoading.find(".number").text(numberOfTotalCommits)

            // Template object.
            var $tr = $("<tr><td class='author'></td><td class='message'></td><td class='date'></td></tr>");

            for (var i = 0; i < response.commits.length; i++) {
                var $newTr = $tr.clone();

                var commitUrl = "https://github.com/" + getUserRepoBySpace(response.commits[i]["space"]) + "/commit/" + response.commits[i]["id"];
                var $commitLink = $("<a target='_blank'></a>").attr("href", commitUrl).text(response.commits[i]["message"]);

                // Check if the commit exists in the upstream repo.
                if (getUserBySpace(response.commits[i]["space"]) != getUserBySpace(0)) {
                    $newTr.addClass("non-merged");
                }

                $newTr.find(".author")      .text(response.commits[i]["login"] || "(" + response.commits[i]["author"] + ")"); // "login" may be empty sometimes.
                $newTr.find(".message")     .html($commitLink);
                $newTr.find(".date")        .text(response.commits[i]["date"]);

                $commitFilterTable.prepend($newTr);
            }

            var fetchCompleted = false;

            if (numberOfTotalCommits >= config.number_of_max_commits) {
                // We've reached the limit (defined by us).
                fetchCompleted = true;
            }

            if (response.commits.length < config.number_of_commits_per_request) {
                // All the commits are fetched. Let's show them.
                fetchCompleted = true;
            }

            if (fetchCompleted) {
                $commitFilterLoading.hide();
                $commitFilterTable.show();

                /*
                 * Searching uses `innerText` and `innerText` requires elements to
                 * be visible in order to work. This is why we cache them in the
                 * beginnig when all the rows are visible.
                 */
                buildSearchIndex();

                $commitFilterInput.attr("placeholder", "Search in " + numberOfTotalCommits + " commits from " + networkMetaData.users.length + " repos...")
                $commitFilterInput.focus();

            } else {
                // There is more...

                window.setTimeout(function () {
                    fetchCommits(offset + config.number_of_commits_per_request);
                }, config.api_request_interval);
            }

        }).fail(function () {
            retry--;

            if (! retry) {
                $commitFilterLoading.append("<div class='flash flash-error'>Fetch operation failed. You can refresh the page to retry.</div>");
                return;
            }

            // Retry the same request.
            window.setTimeout(function () {
                fetchCommits(offset, retry);
            }, config.api_request_interval);
        });
    }

    function buildSearchIndex() {
        $commitFilterTable.find("tr").each(function () {
            this.setAttribute("search-text", this.innerText.toLowerCase().replace(/\s+/g, " "));
        });
    }

    /**
     * "space" values are numbers returned from the GitHub API. They represent index
     * of the line which a commit's point resides on in the network graph. "0" is
     * the top (horizontal) line, "1" is the one below it, and so on.
     */
    function getUserBySpace(space) {
        for (var i = networkMetaData.blocks.length - 1; i >= 0; i--) {
            if (space < networkMetaData.blocks[i].start) {
                continue;
            }

            return networkMetaData.blocks[i].name;
        }

        return "_user_not_found_";
    }

    function getUserRepo(user) {
        for (var i = 0; i < networkMetaData.users.length; i++) {
            if (networkMetaData.users[i].name == user) {
                return networkMetaData.users[i].name + "/" + networkMetaData.users[i].repo;
            }
        }

        return user + "/_repo_not_found_";
    }

    function getUserRepoBySpace(space) {
        return getUserRepo(getUserBySpace(space));
    }
})();