Skip to content

Commit

Permalink
feat(cli): add option semverTag to history command (#626)
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton authored Apr 29, 2024
1 parent 94ffd45 commit 357b1fa
Show file tree
Hide file tree
Showing 27 changed files with 1,013 additions and 349 deletions.
7 changes: 3 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"esbuild": "^0.19.12",
"multi-progress-bars": "^5.0.3",
"parse-lcov": "^1.0.4",
"semver": "^7.6.0",
"simple-git": "^3.20.0",
"vscode-material-icons": "^0.1.0",
"yargs": "^17.7.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/docs/custom-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ We will extend the file-size example from above to calculate the score based on

Let's extend the options object with a `budget` property and use it in the runner config:

**file-size plugin form section [RunnerFunction](#RunnerFunction)**
**file-size plugin from section [RunnerFunction](#RunnerFunction)**

```typescript
// file-size.plugin.ts
Expand Down
91 changes: 54 additions & 37 deletions packages/cli/src/lib/history/history-command.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,63 @@
import chalk from 'chalk';
import { ArgumentsCamelCase, CommandModule } from 'yargs';
import { HistoryOptions, getHashes, history } from '@code-pushup/core';
import { getCurrentBranchOrTag, safeCheckout, ui } from '@code-pushup/utils';
import { CommandModule } from 'yargs';
import { HistoryOptions, history } from '@code-pushup/core';
import {
LogResult,
getCurrentBranchOrTag,
getHashes,
getSemverTags,
safeCheckout,
ui,
} from '@code-pushup/utils';
import { CLI_NAME } from '../constants';
import { yargsOnlyPluginsOptionsDefinition } from '../implementation/only-plugins.options';
import { HistoryCliOptions } from './history.model';
import { yargsHistoryOptionsDefinition } from './history.options';
import { normalizeHashOptions } from './utils';

const command = 'history';
async function handler(args: unknown) {
ui().logger.info(chalk.bold(CLI_NAME));
ui().logger.info(chalk.gray(`Run ${command}`));

const currentBranch = await getCurrentBranchOrTag();
const { targetBranch: rawTargetBranch, ...opt } = args as HistoryCliOptions &
HistoryOptions;
const {
targetBranch,
from,
to,
maxCount,
onlySemverTags,
...historyOptions
} = await normalizeHashOptions({
...opt,
targetBranch: rawTargetBranch ?? currentBranch,
});

const filterOptions = { targetBranch, from, to, maxCount };
const results: LogResult[] = onlySemverTags
? await getSemverTags(filterOptions)
: await getHashes(filterOptions);

try {
// run history logic
const reports = await history(
{
targetBranch,
...historyOptions,
},
results.map(({ hash }) => hash),
);

ui().logger.log(`Reports: ${reports.length}`);
} finally {
// go back to initial branch
await safeCheckout(currentBranch);
}
}

export function yargsHistoryCommandObject() {
const command = 'history';
return {
command,
describe: 'Collect reports for commit history',
Expand All @@ -23,38 +72,6 @@ export function yargsHistoryCommandObject() {
);
return yargs;
},
handler: async <T>(args: ArgumentsCamelCase<T>) => {
ui().logger.info(chalk.bold(CLI_NAME));
ui().logger.info(chalk.gray(`Run ${command}`));

const currentBranch = await getCurrentBranchOrTag();
const {
targetBranch = currentBranch,
forceCleanStatus,
maxCount,
from,
to,
...restOptions
} = args as unknown as HistoryCliOptions & HistoryOptions;

// determine history to walk
const commits: string[] = await getHashes({ maxCount, from, to });
try {
// run history logic
const reports = await history(
{
...restOptions,
targetBranch,
forceCleanStatus,
},
commits,
);

ui().logger.log(`Reports: ${reports.length}`);
} finally {
// go back to initial branch
await safeCheckout(currentBranch);
}
},
handler,
} satisfies CommandModule;
}
32 changes: 11 additions & 21 deletions packages/cli/src/lib/history/history-command.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,20 @@ vi.mock('simple-git', async () => {
return {
...actual,
simpleGit: () => ({
branch: () => Promise.resolve('dummy'),
raw: () => Promise.resolve('main'),
tag: () => Promise.resolve(`5\n 4\n 3\n 2\n 1`),
show: ([_, __, tag]: string) =>
Promise.resolve(`release v${tag}\n ${tag}`),
checkout: () => Promise.resolve(),
log: ({ maxCount }: { maxCount: number } = { maxCount: 1 }) =>
Promise.resolve({
all: [
{ hash: 'commit-6' },
{ hash: 'commit-5' },
{ hash: 'commit-4' },
{ hash: 'commit-4--release-v2' },
{ hash: 'commit-3' },
{ hash: 'commit-2' },
{ hash: 'commit-2--release-v1' },
{ hash: 'commit-1' },
].slice(-maxCount),
}),
Expand All @@ -53,7 +59,7 @@ vi.mock('simple-git', async () => {
});

describe('history-command', () => {
it('should return the last 5 commits', async () => {
it('should pass targetBranch and forceCleanStatus to core history logic', async () => {
await yargsCli(['history', '--config=/test/code-pushup.config.ts'], {
...DEFAULT_CLI_CONFIGURATION,
commands: [yargsHistoryCommandObject()],
Expand All @@ -62,27 +68,11 @@ describe('history-command', () => {
expect(history).toHaveBeenCalledWith(
expect.objectContaining({
targetBranch: 'main',
forceCleanStatus: false,
}),
['commit-1', 'commit-2', 'commit-3', 'commit-4', 'commit-5'],
expect.any(Array),
);

expect(safeCheckout).toHaveBeenCalledTimes(1);
});

it('should have 2 commits to crawl in history if maxCount is set to 2', async () => {
await yargsCli(
['history', '--config=/test/code-pushup.config.ts', '--maxCount=2'],
{
...DEFAULT_CLI_CONFIGURATION,
commands: [yargsHistoryCommandObject()],
},
).parseAsync();

expect(history).toHaveBeenCalledWith(expect.any(Object), [
'commit-1',
'commit-2',
]);

expect(safeCheckout).toHaveBeenCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions packages/cli/src/lib/history/history.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { HistoryOnlyOptions } from '@code-pushup/core';

export type HistoryCliOptions = {
targetBranch?: string;
onlySemverTags?: boolean;
} & Pick<LogOptions, 'maxCount' | 'from' | 'to'> &
HistoryOnlyOptions;
6 changes: 5 additions & 1 deletion packages/cli/src/lib/history/history.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ export function yargsHistoryOptionsDefinition(): Record<
targetBranch: {
describe: 'Branch to crawl history',
type: 'string',
default: 'main',
},
onlySemverTags: {
describe: 'Skip commits not tagged with a semantic version',
type: 'boolean',
default: false,
},
forceCleanStatus: {
describe:
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/src/lib/history/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { HistoryOptions } from '@code-pushup/core';
import { getHashFromTag, isSemver } from '@code-pushup/utils';
import { HistoryCliOptions } from './history.model';

export async function normalizeHashOptions(
processArgs: HistoryCliOptions & HistoryOptions,
): Promise<HistoryCliOptions & HistoryOptions> {
const {
onlySemverTags,
// overwritten
maxCount,
...opt
} = processArgs;

// eslint-disable-next-line functional/no-let, prefer-const
let { from, to, ...processOptions } = opt;
// if no semver filter is used resolve hash of tags, as hashes are used to collect history
if (!onlySemverTags) {
if (from && isSemver(from)) {
const { hash } = await getHashFromTag(from);
from = hash;
}
if (to && isSemver(to)) {
const { hash } = await getHashFromTag(to);
to = hash;
}
}

return {
...processOptions,
onlySemverTags,
maxCount: maxCount && maxCount > 0 ? maxCount : undefined,
from,
to,
};
}
120 changes: 120 additions & 0 deletions packages/cli/src/lib/history/utils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, expect, vi } from 'vitest';
import { type HistoryOptions } from '@code-pushup/core';
import { HistoryCliOptions } from './history.model';
import { normalizeHashOptions } from './utils';

vi.mock('simple-git', async () => {
const actual = await vi.importActual('simple-git');
const orderedTagsHistory = ['2.0.0', '1.0.0'];
return {
...actual,
simpleGit: () => ({
branch: () => Promise.resolve('dummy'),
raw: () => Promise.resolve('main'),
tag: () => Promise.resolve(orderedTagsHistory.join('\n')),
show: ([_, __, tag]: string) =>
orderedTagsHistory.includes(tag || '')
? Promise.resolve(`${tag}\ncommit--release-v${tag}`)
: Promise.reject('NOT FOUND TAG'),
checkout: () => Promise.resolve(),
log: ({ maxCount }: { maxCount: number } = { maxCount: 1 }) =>
Promise.resolve({
all: [
{ hash: 'commit-6' },
{ hash: 'commit-5' },
{ hash: `commit--release-v${orderedTagsHistory.at(0)}` },
{ hash: 'commit-3' },
{ hash: `commit--release-v${orderedTagsHistory.at(1)}` },
{ hash: 'commit-1' },
].slice(-maxCount),
}),
}),
};
});

describe('normalizeHashOptions', () => {
it('should forwards other options', async () => {
await expect(
normalizeHashOptions({
test: 42,
} as unknown as HistoryCliOptions & HistoryOptions),
).resolves.toEqual(
expect.objectContaining({
test: 42,
}),
);
});

it('should set "maxCount" to undefined if "0" is passed', async () => {
await expect(
normalizeHashOptions({ maxCount: 0 } as HistoryCliOptions &
HistoryOptions),
).resolves.toEqual(
expect.objectContaining({
maxCount: undefined,
}),
);
});

it('should forward hashes "from" and "to" as is if "onlySemverTags" is false', async () => {
await expect(
normalizeHashOptions({
from: 'commit-3',
to: 'commit-1',
} as HistoryCliOptions & HistoryOptions),
).resolves.toEqual(
expect.objectContaining({
from: 'commit-3',
to: 'commit-1',
}),
);
});

it('should transform tags "from" and "to" to commit hashes if "onlySemverTags" is false', async () => {
await expect(
normalizeHashOptions({
onlySemverTags: false,
from: '2.0.0',
to: '1.0.0',
} as HistoryCliOptions & HistoryOptions),
).resolves.toEqual(
expect.objectContaining({
onlySemverTags: false,
from: 'commit--release-v2.0.0',
to: 'commit--release-v1.0.0',
}),
);
});

it('should forward tags "from" and "to" if "onlySemverTags" is true', async () => {
await expect(
normalizeHashOptions({
onlySemverTags: true,
from: '2.0.0',
to: '1.0.0',
} as HistoryCliOptions & HistoryOptions),
).resolves.toEqual(
expect.objectContaining({
onlySemverTags: true,
from: '2.0.0',
to: '1.0.0',
}),
);
});

it('should forward hashes "from" and "to" if "onlySemverTags" is true', async () => {
await expect(
normalizeHashOptions({
onlySemverTags: true,
from: 'commit-3',
to: 'commit-1',
} as HistoryCliOptions & HistoryOptions),
).resolves.toEqual(
expect.objectContaining({
onlySemverTags: true,
from: 'commit-3',
to: 'commit-1',
}),
);
});
});
Loading

0 comments on commit 357b1fa

Please sign in to comment.