extract logic so it can be used with new command

This commit is contained in:
Brendan Forster 2018-02-04 13:32:55 +11:00
parent 255f92962c
commit 3c17c19ee0
5 changed files with 112 additions and 135 deletions

17
script/changelog/git.ts Normal file
View file

@ -0,0 +1,17 @@
import { spawn } from './spawn'
export async function getLogLines(
previousVersion: string
): Promise<ReadonlyArray<string>> {
const log = await spawn('git', [
'log',
`...${previousVersion}`,
'--merges',
'--grep="Merge pull request"',
'--format=format:%s',
'-z',
'--',
])
return log.split('\0')
}

View file

@ -0,0 +1,84 @@
import { fetchPR, IAPIPR } from './api'
const PlaceholderChangeType = '???'
const OfficialOwner = 'desktop'
interface IParsedCommit {
readonly prID: number
readonly owner: string
}
function parseCommitTitle(line: string): IParsedCommit {
// E.g.: Merge pull request #2424 from desktop/fix-shrinkwrap-file
const re = /^Merge pull request #(\d+) from (.+?)\/.*$/
const matches = line.match(re)
if (!matches || matches.length !== 3) {
throw new Error(`Unable to parse '${line}'`)
}
const id = parseInt(matches[1], 10)
if (isNaN(id)) {
throw new Error(`Unable to parse PR number from '${line}': ${matches[1]}`)
}
return {
prID: id,
owner: matches[2],
}
}
function capitalized(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
function getChangelogEntry(commit: IParsedCommit, pr: IAPIPR): string {
let issueRef = ''
let type = PlaceholderChangeType
const description = capitalized(pr.title)
const re = /Fixes #(\d+)/gi
let match
do {
match = re.exec(pr.body)
if (match && match.length > 1) {
issueRef += ` #${match[1]}`
}
} while (match)
if (issueRef.length) {
type = 'Fixed'
} else {
issueRef = ` #${commit.prID}`
}
let attribution = ''
if (commit.owner !== OfficialOwner) {
attribution = `. Thanks @${commit.owner}!`
}
return `[${type}] ${description} -${issueRef}${attribution}`
}
export async function getChangelogEntries(
lines: ReadonlyArray<string>
): Promise<ReadonlyArray<string>> {
const entries = []
for (const line of lines) {
try {
const commit = parseCommitTitle(line)
const pr = await fetchPR(commit.prID)
if (!pr) {
throw new Error(`Unable to get PR from API: ${commit.prID}`)
}
const entry = getChangelogEntry(commit, pr)
entries.push(entry)
} catch (e) {
console.warn('Unable to parse line, using the full message.', e)
entries.push(`[${PlaceholderChangeType}] ${line}`)
}
}
return entries
}

View file

@ -1,108 +1,10 @@
import { spawn } from './spawn'
import { fetchPR, IAPIPR } from './api'
import { getLogLines } from './git'
import { getChangelogEntries } from './parser'
import { sort as semverSort } from 'semver'
const jsonStringify: (obj: any) => string = require('json-pretty')
const PlaceholderChangeType = '???'
const OfficialOwner = 'desktop'
async function getLogLines(
previousVersion: string
): Promise<ReadonlyArray<string>> {
const log = await spawn('git', [
'log',
`...${previousVersion}`,
'--merges',
'--grep="Merge pull request"',
'--format=format:%s',
'-z',
'--',
])
return log.split('\0')
}
interface IParsedCommit {
readonly prID: number
readonly owner: string
}
function parseCommitTitle(line: string): IParsedCommit {
// E.g.: Merge pull request #2424 from desktop/fix-shrinkwrap-file
const re = /^Merge pull request #(\d+) from (.+?)\/.*$/
const matches = line.match(re)
if (!matches || matches.length !== 3) {
throw new Error(`Unable to parse '${line}'`)
}
const id = parseInt(matches[1], 10)
if (isNaN(id)) {
throw new Error(`Unable to parse PR number from '${line}': ${matches[1]}`)
}
return {
prID: id,
owner: matches[2],
}
}
function capitalized(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
function getChangelogEntry(commit: IParsedCommit, pr: IAPIPR): string {
let issueRef = ''
let type = PlaceholderChangeType
const description = capitalized(pr.title)
const re = /Fixes #(\d+)/gi
let match
do {
match = re.exec(pr.body)
if (match && match.length > 1) {
issueRef += ` #${match[1]}`
}
} while (match)
if (issueRef.length) {
type = 'Fixed'
} else {
issueRef = ` #${commit.prID}`
}
let attribution = ''
if (commit.owner !== OfficialOwner) {
attribution = `. Thanks @${commit.owner}!`
}
return `[${type}] ${description} -${issueRef}${attribution}`
}
async function getChangelogEntries(
lines: ReadonlyArray<string>
): Promise<ReadonlyArray<string>> {
const entries = []
for (const line of lines) {
try {
const commit = parseCommitTitle(line)
const pr = await fetchPR(commit.prID)
if (!pr) {
throw new Error(`Unable to get PR from API: ${commit.prID}`)
}
const entry = getChangelogEntry(commit, pr)
entries.push(entry)
} catch (e) {
console.warn('Unable to parse line, using the full message.', e)
entries.push(`[${PlaceholderChangeType}] ${line}`)
}
}
return entries
}
export async function run(args: ReadonlyArray<string>): Promise<void> {
try {
await spawn('git', ['--version'])

View file

@ -1,4 +1,6 @@
import { spawn } from './spawn'
import { spawn } from '../changelog/spawn'
import { getLogLines } from '../changelog/git'
import { getChangelogEntries } from '../changelog/parser'
import { sort as semverSort, SemVer } from 'semver'
import { getNextVersionNumber } from './version'
@ -51,10 +53,13 @@ export async function run(args: ReadonlyArray<string>): Promise<void> {
const channel = parseChannel(args[0])
const excludeBetaReleases = channel === 'production'
const latestVersion = await getLatestRelease(excludeBetaReleases)
const nextVersion = getNextVersionNumber(latestVersion, channel)
const previousVersion = await getLatestRelease(excludeBetaReleases)
const nextVersion = getNextVersionNumber(previousVersion, channel)
const lines = await getLogLines(previousVersion)
const changelogEntries = await getChangelogEntries(lines)
throw new Error(
`Drafting a release from ${latestVersion} which will be ${nextVersion}`
`Drafting a release from ${previousVersion} which will be ${nextVersion}`
)
}

View file

@ -1,31 +0,0 @@
import * as ChildProcess from 'child_process'
export function spawn(
cmd: string,
args: ReadonlyArray<string>
): Promise<string> {
return new Promise((resolve, reject) => {
const child = ChildProcess.spawn(cmd, args as string[], { shell: true })
let receivedData = ''
child.on('error', reject)
child.stdout.on('data', data => {
receivedData += data
})
child.on('close', (code, signal) => {
if (code === 0) {
resolve(receivedData)
} else {
reject(
new Error(
`'${cmd} ${args.join(
' '
)}' exited with code ${code}, signal ${signal}`
)
)
}
})
})
}