GitHub First Commit

Add a link to a GitHub repo's first commit

As of 2020-05-08. See the latest version.

  1. // ==UserScript==
  2. // @name GitHub First Commit
  3. // @description Add a link to a GitHub repo's first commit
  4. // @author chocolateboy
  5. // @copyright chocolateboy
  6. // @version 2.5.5
  7. // @namespace https://github.com/chocolateboy/userscripts
  8. // @license GPL: https://www.gnu.org/copyleft/gpl.html
  9. // @include https://github.com/
  10. // @include https://github.com/*
  11. // @require https://code.jquery.com/jquery-3.5.1.slim.min.js
  12. // @require https://cdn.jsdelivr.net/gh/pie6k/jquery.initialize@16342abd3d411a20d35390f3e4c966ceb37ec43e/jquery.initialize.min.js
  13. // @grant GM_log
  14. // @inject-into content
  15. // ==/UserScript==
  16.  
  17. // XXX note: the unused grant is a workaround for a Greasemonkey bug:
  18. // https://github.com/greasemonkey/greasemonkey/issues/1614
  19.  
  20. const COMMIT_BAR = 'div.commit-tease.js-details-container'
  21.  
  22. const FIRST_COMMIT =
  23. `<span id="first-commit">
  24. &nbsp; &#124; <a id="first-commit-link" style="cursor: pointer" class="text-inherit">First commit</a>
  25. </span>`
  26.  
  27. // this function extracts the URL of the repo's first commit and navigates to it.
  28. // it is based on code by several developers, a list of whom can be found here:
  29. // https://github.com/FarhadG/init#contributors
  30. //
  31. // XXX it doesn't work on private repos. a way to do that can be found here,
  32. // but it requires an authentication token:
  33. // https://gist.github.com/simonewebdesign/a70f6c89ffd71e6ba4f7dcf7cc74ccf8
  34. function openFirstCommit (user, repo) {
  35. return fetch(`https://api.github.com/repos/${user}/${repo}/commits`)
  36. // the `Link` header has additional URLs for paging.
  37. // parse the original JSON for the case where no other pages exist
  38. .then(res => Promise.all([res.headers.get('link'), res.json()]))
  39.  
  40. .then(([link, commits]) => {
  41. if (link) {
  42. // the link header contains two URLs and has the following
  43. // format (wrapped for readability):
  44. //
  45. // <https://api.github.com/repositories/1234/commits?page=2>;
  46. // rel="next",
  47. // <https://api.github.com/repositories/1234/commits?page=9>;
  48. // rel="last"
  49.  
  50. // extract the URL of the last page (commits are ordered in
  51. // reverse chronological order, like the CLI, so the oldest
  52. // commit is on the last page)
  53. const lastPage = link.match(/^.+?<([^>]+)>;/)[1]
  54.  
  55. // fetch the last page of results
  56. return fetch(lastPage).then(res => res.json())
  57. }
  58.  
  59. // if there's no link, we know we're on the only page
  60. return commits
  61. })
  62.  
  63. // get the last commit and extract the target URL
  64. .then(commits => commits[commits.length - 1].html_url)
  65.  
  66. // navigate there
  67. .then(url => location.href = url)
  68. }
  69.  
  70. // add the "First commit" link as the last child of the commit bar
  71. function addLink () {
  72. const $commitBar = $(this)
  73. const [user, repo] = $('meta[name="octolytics-dimension-repository_network_root_nwo"]')
  74. .attr('content')
  75. .split('/')
  76.  
  77. // the "First commit" link already exists when navigating to a repo's
  78. // homepage via the back button. however, resurrecting the link in this way
  79. // causes its onclick event handler to be unregistered (XXX why?), so we
  80. // still need to re-attach it
  81. let $firstCommit = $commitBar.find('#first-commit')
  82.  
  83. if (!$firstCommit.length) {
  84. $firstCommit = $(FIRST_COMMIT)
  85. $commitBar.append($firstCommit)
  86. }
  87.  
  88. const $link = $firstCommit.find('#first-commit-link')
  89.  
  90. $link.on('click', event => {
  91. $link.text('Loading...')
  92. openFirstCommit(user, repo)
  93. return false
  94. })
  95. }
  96.  
  97. // the commit bar (div.commit-tease) is statically defined in the HTML
  98. // for users who aren't logged in. for logged-in users, it's loaded dynamically
  99. // via an <include-fragment> custom element:
  100. //
  101. // https://github.com/github/include-fragment-element
  102. //
  103. // jquery.initialize fires the callback immediately if the element already exists,
  104. // so it handles both cases
  105. $.initialize(COMMIT_BAR, addLink)