AINode.jsAutomation
AI Git Commit Message Generator
TT
TopicTrick Team
AI Git Commit Message Generator
Never stare at a blank commit message field again. This CLI tool reads your staged diff with git diff --staged, sends it to GPT-4o, and returns a Conventional Commits-formatted message with type, scope, description, and body — in seconds.
This is Tool 26 of the Build 50 AI Automation Tools course.
What You'll Build
- CLI:
node commit.js— generate a commit message for staged changes - Conventional Commits format:
feat(auth): add OAuth2 login flow - Optional body with bullet-point summary of changes
- Git hook integration for automatic suggestions
Setup
bash
mkdir ai-commit && cd ai-commit
npm init -y
npm install openai dotenvbash
# .env
OPENAI_API_KEY=sk-your-key-hereCommit Generator
js
// src/commit.js
import 'dotenv/config';
import { execSync } from 'child_process';
import OpenAI from 'openai';
import readline from 'readline';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
function getStagedDiff() {
try {
const diff = execSync('git diff --staged', { encoding: 'utf-8' });
if (!diff.trim()) {
console.error('No staged changes. Run: git add <files> first.');
process.exit(1);
}
return diff;
} catch {
console.error('Not a git repository or git is not installed.');
process.exit(1);
}
}
function getRecentCommits() {
try {
return execSync('git log --oneline -5', { encoding: 'utf-8' }).trim();
} catch {
return '';
}
}
async function generateCommitMessage(diff, recentCommits) {
const truncatedDiff = diff.length > 15_000
? diff.slice(0, 15_000) + '\n[Diff truncated — showing first 15,000 chars]'
: diff;
const recentContext = recentCommits
? `\nRecent commits for context:\n${recentCommits}\n`
: '';
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `You are an expert software engineer who writes excellent git commit messages.
Analyze the staged diff and generate a Conventional Commits message.
${recentContext}
Conventional Commits format:
<type>(<scope>): <short description>
[optional body: bullet points explaining what changed and why]
[optional footer: BREAKING CHANGE: ..., Closes #123]
Types: feat | fix | docs | style | refactor | perf | test | chore | ci | build
- scope: the module, component, or file area changed (e.g. auth, api, ui, db)
- description: imperative mood, lowercase, max 72 chars, no period
- body: only if the change is complex; bullet points starting with "-"
Return ONLY a JSON object:
{
"message": "complete commit message including body if needed",
"type": "feat | fix | docs | etc",
"scope": "string or null",
"description": "the short description line",
"hasBreakingChange": true/false,
"alternatives": ["2 alternative shorter commit messages"]
}`,
},
{
role: 'user',
content: `Staged diff:\n${truncatedDiff}`,
},
],
temperature: 0.3,
response_format: { type: 'json_object' },
});
return JSON.parse(response.choices[0].message.content);
}
async function confirmAndCommit(message) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(resolve => {
console.log('\n' + '─'.repeat(60));
console.log('Generated commit message:\n');
console.log(message);
console.log('─'.repeat(60));
rl.question('\n[C]ommit / [E]dit / [A]lternatives / [Q]uit? ', answer => {
rl.close();
resolve(answer.toLowerCase());
});
});
}
async function main() {
const diff = getStagedDiff();
const recentCommits = getRecentCommits();
console.log('Analysing staged changes...');
const result = await generateCommitMessage(diff, recentCommits);
const choice = await confirmAndCommit(result.message);
if (choice === 'c') {
execSync(`git commit -m ${JSON.stringify(result.message)}`, { stdio: 'inherit' });
console.log('✅ Committed!');
} else if (choice === 'a') {
console.log('\nAlternatives:');
result.alternatives.forEach((alt, i) => console.log(` ${i + 1}. ${alt}`));
} else if (choice === 'e') {
console.log('Run: git commit (the message is in your clipboard if you copy it)');
console.log(result.message);
} else {
console.log('Aborted.');
}
}
main().catch(console.error);Install Globally
bash
# Add to package.json scripts
# "commit": "node src/commit.js"
# Or link globally
npm link
# Then use: ai-commit (if bin field set)Git Hook Integration
bash
#!/bin/sh
# .git/hooks/prepare-commit-msg
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
# Only run on new commits (not amend, merge, etc.)
if [ -z "$COMMIT_SOURCE" ]; then
echo "Generating AI commit message..."
node /path/to/your/ai-commit/src/hook.js "$COMMIT_MSG_FILE"
fijs
// src/hook.js — writes suggestion to commit msg file
import 'dotenv/config';
import { execSync, writeFileSync } from 'fs';
import { generateCommitMessage } from './commitService.js';
const msgFile = process.argv[2];
try {
const diff = execSync('git diff --staged', { encoding: 'utf-8' });
if (!diff.trim()) process.exit(0);
const result = await generateCommitMessage(diff, '');
writeFileSync(msgFile, result.message);
} catch {
// Fail silently — don't block the commit
process.exit(0);
}bash
chmod +x .git/hooks/prepare-commit-msgTesting
bash
# Stage some changes
git add src/auth/oauth.js
# Generate commit message
node src/commit.jsSample output:
text
Analysing staged changes...
────────────────────────────────────────────────────────────
Generated commit message:
feat(auth): add OAuth2 login flow with Google provider
- Add GoogleStrategy using passport-google-oauth20
- Store OAuth tokens in users table with refresh token
- Redirect to /dashboard on successful authentication
- Add logout route that clears session and revokes token
────────────────────────────────────────────────────────────
[C]ommit / [E]dit / [A]lternatives / [Q]uit?Build 50 AI Automation Tools — Tool 26 of 50
Git commit generator is live. Continue to Tool 27 to build an AI blog post generator.
Summary
- Staged diff only — reads exactly what's committed, not working tree noise
- Recent commits context helps the AI maintain consistent type/scope conventions for the repo
- Interactive flow — review before committing, with alternatives for quick iteration
- Git hook integrates into standard
git commitwithout disrupting workflow - Add
--no-bodyflag for simple one-liners on small changes
Continue to Tool 27: AI Blog Post Generator →
