Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ Tired of writing git commit messages? This tool uses AI ✨ to automatically gen
## Features

- **Automatic Commit Summaries:** Analyzes your staged changes and generates ~~AI-slop~~ high-quality commit messages.
- **Interactive Confirmation:** Prompts you to confirm the commit message before committing.
- **Raw Diff View:** Quickly review your staged changes with a colored diff directly within the editor.
- **Contextual Hints:** Provide additional context to the LLM via the `--hint` flag to improve relevance.
- **Colorful Output:** Provides a visually appealing and easy-to-read output in your terminal.
- **Contextual Hints:** Provide additional context to the LLM via the `--hint` flag to improve relevance.
- **Git Hook Integration:** Easily install as a `prepare-commit-msg` hook to automatically generate summaries when you run `git commit`.
- **Interactive Confirmation:** Prompts you to confirm the commit message before committing.
- **Multiple LLM Providers:** Supports Google Gemini, OpenAI, OpenRouter, and local Llama.cpp instances.
- **Raw Diff View:** Quickly review your staged changes with a colored diff directly within the editor.
- **Setup Wizard:** Easy configuration with an interactive setup wizard.

![diff_view](./docs/diff_view.png)
Expand Down Expand Up @@ -104,18 +105,20 @@ LLAMACPP_MODEL="Meta-Llama-3.1-8B-Instruct-Q4_K_M"

## Flags

| Flag | Shorthand | Description |
| :--------------- | :-------- | :-------------------------------------------------------------------- |
| `--all` | `-a` | Add all tracked files to the commit |
| `--help` | `-h` | Show help |
| `--hint` | `-H` | Provide contextual guidance for the commit summary generation |
| `--llm-provider` | | Override the `LLM_PROVIDER` environment variable |
| `--message` | `-m` | Append a message to the commit summary |
| `--no-verify` | | Bypass pre-commit and commit-msg hooks |
| `--setup-wizard` | | Run the interactive setup wizard |
| `--skip-ci` | | Append `[skip ci]` to the first line of the commit message to skip CI |
| `--version` | `-v` | Display version |
| `--yolo` | | Commit immediately without asking for confirmation |
| Flag | Shorthand | Description |
| :----------------- | :-------- | :-------------------------------------------------------------------- |
| `--all` | `-a` | Add all tracked files to the commit |
| `--help` | `-h` | Show help |
| `--hint` | `-H` | Provide contextual guidance for the commit summary generation |
| `--install-hook` | | Install as a `prepare-commit-msg` hook in the current repository |
| `--llm-provider` | | Override the `LLM_PROVIDER` environment variable |
| `--message` | `-m` | Append a message to the commit summary |
| `--no-verify` | | Bypass pre-commit and commit-msg hooks |
| `--setup-wizard` | | Run the interactive setup wizard |
| `--skip-ci` | | Append `[skip ci]` to the first line of the commit message to skip CI |
| `--uninstall-hook` | | Remove the `prepare-commit-msg` hook from the current repository |
| `--version` | `-v` | Display version |
| `--yolo` | | Commit immediately without asking for confirmation |

## Git Alias

Expand Down
4 changes: 1 addition & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ The prompt is currently a simple string. Using the `text/template` package would
- **Use the `text/template` package to parse and execute the template:** This will allow you to use variables and functions in the prompt.

## 2. Improve User Interaction
The `internal.TextArea` function is not very descriptive.

- **Rename `internal.TextArea` to `editCommitMessage`:** This will make the function's purpose more clear.
~~Rename `internal.TextArea` to `editCommitMessage`~~
- **Improve the user interface for editing the commit message:** Consider if a more robust editor integration (like calling `vim` or `nano`) is necessary, or if the current TUI is sufficient.

## 3. ~~Add a `Makefile`~~
Expand Down
53 changes: 40 additions & 13 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"

"os"

tea "charm.land/bubbletea/v2"
"github.com/cockroachdb/errors"
"github.com/rm-hull/git-commit-summary/internal/git"
Expand All @@ -24,11 +26,29 @@ type App struct {
}

type RunOptions struct {
UserMessage string
Hint string
Yolo bool
SkipCI bool
NoVerify bool
CommitMsgFile string
UserMessage string
Hint string
Yolo bool
SkipCI bool
NoVerify bool
}

func(ro *RunOptions) HandleError(err error) {
if err != nil {
if errors.Is(err, interfaces.ErrAborted) {
fmt.Println(ui.BoldRed.Render("ABORTED!"))
exitcode := 0
if ro.CommitMsgFile != "" {
exitcode = 1
}
os.Exit(exitcode)
} else {
prefix := ui.BoldRed.Render("ERROR:")
fmt.Fprintf(os.Stderr, "%s %v\n", prefix, err)
os.Exit(1)
}
}
}

