GitHub First Commit

Add a link to a GitHub repo's first commit

Versão de: 19/12/2023. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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