Skip to content

Commit 1860b07

Browse files
authored
feat(cli): add history command (#273)
1 parent 94da246 commit 1860b07

18 files changed

Lines changed: 452 additions & 54 deletions

.prettierignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
/dist
33
/coverage
44
/.nx/cache
5-
__snapshots__
5+
__snapshots__

e2e/cli-e2e/tests/__snapshots__/help.e2e.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Commands:
99
code-pushup autorun Shortcut for running collect followed by upload
1010
code-pushup collect Run Plugins and collect results
1111
code-pushup upload Upload report results to the portal
12+
code-pushup history Collect reports for commit history
1213
code-pushup compare Compare 2 report files and create a diff file
1314
code-pushup print-config Print config
1415

packages/cli/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,25 @@ Run plugins, collect results and upload the report to the Code PushUp portal.
240240

241241
Refer to the [Common Command Options](#common-command-options) for the list of available options.
242242

243+
#### `history` command
244+
245+
Usage:
246+
`code-pushup history`
247+
248+
Description:
249+
Run plugins, collect results and upload the report to the Code PushUp portal for a specified number of commits.
250+
251+
Refer to the [Common Command Options](#common-command-options) for the list of available options.
252+
253+
| Option | Type | Default | Description |
254+
| ------------------------ | --------- | ------- | ---------------------------------------------------------------- |
255+
| **`--targetBranch`** | `string` | 'main' | Branch to crawl history. |
256+
| **`--forceCleanStatus`** | `boolean` | `false` | If we reset the status to a clean git history forcefully or not. |
257+
| **`--maxCount`** | `number` | 5 | Number of commits. |
258+
| **`--skipUploads`** | `boolean` | `false` | Upload created reports |
259+
| **`--from`** | `string` | n/a | Hash to start in history |
260+
| **`--to`** | `string` | n/a | Hash to end in history |
261+
243262
### `compare` command
244263

245264
Usage:

packages/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
"dependencies": {
99
"@code-pushup/models": "0.35.0",
1010
"@code-pushup/core": "0.35.0",
11+
"@code-pushup/utils": "0.35.0",
1112
"yargs": "^17.7.2",
1213
"chalk": "^5.3.0",
13-
"@code-pushup/utils": "0.35.0"
14+
"simple-git": "^3.20.0"
1415
}
1516
}

packages/cli/src/lib/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CommandModule } from 'yargs';
22
import { yargsAutorunCommandObject } from './autorun/autorun-command';
33
import { yargsCollectCommandObject } from './collect/collect-command';
44
import { yargsCompareCommandObject } from './compare/compare-command';
5+
import { yargsHistoryCommandObject } from './history/history-command';
56
import { yargsConfigCommandObject } from './print-config/print-config-command';
67
import { yargsUploadCommandObject } from './upload/upload-command';
78

@@ -13,6 +14,7 @@ export const commands: CommandModule[] = [
1314
yargsAutorunCommandObject(),
1415
yargsCollectCommandObject(),
1516
yargsUploadCommandObject(),
17+
yargsHistoryCommandObject(),
1618
yargsCompareCommandObject(),
1719
yargsConfigCommandObject(),
1820
];
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import chalk from 'chalk';
2+
import { ArgumentsCamelCase, CommandModule } from 'yargs';
3+
import { HistoryOptions, getHashes, history } from '@code-pushup/core';
4+
import { getCurrentBranchOrTag, safeCheckout, ui } from '@code-pushup/utils';
5+
import { CLI_NAME } from '../constants';
6+
import { yargsOnlyPluginsOptionsDefinition } from '../implementation/only-plugins.options';
7+
import { HistoryCliOptions } from './history.model';
8+
import { yargsHistoryOptionsDefinition } from './history.options';
9+
10+
export function yargsHistoryCommandObject() {
11+
const command = 'history';
12+
return {
13+
command,
14+
describe: 'Collect reports for commit history',
15+
builder: yargs => {
16+
yargs.options({
17+
...yargsHistoryOptionsDefinition(),
18+
...yargsOnlyPluginsOptionsDefinition(),
19+
});
20+
yargs.group(
21+
Object.keys(yargsHistoryOptionsDefinition()),
22+
'History Options:',
23+
);
24+
return yargs;
25+
},
26+
handler: async <T>(args: ArgumentsCamelCase<T>) => {
27+
ui().logger.info(chalk.bold(CLI_NAME));
28+
ui().logger.info(chalk.gray(`Run ${command}`));
29+
30+
const currentBranch = await getCurrentBranchOrTag();
31+
const {
32+
targetBranch = currentBranch,
33+
forceCleanStatus,
34+
maxCount,
35+
from,
36+
to,
37+
...restOptions
38+
} = args as unknown as HistoryCliOptions & HistoryOptions;
39+
40+
// determine history to walk
41+
const commits: string[] = await getHashes({ maxCount, from, to });
42+
try {
43+
// run history logic
44+
const reports = await history(
45+
{
46+
...restOptions,
47+
targetBranch,
48+
forceCleanStatus,
49+
},
50+
commits,
51+
);
52+
53+
ui().logger.log(`Reports: ${reports.length}`);
54+
} finally {
55+
// go back to initial branch
56+
await safeCheckout(currentBranch);
57+
}
58+
},
59+
} satisfies CommandModule;
60+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, expect, vi } from 'vitest';
2+
import { type HistoryOptions, history } from '@code-pushup/core';
3+
import { safeCheckout } from '@code-pushup/utils';
4+
import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants';
5+
import { yargsCli } from '../yargs-cli';
6+
import { yargsHistoryCommandObject } from './history-command';
7+
8+
vi.mock('@code-pushup/core', async () => {
9+
const {
10+
MINIMAL_HISTORY_CONFIG_MOCK,
11+
}: typeof import('@code-pushup/test-utils') = await vi.importActual(
12+
'@code-pushup/test-utils',
13+
);
14+
const core: object = await vi.importActual('@code-pushup/core');
15+
return {
16+
...core,
17+
history: vi
18+
.fn()
19+
.mockImplementation((options: HistoryOptions, commits: string[]) =>
20+
commits.map(commit => `${commit}-report.json`),
21+
),
22+
readRcByPath: vi.fn().mockResolvedValue(MINIMAL_HISTORY_CONFIG_MOCK),
23+
};
24+
});
25+
26+
vi.mock('@code-pushup/utils', async () => {
27+
const utils: object = await vi.importActual('@code-pushup/utils');
28+
return {
29+
...utils,
30+
safeCheckout: vi.fn(),
31+
getCurrentBranchOrTag: vi.fn().mockReturnValue('main'),
32+
};
33+
});
34+
35+
vi.mock('simple-git', async () => {
36+
const actual = await vi.importActual('simple-git');
37+
return {
38+
...actual,
39+
simpleGit: () => ({
40+
log: ({ maxCount }: { maxCount: number } = { maxCount: 1 }) =>
41+
Promise.resolve({
42+
all: [
43+
{ hash: 'commit-6' },
44+
{ hash: 'commit-5' },
45+
{ hash: 'commit-4' },
46+
{ hash: 'commit-3' },
47+
{ hash: 'commit-2' },
48+
{ hash: 'commit-1' },
49+
].slice(-maxCount),
50+
}),
51+
}),
52+
};
53+
});
54+
55+
describe('history-command', () => {
56+
it('should return the last 5 commits', async () => {
57+
await yargsCli(['history', '--config=/test/code-pushup.config.ts'], {
58+
...DEFAULT_CLI_CONFIGURATION,
59+
commands: [yargsHistoryCommandObject()],
60+
}).parseAsync();
61+
62+
expect(history).toHaveBeenCalledWith(
63+
expect.objectContaining({
64+
targetBranch: 'main',
65+
}),
66+
['commit-1', 'commit-2', 'commit-3', 'commit-4', 'commit-5'],
67+
);
68+
69+
expect(safeCheckout).toHaveBeenCalledTimes(1);
70+
});
71+
72+
it('should have 2 commits to crawl in history if maxCount is set to 2', async () => {
73+
await yargsCli(
74+
['history', '--config=/test/code-pushup.config.ts', '--maxCount=2'],
75+
{
76+
...DEFAULT_CLI_CONFIGURATION,
77+
commands: [yargsHistoryCommandObject()],
78+
},
79+
).parseAsync();
80+
81+
expect(history).toHaveBeenCalledWith(expect.any(Object), [
82+
'commit-1',
83+
'commit-2',
84+
]);
85+
86+
expect(safeCheckout).toHaveBeenCalledTimes(1);
87+
});
88+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { type LogOptions } from 'simple-git';
2+
import { HistoryOnlyOptions } from '@code-pushup/core';
3+
4+
export type HistoryCliOptions = {
5+
targetBranch?: string;
6+
} & Pick<LogOptions, 'maxCount' | 'from' | 'to'> &
7+
HistoryOnlyOptions;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Options } from 'yargs';
2+
import { HistoryCliOptions } from './history.model';
3+
4+
export function yargsHistoryOptionsDefinition(): Record<
5+
keyof HistoryCliOptions,
6+
Options
7+
> {
8+
return {
9+
targetBranch: {
10+
describe: 'Branch to crawl history',
11+
type: 'string',
12+
default: 'main',
13+
},
14+
forceCleanStatus: {
15+
describe:
16+
'If we reset the status to a clean git history forcefully or not.',
17+
type: 'boolean',
18+
default: false,
19+
},
20+
skipUploads: {
21+
describe: 'Upload created reports',
22+
type: 'boolean',
23+
default: false,
24+
},
25+
maxCount: {
26+
// https://git-scm.com/docs/git-log#Documentation/git-log.txt---max-countltnumbergt
27+
describe: 'Number of steps in history',
28+
type: 'number',
29+
// eslint-disable-next-line no-magic-numbers
30+
default: 5,
31+
},
32+
from: {
33+
// https://git-scm.com/docs/git-log#Documentation/git-log.txt-ltrevision-rangegt
34+
describe: 'hash to first commit in history',
35+
type: 'string',
36+
},
37+
to: {
38+
// https://git-scm.com/docs/git-log#Documentation/git-log.txt-ltrevision-rangegt
39+
describe: 'hash to last commit in history',
40+
type: 'string',
41+
},
42+
};
43+
}

packages/cli/src/lib/implementation/core-config.middleware.unit.test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { describe, expect, vi } from 'vitest';
22
import { autoloadRc, readRcByPath } from '@code-pushup/core';
33
import { coreConfigMiddleware } from './core-config.middleware';
4+
import { CoreConfigCliOptions } from './core-config.model';
5+
import { GeneralCliOptions } from './global.model';
6+
import { OnlyPluginsOptions } from './only-plugins.model';
47

58
vi.mock('@code-pushup/core', async () => {
69
const { CORE_CONFIG_MOCK }: typeof import('@code-pushup/test-utils') =
@@ -15,12 +18,16 @@ vi.mock('@code-pushup/core', async () => {
1518

1619
describe('coreConfigMiddleware', () => {
1720
it('should attempt to load code-pushup.config.(ts|mjs|js) by default', async () => {
18-
await coreConfigMiddleware({});
21+
await coreConfigMiddleware(
22+
{} as GeneralCliOptions & CoreConfigCliOptions & OnlyPluginsOptions,
23+
);
1924
expect(autoloadRc).toHaveBeenCalled();
2025
});
2126

2227
it('should directly attempt to load passed config', async () => {
23-
await coreConfigMiddleware({ config: 'cli/custom-config.mjs' });
28+
await coreConfigMiddleware({
29+
config: 'cli/custom-config.mjs',
30+
} as GeneralCliOptions & CoreConfigCliOptions & OnlyPluginsOptions);
2431
expect(autoloadRc).not.toHaveBeenCalled();
2532
expect(readRcByPath).toHaveBeenCalledWith(
2633
'cli/custom-config.mjs',
@@ -29,15 +36,17 @@ describe('coreConfigMiddleware', () => {
2936
});
3037

3138
it('should forward --tsconfig option to config autoload', async () => {
32-
await coreConfigMiddleware({ tsconfig: 'tsconfig.base.json' });
39+
await coreConfigMiddleware({
40+
tsconfig: 'tsconfig.base.json',
41+
} as GeneralCliOptions & CoreConfigCliOptions & OnlyPluginsOptions);
3342
expect(autoloadRc).toHaveBeenCalledWith('tsconfig.base.json');
3443
});
3544

3645
it('should forward --tsconfig option to custom config load', async () => {
3746
await coreConfigMiddleware({
3847
config: 'apps/website/code-pushup.config.ts',
3948
tsconfig: 'apps/website/tsconfig.json',
40-
});
49+
} as GeneralCliOptions & CoreConfigCliOptions & OnlyPluginsOptions);
4150
expect(readRcByPath).toHaveBeenCalledWith(
4251
'apps/website/code-pushup.config.ts',
4352
'apps/website/tsconfig.json',

0 commit comments

Comments
 (0)