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 2 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
94 changes: 94 additions & 0 deletions packages/core/src/lib/history.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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';

// we need a separate folder that is not cleaned in `global-setup.ts`, otherwise the tests can't execute in parallel
const gitTestFolder = 'core-history-git-test';
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
describe('git utils in a git repo', () => {
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
const baseDir = join(process.cwd(), gitTestFolder);
let emptyGit: SimpleGit;

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

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

describe('without a branch and commits', () => {
it('getHashes should throw', async () => {
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
await expect(getHashes({}, emptyGit)).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 emptyGit.add('README.md');
await emptyGit.commit('Create README');
// eslint-disable-next-line functional/immutable-data
commits.push((await emptyGit.log()).latest?.hash);
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved

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

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

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

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

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

it('getHashes should get last 2 commits from log if maxCount is set to 2', async () => {
await expect(getHashes({ maxCount: 2 }, emptyGit)).resolves.toStrictEqual(
[commits[1], commits[2]],
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
);
});

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

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

it('getHashes should throw if "from" or "to" are invalid', async () => {
await expect(
getHashes({ from: undefined, to: 'a' }, emptyGit),
).rejects.toThrow('from has to be defined');
await expect(
getHashes({ from: 'a', to: undefined }, emptyGit),
).rejects.toThrow('to has to be defined');
});
});
});
110 changes: 110 additions & 0 deletions packages/core/src/lib/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { LogResult, simpleGit } from 'simple-git';
import {
CoreConfig,
Format,
PersistConfig,
UploadConfig,
uploadConfigSchema,
} from '@code-pushup/models';
import { getCurrentBranchOrTag, safeCheckout } from '@code-pushup/utils';
import { collectAndPersistReports } from './collect-and-persist';
import { GlobalOptions } from './types';
import { upload as uploadCommandLogic } from './upload';
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved

export type HistoryOnlyOptions = {
targetBranch?: string;
uploadReports?: boolean;
forceCleanStatus?: true;
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
};
export type HistoryOptions = Required<
Pick<CoreConfig, 'plugins' | 'categories'> & {
persist: Required<PersistConfig>;
} & { upload: Required<UploadConfig> }
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
> &
GlobalOptions &
HistoryOnlyOptions;

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

const { uploadReports = true } = config as unknown as HistoryOptions;
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
if (!uploadReports) {
console.warn('Upload is skipped because uploadReports is set to false');
}

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

const currentConfig: HistoryOptions = {
...config,
persist: {
...config.persist,
format: ['json' as Format],
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
filename: `${commit}-report`,
},
};

await collectAndPersistReports(currentConfig);

if (uploadReports) {
const result = uploadConfigSchema.safeParse(currentConfig.upload);
if (result.success) {
await uploadCommandLogic({ ...currentConfig, upload: result.data });
} else {
console.error(`Collecting ${commit} failed.`);
console.error(result.error);
}
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
}

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

await safeCheckout(initialBranch, {
forceCleanStatus: config.forceCleanStatus,
});

return reports;
}

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

if (from || to) {
// validate from & to
if (from === undefined || from === '') {
throw new Error('from has to be defined');
}
if (to === undefined || to === '') {
throw new Error('to has to be defined');
}
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved

const logsFromTo = await git.log({ from, to, maxCount });
return prepareHashes(logsFromTo);
}

const logs = await git.log(maxCount ? { maxCount } : {});
return prepareHashes(logs);
}

export function prepareHashes(logs: LogResult): string[] {
return (
logs.all
.map(({ hash }) => hash)
// sort from oldest to newest
.reverse()
);
}
137 changes: 137 additions & 0 deletions packages/core/src/lib/history.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, expect, vi } from 'vitest';
import { MINIMAL_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');
let currentBranchOrTag = 'main';
return {
...utils,
safeCheckout: vi.fn().mockImplementation((branch: string) => {
currentBranchOrTag = branch;
}),
getCurrentBranchOrTag: vi.fn().mockImplementation(() => currentBranchOrTag),
};
});
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved

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 () => {
const historyOptions: HistoryOptions = {
...(MINIMAL_CONFIG_MOCK as HistoryOptions),
};
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved

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

expect(getCurrentBranchOrTag).toHaveBeenCalledTimes(1);
expect(getCurrentBranchOrTag).toHaveReturnedWith('main');
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved

expect(safeCheckout).toHaveBeenCalledTimes(3);
// walk commit history
expect(safeCheckout).toHaveBeenNthCalledWith(1, 'abc', {
forceCleanStatus: undefined,
});
expect(safeCheckout).toHaveBeenNthCalledWith(2, 'def', {
forceCleanStatus: undefined,
});
// reset
expect(safeCheckout).toHaveBeenNthCalledWith(3, 'main', {
forceCleanStatus: undefined,
});
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
});

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

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

expect(results).toHaveLength(2);
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
expect(results).toStrictEqual(['abc-report', 'def-report']);
});

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

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_CONFIG_MOCK as HistoryOptions),
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).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
persist: expect.objectContaining({ filename: 'abc-report' }),
}),
);
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
});

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

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

it('should not call upload if upload config is not given', async () => {
const historyOptions: HistoryOptions = {
...(MINIMAL_CONFIG_MOCK as HistoryOptions),
};
await history(historyOptions, ['abc']);

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

describe('prepareHashes', () => {
it('should get all commits from log if no option is passed', () => {
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
expect(
prepareHashes({ all: [{ hash: '1' }, { hash: '5' }] }),
BioPhoton marked this conversation as resolved.
Show resolved Hide resolved
).toStrictEqual(['5', '1']);
});
});
2 changes: 2 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export {
getLatestCommit,
toGitPath,
validateCommitData,
getCurrentBranchOrTag,
safeCheckout,
} from './lib/git';
export { groupByStatus } from './lib/group-by-status';
export {
Expand Down