diff --git a/src/commands/prepare.ts b/src/commands/prepare.ts index 3a37358e..7635eaac 100644 --- a/src/commands/prepare.ts +++ b/src/commands/prepare.ts @@ -242,17 +242,25 @@ export async function runPreReleaseCommand( // Not running pre-release command logger.warn('Not running the pre-release command: no command specified'); return false; - } else if (preReleaseCommand) { + } + + // This is a workaround for the case when the old version is empty, which + // should only happen when the project is new and has no version yet. + // Instead of using an empty string, we use "0.0.0" as the old version to + // avoid breaking the pre-release command as most scripts expect a non-empty + // version string. + const nonEmptyOldVersion = oldVersion || '0.0.0'; + if (preReleaseCommand) { [sysCommand, ...args] = shellQuote.parse(preReleaseCommand) as string[]; } else { sysCommand = '/bin/bash'; args = [DEFAULT_BUMP_VERSION_PATH]; } - args = [...args, oldVersion, newVersion]; - logger.info(`Running the pre-release command...`); + args = [...args, nonEmptyOldVersion, newVersion]; + logger.info('Running the pre-release command...'); const additionalEnv = { CRAFT_NEW_VERSION: newVersion, - CRAFT_OLD_VERSION: oldVersion, + CRAFT_OLD_VERSION: nonEmptyOldVersion, }; await spawnProcess(sysCommand, args, { env: { ...process.env, ...additionalEnv }, @@ -354,7 +362,9 @@ async function prepareChangelog( `Changelog policy is set to "${changelogPolicy}", nothing to do.` ); return; - } else if ( + } + + if ( changelogPolicy !== ChangelogPolicy.Auto && changelogPolicy !== ChangelogPolicy.Simple ) { diff --git a/src/utils/__tests__/git.test.ts b/src/utils/__tests__/git.test.ts index c1fadfa5..3f15f75d 100644 --- a/src/utils/__tests__/git.test.ts +++ b/src/utils/__tests__/git.test.ts @@ -13,29 +13,15 @@ describe('getLatestTag', () => { expect(git.raw).toHaveBeenCalledWith('describe', '--tags', '--abbrev=0'); }); - it('logs a helpful error message if the git call throws', async () => { + it('moves on with empty string when no tags are found', async () => { loggerModule.setLevel(loggerModule.LogLevel.Debug); - const loggerErrorSpy = jest - .spyOn(loggerModule.logger, 'error') - .mockImplementation(() => { - // just to avoid spamming the test output - }); - const error = new Error('Nothing to describe'); + const error = new Error('fatal: No names found'); const git = { raw: jest.fn().mockRejectedValue(error), } as any; - try { - await getLatestTag(git); - } catch (e) { - expect(e).toBe(error); - } - - expect(loggerErrorSpy).toHaveBeenCalledWith( - expect.stringContaining( - "If you're releasing for the first time, check if your repo contains any tags" - ) - ); + const latestTag = await getLatestTag(git); + expect(latestTag).toBe(''); }); }); diff --git a/src/utils/git.ts b/src/utils/git.ts index 210c8f8f..35ff3470 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,4 +1,4 @@ -import simpleGit, { SimpleGit } from 'simple-git'; +import simpleGit, { type SimpleGit, type LogOptions, type Options } from 'simple-git'; import { getConfigFileDir } from '../config'; import { ConfigurationError } from './errors'; @@ -20,6 +20,8 @@ export interface GitChange { // https://docs.github.com/en/rest/commits/commits#list-pull-requests-associated-with-a-commit export const PRExtractor = /(?<=\(#)\d+(?=\)$)/; +export const defaultInitialTag = '0.0.0'; + export async function getDefaultBranch( git: SimpleGit, remoteName: string @@ -34,15 +36,20 @@ export async function getDefaultBranch( } export async function getLatestTag(git: SimpleGit): Promise { - // This part is courtesy of https://stackoverflow.com/a/7261049/90297 try { + // This part is courtesy of https://stackoverflow.com/a/7261049/90297 return (await git.raw('describe', '--tags', '--abbrev=0')).trim(); - } catch (e) { - logger.error( - 'Couldn\'t get the latest tag! If you\'re releasing for the first time, check if your repo contains any tags. If not, add one manually and try again: `git tag 0.0.0 "$(git log -1 --reverse --format=%h)"' - ); - // handle this error in the global error handler - throw e; + } catch (err) { + // If there are no tags, return an empty string + if ( + err instanceof Error && + ( + err.message.startsWith('fatal: No names found') || + err.message.startsWith('Nothing to describe')) + ) { + return ''; + } + throw err; } } @@ -50,8 +57,7 @@ export async function getChangesSince( git: SimpleGit, rev: string ): Promise { - const { all: commits } = await git.log({ - from: rev, + const gitLogArgs: Options | LogOptions = { to: 'HEAD', // The symmetric option defaults to true, giving us all the different commits // reachable from both `from` and `to` whereas what we are interested in is only the ones @@ -65,7 +71,12 @@ export async function getChangesSince( // this should still return all commits for individual repos when run from // the repo root. file: '.', - }); + }; + + if (rev) { + gitLogArgs.from = rev; + } + const { all: commits } = await git.log(gitLogArgs); return commits.map(commit => ({ hash: commit.hash, title: commit.message, @@ -79,7 +90,7 @@ export function stripRemoteName( remoteName: string ): string { const branchName = branch || ''; - const remotePrefix = remoteName + '/'; + const remotePrefix = `${remoteName}/`; if (branchName.startsWith(remotePrefix)) { return branchName.slice(remotePrefix.length); } @@ -90,7 +101,7 @@ export async function getGitClient(): Promise { const configFileDir = getConfigFileDir() || '.'; // Move to the directory where the config file is located process.chdir(configFileDir); - logger.debug(`Working directory:`, process.cwd()); + logger.debug("Working directory:", process.cwd()); const git = simpleGit(configFileDir); const isRepo = await git.checkIsRepo();