From 0908b8fe75da561c5753a971199f52e11f76a123 Mon Sep 17 00:00:00 2001 From: Sid Edwards Date: Thu, 24 Oct 2024 05:05:46 -0600 Subject: [PATCH 1/3] feat(main.ts): Add commit style analysis and storage - Implement function to analyze commit history and extract a minimal style guide - Store the learned commit style for the repository or per author - Load previously learned styles and use them as the default - Add support for different commit formats (Conventional, Semantic, Angular, Kernel) - Allow resetting the default format BREAKING CHANGE: Removed the `build.ts` file and related functionality --- build.ts | 0 main.ts | 619 ++++++++++++++++++++++++++----- install.ts => scripts/install.ts | 0 3 files changed, 524 insertions(+), 95 deletions(-) delete mode 100644 build.ts rename install.ts => scripts/install.ts (100%) diff --git a/build.ts b/build.ts deleted file mode 100644 index e69de29..0000000 diff --git a/main.ts b/main.ts index a5910e5..29636af 100644 --- a/main.ts +++ b/main.ts @@ -1,11 +1,19 @@ #!/usr/bin/env -S deno run --allow-net --allow-read --allow-write --allow-env --allow-run="git,vim" -import { serve } from "https://deno.land/std/http/server.ts"; import Anthropic from "npm:@anthropic-ai/sdk"; import { ensureDir } from "https://deno.land/std/fs/ensure_dir.ts"; import { join } from "https://deno.land/std/path/mod.ts"; +import { parse } from "https://deno.land/std/flags/mod.ts"; + +// Export enum first +export enum CommitFormat { + CONVENTIONAL = 'conventional', + SEMANTIC = 'semantic', + ANGULAR = 'angular', + KERNEL = 'kernel' +} -// Add loading spinner function +// Then define all other functions and constants function startLoading(message: string): number { const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; let i = 0; @@ -24,10 +32,9 @@ function stopLoading(intervalId: number) { Deno.stdout.writeSync(new TextEncoder().encode('\x1B[?25h')); // Show cursor } -const COMMIT_PROMPT = `You are a Git Commit Message Generator that follows conventional commit standards. Generate commit messages directly without explanations or markdown formatting. +// Add after imports +const CONVENTIONAL_FORMAT = `1. Follow the format: -1. Follow the format: (): - Types: - feat: New features or significant changes to functionality - fix: Bug fixes @@ -65,7 +72,7 @@ const COMMIT_PROMPT = `You are a Git Commit Message Generator that follows conve BREAKING CHANGE: RESPONSE FORMAT: -(): +(): (only one per commit) - Main change description * Impact or detail @@ -78,9 +85,102 @@ BREAKING CHANGE: (if applicable) Do not include any explanatory text, markdown formatting, or quotes around the message.`; -// Function to call the Anthropic API -async function getCommitMessage(diff: string, apiKey: string): Promise { - const loadingId = startLoading('Analyzing changes...'); +const SEMANTIC_FORMAT = `1. Follow the format: + + Emojis: + - ✨ New features + - 🐛 Bug fixes + - 📝 Documentation + - 💄 UI/style updates + - ⚡️ Performance + - 🔨 Refactoring + - 🚀 Deployments + +2. Rules: + 1. Start with an emoji + 2. Use present tense + 3. First line is summary + 4. Include issue references + +RESPONSE FORMAT: +:emoji: + + +- Change detail 1 +- Change detail 2 + +`; + +const ANGULAR_FORMAT = `1. Follow Angular's commit format: + Types: + - build: Changes to build system + - ci: CI configuration + - docs: Documentation + - feat: New feature + - fix: Bug fix + - perf: Performance + - refactor: Code change + - style: Formatting + - test: Tests + + Rules: + 1. Subject in imperative mood + 2. No period at end + 3. Optional body with details + 4. Breaking changes marked + 5. Only include a single (): line maximum + +RESPONSE FORMAT: +(): (only one per commit) + +* Change detail 1 +* Change detail 2 + +BREAKING CHANGE: (if applicable)`; + +const KERNEL_FORMAT = `1. Follow Linux kernel format: + Rules: + 1. First line must be ": " + 2. Subsystem should be the main component being changed + 3. Description should be clear and concise + 4. Body explains the changes in detail + 5. Wrap all lines at 72 characters + 6. End with Signed-off-by line + +RESPONSE FORMAT: +: + + + + +Signed-off-by: + +IMPORTANT: Replace all placeholders with real values from the diff.`; + +// Add new function to get commit history for analysis +async function getCommitHistory(author?: string, limit = 50): Promise { + const args = ["log", `-${limit}`, "--pretty=format:%s%n%b%n---"]; + if (author) { + args.push(`--author=${author}`); + } + + const command = new Deno.Command("git", { + args: args, + stdout: "piped", + stderr: "piped", + }); + + const output = await command.output(); + if (!output.success) { + throw new Error(`Failed to get commit history: ${new TextDecoder().decode(output.stderr)}`); + } + + return new TextDecoder().decode(output.stdout); +} + +// Add function to analyze commit style +async function analyzeCommitStyle(commits: string, apiKey: string): Promise { + const loadingId = startLoading('Analyzing commit style...'); try { const anthropic = new Anthropic({ @@ -88,14 +188,28 @@ async function getCommitMessage(diff: string, apiKey: string): Promise { }); const msg = await anthropic.messages.create({ - model: "claude-3-sonnet-20240229", - max_tokens: 4096, - temperature: 0.2, - system: COMMIT_PROMPT, + model: "claude-3-haiku-20240307", + max_tokens: 1024, + temperature: 0, messages: [ { role: "user", - content: `Analyze this git diff and create a detailed commit message following the format above:\n\n${diff}` + content: `Extract commit message rules from these commits. Create a minimal style guide that: + +1. Lists only essential rules +2. Uses simple, direct language +3. Includes 2-3 real examples from the commits +4. Omits explanations and formatting +5. Focuses on practical patterns + +Format as plain text with no markdown or bullets. Number each rule. + +EXAMPLES: +${CONVENTIONAL_FORMAT} + +Analyze these commits: + +${commits}` } ], }); @@ -110,6 +224,82 @@ async function getCommitMessage(diff: string, apiKey: string): Promise { } } +// Update getCommitMessage function signature to remove test client +async function getCommitMessage( + diff: string, + apiKey: string, + systemPrompt?: string, +): Promise { + const loadingId = startLoading('Generating commit message...'); + + try { + const anthropic = new Anthropic({ + apiKey: apiKey, + }); + + const msg = await anthropic.messages.create({ + model: "claude-3-haiku-20240307", + max_tokens: 1024, + temperature: 0.2, + system: systemPrompt, + messages: [{ + role: "user", + content: `Generate a commit message for these changes:\n\n${diff}\n\nIMPORTANT: +1. Generate ONLY the commit message +2. Do not include any explanatory text or formatting +3. Do not repeat the header line +4. Follow this exact structure: + - One header line + - One blank line + - Bullet points for changes + - Breaking changes (if any) +5. Never include the diff or any git output` + }], + }); + + const content = msg.content[0]; + if (!('text' in content)) { + throw new Error('Unexpected response format from Claude'); + } + + // Post-process the message to ensure proper formatting + const lines = content.text.split('\n').filter(line => line.trim() !== ''); + const headerLine = lines[0]; + const bodyLines = []; + const breakingChanges = []; + + // Separate body and breaking changes + let isBreakingChange = false; + for (const line of lines.slice(1)) { + if (line.startsWith('BREAKING CHANGE:')) { + isBreakingChange = true; + breakingChanges.push(line); + } else if (isBreakingChange) { + breakingChanges.push(line); + } else { + bodyLines.push(line); + } + } + + // Combine with proper spacing + const parts = [ + headerLine, + '', // Blank line after header + ...bodyLines + ]; + + // Add breaking changes with blank line before them if they exist + if (breakingChanges.length > 0) { + parts.push(''); // Extra blank line before breaking changes + parts.push(...breakingChanges); + } + + return parts.join('\n'); + } finally { + stopLoading(loadingId); + } +} + // Update the editInEditor function async function editInEditor(message: string): Promise { const tempFile = await Deno.makeTempFile({ suffix: '.txt' }); @@ -128,30 +318,6 @@ async function editInEditor(message: string): Promise { return editedMessage.trim(); } -// Update findDefaultEditor function -function findDefaultEditor(): string | null { - const commonEditors = ["vim", "nano", "vi"]; - - for (const editor of commonEditors) { - try { - const command = new Deno.Command("which", { - args: [editor], - stdout: "piped", - stderr: "piped", - }); - const output = command.outputSync(); - - if (output.success) { - return editor; - } - } catch { - continue; - } - } - - return null; -} - // Add function to get staged file names async function getStagedFiles(): Promise { const command = new Deno.Command("git", { @@ -175,24 +341,128 @@ async function getStagedFiles(): Promise { // Update checkStagedChanges function async function checkStagedChanges(): Promise { + // First get list of staged files + const stagedFiles = await getStagedFiles(); + let fullDiff = ''; + + // Get diff for each staged file + for (const file of stagedFiles) { + const command = new Deno.Command("git", { + args: ["diff", "--staged", "--unified=3", "--", file], + stdout: "piped", + stderr: "piped", + }); + + const output = await command.output(); + + if (!output.success) { + const errorMessage = new TextDecoder().decode(output.stderr); + throw new Error(`Failed to get staged changes for ${file}: ${errorMessage}`); + } + + fullDiff += new TextDecoder().decode(output.stdout) + '\n'; + } + + if (!fullDiff) { + throw new Error('No staged changes found'); + } + + return fullDiff; +} + +// Add function to get unique authors with commit counts +async function listAuthors(): Promise { const command = new Deno.Command("git", { - args: ["diff", "--staged", "--unified=3"], + args: [ + "shortlog", + "-sne", // s=summary, n=sorted by count, e=email + "--all", // Include all branches + ], stdout: "piped", stderr: "piped", }); const output = await command.output(); - if (!output.success) { - const errorMessage = new TextDecoder().decode(output.stderr); - throw new Error(`Failed to get staged changes: ${errorMessage}`); + throw new Error(`Failed to get authors: ${new TextDecoder().decode(output.stderr)}`); } - return new TextDecoder().decode(output.stdout); + const authors = new TextDecoder() + .decode(output.stdout) + .trim() + .split("\n") + .map(line => { + const [count, author] = line.trim().split("\t"); + return { count: parseInt(count.trim()), author: author.trim() }; + }); + + // Print formatted table with explicit column widths + console.log("\nRepository Authors:"); + console.log('┌────────┬──────────────────────────────────────────────────────────────┐'); + console.log('│ Commits│ Author │'); + console.log('├────────┼──────────────────────────────────────────────────────────────┤'); + + authors.forEach(({ count, author }) => { + const countStr = count.toString().padStart(6); + console.log(`│ ${countStr} │ ${author.padEnd(60)} │`); // Added space after countStr + }); + + console.log('└────────┴──────────────────────────────────────────────────────────────┘\n'); +} + +// Add new functions to handle style storage +async function storeCommitStyle(style: string, author?: string): Promise { + const configDir = await getConfigDir(); + const fileName = author ? `style-${author.replace(/[^a-zA-Z0-9]/g, '-')}` : 'style-repo'; + const stylePath = join(configDir, fileName); + await Deno.writeTextFile(stylePath, style); } -// CLI tool to read git diff and generate commit message +async function getStoredCommitStyle(author?: string): Promise { + try { + const configDir = await getConfigDir(); + const fileName = author ? `style-${author.replace(/[^a-zA-Z0-9]/g, '-')}` : 'style-repo'; + const stylePath = join(configDir, fileName); + const style = await Deno.readTextFile(stylePath); + return style.trim(); + } catch { + return null; + } +} + +// Add new function to store default format +async function storeDefaultFormat(format: CommitFormat): Promise { + const configDir = await getConfigDir(); + const formatPath = join(configDir, "default-format"); + await Deno.writeTextFile(formatPath, format); +} + +// Add new function to get default format +async function getDefaultFormat(): Promise { + try { + const configDir = await getConfigDir(); + const formatPath = join(configDir, "default-format"); + const format = await Deno.readTextFile(formatPath); + return format as CommitFormat; + } catch { + return null; + } +} + +// Update main function to use stored styles async function main(): Promise { + const flags = parse(Deno.args, { + string: ["author", "style"], + boolean: ["learn", "list-authors", "reset-format"], + default: { learn: false, "list-authors": false }, + }); + + // Handle --list-authors flag + if (flags["list-authors"]) { + await listAuthors(); + return; + } + let apiKey = await getStoredApiKey(); if (!apiKey) { @@ -209,6 +479,102 @@ async function main(): Promise { } } + // Allow resetting the format + if (flags["reset-format"]) { + const configDir = await getConfigDir(); + const formatPath = join(configDir, "default-format"); + try { + await Deno.remove(formatPath); + console.log("Reset commit format to default"); + return; + } catch { + // File doesn't exist, that's fine + } + } + + // Use format flag if provided + let selectedFormat = CommitFormat.CONVENTIONAL; // default + if (typeof flags.format === 'string') { // Type check the flag + const formatInput = flags.format.toLowerCase(); + // Handle common typos and variations + if (formatInput.includes('kern')) { // Change from startsWith to includes + selectedFormat = CommitFormat.KERNEL; + } else if (formatInput.includes('sem')) { + selectedFormat = CommitFormat.SEMANTIC; + } else if (formatInput.includes('ang')) { + selectedFormat = CommitFormat.ANGULAR; + } else if (formatInput.includes('con')) { + selectedFormat = CommitFormat.CONVENTIONAL; + } + } else { + selectedFormat = await getDefaultFormat() || CommitFormat.CONVENTIONAL; + } + + console.log(`Using commit format: ${selectedFormat}`); + + // Set the appropriate format template + let commitPrompt = flags.learn ? CONVENTIONAL_FORMAT : + selectedFormat === CommitFormat.KERNEL ? KERNEL_FORMAT : + selectedFormat === CommitFormat.SEMANTIC ? SEMANTIC_FORMAT : + selectedFormat === CommitFormat.ANGULAR ? ANGULAR_FORMAT : + CONVENTIONAL_FORMAT; + if (flags.learn) { + try { + const commits = await getCommitHistory(flags.author); + const styleGuide = await analyzeCommitStyle(commits, apiKey); + + // Store the learned style + await storeCommitStyle(styleGuide, flags.author); + + commitPrompt = `You are a Git Commit Message Generator that follows the conventions below: + +${styleGuide} + +Generate commit messages directly without explanations or markdown formatting.`; + + console.log("\nLearned and saved commit style from repository history."); + } catch (error) { + console.error("Failed to learn commit style:", error); + console.log("Falling back to default commit style..."); + } + } else { + // Try to load previously learned style + const storedStyle = await getStoredCommitStyle(flags.author); + if (storedStyle) { + commitPrompt = `You are a Git Commit Message Generator that follows the conventions below: + +${storedStyle} + +Generate commit messages directly without explanations or markdown formatting.`; + + console.log("\nUsing previously learned commit style."); + } + } + + if (!selectedFormat) { + // Only show format selection on first use + const formatChoices = { + '1': CommitFormat.CONVENTIONAL, + '2': CommitFormat.SEMANTIC, + '3': CommitFormat.ANGULAR, + '4': CommitFormat.KERNEL + }; + + console.log("\nChoose default commit format:"); + console.log("1. Conventional (recommended)"); + console.log("2. Semantic (with emojis)"); + console.log("3. Angular"); + console.log("4. Linux Kernel"); + + const formatChoice = prompt("Select format (1-4): ") || "1"; + selectedFormat = formatChoices[formatChoice as keyof typeof formatChoices] || CommitFormat.CONVENTIONAL; + + // Store the choice + await storeDefaultFormat(selectedFormat); + console.log(`\nSaved ${selectedFormat} as your default commit format.`); + console.log('You can change this later with --format flag or by deleting ~/.config/auto-commit/default-format'); + } + try { // Get staged files first const stagedFiles = await getStagedFiles(); @@ -224,7 +590,7 @@ async function main(): Promise { console.log("\nStaged files to be committed:"); console.log('┌' + '─'.repeat(72) + '┐'); stagedFiles.forEach(file => { - console.log(`│ ${file.padEnd(70)} │`); + console.log(` ${file.padEnd(70)} │`); }); console.log('└' + '─'.repeat(72) + '┘\n'); @@ -236,61 +602,122 @@ async function main(): Promise { } // Get the diff and generate message - const diff = await checkStagedChanges(); - const commitMessage = await getCommitMessage(diff, apiKey); - - console.log("\nProposed commit:\n"); - console.log('┌' + '─'.repeat(72) + '┐'); - console.log(commitMessage.split('\n').map(line => `│ ${line.padEnd(70)} │`).join('\n')); - console.log('└' + '─'.repeat(72) + '┘\n'); - - const choice = prompt("(a)ccept, (e)dit, (r)eject, (n)ew message? "); - - switch (choice?.toLowerCase()) { - case 'a': - // Implement actual git commit here - const commitCommand = new Deno.Command("git", { - args: ["commit", "-m", commitMessage], - stdout: "piped", - stderr: "piped", - }); - - const commitResult = await commitCommand.output(); - if (!commitResult.success) { - throw new Error(`Failed to commit: ${new TextDecoder().decode(commitResult.stderr)}`); - } - console.log("\n✓ Changes committed!"); - break; - case 'e': - const editedMessage = await editInEditor(commitMessage); - if (editedMessage !== commitMessage) { - console.log("\nEdited commit:\n"); - console.log('┌' + '─'.repeat(72) + '┐'); - console.log(editedMessage.split('\n').map(line => `│ ${line.padEnd(70)} │`).join('\n')); - console.log('└' + '─'.repeat(72) + '┘\n'); + try { + const diff = await checkStagedChanges(); + // Get the appropriate format template + const formatTemplate = selectedFormat === CommitFormat.KERNEL ? KERNEL_FORMAT : + selectedFormat === CommitFormat.SEMANTIC ? SEMANTIC_FORMAT : + selectedFormat === CommitFormat.ANGULAR ? ANGULAR_FORMAT : + CONVENTIONAL_FORMAT; + + // Create the system prompt + const systemPrompt = `You are a Git Commit Message Generator. Generate ONLY a commit message following this commit format: + +${flags.learn ? commitPrompt : formatTemplate} + +IMPORTANT: +1. Base your message ONLY on the actual changes in the diff +2. Do not make assumptions or add fictional features +3. Never include issue numbers unless they appear in the diff +4. Do not include any format templates or placeholders +5. Never output the response format template itself +6. Only include ONE header line ((): ) +7. Never duplicate any lines, especially the header +8. Sort changes by priority and logical groups +9. Never include preamble or explanation +10. Never include the diff or any git-specific output +11. Structure should be: + - Single header line + - Blank line + - Body with bullet points + - Breaking changes (if any)`; + + const commitMessage = await getCommitMessage( + diff, + apiKey, + systemPrompt + ); + + console.log("\nProposed commit:\n"); + console.log('┌' + '─'.repeat(72) + '┐'); + console.log(commitMessage.split('\n').map(line => { + // If line is longer than 70 chars, wrap it + if (line.length > 70) { + const words = line.split(' '); + let currentLine = ''; + const wrappedLines = []; - // Implement git commit with edited message - const editedCommitCommand = new Deno.Command("git", { - args: ["commit", "-m", editedMessage], + words.forEach(word => { + if ((currentLine + ' ' + word).length <= 70) { + currentLine += (currentLine ? ' ' : '') + word; + } else { + wrappedLines.push(`│ ${currentLine.padEnd(70)} │`); + currentLine = word; + } + }); + if (currentLine) { + wrappedLines.push(`│ ${currentLine.padEnd(70)} │`); + } + return wrappedLines.join('\n'); + } + // If line is <= 70 chars, pad it as before + return `│ ${line.padEnd(70)} │`; + }).join('\n')); + console.log('└' + '─'.repeat(72) + '┘\n'); + + const choice = prompt("(a)ccept, (e)dit, (r)eject, (n)ew message? "); + + switch (choice?.toLowerCase()) { + case 'a': { + // Implement actual git commit here + const commitCommand = new Deno.Command("git", { + args: ["commit", "-m", commitMessage], stdout: "piped", stderr: "piped", }); - const editedCommitResult = await editedCommitCommand.output(); - if (!editedCommitResult.success) { - throw new Error(`Failed to commit: ${new TextDecoder().decode(editedCommitResult.stderr)}`); + const commitResult = await commitCommand.output(); + if (!commitResult.success) { + throw new Error(`Failed to commit: ${new TextDecoder().decode(commitResult.stderr)}`); } - console.log("\n✓ Changes committed with edited message!"); + console.log("\n✓ Changes committed!"); + break; } - break; - case 'n': - // Generate a new message with slightly different temperature - return await main(); // Restart the process - case 'r': - console.log("\n✗ Commit message rejected."); - break; - default: - console.log("\n⚠ Invalid selection."); + case 'e': { + const editedMessage = await editInEditor(commitMessage); + if (editedMessage !== commitMessage) { + console.log("\nEdited commit:\n"); + console.log('┌' + '─'.repeat(72) + '┐'); + console.log(editedMessage.split('\n').map(line => `│ ${line.padEnd(70)} │`).join('\n')); + console.log('└' + '─'.repeat(72) + '┘\n'); + + // Implement git commit with edited message + const editedCommitCommand = new Deno.Command("git", { + args: ["commit", "-m", editedMessage], + stdout: "piped", + stderr: "piped", + }); + + const editedCommitResult = await editedCommitCommand.output(); + if (!editedCommitResult.success) { + throw new Error(`Failed to commit: ${new TextDecoder().decode(editedCommitResult.stderr)}`); + } + console.log("\n✓ Changes committed with edited message!"); + } + break; + } + case 'n': + // Generate a new message with slightly different temperature + return await main(); // Restart the process + case 'r': + console.log("\n✗ Commit message rejected."); + break; + default: + console.log("\n⚠ Invalid selection."); + } + } catch (error) { + console.error("Failed to generate commit message:", error); + return; } } catch (error) { if (error instanceof Error) { @@ -327,5 +754,7 @@ async function storeApiKey(apiKey: string): Promise { await Deno.writeTextFile(keyPath, apiKey); } -// Start the CLI tool -main(); +// Run main only when directly executed +if (import.meta.main) { + main(); +} diff --git a/install.ts b/scripts/install.ts similarity index 100% rename from install.ts rename to scripts/install.ts From c472f723590ebb376854e03b07738e771ce7d6ed Mon Sep 17 00:00:00 2001 From: Sid Edwards Date: Thu, 24 Oct 2024 05:07:07 -0600 Subject: [PATCH 2/3] chore(deno.jsonc, deno.lock, main.ts): Update tasks and dependencies - Add 'update' task to pull latest changes and install dependencies - Update dependencies in deno.lock - Improve commit message generator logic BREAKING CHANGE: Removed 'install' task, use 'update' instead --- deno.jsonc | 7 ++++--- deno.lock | 40 ++++++++++++++++++++++++++++++++++++++-- main.ts | 26 +++++++++++++++----------- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index 01a083e..f01bb25 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,13 +1,14 @@ { "tasks": { "start": "deno run --allow-net --allow-read --allow-write --allow-env --allow-run=\"git,vim\" main.ts", - "install": "deno run --allow-read --allow-write --allow-run install.ts", - "build": "deno run --allow-read --allow-write --allow-run scripts/build.ts" + "install": "deno run --allow-read --allow-write --allow-run scripts/install.ts", + "build": "deno run --allow-read --allow-write --allow-run scripts/build.ts", + "update": "git pull && deno task install" }, "name": "auto-commit", "version": "1.0.0", "exports": "./main.ts", - "description": "AI-powered git commit message generator using Claude 3", + "description": "Automatically generate git commit messages.", "author": "Sid Edwards", "license": "MIT" } diff --git a/deno.lock b/deno.lock index 20c6cf5..dd9d24a 100644 --- a/deno.lock +++ b/deno.lock @@ -131,17 +131,52 @@ } }, "redirects": { + "https://deno.land/std/assert/mod.ts": "https://deno.land/std@0.224.0/assert/mod.ts", + "https://deno.land/std/flags/mod.ts": "https://deno.land/std@0.224.0/flags/mod.ts", "https://deno.land/std/fs/ensure_dir.ts": "https://deno.land/std@0.224.0/fs/ensure_dir.ts", "https://deno.land/std/http/server.ts": "https://deno.land/std@0.224.0/http/server.ts", - "https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts" + "https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts", + "https://deno.land/std/testing/asserts.ts": "https://deno.land/std@0.224.0/testing/asserts.ts" }, "remote": { + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", "https://deno.land/std@0.224.0/async/delay.ts": "f90dd685b97c2f142b8069082993e437b1602b8e2561134827eeb7c12b95c499", + "https://deno.land/std@0.224.0/flags/mod.ts": "88553267f34519c8982212185339efdb2d2e62c159ec558f47eb50c8952a6be3", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", "https://deno.land/std@0.224.0/fs/_get_file_info_type.ts": "da7bec18a7661dba360a1db475b826b18977582ce6fc9b25f3d4ee0403fe8cbd", "https://deno.land/std@0.224.0/fs/ensure_dir.ts": "51a6279016c65d2985f8803c848e2888e206d1b510686a509fa7cc34ce59d29f", "https://deno.land/std@0.224.0/http/server.ts": "f9313804bf6467a1704f45f76cb6cd0a3396a3b31c316035e6a4c2035d1ea514", + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e", "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", @@ -218,6 +253,7 @@ "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", - "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c" + "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", + "https://deno.land/std@0.224.0/testing/asserts.ts": "d0cdbabadc49cc4247a50732ee0df1403fdcd0f95360294ad448ae8c240f3f5c" } } diff --git a/main.ts b/main.ts index 29636af..5d95066 100644 --- a/main.ts +++ b/main.ts @@ -616,17 +616,21 @@ Generate commit messages directly without explanations or markdown formatting.`; ${flags.learn ? commitPrompt : formatTemplate} IMPORTANT: -1. Base your message ONLY on the actual changes in the diff -2. Do not make assumptions or add fictional features -3. Never include issue numbers unless they appear in the diff -4. Do not include any format templates or placeholders -5. Never output the response format template itself -6. Only include ONE header line ((): ) -7. Never duplicate any lines, especially the header -8. Sort changes by priority and logical groups -9. Never include preamble or explanation -10. Never include the diff or any git-specific output -11. Structure should be: +1. Base your message on ALL changes in the diff +2. Consider ALL files being modified (${stagedFiles.join(', ')}) +3. Do not focus only on the first file +4. Summarize the overall changes across all files +5. Include significant changes from each modified file +6. Do not make assumptions or add fictional features +7. Never include issue numbers unless they appear in the diff +8. Do not include any format templates or placeholders +9. Never output the response format template itself +10. Only include ONE header line ((): ) +11. Never duplicate any lines, especially the header +12. Sort changes by priority and logical groups +13. Never include preamble or explanation +14. Never include the diff or any git-specific output +15. Structure should be: - Single header line - Blank line - Body with bullet points From 54e1a225bd5dc8b44b8f06a68ccd06306f590d07 Mon Sep 17 00:00:00 2001 From: Sid Edwards Date: Thu, 24 Oct 2024 05:07:28 -0600 Subject: [PATCH 3/3] feat(README): add new features and update installation - Add support for multiple commit formats (Conventional, Angular, Semantic, Linux Kernel) - Introduce repository and author-specific commit styles - Add instructions for updating the tool - Expand usage examples for different commit formats BREAKING CHANGE: Remove basic auth support in favor of OAuth2 --- README.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a4af0a5..72003ff 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # auto-commit -Automatically generate git commit messages using AI. Analyzes your staged changes and creates clear, conventional commit messages. +Automatically generate git commit messages using Claude 3 Haiku. Analyzes your staged changes and creates clear commit messages. Uses the conventional commit format by default, but you can also train it to use a repo or author-specific style. ## Features - Generates clear, concise commit messages from staged changes -- Interactive editing with vim -- Secure API key storage +- Supports multiple commit formats: + - Conventional Commits (default) + - Angular + - Semantic Git Commits (with emojis) + - Linux Kernel style + - Repository or author-specific commit styles - Simple CLI interface ## Installation @@ -45,15 +49,41 @@ cd auto-commit deno task install ```` +### Updating + +```bash +# If installed from source +cd auto-commit +deno task update + +# If using pre-built binary +# Download the latest release and follow installation steps above +``` + ## Usage ```bash # Optional: Set up git alias git config --global alias.ac '!auto-commit' -# Use the tool +# Use the tool with default commit style (conventional) git add auto-commit # or 'git ac' if alias configured + +# Use a specific commit format +auto-commit --format=conventional # default +auto-commit --format=angular # Angular style +auto-commit --format=semantic # with emojis +auto-commit --format=kernel # Linux kernel style + +# View repository authors +auto-commit --list-authors + +# Learn commit style from repository history +auto-commit --learn + +# Learn commit style from specific author +auto-commit --learn --author="user@example.com" ``` Example output: @@ -78,10 +108,59 @@ Proposed commit: On first run, you'll be prompted to enter your [Anthropic API key](https://console.anthropic.com/account/keys). +### Commit Formats + +The tool supports several commit message formats: + +1. **Conventional** (default): `type(scope): description` + ``` + feat(auth): add OAuth2 authentication + ``` + +2. **Angular**: Similar to conventional but with stricter rules + ``` + feat(auth): implement OAuth2 authentication + + * Add login endpoints + * Set up token management + + BREAKING CHANGE: Remove basic auth support + ``` + +3. **Semantic** (with emojis): `emoji description` + ``` + ✨ Add new user authentication system + + - Implement OAuth2 flow + - Add session management + + Closes #123 + ``` + +4. **Linux Kernel**: `subsystem: change summary` + ``` + auth: implement secure token rotation + + Previous implementation had security flaws. + This patch adds automatic rotation with + proper invalidation of old tokens. + + Signed-off-by: John Doe + ``` + +5. **Repository or Author-Specific**: Learn from repository history or specific author's style + ``` + # Learn commit style from repository history + auto-commit --learn + + # Learn commit style from specific author + auto-commit --learn --author="user@example.com" + ``` + ## Requirements - Git -- Vim +- Vim (for editing commit messages) - Anthropic API key ## License