Skip to content

Instantly share code, notes, and snippets.

@kylesnowschwartz
Last active March 4, 2025 21:59
Show Gist options
  • Save kylesnowschwartz/819dc1646b5cf5700f30e0071ef8f681 to your computer and use it in GitHub Desktop.
Save kylesnowschwartz/819dc1646b5cf5700f30e0071ef8f681 to your computer and use it in GitHub Desktop.
import { spawnSync } from "child_process";
/**
* Script to automate the git commit process with AI-generated commit messages.
* It checks for staged changes, generates a commit message, and prompts the user to review or edit the message before committing.
* Original source: https://microsoft.github.io/genaiscript/samples/gcm/
*/
script({
title: 'Git Commit Message',
description: "Generate a commit message for all staged changes",
model: "none",
parameters: {
chunkSize: {
type: "number",
default: 10000,
description: "Maximum number of tokens per chunk",
},
maxChunks: {
type: "number",
default: 4,
description:
"Safeguard against huge commits. Asks confirmation to the user before running more than maxChunks chunks",
},
},
})
const { chunkSize, maxChunks } = env.vars
// console.debug(`config: ${JSON.stringify({ chunkSize, maxChunks })}`)
// Check for staged changes and stage all changes if none are staged
const diff = await git.diff({
staged: true,
askStageOnEmpty: true,
})
// If no staged changes are found, cancel the script with a message
if (!diff) cancel("no staged changes")
// Display the diff of staged changes in the console
// console.debug(diff)
// chunk if case of massive diff
const chunks = await tokenizers.chunk(diff, { chunkSize })
if (chunks.length > 1) {
console.log(`staged changes chunked into ${chunks.length} parts`)
if (chunks.length > maxChunks) {
const res = await host.confirm(
`This is a big diff with ${chunks.length} chunks, do you want to proceed?`
)
if (!res) cancel("user cancelled")
}
}
export const commitSpecifications = `
(Interpret the keywords “MUST,” “MUST NOT,” “REQUIRED,” “SHALL,” “SHALL NOT,”
“SHOULD,” “SHOULD NOT,” “RECOMMENDED,” “MAY,” and “OPTIONAL” as described in RFC 2119.)
1. Commits MUST be prefixed with a type (e.g., feat, fix) followed by an OPTIONAL scope,
an OPTIONAL !, and the REQUIRED terminal colon and space.
2. The type feat MUST be used when a commit adds a new feature.
3. The type fix MUST be used when a commit represents a bug fix.
4. A scope MAY be provided in parentheses after a type (e.g., fix(parser):).
The scope MUST be a noun describing a section of the codebase.
5. A description MUST immediately follow the prefix (type[(scope)]:).
The description SHOULD be a short summary of the changes.
6. A longer commit body MAY be provided after the short description.
The body MUST begin exactly one blank line after the description.
7. The commit body is free-form and MAY consist of any number of newline-separated paragraphs.
8. One or more footers MAY be provided exactly one blank line after the body.
Each footer MUST consist of a token followed by :<space> or <space>#,
then a string value (inspired by the git trailer convention).
9. A footer token MUST use - in place of whitespace characters (e.g., Acked-by).
An exception is BREAKING CHANGE, which MAY also be used as a token.
10. A footer’s value MAY contain spaces and newlines.
Parsing MUST terminate when the next valid footer token/separator pair is observed.
11. Breaking changes MUST be indicated in the type/scope prefix or in the footer.
12. If indicated in the footer, a breaking change MUST use the token BREAKING CHANGE:
followed by a short description (e.g., BREAKING CHANGE: environment variables now take precedence...).
13. If indicated in the prefix, a ! MUST appear immediately before the colon (e.g., feat!: ...).
If ! is used, BREAKING CHANGE: MAY be omitted from the footer,
and the commit description SHALL describe the breaking change.
14. Types other than feat and fix MAY be used (e.g., docs:, style:, refactor:).
`;
const addInstructions = (ctx) => {
ctx.$`
- A commit message must follow this format:
<type>: <description>
<body>
<footer>
- <type>
- is one of: feat, fix, docs, style, refactor, perf, test, build, ci, maintain, revert, tweak, fixup, lint.
- followed by an OPTIONAL scope in () brackets.
- <description> is a SINGLE-LINE, imperative, present-tense summary.
- Is MANDATORY.
- Maximum length of 50 characters.
- <body> is a one or more line description of the changes.
- is OPTIONAL.
- Each line MUST be at most 72 characters.
- The diff is generated by "git diff."
- DO NOT use Markdown syntax.
- DO NOT add quotes, single quotes, or code blocks.
- DO NOT confuse deleted lines (starting with "-") and added lines (starting with "+").
- Follow Conventional Commit Specifications: ${commitSpecifications}.
`;
}
let choice
let message
do {
// Generate a conventional commit message based on the staged changes diff
message = ""
for (const chunk of chunks) {
const res = await runPrompt(
(_) => {
_.def("GIT_DIFF", chunk, {
maxTokens: 10000,
language: "diff",
detectPromptInjection: "available",
})
_.$`Generate a git commit message that summarizes the changes in GIT_DIFF based on the following instructions:`
addInstructions(_)
},
{
model: 'ollama:phi4:latest',
label: "generate commit message", // Label for the prompt task
system: [
'system',
'system.assistant',
'system.safety_jailbreak',
'system.safety_harmful_content',
'system.safety_validate_harmful_content'
],
systemSafety: true,
responseType: "text",
}
)
if (res.error) throw res.error
message += res.text + "\n"
}
// since we've concatenated the chunks, let's compress it back into a single sentence again
if (chunks.length > 1) {
const res = await runPrompt(
(_) => {
_.$`Generate a git conventional commit message that summarizes the <COMMIT_MESSAGES> based on the following instructions:`
addInstructions(_)
_.def("COMMIT_MESSAGES", message)
},
{
model: "large",
label: "summarize chunk commit messages",
system: ["system.assistant"],
systemSafety: true,
responseType: "text",
}
)
if (res.error) throw res.error
message = res.text
}
message = parsers.unthink(message?.trim())
if (!message) {
console.log(
"No commit message generated, did you configure the LLM model?"
)
break
}
// Prompt user to accept, edit, or regenerate the commit message
choice = await host.select(message, [
{
value: "commit",
description: "accept message and commit",
},
{
value: "edit",
description: "edit message and commit",
},
{
value: "regenerate",
description: "regenerate message",
},
{
value: "cancel",
description: "exit this script",
},
])
// Handle user's choice for commit message
if (choice === "edit") {
// Exit the script and take the generated message for manual editing
// Use spawn from Node's child_proces here
// 1) Launch git commit in an interactive editor
const spawnResult = spawnSync("git", ["commit", "-m", message, "--edit"], {
stdio: "inherit",
})
// 2) After the editor closes, forcibly exit the entire script
console.log("Editor closed, exit code:", spawnResult.status)
process.exit(spawnResult.status)
}
// If user chooses to commit, execute the git commit and optionally push changes
if (choice === "commit" && message) {
console.log(await git.exec(["commit", "-m", message]))
if (await host.confirm("Push changes?", { default: true }))
console.log(await git.exec("push"))
break
}
if (choice === "cancel") {
cancel("user canceled")
}
} while (choice !== "commit")
@delexw
Copy link

delexw commented Mar 4, 2025

model: 'ollama:deepseek-r1:32b' can be configured into script({}). It will be shared with all chats. I am seeing the model: "large" used to aggregate all commit message

@delexw
Copy link

delexw commented Mar 4, 2025

is a single-line, imperative, present-tense summary

It is worth a UPPER CASE to highlight importance. I found those local modal sometimes generates multiple line commit messages

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment