Last active
March 4, 2025 21:59
-
-
Save kylesnowschwartz/819dc1646b5cf5700f30e0071ef8f681 to your computer and use it in GitHub Desktop.
generate-commit script using https://github.com/microsoft/genaiscript and https://ollama.com/library/phi4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
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
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