Skip to content

Commit 98a6484

Browse files
committed
feat(commitlint): enhance prompt templates for commit message generation
The commit message generation process has been improved by introducing new prompt templates that provide clearer guidelines and structure for creating commit messages. This includes a more detailed system prompt that outlines the requirements and conventions, as well as user prompts that specify the expected output format. These changes aim to enhance the accuracy and consistency of generated commit messages, ensuring they adhere to the specified commitlint rules and conventions.
1 parent bcbef11 commit 98a6484

File tree

2 files changed

+170
-96
lines changed

2 files changed

+170
-96
lines changed

src/modules/commitlint/prompts.ts

Lines changed: 105 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import {
1111

1212
import { getConfig } from '../../commands/config';
1313
import { i18n, I18nLocals } from '../../i18n';
14-
import { IDENTITY, INIT_DIFF_PROMPT, getConsistencyContent } from '../../prompts';
14+
import {
15+
INIT_DIFF_PROMPT,
16+
getConsistencyContent,
17+
getSharedGuidelines,
18+
SharedGuidelines,
19+
getCommitStructure
20+
} from '../../prompts';
1521

1622
const config = getConfig();
1723
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
@@ -197,20 +203,71 @@ export const inferPromptsFromCommitlintConfig = (
197203
.filter((prompt) => prompt !== null) as string[];
198204
};
199205

200-
/**
201-
* Breaking down commit message structure for conventional commit, and mapping bits with
202-
* ubiquitous language from @commitlint.
203-
* While gpt-4 does this on it self, gpt-3.5 can't map this on his own atm.
204-
*/
205-
const STRUCTURE_OF_COMMIT = config.OCO_OMIT_SCOPE
206-
? `
207-
- Header of commit is composed of type and subject: <type-of-commit>: <subject-of-commit>
208-
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`
209-
: `
210-
- Header of commit is composed of type, scope, subject: <type-of-commit>(<scope-of-commit>): <subject-of-commit>
211-
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`;
212-
213-
// Prompt to generate LLM-readable rules based on @commitlint rules.
206+
const SYSTEM_PROMPT_TEMPLATE = (language: string) => {
207+
const guidelines: SharedGuidelines = getSharedGuidelines(language, true, false);
208+
209+
return `${guidelines.missionBase} and maintains the style of the reference message.
210+
211+
Here are the specific requirements and conventions that should be strictly followed:
212+
213+
Message Structure:
214+
${getCommitStructure(guidelines.headerFormat)}
215+
216+
${guidelines.guidelinesSection}
217+
218+
Commit Rules:
219+
\${rules}
220+
221+
Additional Requirements:
222+
\${requirements}
223+
224+
Output Format:
225+
- Generate a single commit message that:
226+
1. Follows all commitlint rules above
227+
2. Maintains the style and features of the reference message
228+
3. Follows the specific requirements above
229+
- The message should be a complete, valid commit message that would pass commitlint validation
230+
- Do not include any JSON formatting or additional text
231+
- The message should be ready to use as a git commit message`;
232+
};
233+
234+
const INIT_MAIN_SYSTEM_TEMPLATE = (language: string) => {
235+
const guidelines: SharedGuidelines = getSharedGuidelines(language, true, true);
236+
237+
return `${guidelines.missionBase}
238+
239+
Message Structure:
240+
${getCommitStructure(guidelines.headerFormat)}
241+
242+
${guidelines.guidelinesSection}
243+
244+
Commit Rules:
245+
\${rules}`;
246+
};
247+
248+
const USER_PROMPT_TEMPLATE = `Example Git Diff is to follow:
249+
\`\`\`
250+
\${gitDiff}
251+
\`\`\`
252+
253+
Reference Message Style:
254+
\${referenceStyle}`;
255+
256+
interface PromptTemplateVars {
257+
rules: string;
258+
requirements: string;
259+
referenceStyle: string;
260+
gitDiff: string;
261+
}
262+
263+
const replaceTemplateVars = (template: string, vars: PromptTemplateVars): string => {
264+
return template
265+
.replace(/\${rules}/g, vars.rules)
266+
.replace(/\${requirements}/g, vars.requirements)
267+
.replace(/\${referenceStyle}/g, vars.referenceStyle)
268+
.replace(/\${gitDiff}/g, vars.gitDiff);
269+
};
270+
214271
const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
215272
prompts: string[]
216273
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] => {
@@ -230,76 +287,49 @@ const GEN_COMMITLINT_CONSISTENCY_PROMPT = (
230287
: '- Do not use emoji'
231288
].join('\n');
232289

233-
return [
234-
{
235-
role: 'system',
236-
content: `${IDENTITY} Your mission is to create a clean and comprehensive commit message that follows both the commitlint rules and maintains the style of the reference message.
237-
238-
Here are the specific requirements and conventions that should be strictly followed:
239-
240-
Commit Message Conventions:
241-
- The commit message consists of three parts: Header, Body, and Footer.
242-
- Header:
243-
- Format: ${config.OCO_OMIT_SCOPE ? '`<type>: <subject>`' : '`<type>(<scope>): <subject>`'}
244-
- ${prompts.join('\n- ')}
290+
const rules = prompts.map(p => `- ${p}`).join('\n');
245291

246-
${dynamicRequirements}
247-
248-
Reference Message Style:
249-
${defaultContent}
292+
const templateVars: PromptTemplateVars = {
293+
rules,
294+
requirements: dynamicRequirements,
295+
referenceStyle: defaultContent,
296+
gitDiff: String(INIT_DIFF_PROMPT.content || '')
297+
};
250298

251-
Output Format:
252-
- Generate a single commit message that:
253-
1. Follows all commitlint rules above
254-
2. Maintains the style and features of the reference message
255-
3. Follows the specific requirements above
256-
- The message should be a complete, valid commit message that would pass commitlint validation
257-
- Do not include any JSON formatting or additional text
258-
- The message should be ready to use as a git commit message
299+
const systemContent = replaceTemplateVars(
300+
SYSTEM_PROMPT_TEMPLATE(translation.localLanguage),
301+
templateVars
302+
);
259303

260-
Example Git Diff is to follow:`
261-
},
262-
INIT_DIFF_PROMPT
304+
return [
305+
{ role: 'system', content: systemContent },
306+
{
307+
role: 'user',
308+
content: replaceTemplateVars(USER_PROMPT_TEMPLATE, templateVars)
309+
}
263310
];
264311
};
265312

266-
/**
267-
* Prompt to have LLM generate a message using @commitlint rules.
268-
*
269-
* @param language
270-
* @param prompts
271-
* @returns
272-
*/
273313
const INIT_MAIN_PROMPT = (
274314
language: string,
275315
prompts: string[]
276-
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
277-
role: 'system',
278-
content: `${IDENTITY} Your mission is to create clean and comprehensive commit messages in the given @commitlint convention and explain WHAT were the changes ${config.OCO_WHY ? 'and WHY the changes were done' : ''
279-
}. I'll send you an output of 'git diff --staged' command, and you convert it into a commit message.
280-
${config.OCO_EMOJI
281-
? 'Use GitMoji convention to preface the commit.'
282-
: 'Do not preface the commit with anything.'
283-
}
284-
${config.OCO_DESCRIPTION
285-
? 'Add a short description of WHY the changes are done after the commit message. Don\'t start it with "This commit", just describe the changes.'
286-
: "Don't add any descriptions to the commit, only commit message."
287-
}
288-
Use the present tense. Use ${language} to answer.
289-
${config.OCO_ONE_LINE_COMMIT
290-
? 'Craft a concise commit message that encapsulates all changes made, with an emphasis on the primary updates. If the modifications share a common theme or scope, mention it succinctly; otherwise, leave the scope out to maintain focus. The goal is to provide a clear and unified overview of the changes in a one single message, without diverging into a list of commit per file change.'
291-
: ''
292-
}
293-
${config.OCO_OMIT_SCOPE
294-
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
295-
: ''
296-
}
297-
You will strictly follow the following conventions to generate the content of the commit message:
298-
- ${prompts.join('\n- ')}
299-
`
300-
});
316+
): OpenAI.Chat.Completions.ChatCompletionMessageParam => {
317+
const rules = prompts.map(p => `- ${p}`).join('\n');
318+
319+
const templateVars: PromptTemplateVars = {
320+
rules,
321+
requirements: '',
322+
referenceStyle: '',
323+
gitDiff: ''
324+
};
325+
326+
return {
327+
role: 'system',
328+
content: replaceTemplateVars(INIT_MAIN_SYSTEM_TEMPLATE(language), templateVars)
329+
};
330+
};
301331

