Merge branch 'update/conventional_commit_rules' into 'master'

ci(danger): Change commit message default rules

Closes IDF-7656

See merge request espressif/esp-idf!24428
This commit is contained in:
Tomas Sebestik
2023-06-30 12:21:55 +08:00
5 changed files with 183 additions and 112 deletions

View File

@@ -1,120 +1,172 @@
const { OpenAI } = require("langchain/llms/openai");
const { const {
ChatPromptTemplate, minimumSummaryChars,
SystemMessagePromptTemplate, maximumSummaryChars,
} = require("langchain/prompts"); maximumBodyLineChars,
allowedTypes,
} = require("./mrCommitsConstants.js");
const { gptStandardModelTokens } = require("./mrCommitsConstants.js");
const { ChatPromptTemplate } = require("langchain/prompts");
const { SystemMessagePromptTemplate } = require("langchain/prompts");
const { LLMChain } = require("langchain/chains"); const { LLMChain } = require("langchain/chains");
const { ChatOpenAI } = require("langchain/chat_models/openai"); const { ChatOpenAI } = require("langchain/chat_models/openai");
const openAiTokenCount = require("openai-gpt-token-counter"); const openAiTokenCount = require("openai-gpt-token-counter");
const mrModifiedFiles = danger.git.modified_files;
const mrCommits = danger.gitlab.commits;
module.exports = async function () { module.exports = async function () {
let mrDiff = await getMrGitDiff(mrModifiedFiles);
const mrCommitMessages = getCommitMessages(mrCommits);
// Init output message
let outputDangerMessage = `\n\nPerhaps you could use an AI-generated suggestion for your commit message. Here is one `; let outputDangerMessage = `\n\nPerhaps you could use an AI-generated suggestion for your commit message. Here is one `;
// Setup LLM prompt let mrDiff = await getMrGitDiff(danger.git.modified_files);
const inputPrompt = `You are a helpful assistant that creates suggestions for single git commit message, that user can use to describe all the changes in their merge request. const mrCommitMessages = getCommitMessages(danger.gitlab.commits);
Use git diff: {mrDiff} and users current commit messages: {mrCommitMessages} to get the changes made in the commit. const inputPrompt = getInputPrompt();
const inputLlmTokens = getInputLlmTokens(
Output should be git commit message following the conventional commit format.
Output only git commit message in desired format, without comments and other text.
Do not include lines with JIRA tickets mentions (e.g. "Closes JIRA-123") to the output.
Avoid including temporary commit messages (e.g. "Cleanup", "Merged" or "wip: Test") to the output.
Avoid using vague terms (e.g. "some checks", "add new ones", "few changes" ) in the output.
[EXAMPLE OUTPUT]
feat(scope): add support for component XXX
- adds support for wifi6
- adds validations for logging script
[EXAMPLE OUTPUT]
feat(scope): add support for component XXX
- adds support for wifi6
- adds validations for logging script
- Closes https://github.com/espressif/esp-idf/issues/1234
`;
// Count input tokens for LLM
const mrCommitMessagesTokens = openAiTokenCount(mrCommitMessages.join(" "));
const gitDiffTokens = openAiTokenCount(mrDiff);
const promptTokens = openAiTokenCount(inputPrompt);
const inputLlmTokens =
mrCommitMessagesTokens + gitDiffTokens + promptTokens;
console.log(`Input tokens for LLM: ${inputLlmTokens}`);
if (inputLlmTokens < 4096) {
outputDangerMessage += `(based on your MR git-diff and your current commit messages):\n\n`;
} else {
outputDangerMessage += `(based only on your current commit messages, git-diff of this MR is too big (${inputLlmTokens} tokens) for the AI model):\n\n`;
mrDiff = "";
}
// Call LLM
const generatedCommitMessage = await createAiGitMessage(
inputPrompt, inputPrompt,
mrDiff, mrDiff,
mrCommitMessages mrCommitMessages
); );
console.log(`Input tokens for LLM: ${inputLlmTokens}`);
outputDangerMessage += "```\n" + generatedCommitMessage + "\n```\n"; // Add the generated git message, format to the markdown code block if (inputLlmTokens >= gptStandardModelTokens) {
outputDangerMessage += "\n**NOTE: AI-generated suggestions may not always be correct, please review the suggestion before using it.**" // Add disclaimer mrDiff = ""; // If the input mrDiff is larger than 16k model, don't use mrDiff, use only current commit messages
outputDangerMessage += `(based only on your current commit messages, git-diff of this MR is too big (${inputLlmTokens} tokens) for the AI models):\n\n`;
} else {
outputDangerMessage += `(based on your MR git-diff and your current commit messages):\n\n`;
}
// Generate AI commit message
let generatedCommitMessage = "";
try {
const rawCommitMessage = await createAiGitMessage(
inputPrompt,
mrDiff,
mrCommitMessages
);
generatedCommitMessage = postProcessCommitMessage(rawCommitMessage);
} catch (error) {
console.error("Error in generating AI commit message: ", error);
outputDangerMessage +=
"\nCould not generate commit message due to an error.\n";
}
// Append closing statements ("Closes https://github.com/espressif/esp-idf/issues/XXX") to the generated commit message
let closingStatements = extractClosingStatements(mrCommitMessages);
if (closingStatements.length > 0) {
generatedCommitMessage += "\n\n" + closingStatements;
}
// Add the generated git message, format to the markdown code block
outputDangerMessage += `\n\`\`\`\n${generatedCommitMessage}\n\`\`\`\n`;
outputDangerMessage +=
"\n**NOTE: AI-generated suggestions may not always be correct, please review the suggestion before using it.**"; // Add disclaimer
return outputDangerMessage; return outputDangerMessage;
}; };
async function getMrGitDiff(mrModifiedFiles) { async function getMrGitDiff(mrModifiedFiles) {
let mrDiff = ""; const fileDiffs = await Promise.all(
for (const file of mrModifiedFiles) { mrModifiedFiles.map((file) => danger.git.diffForFile(file))
const fileDiff = await danger.git.diffForFile(file); );
mrDiff += fileDiff.diff.trim(); return fileDiffs.map((fileDiff) => fileDiff.diff.trim()).join(" ");
}
return mrDiff;
} }
function getCommitMessages(mrCommits) { function getCommitMessages(mrCommits) {
let mrCommitMessages = []; return mrCommits.map((commit) => commit.message);
for (const commit of mrCommits) { }
mrCommitMessages.push(commit.message);
}
return mrCommitMessages; function getInputPrompt() {
return `You are a helpful assistant that creates suggestions for single git commit message, that user can use to describe all the changes in their merge request.
Use git diff: {mrDiff} and users current commit messages: {mrCommitMessages} to get the changes made in the commit.
Output should be git commit message following the conventional commit format.
Output only git commit message in desired format, without comments and other text.
Do not include the closing statements ("Closes https://....") in the output.
Here are the strict rules you must follow:
- Avoid mentioning any JIRA tickets (e.g., "Closes JIRA-123").
- Be specific. Don't use vague terms (e.g., "some checks", "add new ones", "few changes").
- The commit message structure should be: <type><(scope/component)>: <summary>
- Types allowed: ${allowedTypes.join(", ")}
- If 'scope/component' is used, it must start with a lowercase letter.
- The 'summary' must NOT end with a period.
- The 'summary' must be between ${minimumSummaryChars} and ${maximumSummaryChars} characters long.
If a 'body' of commit message is used:
- Each line must be no longer than ${maximumBodyLineChars} characters.
- It must be separated from the 'summary' by a blank line.
Examples of correct commit messages:
- With scope and body:
fix(freertos): Fix startup timeout issue
This is a text of commit message body...
- adds support for wifi6
- adds validations for logging script
- Without scope and body:
ci: added target test job for ESP32-Wifi6`;
}
function getInputLlmTokens(inputPrompt, mrDiff, mrCommitMessages) {
const mrCommitMessagesTokens = openAiTokenCount(mrCommitMessages.join(" "));
const gitDiffTokens = openAiTokenCount(mrDiff);
const promptTokens = openAiTokenCount(inputPrompt);
return mrCommitMessagesTokens + gitDiffTokens + promptTokens;
} }
async function createAiGitMessage(inputPrompt, mrDiff, mrCommitMessages) { async function createAiGitMessage(inputPrompt, mrDiff, mrCommitMessages) {
const chat = new ChatOpenAI({ const chat = new ChatOpenAI({ engine: "gpt-3.5-turbo", temperature: 0 });
engine: "gpt-3.5-turbo",
temperature: 0,
});
const chatPrompt = ChatPromptTemplate.fromPromptMessages([ const chatPrompt = ChatPromptTemplate.fromPromptMessages([
SystemMessagePromptTemplate.fromTemplate(inputPrompt), SystemMessagePromptTemplate.fromTemplate(inputPrompt),
]); ]);
const chain = new LLMChain({ prompt: chatPrompt, llm: chat });
const chain = new LLMChain({
prompt: chatPrompt,
llm: chat,
});
const response = await chain.call({ const response = await chain.call({
mrDiff: mrDiff, mrDiff: mrDiff,
mrCommitMessages: mrCommitMessages, mrCommitMessages: mrCommitMessages,
}); });
return response.text; return response.text;
} }
function postProcessCommitMessage(rawCommitMessage) {
// Split the result into lines
let lines = rawCommitMessage.split("\n");
// Format each line
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
// If the line is longer than maximumBodyLineChars, split it into multiple lines
if (line.length > maximumBodyLineChars) {
let newLines = [];
while (line.length > maximumBodyLineChars) {
let lastSpaceIndex = line.lastIndexOf(
" ",
maximumBodyLineChars
);
newLines.push(line.substring(0, lastSpaceIndex));
line = line.substring(lastSpaceIndex + 1);
}
newLines.push(line);
lines[i] = newLines.join("\n");
}
}
// Join the lines back into a single string with a newline between each one
return lines.join("\n");
}
function extractClosingStatements(mrCommitMessages) {
let closingStatements = [];
mrCommitMessages.forEach((message) => {
const lines = message.split("\n");
lines.forEach((line) => {
if (line.startsWith("Closes")) {
closingStatements.push(line);
}
});
});
return closingStatements.join("\n");
}

