lines-to-revs.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. // turn an array of lines from `git ls-remote` into a thing
  2. // vaguely resembling a packument, where docs are a resolved ref
  3. const semver = require('semver')
  4. module.exports = lines => finish(lines.reduce(linesToRevsReducer, {
  5. versions: {},
  6. 'dist-tags': {},
  7. refs: {},
  8. shas: {},
  9. }))
  10. const finish = revs => distTags(shaList(peelTags(revs)))
  11. // We can check out shallow clones on specific SHAs if we have a ref
  12. const shaList = revs => {
  13. Object.keys(revs.refs).forEach(ref => {
  14. const doc = revs.refs[ref]
  15. if (!revs.shas[doc.sha]) {
  16. revs.shas[doc.sha] = [ref]
  17. } else {
  18. revs.shas[doc.sha].push(ref)
  19. }
  20. })
  21. return revs
  22. }
  23. // Replace any tags with their ^{} counterparts, if those exist
  24. const peelTags = revs => {
  25. Object.keys(revs.refs).filter(ref => ref.endsWith('^{}')).forEach(ref => {
  26. const peeled = revs.refs[ref]
  27. const unpeeled = revs.refs[ref.replace(/\^\{\}$/, '')]
  28. if (unpeeled) {
  29. unpeeled.sha = peeled.sha
  30. delete revs.refs[ref]
  31. }
  32. })
  33. return revs
  34. }
  35. const distTags = revs => {
  36. // not entirely sure what situations would result in an
  37. // ichabod repo, but best to be careful in Sleepy Hollow anyway
  38. const HEAD = revs.refs.HEAD || /* istanbul ignore next */ {}
  39. const versions = Object.keys(revs.versions)
  40. versions.forEach(v => {
  41. // simulate a dist-tags with latest pointing at the
  42. // 'latest' branch if one exists and is a version,
  43. // or HEAD if not.
  44. const ver = revs.versions[v]
  45. if (revs.refs.latest && ver.sha === revs.refs.latest.sha) {
  46. revs['dist-tags'].latest = v
  47. } else if (ver.sha === HEAD.sha) {
  48. revs['dist-tags'].HEAD = v
  49. if (!revs.refs.latest) {
  50. revs['dist-tags'].latest = v
  51. }
  52. }
  53. })
  54. return revs
  55. }
  56. const refType = ref => {
  57. if (ref.startsWith('refs/tags/')) {
  58. return 'tag'
  59. }
  60. if (ref.startsWith('refs/heads/')) {
  61. return 'branch'
  62. }
  63. if (ref.startsWith('refs/pull/')) {
  64. return 'pull'
  65. }
  66. if (ref === 'HEAD') {
  67. return 'head'
  68. }
  69. // Could be anything, ignore for now
  70. /* istanbul ignore next */
  71. return 'other'
  72. }
  73. // return the doc, or null if we should ignore it.
  74. const lineToRevDoc = line => {
  75. const split = line.trim().split(/\s+/, 2)
  76. if (split.length < 2) {
  77. return null
  78. }
  79. const sha = split[0].trim()
  80. const rawRef = split[1].trim()
  81. const type = refType(rawRef)
  82. if (type === 'tag') {
  83. // refs/tags/foo^{} is the 'peeled tag', ie the commit
  84. // that is tagged by refs/tags/foo they resolve to the same
  85. // content, just different objects in git's data structure.
  86. // But, we care about the thing the tag POINTS to, not the tag
  87. // object itself, so we only look at the peeled tag refs, and
  88. // ignore the pointer.
  89. // For now, though, we have to save both, because some tags
  90. // don't have peels, if they were not annotated.
  91. const ref = rawRef.slice('refs/tags/'.length)
  92. return { sha, ref, rawRef, type }
  93. }
  94. if (type === 'branch') {
  95. const ref = rawRef.slice('refs/heads/'.length)
  96. return { sha, ref, rawRef, type }
  97. }
  98. if (type === 'pull') {
  99. // NB: merged pull requests installable with #pull/123/merge
  100. // for the merged pr, or #pull/123 for the PR head
  101. const ref = rawRef.slice('refs/'.length).replace(/\/head$/, '')
  102. return { sha, ref, rawRef, type }
  103. }
  104. if (type === 'head') {
  105. const ref = 'HEAD'
  106. return { sha, ref, rawRef, type }
  107. }
  108. // at this point, all we can do is leave the ref un-munged
  109. return { sha, ref: rawRef, rawRef, type }
  110. }
  111. const linesToRevsReducer = (revs, line) => {
  112. const doc = lineToRevDoc(line)
  113. if (!doc) {
  114. return revs
  115. }
  116. revs.refs[doc.ref] = doc
  117. revs.refs[doc.rawRef] = doc
  118. if (doc.type === 'tag') {
  119. // try to pull a semver value out of tags like `release-v1.2.3`
  120. // which is a pretty common pattern.
  121. const match = !doc.ref.endsWith('^{}') &&
  122. doc.ref.match(/v?(\d+\.\d+\.\d+(?:[-+].+)?)$/)
  123. if (match && semver.valid(match[1], true)) {
  124. revs.versions[semver.clean(match[1], true)] = doc
  125. }
  126. }
  127. return revs
  128. }