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