View File

@@ -1,33 +1,27 @@
const {
minimumSummaryChars,
maximumSummaryChars,
maximumBodyLineChars,
allowedTypes,
} = require("./mrCommitsConstants.js");
/** /**
* Check that commit messages are based on the Espressif ESP-IDF project's internal rules for git commit messages. * Check that commit messages are based on the Espressif ESP-IDF project's rules for git commit messages.
* *
* @dangerjs WARN * @dangerjs WARN
*/ */
module.exports = async function () { module.exports = async function () {
const mrCommits = danger.gitlab.commits; const mrCommits = danger.gitlab.commits;
const lint = require("@commitlint/lint").default; const lint = require("@commitlint/lint").default;
const allowedTypes = [
"change",
"ci",
"docs",
"feat",
"fix",
"refactor",
"remove",
"revert",
];
const lintingRules = { const lintingRules = {
// rule definition: [(0-1 = off/on), (always/never = must be/mustn't be), (value)] // rule definition: [(0-1 = off/on), (always/never = must be/mustn't be), (value)]
"body-max-line-length": [1, "always", 100], // Max length of the body line "body-max-line-length": [1, "always", maximumBodyLineChars], // Max length of the body line
"footer-leading-blank": [1, "always"], // Always have a blank line before the footer section "footer-leading-blank": [1, "always"], // Always have a blank line before the footer section
"footer-max-line-length": [1, "always", 100], // Max length of the footer line "footer-max-line-length": [1, "always", maximumBodyLineChars], // Max length of the footer line
"subject-max-length": [1, "always", 50], // Max length of the "Summary" "subject-max-length": [1, "always", maximumSummaryChars], // Max length of the "Summary"
"subject-min-length": [1, "always", 20], // Min length of the "Summary" "subject-min-length": [1, "always", minimumSummaryChars], // Min length of the "Summary"
"scope-case": [1, "always", "lower-case"], // "scope/component" must start with lower-case "scope-case": [1, "always", "lower-case"], // "scope/component" must start with lower-case
// "scope-empty": [1, "never"], // "scope/component" is mandatory
"subject-case": [1, "always", ["sentence-case"]], // "Summary" must start with upper-case
"subject-full-stop": [1, "never", "."], // "Summary" must not end with a full stop (period) "subject-full-stop": [1, "never", "."], // "Summary" must not end with a full stop (period)
"subject-empty": [1, "never"], // "Summary" is mandatory "subject-empty": [1, "never"], // "Summary" is mandatory
"type-case": [1, "always", "lower-case"], // "type/action" must start with lower-case "type-case": [1, "always", "lower-case"], // "type/action" must start with lower-case
@@ -36,6 +30,9 @@ module.exports = async function () {
"body-leading-blank": [1, "always"], // Always have a blank line before the body section "body-leading-blank": [1, "always"], // Always have a blank line before the body section
}; };
// Switcher for AI suggestions (for poor messages)
let generateAISuggestion = false;
// Search for the messages in each commit // Search for the messages in each commit
let issuesAllCommitMessages = []; let issuesAllCommitMessages = [];
@@ -90,11 +87,13 @@ module.exports = async function () {
break; break;
case "subject-empty": case "subject-empty":
issuesSingleCommitMessage.push(`- *summary* looks empty`); issuesSingleCommitMessage.push(`- *summary* looks empty`);
generateAISuggestion = true;
break; break;
case "subject-min-length": case "subject-min-length":
issuesSingleCommitMessage.push( issuesSingleCommitMessage.push(
`- *summary* looks too short` `- *summary* looks too short`
); );
generateAISuggestion = true;
break; break;
case "subject-case": case "subject-case":
issuesSingleCommitMessage.push( issuesSingleCommitMessage.push(
@@ -131,8 +130,9 @@ module.exports = async function () {
if (issuesAllCommitMessages.length) { if (issuesAllCommitMessages.length) {
issuesAllCommitMessages.sort(); issuesAllCommitMessages.sort();
const basicTips = [ const basicTips = [
`- correct format of commit message should be: \`<type/action>(<scope/component>): <Summary>\`, for example \`fix(esp32): Fixed startup timeout issue\``, `- correct format of commit message should be: \`<type/action>(<scope/component>): <summary>\`, for example \`fix(esp32): Fixed startup timeout issue\``,
`- sufficiently descriptive message summary should be between 20 to 50 characters and start with upper case letter`, `- allowed types are: \`${allowedTypes}\``,
`- sufficiently descriptive message summary should be between ${minimumSummaryChars} to ${maximumSummaryChars} characters and start with upper case letter`,
`- avoid Jira references in commit messages (unavailable/irrelevant for our customers)`, `- avoid Jira references in commit messages (unavailable/irrelevant for our customers)`,
`- follow this [commit messages guide](${process.env.DANGER_GITLAB_HOST}/espressif/esp-idf/-/wikis/dev-proc/Commit-messages)`, `- follow this [commit messages guide](${process.env.DANGER_GITLAB_HOST}/espressif/esp-idf/-/wikis/dev-proc/Commit-messages)`,
]; ];
@@ -143,13 +143,16 @@ module.exports = async function () {
\n**Please consider updating these commit messages** - here are some basic tips:\n${basicTips.join( \n**Please consider updating these commit messages** - here are some basic tips:\n${basicTips.join(
"\n" "\n"
)} )}
\n \`TIP:\` You can install commit-msg pre-commit hook (\`pre-commit install -t pre-commit -t commit-msg\`) to run this check when committing.
\n*** \n***
`; `;
// Create AI generated suggestion for git commit message based of gitDiff and current commit messages if (generateAISuggestion) {
const AImessageSuggestion = // Create AI generated suggestion for git commit message based of gitDiff and current commit messages
await require("./aiGenerateGitMessage.js")(); const AImessageSuggestion =
dangerMessage += AImessageSuggestion; await require("./aiGenerateGitMessage.js")();
dangerMessage += AImessageSuggestion;
}
warn(dangerMessage); warn(dangerMessage);
} }

View File

@@ -0,0 +1,16 @@
module.exports = {
gptStandardModelTokens: 4096,
minimumSummaryChars: 20,
maximumSummaryChars: 72,
maximumBodyLineChars: 100,
allowedTypes: [
"change",
"ci",
"docs",
"feat",
"fix",
"refactor",
"remove",
"revert",
],
};

View File

@@ -169,7 +169,7 @@ repos:
- id: check-copyright - id: check-copyright
args: ['--ignore', 'tools/ci/check_copyright_ignore.txt', '--config', 'tools/ci/check_copyright_config.yaml'] args: ['--ignore', 'tools/ci/check_copyright_ignore.txt', '--config', 'tools/ci/check_copyright_config.yaml']
- repo: https://github.com/espressif/conventional-precommit-linter - repo: https://github.com/espressif/conventional-precommit-linter
rev: v1.0.0 rev: v1.2.0
hooks: hooks:
- id: conventional-precommit-linter - id: conventional-precommit-linter
stages: [commit-msg] stages: [commit-msg]

View File

@@ -18,7 +18,7 @@ Install pre-commit hook
1. Go to the IDF Project Directory 1. Go to the IDF Project Directory
2. Run ``pre-commit install --allow-missing-config``. Install hook by this approach will let you commit successfully even in branches without the ``.pre-commit-config.yaml`` 2. Run ``pre-commit install --allow-missing-config -t pre-commit -t commit-msg``. Install hook by this approach will let you commit successfully even in branches without the ``.pre-commit-config.yaml``
3. pre-commit hook will run automatically when you're running ``git commit`` command 3. pre-commit hook will run automatically when you're running ``git commit`` command