Skip to content

Commit

Permalink
Use execaCommand instead of template string tag
Browse files Browse the repository at this point in the history
This is much easier to test
  • Loading branch information
lo1tuma committed Sep 22, 2023
1 parent 0c7ac64 commit b9e3c17
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 35 deletions.
24 changes: 14 additions & 10 deletions source/bin/pr-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
import { createCommand } from 'commander';
import { Octokit } from '@octokit/rest';
import prependFile from 'prepend-file';
import { $ } from 'execa';
import { execaCommand } from 'execa';
import config from '../../package.json';
import { createCliRunner, RunOptions } from '../lib/cli.js';
import { ensureCleanLocalGitStateFactory } from '../lib/ensure-clean-local-git-state.js';
Expand All @@ -14,10 +14,19 @@ import { createChangelogFactory } from '../lib/create-changelog.js';
import { findRemoteAliasFactory } from '../lib/find-remote-alias.js';
import { getPullRequestLabel } from '../lib/get-pull-request-label.js';

const program = createCommand(config.name);

let isTracingEnabled = false;

const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
const findRemoteAlias = findRemoteAliasFactory({ execute: execaCommand });
const githubClient = new Octokit();
const getMergedPullRequests = getMergedPullRequestsFactory({
githubClient,
execute: execaCommand,
getPullRequestLabel
});
const getCurrentDate = (): Date => new Date();

const program = createCommand(config.name);
program
.storeOptionsAsProperties(false)
.description(config.description)
Expand All @@ -29,23 +38,18 @@ program
// eslint-disable-next-line node/no-process-env
const { GH_TOKEN } = process.env;

isTracingEnabled = options.trace === true;
const changelogPath = path.join(process.cwd(), 'CHANGELOG.md');
const runOptions: RunOptions = { sloppy: options.sloppy === true, changelogPath };
const findRemoteAlias = findRemoteAliasFactory({ execute: $ });
const githubClient = new Octokit();
isTracingEnabled = options.trace === true;
if (GH_TOKEN) {
await githubClient.auth({ type: 'token', token: GH_TOKEN });
}
const getMergedPullRequests = getMergedPullRequestsFactory({ githubClient, execute: $, getPullRequestLabel });
const getCurrentDate = (): Date => new Date();
const packageInfoAsText = await fs.readFile(path.join(process.cwd(), 'package.json'), { encoding: 'utf8' });
const packageInfo = JSON.parse(packageInfoAsText);

Check warning on line 47 in source/bin/pr-log.ts

View workflow job for this annotation

GitHub Actions / Node v20

Unsafe assignment of an `any` value

Check warning on line 47 in source/bin/pr-log.ts

View workflow job for this annotation

GitHub Actions / Node v20

