Skip to content

Commit

Permalink
Support alternate tag prefixes
Browse files Browse the repository at this point in the history
We should support alternate prefixes in version tags. At the moment the
project assumes that the prefix v is always used. But some projects use
no prefix, or include the package as a prefix to the version (e.g. in
monorepos).

Fixes #116
  • Loading branch information
Gudahtt committed Oct 31, 2022
1 parent 53a6d42 commit c924579
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 23 deletions.
12 changes: 11 additions & 1 deletion src/changelog.test.ts
Expand Up @@ -13,7 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

describe('Changelog', () => {
it('should allow creating an empty changelog', () => {
const changelog = new Changelog({ repoUrl: 'fake://metamask.io' });
const changelog = new Changelog({
repoUrl: 'fake://metamask.io',
});
expect(changelog.toString()).toStrictEqual(emptyChangelog);
});

it('should allow creating an empty changelog with a custom tag prefix', () => {
const changelog = new Changelog({
repoUrl: 'fake://metamask.io',
tagPrefix: 'example@v',
});
expect(changelog.toString()).toStrictEqual(emptyChangelog);
});
});
32 changes: 26 additions & 6 deletions src/changelog.ts
Expand Up @@ -157,11 +157,13 @@ function getTagUrl(repoUrl: string, tag: string) {
* previous release.
*
* @param repoUrl - The URL for the GitHub repository.
* @param tagPrefix - The prefix used in tags before the version number.
* @param releases - The releases to generate link definitions for.
* @returns The stringified release link definitions.
*/
function stringifyLinkReferenceDefinitions(
repoUrl: string,
tagPrefix: string,
releases: ReleaseMetadata[],
) {
// A list of release versions in descending SemVer order
Expand All @@ -187,7 +189,7 @@ function stringifyLinkReferenceDefinitions(
// the link definition.
const unreleasedLinkReferenceDefinition = `[${unreleased}]: ${
hasReleases
? getCompareUrl(repoUrl, `v${latestSemverVersion}`, 'HEAD')
? getCompareUrl(repoUrl, `${tagPrefix}${latestSemverVersion}`, 'HEAD')
: withTrailingSlash(repoUrl)
}`;

Expand All @@ -199,7 +201,7 @@ function stringifyLinkReferenceDefinitions(
.map(({ version }) => {
let diffUrl;
if (version === chronologicalVersions[chronologicalVersions.length - 1]) {
diffUrl = getTagUrl(repoUrl, `v${version}`);
diffUrl = getTagUrl(repoUrl, `${tagPrefix}${version}`);
} else {
const versionIndex = chronologicalVersions.indexOf(version);
const previousVersion = chronologicalVersions
Expand All @@ -208,8 +210,12 @@ function stringifyLinkReferenceDefinitions(
return semver.gt(version, releaseVersion);
});
diffUrl = previousVersion
? getCompareUrl(repoUrl, `v${previousVersion}`, `v${version}`)
: getTagUrl(repoUrl, `v${version}`);
? getCompareUrl(
repoUrl,
`${tagPrefix}${previousVersion}`,
`${tagPrefix}${version}`,
)
: getTagUrl(repoUrl, `${tagPrefix}${version}`);
}
return `[${version}]: ${diffUrl}`;
})
Expand Down Expand Up @@ -249,16 +255,26 @@ export default class Changelog {

private _repoUrl: string;

private _tagPrefix: string;

/**
* Construct an empty changelog.
*
* @param options - Changelog options.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
*/
constructor({ repoUrl }: { repoUrl: string }) {
constructor({
repoUrl,
tagPrefix = 'v',
}: {
repoUrl: string;
tagPrefix?: string;
}) {
this._releases = [];
this._changes = { [unreleased]: {} };
this._repoUrl = repoUrl;
this._tagPrefix = tagPrefix;
}

/**
Expand Down Expand Up @@ -436,6 +452,10 @@ ${changelogDescription}
${stringifyReleases(this._releases, this._changes)}
${stringifyLinkReferenceDefinitions(this._repoUrl, this._releases)}`;
${stringifyLinkReferenceDefinitions(
this._repoUrl,
this._tagPrefix,
this._releases,
)}`;
}
}
23 changes: 21 additions & 2 deletions src/cli.ts
Expand Up @@ -97,6 +97,7 @@ type UpdateOptions = {
repoUrl: string;
isReleaseCandidate: boolean;
projectRootDirectory?: string;
tagPrefix: string;
};

/**
Expand All @@ -108,13 +109,15 @@ type UpdateOptions = {
* @param options.isReleaseCandidate - Whether the current branch is a release candidate or not.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.projectRootDirectory - The root project directory.
* @param options.tagPrefix - The prefix used in tags before the version number.
*/
async function update({
changelogPath,
currentVersion,
isReleaseCandidate,
repoUrl,
projectRootDirectory,
tagPrefix,
}: UpdateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -124,6 +127,7 @@ async function update({
repoUrl,
isReleaseCandidate,
projectRootDirectory,
tagPrefixes: [tagPrefix],
});

if (newChangelogContent) {
Expand All @@ -139,6 +143,7 @@ type ValidateOptions = {
currentVersion?: Version;
isReleaseCandidate: boolean;
repoUrl: string;
tagPrefix: string;
};

/**
Expand All @@ -149,12 +154,14 @@ type ValidateOptions = {
* @param options.currentVersion - The current project version.
* @param options.isReleaseCandidate - Whether the current branch is a release candidate or not.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
*/
async function validate({
changelogPath,
currentVersion,
isReleaseCandidate,
repoUrl,
tagPrefix,
}: ValidateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -164,6 +171,7 @@ async function validate({
currentVersion,
repoUrl,
isReleaseCandidate,
tagPrefix,
});
} catch (error) {
if (error instanceof ChangelogFormattingError) {
Expand All @@ -182,6 +190,7 @@ async function validate({
type InitOptions = {
changelogPath: string;
repoUrl: string;
tagPrefix: string;
};

/**
Expand All @@ -190,9 +199,10 @@ type InitOptions = {
* @param options - Initialization options.
* @param options.changelogPath - The path to the changelog file.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
*/
async function init({ changelogPath, repoUrl }: InitOptions) {
const changelogContent = await createEmptyChangelog({ repoUrl });
async function init({ changelogPath, repoUrl, tagPrefix }: InitOptions) {
const changelogContent = await createEmptyChangelog({ repoUrl, tagPrefix });
await saveChangelog(changelogPath, changelogContent);
}

Expand Down Expand Up @@ -222,6 +232,11 @@ function configureCommonCommandOptions(_yargs: Argv) {
.option('root', {
description: rootDescription,
type: 'string',
})
.option('tagPrefix', {
default: 'v',
description: 'The prefix used in tags before the version number.',
type: 'string',
});
}

Expand Down Expand Up @@ -282,6 +297,7 @@ async function main() {
rc: isReleaseCandidate,
repo: repoUrl,
root: projectRootDirectory,
tagPrefix,
} = argv;

if (isReleaseCandidate && !currentVersion) {
Expand Down Expand Up @@ -358,18 +374,21 @@ async function main() {
isReleaseCandidate,
repoUrl,
projectRootDirectory,
tagPrefix,
});
} else if (command === 'validate') {
await validate({
changelogPath,
currentVersion,
isReleaseCandidate,
repoUrl,
tagPrefix,
});
} else if (command === 'init') {
await init({
changelogPath,
repoUrl,
tagPrefix,
});
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/init.test.ts
Expand Up @@ -19,4 +19,10 @@ describe('createEmptyChangelog', () => {
emptyChangelog,
);
});

it('creates an empty changelog with a custom tag prefix', () => {
expect(
createEmptyChangelog({ repoUrl: exampleRepoUrl, tagPrefix: 'foo' }),
).toStrictEqual(emptyChangelog);
});
});
11 changes: 9 additions & 2 deletions src/init.ts
Expand Up @@ -5,9 +5,16 @@ import Changelog from './changelog';
*
* @param options - Changelog options.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
* @returns The initial changelog text.
*/
export function createEmptyChangelog({ repoUrl }: { repoUrl: string }) {
const changelog = new Changelog({ repoUrl });
export function createEmptyChangelog({
repoUrl,
tagPrefix = 'v',
}: {
repoUrl: string;
tagPrefix?: string;
}) {
const changelog = new Changelog({ repoUrl, tagPrefix });
return changelog.toString();
}
5 changes: 4 additions & 1 deletion src/parse-changelog.ts
Expand Up @@ -28,17 +28,20 @@ function isValidChangeCategory(category: string): category is ChangeCategory {
* @param options - Options.
* @param options.changelogContent - The changelog to parse.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
* @returns A changelog instance that reflects the changelog text provided.
*/
export function parseChangelog({
changelogContent,
repoUrl,
tagPrefix = 'v',
}: {
changelogContent: string;
repoUrl: string;
tagPrefix?: string;
}) {
const changelogLines = changelogContent.split('\n');
const changelog = new Changelog({ repoUrl });
const changelog = new Changelog({ repoUrl, tagPrefix });

const unreleasedHeaderIndex = changelogLines.indexOf(`## [${unreleased}]`);
if (unreleasedHeaderIndex === -1) {
Expand Down
51 changes: 41 additions & 10 deletions src/update-changelog.ts
Expand Up @@ -5,23 +5,41 @@ import { ChangeCategory, Version } from './constants';
import type Changelog from './changelog';

/**
* Get the most recent tag.
* Get the most recent tag for a project.
*
* @param options - Options.
* @param options.tagPrefixes - A list of tag prefixes to look for, where the first is the intended
* prefix and each subsequent prefix is a fallback in case the previous tag prefixes are not found.
* @returns The most recent tag.
*/
async function getMostRecentTag() {
const revListArgs = ['rev-list', '--tags', '--max-count=1', '--date-order'];
const results = await runCommand('git', revListArgs);
if (results.length === 0) {
async function getMostRecentTag({
tagPrefixes,
}: {
tagPrefixes: [string, ...string[]];
}) {
let mostRecentTagCommitHash: string | null = null;
for (const tagPrefix of tagPrefixes) {
const revListArgs = [
'rev-list',
`--tags=${tagPrefix}*`,
'--max-count=1',
'--date-order',
];
const results = await runCommand('git', revListArgs);
if (results.length) {
mostRecentTagCommitHash = results[0];
break;
}
}

if (mostRecentTagCommitHash === null) {
return null;
}
const [mostRecentTagCommitHash] = results;
const [mostRecentTag] = await runCommand('git', [
'describe',
'--tags',
mostRecentTagCommitHash,
]);
assert.equal(mostRecentTag?.[0], 'v', 'Most recent tag should start with v');
return mostRecentTag;
}

Expand Down Expand Up @@ -140,6 +158,7 @@ export type UpdateChangelogOptions = {
repoUrl: string;
isReleaseCandidate: boolean;
projectRootDirectory?: string;
tagPrefixes?: [string, ...string[]];
};

/**
Expand All @@ -159,6 +178,8 @@ export type UpdateChangelogOptions = {
* filter results from various git commands. This path is assumed to be either
* absolute, or relative to the current directory. Defaults to the root of the
* current git repository.
* @param options.tagPrefixes - A list of tag prefixes to look for, where the first is the intended
* prefix and each subsequent prefix is a fallback in case the previous tag prefixes are not found.
* @returns The updated changelog text.
*/
export async function updateChangelog({
Expand All @@ -167,19 +188,29 @@ export async function updateChangelog({
repoUrl,
isReleaseCandidate,
projectRootDirectory,
tagPrefixes = ['v'],
}: UpdateChangelogOptions) {
if (isReleaseCandidate && !currentVersion) {
throw new Error(
`A version must be specified if 'isReleaseCandidate' is set.`,
);
}
const changelog = parseChangelog({ changelogContent, repoUrl });
const changelog = parseChangelog({
changelogContent,
repoUrl,
tagPrefix: tagPrefixes[0],
});

// Ensure we have all tags on remote
await runCommand('git', ['fetch', '--tags']);
const mostRecentTag = await getMostRecentTag();
const mostRecentTag = await getMostRecentTag({
tagPrefixes,
});

if (isReleaseCandidate && mostRecentTag === `v${currentVersion}`) {
if (
isReleaseCandidate &&
mostRecentTag === `${tagPrefixes[0]}${currentVersion}`
) {
throw new Error(
`Current version already has tag, which is unexpected for a release candidate.`,
);
Expand Down

0 comments on commit c924579

Please sign in to comment.