Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add history logic #541

Merged
merged 32 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
580d00b
feat(core): add history logic
BioPhoton Mar 5, 2024
79709e9
test(core): change test folder
BioPhoton Mar 5, 2024
f784519
Update packages/core/src/lib/history.ts
BioPhoton Mar 7, 2024
12cc764
Update packages/core/src/lib/history.ts
BioPhoton Mar 7, 2024
53e4204
Update packages/core/src/lib/history.ts
BioPhoton Mar 7, 2024
34c5eab
Update packages/core/src/lib/history.ts
BioPhoton Mar 7, 2024
5f63434
Update packages/core/src/lib/history.unit.test.ts
BioPhoton Mar 7, 2024
0df0367
Update packages/core/src/lib/history.unit.test.ts
BioPhoton Mar 7, 2024
fa026f7
refactor(core): adjust history logic
BioPhoton Mar 7, 2024
afd4b7b
Update packages/core/src/lib/history.unit.test.ts
BioPhoton Mar 7, 2024
d0c72d6
Update packages/core/src/lib/history.unit.test.ts
BioPhoton Mar 7, 2024
a982f38
Update packages/core/src/lib/history.unit.test.ts
BioPhoton Mar 7, 2024
2390060
Update packages/core/src/lib/history.integration.test.ts
BioPhoton Mar 7, 2024
634cde2
Update packages/core/src/lib/history.integration.test.ts
BioPhoton Mar 7, 2024
5d8cb50
Update packages/core/src/lib/history.integration.test.ts
BioPhoton Mar 7, 2024
fb3b1dc
Update packages/core/src/lib/history.integration.test.ts
BioPhoton Mar 7, 2024
b122e4d
test(core): adjust history tests
BioPhoton Mar 7, 2024
9a2864b
Merge branch 'main' into add-history-logic-to-core
BioPhoton Mar 7, 2024
3b1c58e
test(utils): adjust helper
BioPhoton Mar 7, 2024
1b388f0
Update packages/core/src/lib/history.unit.test.ts
BioPhoton Mar 7, 2024
42f8245
refactor(core): remove getHashed
BioPhoton Mar 7, 2024
b7e318f
refactor(core): add LogOptions to history options
BioPhoton Mar 7, 2024
340484e
revert
BioPhoton Mar 7, 2024
ebf5035
refactor(core): adjust test logic
BioPhoton Mar 7, 2024
262317b
Update packages/core/src/lib/history.ts
BioPhoton Mar 8, 2024
9e796cb
Update packages/core/src/lib/history.ts
BioPhoton Mar 8, 2024
48327d6
Update packages/core/src/lib/history.unit.test.ts
BioPhoton Mar 8, 2024
a4c35a1
Update packages/core/src/lib/history.unit.test.ts
BioPhoton Mar 8, 2024
b6f62bd
test(plugin-lighthouse): adjust tests
BioPhoton Mar 8, 2024
913a545
test(plugin-lighthouse): adjust tests
BioPhoton Mar 8, 2024
dcd5ff9
refactor: adjust typing
BioPhoton Mar 8, 2024
c9aaa3a
Update minimal-config.mock.ts
BioPhoton Mar 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"@code-pushup/models": "*",
"@code-pushup/utils": "*",
"@code-pushup/portal-client": "^0.6.1",
"chalk": "^5.3.0"
"chalk": "^5.3.0",
"simple-git": "^3.20.0"
},
"type": "commonjs",
"main": "./index.cjs"
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/collect-and-persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { GlobalOptions } from './types';

export type CollectAndPersistReportsOptions = Required<
Pick<CoreConfig, 'plugins' | 'categories'>
> & { persist: Required<PersistConfig> } & GlobalOptions;
> & { persist: Required<PersistConfig> } & Partial<GlobalOptions>;

export async function collectAndPersistReports(
options: CollectAndPersistReportsOptions,
Expand Down
101 changes: 101 additions & 0 deletions packages/core/src/lib/history.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { mkdir, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { type SimpleGit, simpleGit } from 'simple-git';
import { afterAll, beforeAll, describe, expect } from 'vitest';
import { getHashes } from './history';

describe('getHashes', () => {
const baseDir = join(process.cwd(), 'tmp', 'core-history-git-test');
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
let gitMock: SimpleGit;

beforeAll(async () => {
await mkdir(baseDir, { recursive: true });
gitMock = simpleGit(baseDir);
await gitMock.init();
await gitMock.addConfig('user.name', 'John Doe');
await gitMock.addConfig('user.email', '[email protected]');
});

afterAll(async () => {
await rm(baseDir, { recursive: true, force: true });
});

describe('without a branch and commits', () => {
it('should throw', async () => {
await expect(getHashes({}, gitMock)).rejects.toThrow(
"your current branch 'master' does not have any commits yet",
);
});
});

describe('with a branch and commits clean', () => {
const commits: string[] = [];
beforeAll(async () => {
await writeFile(join(baseDir, 'README.md'), '# hello-world\n');
await gitMock.add('README.md');
await gitMock.commit('Create README');
// eslint-disable-next-line functional/immutable-data
commits.push((await gitMock.log()).latest!.hash);

await writeFile(join(baseDir, 'README.md'), '# hello-world-1\n');
await gitMock.add('README.md');
await gitMock.commit('Update README 1');
// eslint-disable-next-line functional/immutable-data
commits.push((await gitMock.log()).latest!.hash);

await writeFile(join(baseDir, 'README.md'), '# hello-world-2\n');
await gitMock.add('README.md');
await gitMock.commit('Update README 2');
// eslint-disable-next-line functional/immutable-data
commits.push((await gitMock.log()).latest!.hash);

await gitMock.branch(['feature-branch']);
await gitMock.checkout(['master']);
});

afterAll(async () => {
await gitMock.checkout(['master']);
await gitMock.deleteLocalBranch('feature-branch');
});

it('getHashes should get all commits from log if no option is passed', async () => {
await expect(getHashes({}, gitMock)).resolves.toStrictEqual(commits);
});

it('getHashes should get last 2 commits from log if maxCount is set to 2', async () => {
await expect(getHashes({ maxCount: 2 }, gitMock)).resolves.toStrictEqual([
commits.at(-2),
commits.at(-1),
]);
});

it('getHashes should get commits from log based on "from"', async () => {
await expect(
getHashes({ from: commits.at(0) }, gitMock),
).resolves.toEqual([commits.at(-2), commits.at(-1)]);
});

it('getHashes should get commits from log based on "from" and "to"', async () => {
await expect(
getHashes({ from: commits.at(-1), to: commits.at(0) }, gitMock),
).resolves.toEqual([commits.at(-2), commits.at(-1)]);
});

it('getHashes should get commits from log based on "from" and "to" and "maxCount"', async () => {
await expect(
getHashes(
{ from: commits.at(-1), to: commits.at(0), maxCount: 1 },
gitMock,
),
).resolves.toEqual([commits.at(-1)]);
});

it('getHashes should throw if "from" is undefined but "to" is defined', async () => {
await expect(
getHashes({ from: undefined, to: 'a' }, gitMock),
).rejects.toThrow(
'git log command needs the "from" option defined to accept the "to" option.',
);
});
});
});
89 changes: 89 additions & 0 deletions packages/core/src/lib/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { LogOptions, LogResult, simpleGit } from 'simple-git';
import { CoreConfig, PersistConfig, UploadConfig } from '@code-pushup/models';
import { getCurrentBranchOrTag, safeCheckout } from '@code-pushup/utils';
import { collectAndPersistReports } from './collect-and-persist';
import { GlobalOptions } from './types';
import { upload } from './upload';

export type HistoryOnlyOptions = {
targetBranch?: string;
skipUploads?: boolean;
forceCleanStatus?: boolean;
};
export type HistoryOptions = Required<
Pick<CoreConfig, 'plugins' | 'categories'>
> & {
persist: Required<PersistConfig>;
upload?: Required<UploadConfig>;
} & HistoryOnlyOptions &
Partial<GlobalOptions>;

export async function history(
config: HistoryOptions,
commits: string[],
): Promise<string[]> {
const initialBranch: string = await getCurrentBranchOrTag();

const { skipUploads = false, forceCleanStatus, persist } = config;

const reports: string[] = [];
// eslint-disable-next-line functional/no-loop-statements
for (const commit of commits) {
console.info(`Collect ${commit}`);
await safeCheckout(commit, forceCleanStatus);

const currentConfig: HistoryOptions = {
...config,
persist: {
...persist,
format: ['json'],
filename: `${commit}-report`,
},
};

await collectAndPersistReports(currentConfig);

if (skipUploads) {
console.warn('Upload is skipped because skipUploads is set to true.');
} else {
if (currentConfig.upload) {
await upload(currentConfig);
} else {
console.warn('Upload is skipped because upload config is undefined.');
}
}

// eslint-disable-next-line functional/immutable-data
reports.push(currentConfig.persist.filename);
}

await safeCheckout(initialBranch, forceCleanStatus);

return reports;
}

export async function getHashes(
options: LogOptions,
git = simpleGit(),
): Promise<string[]> {
const { from, to } = options;

// validate that if to is given also from needs to be given
if (to && !from) {
throw new Error(
'git log command needs the "from" option defined to accept the "to" option.',
);
}

const logs = await git.log(options);
return prepareHashes(logs);
}

export function prepareHashes(logs: LogResult): string[] {
return (
logs.all
.map(({ hash }) => hash)
// sort from oldest to newest
.reverse()
);
}
150 changes: 150 additions & 0 deletions packages/core/src/lib/history.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, vi } from 'vitest';
import { MINIMAL_HISTORY_CONFIG_MOCK } from '@code-pushup/test-utils';
import { getCurrentBranchOrTag, safeCheckout } from '@code-pushup/utils';
import { collectAndPersistReports } from './collect-and-persist';
import { HistoryOptions, history, prepareHashes } from './history';
import { upload } from './upload';

vi.mock('@code-pushup/utils', async () => {
const utils: object = await vi.importActual('@code-pushup/utils');
return {
...utils,
safeCheckout: vi.fn(),
getCurrentBranchOrTag: vi.fn().mockReturnValue('main'),
};
});

vi.mock('./collect-and-persist', () => ({
collectAndPersistReports: vi.fn(),
}));

vi.mock('./upload', () => ({
upload: vi.fn(),
}));

describe('history', () => {
it('should check out all passed commits and reset to initial branch or tag', async () => {
await history(MINIMAL_HISTORY_CONFIG_MOCK, ['abc', 'def']);

expect(getCurrentBranchOrTag).toHaveBeenCalledTimes(1);

expect(safeCheckout).toHaveBeenCalledTimes(3);
// walk commit history
expect(safeCheckout).toHaveBeenNthCalledWith(1, 'abc', undefined);
expect(safeCheckout).toHaveBeenNthCalledWith(2, 'def', undefined);
// reset
expect(safeCheckout).toHaveBeenNthCalledWith(3, 'main', undefined);
});

it('should return correct number of results', async () => {
const historyOptions: HistoryOptions = MINIMAL_HISTORY_CONFIG_MOCK;

const results = await history(historyOptions, ['abc', 'def']);

expect(results).toStrictEqual(['abc-report', 'def-report']);
});

it('should call collect with correct filename and format', async () => {
const historyOptions: HistoryOptions = MINIMAL_HISTORY_CONFIG_MOCK;

await history(historyOptions, ['abc']);
expect(collectAndPersistReports).toHaveBeenCalledTimes(1);
expect(collectAndPersistReports).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
persist: expect.objectContaining({
filename: 'abc-report',
format: ['json'],
}),
}),
);
});

