Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
"extends": ["@ribeirogab/eslint-config/node"],
"globals": {
"vi": true
},
"overrides": [
{
"files": ["tsup.config.ts"],
"files": ["tsup.config.ts", "vitest.config.ts"],
"rules": {
"import/no-default-export": "off"
}
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
node_modules
executables
package-lock.json
lib
lib
tmp/*
!tmp/.gitkeep
test-output
.DS_Store
8 changes: 6 additions & 2 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ test.*
.eslintignore
.eslintrc*
.gitignore
.DS_Store

prettier.config.js
tsup.config.ts
tsconfig.json
.DS_Store
vitest.config.ts

src
executables
tmp
test-output
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@
],
"scripts": {
"build": "tsup",
"publish:npm": "npm run build && npm publish"
"publish:npm": "npm run build && npm publish",
"cfy": "npm run build -- --silent && node bin/cli.js",
"vitest": "vitest --globals --config vitest.config.ts",
"test": "npm run vitest -- run",
"test:unit": "npm run vitest -- --project unit --run",
"test:integration": "npm run vitest -- --project integration --run",
"test:watch": "npm run vitest",
"test:coverage": "npm run vitest -- run --coverage",
"test:ci": "npm run vitest -- run --silent --coverage --reporter=junit --outputFile.junit=./test-output/junit.xml"
},
"bin": {
"commitfy": "bin/cli.js",
Expand All @@ -36,9 +44,12 @@
"devDependencies": {
"@ribeirogab/eslint-config": "^2.0.1",
"@types/node": "^22.1.0",
"@vitest/coverage-v8": "^2.0.5",
"eslint": "^8.57.0",
"tsup": "^8.2.4",
"tsx": "^4.16.5",
"typescript": "^5.5.4"
"typescript": "^5.5.4",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.0.5"
}
}
151 changes: 151 additions & 0 deletions src/commands/generate-commit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { GenerateCommit } from './generate-commit';
import { ProviderEnum } from '@/interfaces';
import { makeProvidersFake } from '@/tests/fakes/providers';
import {
makeAppUtilsFake,
makeEnvUtilsFake,
makeInputUtilsFake,
makeProcessUtilsFake,
} from '@/tests/fakes/utils';

export const makeSut = () => {
const processUtils = makeProcessUtilsFake();
const inputUtils = makeInputUtilsFake();
const providers = makeProvidersFake();
const envUtils = makeEnvUtilsFake();
const appUtils = makeAppUtilsFake();
const sut = new GenerateCommit(
providers,
envUtils,
processUtils,
inputUtils,
appUtils,
);

return {
processUtils,
inputUtils,
providers,
envUtils,
appUtils,
sut,
};
};

describe('GenerateCommit', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('should create an instance of GenerateCommit', () => {
const { sut } = makeSut();

expect(sut).toBeInstanceOf(GenerateCommit);
});

it('should log an error and exit if provider is not set', async () => {
const { sut, appUtils } = makeSut();

const exitSpy = vi.spyOn(process, 'exit').mockImplementation((number) => {
throw new Error('process.exit: ' + number);
});
const loggerMessageSpy = vi.spyOn(appUtils.logger, 'message');
const loggerErrorSpy = vi.spyOn(appUtils.logger, 'error');

await expect(sut.execute()).rejects.toThrow();

expect(loggerErrorSpy).toHaveBeenCalledWith('AI provider not set.');

expect(loggerMessageSpy).toHaveBeenCalledWith(
"Run 'commitfy setup' to set up the provider.",
);

expect(exitSpy).toHaveBeenCalledWith(0);
});

it('should log an error and exit if there are no changes to commit', async () => {
const { sut, appUtils, envUtils, processUtils } = makeSut();

const loggerMessageSpy = vi.spyOn(appUtils.logger, 'error');
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((number) => {
throw new Error('process.exit: ' + number);
});

vi.spyOn(envUtils, 'variables').mockReturnValueOnce({
PROVIDER: ProviderEnum.OpenAI,
});

vi.spyOn(processUtils, 'exec').mockResolvedValueOnce('');

await expect(sut.execute()).rejects.toThrow();
expect(loggerMessageSpy).toHaveBeenCalledWith('No changes to commit.');
expect(exitSpy).toHaveBeenCalledWith(0);
});

it('should execute successfully if provider is set and there are changes to commit', async () => {
const { sut, envUtils, processUtils, inputUtils, providers } = makeSut();

const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((number) => number as never);

const variablesSpy = vi.spyOn(envUtils, 'variables').mockReturnValueOnce({
PROVIDER: ProviderEnum.OpenAI,
});

const generateCommitMessagesSpy = vi
.spyOn(providers[ProviderEnum.OpenAI], 'generateCommitMessages')
.mockResolvedValueOnce(['commit message']);

const execSpy = vi
.spyOn(processUtils, 'exec')
.mockResolvedValueOnce('some changes');

const promptSpy = vi
.spyOn(inputUtils, 'prompt')
.mockResolvedValueOnce('commit message');

await expect(sut.execute()).resolves.not.toThrow();

expect(variablesSpy).toHaveBeenCalled();

expect(generateCommitMessagesSpy).toHaveBeenCalledWith({
diff: 'some changes',
});

expect(execSpy).toHaveBeenCalledWith(`git commit -m "commit message"`, {
showStdout: true,
});

expect(promptSpy).toHaveBeenCalled();
expect(exitSpy).toHaveBeenCalledWith(0);
});

it('should call execute again if user chooses to regenerate', async () => {
const { sut, envUtils, processUtils, inputUtils, providers } = makeSut();

const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});

vi.spyOn(envUtils, 'variables').mockReturnValue({
PROVIDER: ProviderEnum.OpenAI,
});

vi.spyOn(
providers[ProviderEnum.OpenAI],
'generateCommitMessages',
).mockResolvedValueOnce(['commit 1', 'commit 2']);

vi.spyOn(processUtils, 'exec').mockResolvedValueOnce('some changes');

vi.spyOn(inputUtils, 'prompt').mockResolvedValueOnce('↻ regenerate');

const executeSpy = vi.spyOn(sut, 'execute');

await expect(sut.execute()).rejects.toThrow('process.exit called');

expect(executeSpy).toHaveBeenCalledTimes(2);
expect(exitSpy).toHaveBeenCalledWith(0);
});
});
23 changes: 13 additions & 10 deletions src/commands/generate-commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@ import {
type ProcessUtils,
type Provider,
type Providers,
} from '../interfaces';
} from '@/interfaces';

export class GenerateCommit {
private readonly provider: Provider;
private readonly regeneratorText = '↻ regenerate';
private provider: Provider;

constructor(
private readonly providers: Providers,
private readonly envUtils: EnvUtils,
private readonly processUtils: ProcessUtils,
private readonly inputUtils: InputUtils,
private readonly appUtils: AppUtils,
) {
this.provider = this.providers[this.envUtils.get().PROVIDER];
}
) {}

public async execute(): Promise<void> {
this.provider = this.providers[this.envUtils.variables().PROVIDER];

if (!this.provider) {
this.appUtils.logger.error('AI provider not set.');
console.log("Run 'commitfy setup' to set up the provider.");

this.appUtils.logger.message(
"Run 'commitfy setup' to set up the provider.",
);

process.exit(0);
}
Expand All @@ -34,19 +38,18 @@ export class GenerateCommit {
});

if (!diff) {
console.error(`${this.appUtils.name}: no changes to commit.`);
this.appUtils.logger.error('No changes to commit.');

process.exit(0);
}

const commits = await this.provider.generateCommitMessages({ diff });
const oneLineCommits = commits.map((commit) => commit.split('\n')[0]);
const regenerateText = '↻ regenerate';

const choices = [
...oneLineCommits,
InputUtilsCustomChoiceEnum.Separator,
regenerateText,
this.regeneratorText,
];

const response = await this.inputUtils.prompt({
Expand All @@ -55,7 +58,7 @@ export class GenerateCommit {
choices,
});

if (response === regenerateText) {
if (response === this.regeneratorText) {
return this.execute();
}

Expand Down
32 changes: 32 additions & 0 deletions src/commands/get-version.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { GetVersion } from './get-version';
import { makeAppUtilsFake } from '@/tests/fakes/utils';

const makeSut = () => {
const appUtils = makeAppUtilsFake();
const sut = new GetVersion(appUtils);

return {
appUtils,
sut,
};
};

describe('GetVersion', () => {
it('should log the application name and version', () => {
const { sut, appUtils } = makeSut();

sut.execute();

expect(appUtils.logger.message).toHaveBeenCalledWith(
`${appUtils.name} version v${appUtils.version}`,
);
});

it('should not log an error', () => {
const { sut, appUtils } = makeSut();

sut.execute();

expect(appUtils.logger.error).not.toHaveBeenCalled();
});
});
6 changes: 4 additions & 2 deletions src/commands/get-version.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { AppUtils } from '../interfaces';
import type { AppUtils } from '@/interfaces';

export class GetVersion {
constructor(private appUtils: AppUtils) {}

public execute(): void {
console.log(`${this.appUtils.name} version v${this.appUtils.version}`);
this.appUtils.logger.message(
`${this.appUtils.name} version v${this.appUtils.version}`,
);
}
}
33 changes: 33 additions & 0 deletions src/commands/help.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Help } from './help';

const makeSut = () => {
const sut = new Help();

return { sut };
};

describe('Help', () => {
let consoleLogSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
consoleLogSpy.mockRestore();
});

it('should execute without errors', () => {
const { sut } = makeSut();

expect(() => sut.execute()).not.toThrow();
});

it('should log messages when executed', () => {
const { sut } = makeSut();

sut.execute();

expect(consoleLogSpy).toHaveBeenCalled();
});
});
Loading