GitHub First Commit

Add a link to a GitHub repo's first commit

As of 2020-06-26. 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.6.0
  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/eclecto/jQuery-onMutate@79bbb2b8caccabfc9b9ade046fe63f15f593fef6/src/jquery.onmutate.min.js
  13. // @grant GM_log
  14. // @inject-into auto
  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.js-details-container[data-issue-and-pr-hovercards-enabled] > *:last ul'
  21. const FIRST_COMMIT_LABEL = '<span aria-label="First commit"><strong>1st</strong> commit</span>'
  22.  
  23. // this function extracts the URL of the repo's first commit and navigates to it.
  24. // it is based on code by several developers, a list of whom can be found here:
  25. // https://github.com/FarhadG/init#contributors
  26. //
  27. // XXX it doesn't work on private repos. a way to do that can be found here,
  28. // but it requires an authentication token:
  29. // https://gist.github.com/simonewebdesign/a70f6c89ffd71e6ba4f7dcf7cc74ccf8
  30. function openFirstCommit (user, repo) {
  31. return fetch(`https://api.github.com/repos/${user}/${repo}/commits`)
  32. // the `Link` header has additional URLs for paging.
  33. // parse the original JSON for the case where no other pages exist
  34. .then(res => Promise.all([res.headers.get('link'), res.json()]))
  35.  
  36. .then(([link, commits]) => {
  37. if (link) {
  38. // the link header contains two URLs and has the following
  39. // format (wrapped for readability):
  40. //
  41. // <https://api.github.com/repositories/1234/commits?page=2>;
  42. // rel="next",
  43. // <https://api.github.com/repositories/1234/commits?page=9>;
  44. // rel="last"
  45.  
  46. // extract the URL of the last page (commits are ordered in
  47. // reverse chronological order, like the CLI, so the oldest
  48. // commit is on the last page)
  49. const lastPage = link.match(/^.+?<([^>]+)>;/)[1]
  50.  
  51. // fetch the last page of results
  52. return fetch(lastPage).then(res => res.json())
  53. }
  54.  
  55. // if there's no link, we know we're on the only page
  56. return commits
  57. })
  58.  
  59. // get the last commit and extract the target URL
  60. .then(commits => commits[commits.length - 1].html_url)
  61.  
  62. // navigate there
  63. .then(url => location.href = url)
  64. }
  65.  
  66. // add the "First commit" link as the last child of the commit bar
  67. function addLink ($commitBar) {
  68. let $firstCommit = $commitBar.find('#first-commit')
  69.  
  70. if ($firstCommit.length) {
  71. return
  72. }
  73.  
  74. /*
  75. * This is the first LI in the commit bar (UL), which we clone to create the
  76. * "First commit" widget.
  77. *
  78. * <li class="ml-3">
  79. * <a data-pjax="" href="/foo/bar/commits/master" class="link-gray-dark no-underline">
  80. * <svg height="16">...</svg>
  81. *
  82. * <span class="d-none d-sm-inline">
  83. * <strong>42</strong>
  84. * <span aria-label="Commits on master">commits</span>
  85. * </span>
  86. * </a>
  87. * </li>
  88. */
  89.  
  90. // create it
  91. $firstCommit = $commitBar
  92. .find('li')
  93. .eq(0)
  94. .clone()
  95. .attr('id', 'first-commit')
  96.  
  97. const $link = $firstCommit
  98. .find('a')
  99. .removeAttr('href')
  100. .css('cursor', 'pointer')
  101.  
  102. const $label = $(FIRST_COMMIT_LABEL)
  103.  
  104. $link.find('> span').empty().append($label)
  105.  
  106. const [user, repo] = $('meta[name="octolytics-dimension-repository_network_root_nwo"]')
  107. .attr('content')
  108. .split('/')
  109.  
  110. const oldLabelHtml = $label.html()
  111.  
  112. // restore the original label so it has the correct value if we navigate
  113. // back to the repo page without making a new request (e.g. via the back
  114. // button)
  115. $(window).on('unload', () => {
  116. $label.html(oldLabelHtml)
  117. })
  118.  
  119. $link.on('click', () => {
  120. $label.text('Loading...')
  121. openFirstCommit(user, repo)
  122. return false
  123. })
  124.  
  125. $commitBar.append($firstCommit)
  126. }
  127.  
  128. $.onCreate(COMMIT_BAR, addLink, true /* multi */)