Skip to content

Commit f767ad8

Browse files
authored
Merge pull request #24 from ribeirogab/release/v0.1.0
feat: add custom context prompt generation logic
2 parents fb8484d + 9e09f60 commit f767ad8

File tree

17 files changed

+417
-102
lines changed

17 files changed

+417
-102
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "commitfy",
3-
"version": "0.0.14",
3+
"version": "0.1.0",
44
"main": "lib/index.js",
55
"repository": "https://github.com/ribeirogab/commitfy.git",
66
"author": "ribeirogab <[email protected]>",

src/commands/generate-commit.spec.ts

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { GenerateCommit } from './generate-commit';
22
import { ProviderEnum } from '@/interfaces';
33
import { makeProvidersFake } from '@/tests/fakes/providers';
44
import {
5+
DEFAULT_ENV,
56
makeAppUtilsFake,
67
makeEnvUtilsFake,
78
makeInputUtilsFake,
@@ -39,20 +40,21 @@ describe('GenerateCommit', () => {
3940

4041
it('should create an instance of GenerateCommit', () => {
4142
const { sut } = makeSut();
42-
4343
expect(sut).toBeInstanceOf(GenerateCommit);
4444
});
4545

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

49-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((number) => {
50-
throw new Error('process.exit: ' + number);
49+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
50+
throw new Error('process.exit called');
5151
});
5252
const loggerMessageSpy = vi.spyOn(appUtils.logger, 'message');
5353
const loggerErrorSpy = vi.spyOn(appUtils.logger, 'error');
5454

55-
await expect(sut.execute()).rejects.toThrow();
55+
vi.spyOn(processUtils, 'exec').mockResolvedValue('diff');
56+
57+
await expect(sut.execute()).rejects.toThrow('process.exit called');
5658

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

@@ -67,17 +69,18 @@ describe('GenerateCommit', () => {
6769
const { sut, appUtils, envUtils, processUtils } = makeSut();
6870

6971
const loggerMessageSpy = vi.spyOn(appUtils.logger, 'error');
70-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((number) => {
71-
throw new Error('process.exit: ' + number);
72+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
73+
throw new Error('process.exit called');
7274
});
7375

7476
vi.spyOn(envUtils, 'variables').mockReturnValueOnce({
77+
...DEFAULT_ENV,
7578
PROVIDER: ProviderEnum.OpenAI,
7679
});
7780

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

80-
await expect(sut.execute()).rejects.toThrow();
83+
await expect(sut.execute()).rejects.toThrow('process.exit called');
8184
expect(loggerMessageSpy).toHaveBeenCalledWith('No changes to commit.');
8285
expect(exitSpy).toHaveBeenCalledWith(0);
8386
});
@@ -89,63 +92,65 @@ describe('GenerateCommit', () => {
8992
.spyOn(process, 'exit')
9093
.mockImplementation((number) => number as never);
9194

92-
const variablesSpy = vi.spyOn(envUtils, 'variables').mockReturnValueOnce({
95+
vi.spyOn(envUtils, 'variables').mockReturnValueOnce({
96+
...DEFAULT_ENV,
9397
PROVIDER: ProviderEnum.OpenAI,
9498
});
9599

96-
const generateCommitMessagesSpy = vi
97-
.spyOn(providers[ProviderEnum.OpenAI], 'generateCommitMessages')
98-
.mockResolvedValueOnce(['commit message']);
100+
vi.spyOn(
101+
providers[ProviderEnum.OpenAI],
102+
'generateCommitMessages',
103+
).mockResolvedValueOnce(['commit message']);
99104

100-
const execSpy = vi
101-
.spyOn(processUtils, 'exec')
102-
.mockResolvedValueOnce('some changes');
105+
vi.spyOn(processUtils, 'exec').mockResolvedValue('some changes');
103106

104-
const promptSpy = vi
105-
.spyOn(inputUtils, 'prompt')
106-
.mockResolvedValueOnce('commit message');
107+
vi.spyOn(inputUtils, 'prompt').mockResolvedValueOnce('commit message');
107108

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

110-
expect(variablesSpy).toHaveBeenCalled();
111-
112-
expect(generateCommitMessagesSpy).toHaveBeenCalledWith({
113-
diff: 'some changes',
114-
});
111+
expect(
112+
providers[ProviderEnum.OpenAI].generateCommitMessages,
113+
).toHaveBeenCalledOnce();
115114

116-
expect(execSpy).toHaveBeenCalledWith(`git commit -m "commit message"`, {
117-
showStdout: true,
118-
});
115+
expect(processUtils.exec).toHaveBeenCalledWith(
116+
`git commit -m "commit message"`,
117+
{
118+
showStdout: true,
119+
},
120+
);
119121

120-
expect(promptSpy).toHaveBeenCalled();
121122
expect(exitSpy).toHaveBeenCalledWith(0);
122123
});
123124

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

127-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
128+
vi.spyOn(process, 'exit').mockImplementation(() => {
128129
throw new Error('process.exit called');
129130
});
130131

131-
vi.spyOn(envUtils, 'variables').mockReturnValue({
132-
PROVIDER: ProviderEnum.OpenAI,
133-
});
132+
vi.spyOn(envUtils, 'variables')
133+
.mockReturnValueOnce({
134+
...DEFAULT_ENV,
135+
PROVIDER: ProviderEnum.OpenAI,
136+
})
137+
.mockImplementationOnce(() => {
138+
throw new Error('end process');
139+
});
134140

135141
vi.spyOn(
136142
providers[ProviderEnum.OpenAI],
137143
'generateCommitMessages',
138144
).mockResolvedValueOnce(['commit 1', 'commit 2']);
139145

140-
vi.spyOn(processUtils, 'exec').mockResolvedValueOnce('some changes');
146+
vi.spyOn(processUtils, 'exec').mockResolvedValue('some changes');
141147

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

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

146-
await expect(sut.execute()).rejects.toThrow('process.exit called');
152+
await expect(sut.execute()).rejects.toThrow('end process');
147153

148154
expect(executeSpy).toHaveBeenCalledTimes(2);
149-
expect(exitSpy).toHaveBeenCalledWith(0);
150155
});
151156
});

src/commands/generate-commit.ts

Lines changed: 138 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import { CustomConfigKeysEnum, SemanticCommitContextEnum } from '@/constants';
12
import {
23
type AppUtils,
4+
type Env,
35
type EnvUtils,
46
type InputUtils,
57
InputUtilsCustomChoiceEnum,
68
type ProcessUtils,
79
type Provider,
810
type Providers,
11+
SetupContextEnum,
912
} from '@/interfaces';
1013

1114
export class GenerateCommit {
1215
private readonly regeneratorText = '↻ regenerate';
13-
private provider: Provider;
1416

1517
constructor(
1618
private readonly providers: Providers,
@@ -21,29 +23,15 @@ export class GenerateCommit {
2123
) {}
2224

2325
public async execute(): Promise<void> {
24-
this.provider = this.providers[this.envUtils.variables().PROVIDER];
26+
const env = this.envUtils.variables();
27+
const diff = await this.getGitDiff();
28+
const context = await this.getContext({ env });
2529

26-
if (!this.provider) {
27-
this.appUtils.logger.error('AI provider not set.');
28-
29-
this.appUtils.logger.message(
30-
"Run 'commitfy setup' to set up the provider.",
31-
);
32-
33-
process.exit(0);
34-
}
35-
36-
const diff = await this.processUtils.exec('git diff --cached', {
37-
showStdout: false,
30+
const commits = await this.getProvider({ env }).generateCommitMessages({
31+
prompt: this.generatePrompt({ context, env }),
32+
diff,
3833
});
3934

40-
if (!diff) {
41-
this.appUtils.logger.error('No changes to commit.');
42-
43-
process.exit(0);
44-
}
45-
46-
const commits = await this.provider.generateCommitMessages({ diff });
4735
const oneLineCommits = commits.map((commit) => commit.split('\n')[0]);
4836

4937
const choices = [
@@ -68,4 +56,133 @@ export class GenerateCommit {
6856

6957
process.exit(0);
7058
}
59+
60+
private async getContext({ env }: { env: Env }): Promise<string> {
61+
if (env.SETUP_CONTEXT === SetupContextEnum.Automatic) {
62+
return null;
63+
}
64+
65+
const context = await this.inputUtils.prompt({
66+
default: SetupContextEnum.Automatic,
67+
message: 'Choose context for the commit',
68+
type: 'list',
69+
choices: [
70+
{
71+
name: `${SemanticCommitContextEnum.Feat}: (new feature for the user, not a new feature for build script)`,
72+
value: SemanticCommitContextEnum.Feat,
73+
short: SemanticCommitContextEnum.Feat,
74+
},
75+
{
76+
name: `${SemanticCommitContextEnum.Fix}: (bug fix for the user, not a fix to a build script)`,
77+
value: SemanticCommitContextEnum.Fix,
78+
short: SemanticCommitContextEnum.Fix,
79+
},
80+
{
81+
name: `${SemanticCommitContextEnum.Docs}: (changes to the documentation)`,
82+
value: SemanticCommitContextEnum.Docs,
83+
short: SemanticCommitContextEnum.Docs,
84+
},
85+
{
86+
name: `${SemanticCommitContextEnum.Style}: (formatting, missing semi colons, etc; no production code change)`,
87+
value: SemanticCommitContextEnum.Style,
88+
short: SemanticCommitContextEnum.Style,
89+
},
90+
{
91+
name: `${SemanticCommitContextEnum.Refactor}: (refactoring production code, eg. renaming a variable)`,
92+
value: SemanticCommitContextEnum.Refactor,
93+
short: SemanticCommitContextEnum.Refactor,
94+
},
95+
{
96+
name: `${SemanticCommitContextEnum.Test}: (adding missing tests, refactoring tests; no production code change)`,
97+
value: SemanticCommitContextEnum.Test,
98+
short: SemanticCommitContextEnum.Test,
99+
},
100+
{
101+
name: `${SemanticCommitContextEnum.Chore}: (updating grunt tasks etc; no production code change)`,
102+
value: SemanticCommitContextEnum.Chore,
103+
short: SemanticCommitContextEnum.Chore,
104+
},
105+
],
106+
});
107+
108+
return context;
109+
}
110+
111+
private generatePrompt({
112+
context,
113+
env,
114+
}: {
115+
context?: string | null;
116+
env: Env;
117+
}) {
118+
let prompt = context
119+
? env.CONFIG_PROMPT_MANUAL_CONTEXT
120+
: env.CONFIG_PROMPT_AUTOMATIC_CONTEXT;
121+
122+
if (context) {
123+
prompt = prompt.replaceAll(CustomConfigKeysEnum.CustomContext, context);
124+
}
125+
126+
prompt = prompt
127+
.replaceAll(
128+
CustomConfigKeysEnum.CommitLanguage,
129+
env.CONFIG_COMMIT_LANGUAGE,
130+
)
131+
.replaceAll(
132+
CustomConfigKeysEnum.MaxCommitCharacters,
133+
String(env.CONFIG_MAX_COMMIT_CHARACTERS),
134+
);
135+
136+
return prompt;
137+
}
138+
139+
private async getGitDiff(): Promise<string> {
140+
try {
141+
await this.processUtils.exec('git --version', {
142+
showStdout: false,
143+
});
144+
} catch {
145+
this.appUtils.logger.error('Git is not installed.');
146+
147+
process.exit(0);
148+
}
149+
150+
try {
151+
await this.processUtils.exec('git status', {
152+
showStdout: false,
153+
});
154+
} catch {
155+
this.appUtils.logger.error('Current directory is not a git repository.');
156+
157+
process.exit(0);
158+
}
159+
160+
const diff = await this.processUtils.exec('git diff --cached', {
161+
showStdout: false,
162+
});
163+
164+
if (!diff) {
165+
this.appUtils.logger.error('No changes to commit.');
166+
167+
process.exit(0);
168+
}
169+
170+
return diff;
171+
}
172+
173+
private getProvider({ env }: { env: Env }): Provider {
174+
const provider = this.providers[env.PROVIDER];
175+
176+
if (!provider) {
177+
this.appUtils.logger.error('AI provider not set.');
178+
179+
this.appUtils.logger.message(
180+
"Run 'commitfy setup' to set up the provider.",
181+
);
182+
183+
process.exit(0);
184+
}
185+
186+
return provider;
187+
}
71188
}

0 commit comments

Comments
 (0)