GitHub Network Ninja

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

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

QingJ © 2025

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