123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- #!/usr/bin/env node
- const { GitProcess } = require('dugite')
- const minimist = require('minimist')
- const path = require('path')
- const semver = require('semver')
- const notesGenerator = require('./notes.js')
- const gitDir = path.resolve(__dirname, '..', '..')
- const semverify = version => version.replace(/^origin\//, '').replace('x', '0').replace(/-/g, '.')
- const runGit = async (args) => {
- const response = await GitProcess.exec(args, gitDir)
- if (response.exitCode !== 0) {
- throw new Error(response.stderr.trim())
- }
- return response.stdout.trim()
- }
- const tagIsSupported = tag => tag && !tag.includes('nightly') && !tag.includes('unsupported')
- const tagIsBeta = tag => tag.includes('beta')
- const tagIsStable = tag => tagIsSupported(tag) && !tagIsBeta(tag)
- const getTagsOf = async (point) => {
- return (await runGit(['tag', '--merged', point]))
- .split('\n')
- .map(tag => tag.trim())
- .filter(tag => semver.valid(tag))
- .sort(semver.compare)
- }
- const getTagsOnBranch = async (point) => {
- const masterTags = await getTagsOf('master')
- if (point === 'master') {
- return masterTags
- }
- const masterTagsSet = new Set(masterTags)
- return (await getTagsOf(point)).filter(tag => !masterTagsSet.has(tag))
- }
- const getBranchOf = async (point) => {
- const branches = (await runGit(['branch', '-a', '--contains', point]))
- .split('\n')
- .map(branch => branch.trim())
- .filter(branch => !!branch)
- const current = branches.find(branch => branch.startsWith('* '))
- return current ? current.slice(2) : branches.shift()
- }
- const getAllBranches = async () => {
- return (await runGit(['branch', '--remote']))
- .split('\n')
- .map(branch => branch.trim())
- .filter(branch => !!branch)
- .filter(branch => branch !== 'origin/HEAD -> origin/master')
- .sort()
- }
- const getStabilizationBranches = async () => {
- return (await getAllBranches())
- .filter(branch => /^origin\/\d+-\d+-x$/.test(branch))
- }
- const getPreviousStabilizationBranch = async (current) => {
- const stabilizationBranches = (await getStabilizationBranches())
- .filter(branch => branch !== current && branch !== `origin/${current}`)
- if (!semver.valid(current)) {
- // since we don't seem to be on a stabilization branch right now,
- // pick a placeholder name that will yield the newest branch
- // as a comparison point.
- current = 'v999.999.999'
- }
- let newestMatch = null
- for (const branch of stabilizationBranches) {
- if (semver.gte(semverify(branch), semverify(current))) {
- continue
- }
- if (newestMatch && semver.lte(semverify(branch), semverify(newestMatch))) {
- continue
- }
- newestMatch = branch
- }
- return newestMatch
- }
- const getPreviousPoint = async (point) => {
- const currentBranch = await getBranchOf(point)
- const currentTag = (await getTagsOf(point)).filter(tag => tagIsSupported(tag)).pop()
- const currentIsStable = tagIsStable(currentTag)
- try {
- // First see if there's an earlier tag on the same branch
- // that can serve as a reference point.
- let tags = (await getTagsOnBranch(`${point}^`)).filter(tag => tagIsSupported(tag))
- if (currentIsStable) {
- tags = tags.filter(tag => tagIsStable(tag))
- }
- if (tags.length) {
- return tags.pop()
- }
- } catch (error) {
- console.log('error', error)
- }
- // Otherwise, use the newest stable release that preceeds this branch.
- // To reach that you may have to walk past >1 branch, e.g. to get past
- // 2-1-x which never had a stable release.
- let branch = currentBranch
- while (branch) {
- const prevBranch = await getPreviousStabilizationBranch(branch)
- const tags = (await getTagsOnBranch(prevBranch)).filter(tag => tagIsStable(tag))
- if (tags.length) {
- return tags.pop()
- }
- branch = prevBranch
- }
- }
- async function getReleaseNotes (range, newVersion, explicitLinks) {
- const rangeList = range.split('..') || ['HEAD']
- const to = rangeList.pop()
- const from = rangeList.pop() || (await getPreviousPoint(to))
- if (!newVersion) {
- newVersion = to
- }
- console.log(`Generating release notes between ${from} and ${to} for version ${newVersion}`)
- const notes = await notesGenerator.get(from, to, newVersion)
- const ret = {
- text: notesGenerator.render(notes, explicitLinks)
- }
- if (notes.unknown.length) {
- ret.warning = `You have ${notes.unknown.length} unknown release notes. Please fix them before releasing.`
- }
- return ret
- }
- async function main () {
- const opts = minimist(process.argv.slice(2), {
- boolean: [ 'explicit-links', 'help' ],
- string: [ 'version' ]
- })
- opts.range = opts._.shift()
- if (opts.help || !opts.range) {
- const name = path.basename(process.argv[1])
- console.log(`
- easy usage: ${name} version
- full usage: ${name} [begin..]end [--version version] [--explicit-links]
- * 'begin' and 'end' are two git references -- tags, branches, etc --
- from which the release notes are generated.
- * if omitted, 'begin' defaults to the previous tag in end's branch.
- * if omitted, 'version' defaults to 'end'. Specifying a version is
- useful if you're making notes on a new version that isn't tagged yet.
- * 'explicit-links' makes every note's issue, commit, or pull an MD link
- For example, these invocations are equivalent:
- ${process.argv[1]} v4.0.1
- ${process.argv[1]} v4.0.0..v4.0.1 --version v4.0.1
- `)
- return 0
- }
- const notes = await getReleaseNotes(opts.range, opts.version, opts['explicit-links'])
- console.log(notes.text)
- if (notes.warning) {
- throw new Error(notes.warning)
- }
- }
- if (process.mainModule === module) {
- main().catch((err) => {
- console.error('Error Occurred:', err)
- process.exit(1)
- })
- }
- module.exports = getReleaseNotes
|