lines-to-revs.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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) { revs['dist-tags'].latest = v }
  50. }
  51. })
  52. return revs
  53. }
  54. const refType = ref => {
  55. if (ref.startsWith('refs/tags/')) {
  56. return 'tag'
  57. }
  58. if (ref.startsWith('refs/heads/')) {
  59. return 'branch'
  60. }
  61. if (ref.startsWith('refs/pull/')) {
  62. return 'pull'
  63. }
  64. if (ref === 'HEAD') {
  65. return 'head'
  66. }
  67. // Could be anything, ignore for now
  68. /* istanbul ignore next */
  69. return 'other'
  70. }
  71. // return the doc, or null if we should ignore it.
  72. const lineToRevDoc = line => {
  73. const split = line.trim().split(/\s+/, 2)
  74. if (split.length < 2) { return null }
  75. const sha = split[0].trim()
  76. const rawRef = split[1].trim()
  77. const type = refType(rawRef)
  78. if (type === 'tag') {
  79. // refs/tags/foo^{} is the 'peeled tag', ie the commit
  80. // that is tagged by refs/tags/foo they resolve to the same
  81. // content, just different objects in git's data structure.
  82. // But, we care about the thing the tag POINTS to, not the tag
  83. // object itself, so we only look at the peeled tag refs, and
  84. // ignore the pointer.
  85. // For now, though, we have to save both, because some tags
  86. // don't have peels, if they were not annotated.
  87. const ref = rawRef.substr('refs/tags/'.length)
  88. return { sha, ref, rawRef, type }
  89. }
  90. if (type === 'branch') {
  91. const ref = rawRef.substr('refs/heads/'.length)
  92. return { sha, ref, rawRef, type }
  93. }
  94. if (type === 'pull') {
  95. // NB: merged pull requests installable with #pull/123/merge
  96. // for the merged pr, or #pull/123 for the PR head
  97. const ref = rawRef.substr('refs/'.length).replace(/\/head$/, '')
  98. return { sha, ref, rawRef, type }
  99. }
  100. if (type === 'head') {
  101. const ref = 'HEAD'
  102. return { sha, ref, rawRef, type }
  103. }
  104. // at this point, all we can do is leave the ref un-munged
  105. return { sha, ref: rawRef, rawRef, type }
  106. }
  107. const linesToRevsReducer = (revs, line) => {
  108. const doc = lineToRevDoc(line)
  109. if (!doc) { return revs }
  110. revs.refs[doc.ref] = doc
  111. revs.refs[doc.rawRef] = doc
  112. if (doc.type === 'tag') {
  113. // try to pull a semver value out of tags like `release-v1.2.3`
  114. // which is a pretty common pattern.
  115. const match = !doc.ref.endsWith('^{}') &&
  116. doc.ref.match(/v?(\d+\.\d+\.\d+(?:[-+].+)?)$/)
  117. if (match && semver.valid(match[1], true)) {
  118. revs.versions[semver.clean(match[1], true)] = doc
  119. }
  120. }
  121. return revs
  122. }