GitHub First Commit

Add a link to a GitHub repo's first commit

2020-11-30 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  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.7.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://cdn.jsdelivr.net/npm/cash-dom@8.1.0/dist/cash.min.js
  12. // @grant GM_log
  13. // @inject-into auto
  14. // ==/UserScript==
  15.  
  16. const COMMIT_BAR = 'div.js-details-container[data-issue-and-pr-hovercards-enabled] > *:last-child ul'
  17. const FIRST_COMMIT_LABEL = '<span aria-label="First commit"><strong>1st</strong> commit</span>'
  18.  
  19. // this function extracts the URL of the repo's first commit and navigates to it.
  20. // it is based on code by several developers, a list of whom can be found here:
  21. // https://github.com/FarhadG/init#contributors
  22. //
  23. // XXX it doesn't work on private repos. a way to do that can be found here,
  24. // but it requires an authentication token:
  25. // https://gist.github.com/simonewebdesign/a70f6c89ffd71e6ba4f7dcf7cc74ccf8
  26. function openFirstCommit (user, repo) {
  27. return fetch(`https://api.github.com/repos/${user}/${repo}/commits`)
  28. // the `Link` header has additional URLs for paging.
  29. // parse the original JSON for the case where no other pages exist
  30. .then(res => Promise.all([res.headers.get('link'), res.json()]))
  31.  
  32. .then(([link, commits]) => {
  33. if (link) {
  34. // the link header contains two URLs and has the following
  35. // format (wrapped for readability):
  36. //
  37. // <https://api.github.com/repositories/1234/commits?page=2>;
  38. // rel="next",
  39. // <https://api.github.com/repositories/1234/commits?page=9>;
  40. // rel="last"
  41.  
  42. // extract the URL of the last page (commits are ordered in
  43. // reverse chronological order, like the git CLI, so the oldest
  44. // commit is on the last page)
  45. const lastPage = link.match(/^.+?<([^>]+)>;/)[1]
  46.  
  47. // fetch the last page of results
  48. return fetch(lastPage).then(res => res.json())
  49. }
  50.  
  51. // if there's no link, we know we're on the only page
  52. return commits
  53. })
  54.  
  55. // get the last commit and navigate to its target URL
  56. .then(commits => {
  57. location.href = commits[commits.length - 1].html_url
  58. })
  59. }
  60.  
  61. // add the "First commit" link as the last child of the commit bar
  62. //
  63. // XXX on most sites, hitting the back button takes you back to a snapshot of
  64. // the previous DOM tree, e.g. navigating away from a page on Hacker News
  65. // highlighted via Hacker News Highlighter [1], then back to the highlighted
  66. // page, retains the DOM changes (the highlighting). that isn't the case on
  67. // GitHub, which triggers network requests and fragment/page reloads when
  68. // navigating (back) to any (SPA) page, even when not logged in. this means, for
  69. // example, we don't need to check if the first-commit widget already exists (it
  70. // never does), and don't need to restore its old label when navigating away
  71. // from a page (since the widget will always be created from scratch rather than
  72. // reused)
  73. //
  74. // this has not always been the case, and may not be the case in the future (and
  75. // may not be the case in some scenarios I haven't tested, e.g. on mobile), so
  76. // rather than taking it for granted, we assume the site behaves like every
  77. // other site in this respect and code to that (defensive coding). this costs
  78. // nothing (apart from this explanation) and saves us having to scramble for a
  79. // fix if the implementation changes.
  80. //
  81. // [1] https://greatest.deepsurf.us/en/scripts/39311-hacker-news-highlighter
  82. function run () {
  83. const $commitBar = $(COMMIT_BAR)
  84.  
  85. // bail if it's not a repo page
  86. if (!$commitBar.length) {
  87. return
  88. }
  89.  
  90. // bail if the widget already exists
  91. let $firstCommit = $commitBar.find('#first-commit')
  92.  
  93. if ($firstCommit.length) {
  94. return
  95. }
  96.  
  97. /*
  98. * This is the first LI in the commit bar (UL), which we clone to create the
  99. * "First commit" widget.
  100. *
  101. * <li class="ml-3">
  102. * <a data-pjax="" href="/foo/bar/commits/master" class="link-gray-dark no-underline">
  103. * <svg height="16">...</svg>
  104. *
  105. * <span class="d-none d-sm-inline">
  106. * <strong>42</strong>
  107. * <span aria-label="Commits on master">commits</span>
  108. * </span>
  109. * </a>
  110. * </li>
  111. */
  112.  
  113. // create it
  114. $firstCommit = $commitBar
  115. .find('li')
  116. .eq(0)
  117. .clone()
  118. .attr('id', 'first-commit')
  119.  
  120. const $link = $firstCommit
  121. .find('a')
  122. .removeAttr('href')
  123. .css('cursor', 'pointer')
  124.  
  125. const $label = $(FIRST_COMMIT_LABEL)
  126.  
  127. $link.find(':scope > span').empty().append($label)
  128.  
  129. const [user, repo] = $('meta[name="octolytics-dimension-repository_network_root_nwo"]')
  130. .attr('content')
  131. .split('/')
  132.  
  133. // before navigating away from the page, restore the original label. this
  134. // ensures it has the correct value if we navigate back to the repo page
  135. // without making a new request (e.g. via the back button)
  136. const oldLabelHtml = $label.html()
  137.  
  138. $(window).on('unload', () => {
  139. $label.html(oldLabelHtml)
  140. })
  141.  
  142. $link.on('click', () => {
  143. $label.text('Loading...')
  144. openFirstCommit(user, repo)
  145. return false // stop processing the click
  146. })
  147.  
  148. $commitBar.append($firstCommit)
  149. }
  150.  
  151. $(document).on('pjax:end', run) // run on pjax page loads
  152. $(run) // run on full page loads