func NewApp(provider llmprovider.Provider, git interfaces.GitClient, prompt string, includeProjectContext bool, recentCommitsCount int) *App {
Expand Down Expand Up @@ -72,14 +92,21 @@ func (app *App) Run(ctx context.Context, opts RunOptions) error {
}

if m.Action() == ui.Commit {
if opts.Yolo {
fmt.Println(ui.Green.Bold(true).Render("COMMIT MESSAGE:"))
fmt.Println(m.CommitMessage())
fmt.Println()
}
err = app.git.Commit(ctx, m.CommitMessage(), opts.SkipCI, opts.NoVerify)
if err != nil {
return err
if opts.CommitMsgFile != "" {
err = os.WriteFile(opts.CommitMsgFile, []byte(m.CommitMessage()), 0644)
if err != nil {
return errors.Wrap(err, "failed to write commit message to file")
}
} else {
if opts.Yolo {
fmt.Println(ui.Green.Bold(true).Render("COMMIT MESSAGE:"))
fmt.Println(m.CommitMessage())
fmt.Println()
}
err = app.git.Commit(ctx, m.CommitMessage(), opts.SkipCI, opts.NoVerify)
if err != nil {
return err
}
}
}

Expand Down
1 change: 1 addition & 0 deletions internal/git/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ func (c *Client) Commit(ctx context.Context, message string, skipCI, noVerify bo
args = append(args, "-F", tmpfile.Name())

cmd := exec.CommandContext(ctx, "git", args...)
cmd.Env = append(os.Environ(), "GIT_COMMIT_SUMMARY_IGNORE_HOOK=1")

// Connect stdout/stderr of git to our program’s stdout/stderr
cmd.Stdout = os.Stdout
Expand Down
73 changes: 73 additions & 0 deletions internal/setup/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package setup

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/rm-hull/git-commit-summary/internal/ui"
)
Comment thread
rm-hull marked this conversation as resolved.

func InstallHook() error {
gitDir, err := exec.Command("git", "rev-parse", "--git-dir").Output()
if err != nil {
return fmt.Errorf("not a git repository: %w", err)
}

gitDirStr := strings.TrimSpace(string(gitDir))
hookPath := filepath.Join(gitDirStr, "hooks", "prepare-commit-msg")

if _, err := os.Stat(hookPath); err == nil {
existingContent, err := os.ReadFile(hookPath)
if err == nil && !strings.Contains(string(existingContent), "git-commit-summary") {
return fmt.Errorf("a prepare-commit-msg hook already exists at %s; please back up and remove it before installing", hookPath)
}
}

err = os.MkdirAll(filepath.Dir(hookPath), 0755)
if err != nil {
return fmt.Errorf("failed to create hooks directory: %w", err)
}

content := "#!/bin/sh\nCMD=$(which git-commit-summary)\nif [ -n \"$CMD\" ]; then\n \"$CMD\" --yolo \"$1\"\nfi\n"
err = os.WriteFile(hookPath, []byte(content), 0755)
Comment thread
rm-hull marked this conversation as resolved.
if err != nil {
return fmt.Errorf("failed to write hook file: %w", err)
}

fmt.Println(ui.Green.Bold(true).Render("Git hook installed successfully!"))
return nil
}

func UninstallHook() error {
gitDir, err := exec.Command("git", "rev-parse", "--git-dir").Output()
if err != nil {
return fmt.Errorf("not a git repository: %w", err)
}

gitDirStr := strings.TrimSpace(string(gitDir))
hookPath := filepath.Join(gitDirStr, "hooks", "prepare-commit-msg")
if _, err := os.Stat(hookPath); os.IsNotExist(err) {
fmt.Println(ui.BoldYellow.Render("Hook not found, nothing to uninstall."))
return nil
}

existingContent, err := os.ReadFile(hookPath)
if err != nil {
return fmt.Errorf("failed to read hook file: %w", err)
}
if !strings.Contains(string(existingContent), "git-commit-summary") {
fmt.Println(ui.BoldYellow.Render("Hook was not installed by git-commit-summary, skipping uninstall."))
return nil
}

err = os.Remove(hookPath)
if err != nil {
return fmt.Errorf("failed to remove hook file: %w", err)
}

fmt.Println(ui.Green.Bold(true).Render("Git hook uninstalled successfully!"))
return nil
}
64 changes: 38 additions & 26 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,39 @@ import (
)

func main() {
if os.Getenv("GIT_COMMIT_SUMMARY_IGNORE_HOOK") == "1" {
os.Exit(0)
}

runOpts := app.RunOptions{}

cfg, err := config.Load()
handleError(err)
runOpts.HandleError(err)

var llmProvider string
var runSetupWizard *bool
var showVersion *bool
var addAll *bool

runOptions := app.RunOptions{}
var installHook *bool
var uninstallHook *bool

rootCmd := &cobra.Command{
Use: "git-commit-summary",
Short: fmt.Sprintf("Generate a commit summary using Gemini, OpenAI, Llama.cpp, OpenRouter (version: %s)", versioninfo.Short()),
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
runOpts.CommitMsgFile = args[0]
}
if *installHook {
Comment thread
rm-hull marked this conversation as resolved.
err := setup.InstallHook()
runOpts.HandleError(err)
os.Exit(0)
}
if *uninstallHook {
err := setup.UninstallHook()
runOpts.HandleError(err)
os.Exit(0)
}
if *showVersion {
fmt.Println(versioninfo.Short())
os.Exit(0)
Expand All @@ -44,10 +63,10 @@ func main() {
if err != nil || cfg.IsTestMode() || *runSetupWizard {
newCfg, err := setup.Run(cfg)
if err != nil {
handleError(errors.Wrap(err, "failed to run setup wizard"))
runOpts.HandleError(errors.Wrap(err, "failed to run setup wizard"))
}
if err := newCfg.Save(); err != nil {
handleError(errors.Wrap(err, "failed to save new configuration"))
runOpts.HandleError(errors.Wrap(err, "failed to save new configuration"))
}
cfg = newCfg
}
Expand All @@ -60,36 +79,29 @@ func main() {
ctx := context.Background()

provider, err := llmprovider.NewProvider(ctx, cfg)
handleError(err)
if errors.Is(err, interfaces.ErrAborted) && runOpts.CommitMsgFile != "" {
fmt.Println(ui.BoldRed.Render("ABORTED!"))
os.Exit(1)
}
runOpts.HandleError(err)

application := app.NewApp(provider, git.NewClient(*addAll), cfg.Prompt, cfg.IncludeProjectContext, cfg.RecentCommitsCount)
err = application.Run(ctx, runOptions)
handleError(err)
err = application.Run(ctx, runOpts)
runOpts.HandleError(err)
},
}

showVersion = rootCmd.PersistentFlags().BoolP("version", "v", false, "Display version information")
runSetupWizard = rootCmd.PersistentFlags().Bool("setup-wizard", false, "Run setup wizard")
addAll = rootCmd.PersistentFlags().BoolP("all", "a", false, "Add all tracked files to the commit")
rootCmd.PersistentFlags().BoolVar(&runOptions.Yolo, "yolo", false, "Commit immediately without asking for confirmation")
rootCmd.PersistentFlags().BoolVar(&runOptions.SkipCI, "skip-ci", false, "Append [skip ci] to the commit message")
rootCmd.PersistentFlags().BoolVar(&runOptions.NoVerify, "no-verify", false, "Bypass pre-commit and commit-msg hooks")
rootCmd.PersistentFlags().StringVarP(&runOptions.UserMessage, "message", "m", "", "Append a message to the commit summary")
rootCmd.PersistentFlags().StringVarP(&runOptions.Hint, "hint", "H", "", "Provide contextual guidance for the commit summary generation")
rootCmd.PersistentFlags().BoolVar(&runOpts.Yolo, "yolo", false, "Commit immediately without asking for confirmation")
rootCmd.PersistentFlags().BoolVar(&runOpts.SkipCI, "skip-ci", false, "Append [skip ci] to the commit message")
rootCmd.PersistentFlags().BoolVar(&runOpts.NoVerify, "no-verify", false, "Bypass pre-commit and commit-msg hooks")
rootCmd.PersistentFlags().StringVarP(&runOpts.UserMessage, "message", "m", "", "Append a message to the commit summary")
rootCmd.PersistentFlags().StringVarP(&runOpts.Hint, "hint", "H", "", "Provide contextual guidance for the commit summary generation")
rootCmd.PersistentFlags().StringVar(&llmProvider, "llm-provider", cfg.LLMProvider, "Use specific LLM provider, overrides environment variable LLM_PROVIDER")
installHook = rootCmd.PersistentFlags().Bool("install-hook", false, "Install git-commit-summary as a prepare-commit-msg hook")
uninstallHook = rootCmd.PersistentFlags().Bool("uninstall-hook", false, "Uninstall git-commit-summary as a prepare-commit-msg hook")

_ = rootCmd.Execute()
}

func handleError(err error) {
if err != nil {
if errors.Is(err, interfaces.ErrAborted) {
fmt.Println(ui.BoldRed.Render("ABORTED!"))
os.Exit(0)
} else {
prefix := ui.BoldRed.Render("ERROR:")
fmt.Fprintf(os.Stderr, "%s %v\n", prefix, err)
os.Exit(1)
}
}
}
Loading