302332
export const commitlintPrompts = {
303333
INIT_MAIN_PROMPT,
304334
GEN_COMMITLINT_CONSISTENCY_PROMPT
305-
};
335+
};

src/prompts.ts

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@ import { removeConventionalCommitWord } from './utils/removeConventionalCommitWo
1111
const config = getConfig();
1212
const translation = i18n[(config.OCO_LANGUAGE as I18nLocals) || 'en'];
1313

14-
export const IDENTITY =
15-
'You are to act as an author of a commit message in git.';
1614

15+
16+
17+
export interface SharedGuidelines {
18+
missionBase: string;
19+
guidelinesSection: string;
20+
headerFormat: string;
21+
}
22+
23+
// Internal constants
1724
const GITMOJI_HELP = `Use GitMoji convention to preface the commit. Here are some help to choose the right emoji (emoji, description):
1825
🐛, Fix a bug;
1926
✨, Introduce new features;
@@ -94,6 +101,7 @@ const FULL_GITMOJI_SPEC = `${GITMOJI_HELP}
94101
const CONVENTIONAL_COMMIT_KEYWORDS =
95102
'Do not preface the commit with anything, except for the conventional commit keywords: fix, feat, build, chore, ci, docs, style, refactor, perf, test.';
96103

104+
// Internal helper functions
97105
const getCommitConvention = (fullGitMojiSpec: boolean) =>
98106
config.OCO_EMOJI
99107
? fullGitMojiSpec
@@ -116,41 +124,77 @@ const getScopeInstruction = () =>
116124
? 'Do not include a scope in the commit message format. Use the format: <type>: <subject>'
117125
: '';
118126