it('should call upload by default', async () => {
const historyOptions: HistoryOptions = {
...MINIMAL_HISTORY_CONFIG_MOCK,
upload: {
server: 'https://server.com/api',
project: 'cli',
apiKey: '1234',
organization: 'code-pushup',
timeout: 4000,
},
};
await history(historyOptions, ['abc']);

expect(upload).toHaveBeenCalledTimes(1);
expect(upload).toHaveBeenCalledWith(
expect.objectContaining({
persist: expect.objectContaining({ filename: 'abc-report' }),
}),
);
});

it('should not call upload if skipUploads is set to false', async () => {
const historyOptions: HistoryOptions = {
...MINIMAL_HISTORY_CONFIG_MOCK,
upload: {
server: 'https://server.com/api',
project: 'cli',
apiKey: '1234',
organization: 'code-pushup',
timeout: 4000,
},
skipUploads: true,
};
await history(historyOptions, ['abc']);

expect(upload).not.toHaveBeenCalled();
});

it('should not call upload if upload config is not given', async () => {
await history(MINIMAL_HISTORY_CONFIG_MOCK, ['abc']);

expect(upload).not.toHaveBeenCalled();
});
});

describe('prepareHashes', () => {
it('should return commit hashes in reverse order', () => {
expect(
prepareHashes({
all: [
{
hash: '22287eb716a84f82b5d59e7238ffcae7147f707a',
date: 'Thu Mar 7 20:13:33 2024 +0100',
message:
'test: change test reported to basic in order to work on Windows',
refs: 'string',
body: '',
author_name: 'John Doe',
author_email: '[email protected]',
},
{
hash: '111b284e48ddf464a498dcf22426a9ce65e2c01c',
date: 'Thu Mar 7 20:13:34 2024 +0100',
message: 'chore: exclude fixtures from ESLint',
refs: 'string',
body: '',
author_name: 'Jane Doe',
author_email: '[email protected]',
},
],
total: 2,
latest: {
hash: '22287eb716a84f82b5d59e7238ffcae7147f707a',
date: 'Thu Mar 7 20:13:33 2024 +0100',
message:
'test: change test reported to basic in order to work on Windows',
refs: 'string',
body: '',
author_name: 'John Doe',
author_email: '[email protected]',
},
}),
).toStrictEqual([
'111b284e48ddf464a498dcf22426a9ce65e2c01c',
'22287eb716a84f82b5d59e7238ffcae7147f707a',
]);
});
});
2 changes: 1 addition & 1 deletion packages/core/src/lib/implementation/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { executePlugins } from './execute-plugin';
export type CollectOptions = Required<
Pick<CoreConfig, 'plugins' | 'categories'>
> &
GlobalOptions;
Partial<GlobalOptions>;

/**
* Run audits, collect plugin output and aggregate it into a JSON object
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/implementation/execute-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export async function executePlugin(
*/
export async function executePlugins(
plugins: PluginConfig[],
options?: { progress: boolean },
options?: { progress?: boolean },
): Promise<PluginReport[]> {
const { progress = false } = options ?? {};

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { GlobalOptions } from './types';

export type UploadOptions = { upload?: UploadConfig } & {
persist: Required<PersistConfig>;
} & GlobalOptions;
} & Partial<GlobalOptions>;

/**
* Uploads collected audits to the portal
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export {
getGitRoot,
getLatestCommit,
toGitPath,
getCurrentBranchOrTag,
safeCheckout,
} from './lib/git';
export { groupByStatus } from './lib/group-by-status';
export {
Expand Down
Loading