Skip to content

Commit

Permalink
feat(utils): add git helper (#469)
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton committed Feb 26, 2024
1 parent 1602197 commit d927a61
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 6 deletions.
101 changes: 99 additions & 2 deletions packages/utils/src/lib/git.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { type SimpleGit, simpleGit } from 'simple-git';
import { expect } from 'vitest';
import { getGitRoot, getLatestCommit, toGitPath } from './git';
import {
getCurrentBranchOrTag,
getGitRoot,
getLatestCommit,
guardAgainstLocalChanges,
safeCheckout,
toGitPath,
} from './git';
import { toUnixPath } from './transform';

describe('git utils', () => {
describe('git utils in a git repo with a branch and commits', () => {
const baseDir = join(process.cwd(), 'tmp', 'testing-git-repo');
const changesDir = join(baseDir, 'changes-dir');
let git: SimpleGit;

beforeAll(async () => {
Expand All @@ -21,12 +29,26 @@ describe('git utils', () => {

await git.add('README.md');
await git.commit('Create README');

await git.checkout(['master']);
});

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

beforeEach(async () => {
await git.checkout(['-b', 'feature-branch']);
await git.checkout(['master']);
});

afterEach(async () => {
// @TODO try why restore/stash/clean/reset hard etc does not work
await rm(changesDir, { recursive: true, force: true });
await git.checkout(['master']);
await git.deleteLocalBranch('feature-branch');
});

it('should log latest commit', async () => {
const gitCommitDateRegex =
/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{1,2} \d{2}:\d{2}:\d{2} \d{4} [+|-]\d{4}$/;
Expand Down Expand Up @@ -60,4 +82,79 @@ describe('git utils', () => {
'Backend/API/Startup.cs',
);
});

it('guardAgainstLocalChanges should throw if history is dirty', async () => {
await mkdir(changesDir, { recursive: true });
await writeFile(join(changesDir, 'change.md'), '# hello-change\n');
await expect(guardAgainstLocalChanges(git)).rejects.toThrow(
'Working directory needs to be clean before we you can proceed. Commit your local changes or stash them.',
);
});

it('guardAgainstLocalChanges should not throw if history is clean', async () => {
await expect(guardAgainstLocalChanges(git)).resolves.toBeUndefined();
});

it('safeCheckout should checkout target branch in clean state', async () => {
await expect(git.branch()).resolves.toEqual(
expect.objectContaining({ current: 'master' }),
);
await expect(
safeCheckout('feature-branch', {}, git),
).resolves.toBeUndefined();
await expect(git.branch()).resolves.toEqual(
expect.objectContaining({ current: 'feature-branch' }),
);
});

it('safeCheckout should throw if history is dirty', async () => {
await mkdir(changesDir, { recursive: true });
await writeFile(join(changesDir, 'change.md'), '# hello-change\n');
await expect(safeCheckout('master', {}, git)).rejects.toThrow(
'Working directory needs to be clean before we you can proceed. Commit your local changes or stash them.',
);
});

it('safeCheckout should clean local changes and check out to feature-branch', async () => {
// needs to get reset to be clean
await mkdir(changesDir, { recursive: true });
await writeFile(join(changesDir, 'change.md'), '# hello-change\n');
// needs to get cleaned to be clean
await writeFile(join(baseDir, 'README.md'), '# hello-world-2\n');

await expect(
safeCheckout('feature-branch', { forceCleanStatus: true }, git),
).resolves.toBeUndefined();
await expect(git.branch()).resolves.toEqual(
expect.objectContaining({ current: 'feature-branch' }),
);
await expect(git.status()).resolves.toEqual(
expect.objectContaining({ files: [] }),
);
});

it('getCurrentBranchOrTag should log current branch', async () => {
await expect(getCurrentBranchOrTag(git)).resolves.toBe('master');
});
});

describe('git utils in a git repo without a branch and commits', () => {
const baseDir = join(process.cwd(), 'tmp', 'testing-git-repo');
let git: SimpleGit;

beforeAll(async () => {
await mkdir(baseDir, { recursive: true });
git = simpleGit(baseDir);
await git.init();
});

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

it('getCurrentBranchOrTag should throw if no branch is given', async () => {
await expect(getCurrentBranchOrTag(git)).rejects.toThrow(
'Could not get current tag or branch.',
);
});
});
56 changes: 53 additions & 3 deletions packages/utils/src/lib/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,67 @@ export async function toGitPath(

export function validateCommitData(
commitData: CommitData | null,
options: { throwError?: boolean } = {},
options: { throwError?: true } = {},
): commitData is CommitData {
const { throwError = false } = options;
if (!commitData) {
const msg = 'no commit data available';
if (throwError) {
if (options?.throwError) {
throw new Error(msg);
} else {
// @TODO replace with ui().logger.warning
console.warn(msg);
return false;
}
}
return true;
}

export async function guardAgainstLocalChanges(
git = simpleGit(),
): Promise<void> {
const isClean = await git.status(['-s']).then(r => r.files.length === 0);
if (!isClean) {
throw new Error(
'Working directory needs to be clean before we you can proceed. Commit your local changes or stash them.',
);
}
}

export async function getCurrentBranchOrTag(
git = simpleGit(),
): Promise<string> {
try {
const branch = await git.branch().then(r => r.current);
// eslint-disable-next-line unicorn/prefer-ternary
if (branch) {
return branch;
} else {
// If no current branch, try to get the tag
// @TODO use simple git
return await git
.raw(['describe', '--tags', '--exact-match'])
.then(out => out.trim());
}
} catch {
// Return a custom error message when something goes wrong
throw new Error('Could not get current tag or branch.');
}
}

export async function safeCheckout(
branchOrHash: string,
options: {
forceCleanStatus?: true;
} = {},
git = simpleGit(),
): Promise<void> {
// git requires a clean history to check out a branch
if (options?.forceCleanStatus) {
await git.raw(['reset', '--hard']);
await git.clean(['f', 'd']);
// @TODO replace with ui().logger.info
console.info(`git status cleaned`);
}
await guardAgainstLocalChanges(git);
await git.checkout(branchOrHash);
}
2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
],
"@code-pushup/models": ["packages/models/src/index.ts"],
"@code-pushup/nx-plugin": ["packages/nx-plugin/src/index.ts"],
"@code-pushup/test-utils": ["testing/test-utils/src/index.ts"],
"@code-pushup/test-setup": ["testing/test-setup/src/index.ts"],
"@code-pushup/test-utils": ["testing/test-utils/src/index.ts"],
"@code-pushup/utils": ["packages/utils/src/index.ts"]
}
},
Expand Down

0 comments on commit d927a61

Please sign in to comment.