119-
/**
120-
* Get the context of the user input
121-
* @param extraArgs - The arguments passed to the command line
122-
* @example
123-
* $ oco -- This is a context used to generate the commit message
124-
* @returns - The context of the user input
125-
*/
126127
const userInputCodeContext = (context: string) => {
127128
if (context !== '' && context !== ' ') {
128129
return `Additional context provided by the user: <context>${context}</context>\nConsider this context when generating the commit message, incorporating relevant information when appropriate.`;
129130
}
130131
return '';
131132
};
132133

134+
const IDENTITY = 'You are to act as an author of a commit message in git.';
135+
136+
const getMissionStatement = (commitConvention: string, includeWhy = true) =>
137+
`${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the ${commitConvention}${includeWhy ? ' and explain WHAT were the changes and mainly WHY the changes were done' : ''}.`;
138+
139+
const getGeneralGuidelines = (language: string) =>
140+
`Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`;
141+
142+
const getCommitConventionName = (fullGitMojiSpec: boolean) =>
143+
fullGitMojiSpec ? 'GitMoji specification' : 'Conventional Commit Convention';
144+
145+
/**
146+
* Breaking down commit message structure for conventional commit, and mapping bits with
147+
* ubiquitous language from @commitlint.
148+
* While gpt-4 does this on it self, gpt-3.5 can't map this on his own atm.
149+
*/
150+
export const getHeaderFormat = () =>
151+
config.OCO_OMIT_SCOPE ? '`<type>: <subject>`' : '`<type>(<scope>): <subject>`';
152+
153+
export const getHeaderParts = () =>
154+
config.OCO_OMIT_SCOPE
155+
? ['type', 'subject']
156+
: ['type', 'scope', 'subject'];
157+
158+
export const getCommitStructure = (headerFormat: string) => {
159+
const headerParts = getHeaderParts();
160+
161+
return `
162+
- Header of commit is composed of ${headerParts.join(', ')}: ${headerFormat}
163+
- Description of commit is composed of body and footer (optional): <body-of-commit>\n<footer(s)-of-commit>`;
164+
};
165+
166+
// Public function that encapsulates all the guidelines
167+
export const getSharedGuidelines = (language: string, useFullGitMojiSpec = false, includeWhy = true): SharedGuidelines => {
168+
const commitConvention = getCommitConventionName(useFullGitMojiSpec);
169+
const missionBase = getMissionStatement(commitConvention, includeWhy);
170+
const headerFormat = getHeaderFormat();
171+
172+
const guidelinesSection = `Commit Message Guidelines:
173+
${getCommitConvention(useFullGitMojiSpec)}
174+
${getDescriptionInstruction()}
175+
${getOneLineCommitInstruction()}
176+
${getScopeInstruction()}
177+
${getGeneralGuidelines(language)}`;
178+
179+
return {
180+
missionBase,
181+
guidelinesSection,
182+
headerFormat
183+
};
184+
};
185+
186+
const DIFF_INSTRUCTION = "I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message.";
133187
const INIT_MAIN_PROMPT = (
134188
language: string,
135189
fullGitMojiSpec: boolean,
136190
context: string
137191
): OpenAI.Chat.Completions.ChatCompletionMessageParam => ({
138192
role: 'system',
139193
content: (() => {
140-
const commitConvention = fullGitMojiSpec
141-
? 'GitMoji specification'
142-
: 'Conventional Commit Convention';
143-
const missionStatement = `${IDENTITY} Your mission is to create clean and comprehensive commit messages as per the ${commitConvention} and explain WHAT were the changes and mainly WHY the changes were done.`;
144-
const diffInstruction =
145-
"I'll send you an output of 'git diff --staged' command, and you are to convert it into a commit message.";
146-
const conventionGuidelines = getCommitConvention(fullGitMojiSpec);
147-
const descriptionGuideline = getDescriptionInstruction();
148-
const oneLineCommitGuideline = getOneLineCommitInstruction();
149-
const scopeInstruction = getScopeInstruction();
150-
const generalGuidelines = `Use the present tense. Lines must not be longer than 74 characters. Use ${language} for the commit message.`;
194+
const guidelines = getSharedGuidelines(language, fullGitMojiSpec, true);
151195
const userInputContext = userInputCodeContext(context);
152196

153-
return `${missionStatement}\n${diffInstruction}\n${conventionGuidelines}\n${descriptionGuideline}\n${oneLineCommitGuideline}\n${scopeInstruction}\n${generalGuidelines}\n${userInputContext}`;
197+
return `${guidelines.missionBase}\n${DIFF_INSTRUCTION}\n${guidelines.guidelinesSection}\n${userInputContext}`;
154198
})()
155199
});
156200

0 commit comments

Comments
 (0)