GitHub First Commit

Add a link to a GitHub repo's first commit

Od 19.12.2023.. Pogledajte najnovija verzija.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name          GitHub First Commit
// @description   Add a link to a GitHub repo's first commit
// @author        chocolateboy
// @copyright     chocolateboy
// @version       3.0.0
// @namespace     https://github.com/chocolateboy/userscripts
// @license       GPL
// @include       https://github.com/
// @include       https://github.com/*
// @require       https://cdn.jsdelivr.net/npm/[email protected]/dist/cash.min.js
// @grant         GM_log
// @noframes
// @run-at        document-start
// ==/UserScript==

// NOTE This file is generated from src/github-first-commit.user.ts and should not be edited directly.

"use strict";
(() => {
  // src/lib/util.ts
  var pipe = (value, fn) => fn(value);

  // src/github-first-commit/util.ts
  function openFirstCommit(user, repo) {
    return fetch(`https://api.github.com/repos/${user}/${repo}/commits`).then((res) => Promise.all([res.headers.get("link"), res.json()])).then(([link, commits]) => {
      if (!link) {
        return commits;
      }
      const lastPage = link.match(/^.+?<([^>]+)>;/)[1];
      return fetch(lastPage).then((res) => res.json());
    }).then((commits) => {
      if (Array.isArray(commits)) {
        location.href = commits[commits.length - 1].html_url;
      } else {
        console.error(commits);
      }
    });
  }

  // src/github-first-commit/first-commit.ts
  var DEFAULT_TIMEOUT = 1e3;
  var getCommitHistoryButton = (root) => {
    return root.querySelector("svg.octicon.octicon-history")?.closest("a:not(.react-last-commit-history-icon)") || null;
  };
  var FirstCommit = class {
    constructor(state, options = {}) {
      this.state = state;
      this.timeout = options.timeout || DEFAULT_TIMEOUT;
    }
    isLoggedIn = false;
    timeout;
    append($target, $firstCommit) {
      const $targetLi = $target.parent("li");
      const $firstCommitLi = $($targetLi[0].cloneNode(false)).empty().append($firstCommit);
      $targetLi.after($firstCommitLi);
    }
    /*
     * add the "1st Commit" button after the commit-history ("123 Commits") button
     */
    attach(target) {
      console.log("inside attach:", target);
      const $target = $(target);
      const $firstCommit = $target.clone().removeAttr("href data-pjax data-turbo-frame").removeClass("react-last-commit-history-group").attr({
        "aria-label": "First commit",
        "id": "first-commit"
      }).css("cursor", "pointer");
      const $label = this.findLabel($firstCommit);
      $label.text("1st Commit");
      const [user, repo] = $('meta[name="octolytics-dimension-repository_network_root_nwo"][content]').attr("content").split("/");
      $firstCommit.one("click", () => {
        $label.text("Loading...");
        openFirstCommit(user, repo);
        return false;
      });
      console.log("attaching first-commit button:", $firstCommit[0]);
      this.append($target, $firstCommit);
    }
    findLabel($firstCommit) {
      const $label = $firstCommit.find(":scope span > strong").first();
      $label.nextAll().remove();
      return $label;
    }
    getRoot() {
      return document.getElementById("js-repo-pjax-container");
    }
    handleFirstCommitButton(firstCommit) {
      console.debug("removing obsolete first-commit button");
      firstCommit.remove();
      return true;
    }
    // in most cases, the "turbo:load" event signals that the (SPA) page has
    // finished loading and is ready to be queried and updated (i.e. the SPA
    // equivalent of DOMContentLoaded), but that's not the case for the
    // commit-history button, which can either be:
    //
    // a) already loaded (full page load)
    // b) not there yet (still loading)
    // c) already loaded or still loading, but invalid
    //
    // b) and c) can occur when navigating to a repo page via the back button or via
    // on-site links, including self-links (i.e. from a repo page to itself).
    //
    // in the c) case, the old button is displayed (with the old first-commit button
    // still attached) before being replaced by the final, updated version, unless
    // the user is not logged in, in which case the old first-commit button is not
    // replaced.
    //
    // this method handles all 3 cases
    onLoad(_event) {
      const state = this.state;
      const root = this.getRoot();
      if (!root) {
        console.warn("can't find root element!");
        return;
      }
      let timerHandle = 0;
      let disconnected = false;
      const disconnect = () => {
        if (disconnected) {
          return;
        }
        disconnected = true;
        observer.disconnect();
        if (timerHandle) {
          pipe(timerHandle, ($timerHandle) => {
            timerHandle = 0;
            clearTimeout($timerHandle);
          });
        }
      };
      const timeout = () => {
        console.warn(`timed out after ${this.timeout}ms`);
        disconnect();
      };
      const callback = (mutations) => {
        console.debug("inside mutation callback:", mutations);
        if (!root.isConnected) {
          console.warn("root is not connected:", root);
          disconnect();
          return;
        }
        if (generation !== state.generation) {
          console.warn("obsolete page:", { generation, state });
          disconnect();
          return;
        }
        const firstCommit = document.getElementById("first-commit");
        if (firstCommit) {
          console.debug("obsolete button:", firstCommit);
          const handled = this.handleFirstCommitButton(firstCommit);
          if (!handled) {
            return;
          }
        }
        const commitHistoryButton = getCommitHistoryButton(root);
        if (commitHistoryButton) {
          console.debug("found commit-history button");
          disconnect();
          queueMicrotask(() => this.attach(commitHistoryButton));
        }
      };
      const generation = state.generation;
      const observer = new MutationObserver(callback);
      callback([], observer);
      if (!disconnected) {
        timerHandle = setTimeout(timeout, this.timeout);
        observer.observe(root, { childList: true, subtree: true });
      }
    }
  };

  // src/github-first-commit/first-commit-logged-in.ts
  var FirstCommitLoggedIn = class extends FirstCommit {
    isLoggedIn = true;
    append($target, $firstCommit) {
      $target.after($firstCommit);
    }
    findLabel($firstCommit) {
      return $firstCommit.find(':scope [data-component="text"] > span').first();
    }
    getRoot() {
      return document.querySelector('[partial-name="repos-overview"]') || super.getRoot();
    }
    handleFirstCommitButton(_firstCommit) {
      return false;
    }
  };

  // src/github-first-commit.user.ts
  // @license       GPL
  var REPO_PATH = /^\/[^\/]+\/[^\/]+$/;
  var TIMEOUT = 1e3;
  var USER_LOGIN = 'meta[name="user-login"][content]:not([content=""])';
  var main = () => {
    const state = { generation: 0 };
    $(document).on("turbo:load", (event) => {
      ++state.generation;
      console.log("inside turbo:load", { ...state, event });
      if (!REPO_PATH.test(location.pathname)) {
        console.log("skipping invalid path");
        return;
      }
      const isLoggedIn = document.querySelector(USER_LOGIN);
      const handler = isLoggedIn ? new FirstCommitLoggedIn(state, { timeout: TIMEOUT }) : new FirstCommit(state, { timeout: TIMEOUT });
      handler.onLoad(event);
    });
  };
  console.debug("inside:", GM_info.script.name);
  main();
})();