Unsafe assignment of an `any` value
const dependencies = {
githubClient,
prependFile,
packageInfo,

Check warning on line 51 in source/bin/pr-log.ts

View workflow job for this annotation

GitHub Actions / Node v20

Unsafe assignment of an `any` value

Check warning on line 51 in source/bin/pr-log.ts

View workflow job for this annotation

GitHub Actions / Node v20

Unsafe assignment of an `any` value
ensureCleanLocalGitState: ensureCleanLocalGitStateFactory({ execute: $, findRemoteAlias }),
ensureCleanLocalGitState: ensureCleanLocalGitStateFactory({ execute: execaCommand, findRemoteAlias }),
getMergedPullRequests,
createChangelog: createChangelogFactory({ getCurrentDate, packageInfo })

Check warning on line 54 in source/bin/pr-log.ts

View workflow job for this annotation

GitHub Actions / Node v20

Unsafe assignment of an `any` value

Check warning on line 54 in source/bin/pr-log.ts

View workflow job for this annotation

GitHub Actions / Node v20

Unsafe assignment of an `any` value
};
Expand Down
8 changes: 4 additions & 4 deletions source/lib/ensure-clean-local-git-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ function factory({ status = '', revParse = 'master', revList = '' } = {}, execut
const dependencies = { execute, findRemoteAlias } as unknown as EnsureCleanLocalGitStateDependencies;

execute.resolves({ stdout: '' });
execute.withArgs(['git status -s']).resolves({ stdout: status });
execute.withArgs(['git rev-parse --abbrev-ref HEAD']).resolves({ stdout: revParse });
execute.withArgs(['git rev-list --left-right master...', ''], 'origin/master').resolves({ stdout: revList });
execute.withArgs('git status -s').resolves({ stdout: status });
execute.withArgs('git rev-parse --abbrev-ref HEAD').resolves({ stdout: revParse });
execute.withArgs('git rev-list --left-right master...origin/master').resolves({ stdout: revList });

findRemoteAlias.resolves(remoteAlias);

Expand Down Expand Up @@ -52,7 +52,7 @@ test('fetches the remote repository', async (t) => {

await ensureCleanLocalGitState(githubRepo);

t.true(execute.calledWithExactly(['git fetch ', ''], 'origin'));
t.true(execute.calledWithExactly('git fetch origin'));
});

test('fulfills if the local git state is clean', async (t) => {
Expand Down
12 changes: 6 additions & 6 deletions source/lib/ensure-clean-local-git-state.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Execa$ } from 'execa';
import { execaCommand } from 'execa';
import type { FindRemoteAlias } from './find-remote-alias.js';

export interface EnsureCleanLocalGitStateDependencies {
readonly execute: Execa$;
readonly execute: typeof execaCommand;
readonly findRemoteAlias: FindRemoteAlias;
}

Expand All @@ -14,27 +14,27 @@ export function ensureCleanLocalGitStateFactory(
const { execute, findRemoteAlias } = dependencies;

async function ensureCleanLocalCopy(): Promise<void> {
const status = await execute`git status -s`;
const status = await execute('git status -s');
if (status.stdout.trim() !== '') {
throw new Error('Local copy is not clean');
}
}

async function ensureMasterBranch(): Promise<void> {
const branchName = await execute`git rev-parse --abbrev-ref HEAD`;
const branchName = await execute('git rev-parse --abbrev-ref HEAD');
if (branchName.stdout.trim() !== 'master') {
throw new Error('Not on master branch');
}
}

async function fetchRemote(remoteAlias: string): Promise<void> {
await execute`git fetch ${remoteAlias}`;
await execute(`git fetch ${remoteAlias}`);
}

async function ensureLocalIsEqualToRemote(remoteAlias: string): Promise<void> {
const remoteBranch = `${remoteAlias}/master`;

const result = await execute`git rev-list --left-right master...${remoteBranch}`;
const result = await execute(`git rev-list --left-right master...${remoteBranch}`);
const commits = result.stdout.split('\n');

let commitsAhead = 0;
Expand Down
6 changes: 3 additions & 3 deletions source/lib/find-remote-alias.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $ } from 'execa';
import { execaCommand } from 'execa';
import parseGitUrl from 'git-url-parse';

function isSameGitUrl(gitUrlA: string, gitUrlB: string): boolean {
Expand All @@ -15,7 +15,7 @@ function getGitUrl(githubRepo: string): string {
}

export interface FindRemoteAliasDependencies {
readonly execute: typeof $;
readonly execute: typeof execaCommand;
}

export type FindRemoteAlias = (githubRepo: string) => Promise<string>;
Expand All @@ -26,7 +26,7 @@ export function findRemoteAliasFactory(dependencies: FindRemoteAliasDependencies
return async function findRemoteAlias(githubRepo: string) {
const gitRemote = getGitUrl(githubRepo);

const output = await execute`git remote -v`;
const output = await execute('git remote -v');
const remotes = output.stdout.split('\n').map((remote: string) => {
const tokens = remote.split(/\s/);

Expand Down
16 changes: 8 additions & 8 deletions source/lib/get-merged-pull-requests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ function factory(overrides: Overrides = {}): GetMergedPullRequests {
getPullRequestLabel.resolves('bug');

execute.resolves({ stdout: '' });
execute.withArgs(['git tag --list']).resolves({ stdout: tag });
execute.withArgs('git tag --list').resolves({ stdout: tag });
execute
.withArgs(['git log --no-color --pretty=format:"%s (%b)" --merges ', '..HEAD'], latestVersion)
.withArgs(`git log --no-color --pretty=format:"%s (%b)" --merges ${latestVersion}..HEAD`)
.resolves({ stdout: log });

const dependencies = { getPullRequestLabel, execute } as unknown as GetMergedPullRequestsDependencies;
Expand All @@ -35,31 +35,31 @@ function factory(overrides: Overrides = {}): GetMergedPullRequests {
test('ignores non-semver tag', async (t) => {
const execute = stub();
const getMergedPullRequests = factory({ tag: '0.0.1\nfoo\n0.0.2\n0.0.0.0.1', execute });
const expectedGitLogCommand = [['git log --no-color --pretty=format:"%s (%b)" --merges ', '..HEAD'], '0.0.2'];
const expectedGitLogCommand = 'git log --no-color --pretty=format:"%s (%b)" --merges 0.0.2..HEAD';

await getMergedPullRequests(anyRepo, defaultValidLabels);

t.true(execute.calledWith(...expectedGitLogCommand));
t.true(execute.calledWith(expectedGitLogCommand));
});

test('always uses the highest version', async (t) => {
const execute = stub();
const getMergedPullRequests = factory({ tag: '1.0.0\n0.0.0\n0.7.5\n2.0.0\n0.2.5\n0.5.0', execute });
const expectedGitLogCommand = [['git log --no-color --pretty=format:"%s (%b)" --merges ', '..HEAD'], '2.0.0'];
const expectedGitLogCommand = 'git log --no-color --pretty=format:"%s (%b)" --merges 2.0.0..HEAD';

await getMergedPullRequests(anyRepo, defaultValidLabels);

t.true(execute.calledWith(...expectedGitLogCommand));
t.true(execute.calledWith(expectedGitLogCommand));
});

test('ignores prerelease versions', async (t) => {
const execute = stub();
const getMergedPullRequests = factory({ tag: '1.0.0\n0.0.0\n0.7.5\n2.0.0\n0.2.5\n3.0.0-alpha.1', execute });
const expectedGitLogCommand = [['git log --no-color --pretty=format:"%s (%b)" --merges ', '..HEAD'], '2.0.0'];
const expectedGitLogCommand = 'git log --no-color --pretty=format:"%s (%b)" --merges 2.0.0..HEAD';

await getMergedPullRequests(anyRepo, defaultValidLabels);

t.true(execute.calledWith(...expectedGitLogCommand));
t.true(execute.calledWith(expectedGitLogCommand));
});

test('extracts id, title and label for merged pull requests', async (t) => {
Expand Down
8 changes: 4 additions & 4 deletions source/lib/get-merged-pull-requests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { $ } from 'execa';
import { execaCommand } from 'execa';
import semver from 'semver';
import type { Octokit } from '@octokit/rest';
import type { GetPullRequestLabel } from './get-pull-request-label.js';
Expand All @@ -13,7 +13,7 @@ export interface PullRequestWithLabel extends PullRequest {
}

export interface GetMergedPullRequestsDependencies {
readonly execute: typeof $;
readonly execute: typeof execaCommand;
readonly getPullRequestLabel: GetPullRequestLabel;
readonly githubClient: Octokit;
}
Expand All @@ -31,7 +31,7 @@ export function getMergedPullRequestsFactory(dependencies: GetMergedPullRequests
const { execute, getPullRequestLabel } = dependencies;

async function getLatestVersionTag(): Promise<string> {
const result = await execute`git tag --list`;
const result = await execute('git tag --list');
const tags = result.stdout.split('\n');
const versionTags = tags.filter((tag: string) => semver.valid(tag) && !semver.prerelease(tag));
const orderedVersionTags = versionTags.sort(semver.compare);
Expand All @@ -45,7 +45,7 @@ export function getMergedPullRequestsFactory(dependencies: GetMergedPullRequests
}

async function getPullRequests(fromTag: string): Promise<readonly PullRequest[]> {
const result = await execute`git log --no-color --pretty=format:"%s (%b)" --merges ${fromTag}..HEAD`;
const result = await execute(`git log --no-color --pretty=format:"%s (%b)" --merges ${fromTag}..HEAD`);
const mergeCommits = result.stdout.replaceAll(/[\n\r]+\)/g, ')').split('\n');

return mergeCommits
Expand Down

0 comments on commit b9e3c17

Please sign in to comment.