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
216 changes: 193 additions & 23 deletions cmd/propose.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package cmd

import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"

"github.com/fatih/color"
"github.com/spf13/cobra"
Expand All @@ -17,16 +19,29 @@ import (
)

var (
stagedFlag bool
summaryFlag bool
autoFlag bool
dryRunFlag bool
debugFlag bool
stagedFlag bool
summaryFlag bool
autoFlag bool
dryRunFlag bool
debugFlag bool
contextFlag bool
maxSuggestions int

proposeCmd = &cobra.Command{
Use: "propose",
Short: "Propose a commit message from a git diff",
RunE: runPropose,
Short: "Propose commit messages from git diff",
Long: `Analyze staged changes and suggest commit messages based on the context.

When using --interactive (-i) or --suggestions (-s), multiple suggestions will be shown
ranked by how well they match the context (file types, changes, purposes).

The --context flag shows what was analyzed to help understand the suggestions.`,
Example: ` gitmit propose # Get best suggestion
gitmit propose -i # Choose from multiple suggestions
gitmit propose -s # Show ranked suggestions
gitmit propose --context # Show what was analyzed
gitmit propose --auto # Auto-commit with best suggestion`,
RunE: runPropose,
}
)

Expand All @@ -35,9 +50,11 @@ func init() {

proposeCmd.Flags().BoolVar(&stagedFlag, "staged", true, "Only parse staged files (default: true)")
proposeCmd.Flags().BoolVar(&summaryFlag, "summary", false, "Print short output (summary only)")
proposeCmd.Flags().BoolVar(&autoFlag, "auto", false, "Commit with the generated message")
proposeCmd.Flags().BoolVar(&autoFlag, "auto", false, "Auto-commit with the generated message")
proposeCmd.Flags().BoolVar(&dryRunFlag, "dry-run", false, "Preview without committing")
proposeCmd.Flags().BoolVar(&debugFlag, "debug", false, "Print debug info (analyzer output + chosen templates)")
proposeCmd.Flags().BoolVar(&contextFlag, "context", false, "Show what was analyzed to generate suggestions")
proposeCmd.Flags().IntVar(&maxSuggestions, "max-suggestions", 5, "Maximum number of suggestions to show")
}

func runPropose(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -72,13 +89,33 @@ func runPropose(cmd *cobra.Command, args []string) error {
return err
}

// Show analysis context if requested
if contextFlag || debugFlag {
color.Blue("\n📊 Analysis Context:")
fmt.Printf("Action: %s\n", commitMessage.Action)
fmt.Printf("Topic: %s\n", commitMessage.Topic)
if commitMessage.Item != "" {
fmt.Printf("Item: %s\n", commitMessage.Item)
}
if commitMessage.Purpose != "" {
fmt.Printf("Purpose: %s\n", commitMessage.Purpose)
}
if commitMessage.Scope != "" {
fmt.Printf("Scope: %s\n", commitMessage.Scope)
}
fmt.Printf("Files: +%d -%d\n", commitMessage.TotalAdded, commitMessage.TotalRemoved)
if len(commitMessage.FileExtensions) > 0 {
fmt.Printf("Types: %v\n", commitMessage.FileExtensions)
}
fmt.Println()
}

if debugFlag {
// Print analyzer output
fmt.Printf("Analyzer result: %+v\n", commitMessage)
// Print available templates/action/topic info from templater
// Print more detailed debug info
fmt.Printf("Full analyzer output: %+v\n", commitMessage)
if act, tpls := templater.DebugInfo(commitMessage); tpls != nil {
fmt.Printf("Resolved action key: %s\n", act)
fmt.Printf("Candidate templates (first 10):\n")
fmt.Printf("Template group: %s\n", act)
fmt.Printf("Candidate templates:\n")
for i, t := range tpls {
if i >= 10 {
break
Expand All @@ -88,21 +125,154 @@ func runPropose(cmd *cobra.Command, args []string) error {
}
}

initialMessage, err := templater.GetMessage(commitMessage)
if err != nil {
return err
// Get multiple suggestions if interactive/suggestions mode
var suggestions []string
if interactiveFlag || suggestionsFlag {
suggestions, err = templater.GetSuggestions(commitMessage, maxSuggestions)
if err != nil {
return err
}
} else {
// Just get best message
msg, err := templater.GetMessage(commitMessage)
if err != nil {
return err
}
suggestions = []string{msg}
}

formatter := formatter.NewFormatter()
finalMessage := formatter.FormatMessage(initialMessage, commitMessage.IsMajor)

if summaryFlag {
fmt.Println(finalMessage)
if len(suggestions) == 0 {
return fmt.Errorf("no suitable commit messages found")
}

// Format all suggestions
formattedSuggestions := make([]string, len(suggestions))
for i, msg := range suggestions {
formattedSuggestions[i] = formatter.FormatMessage(msg, commitMessage.IsMajor)
}

// Default to first/best suggestion
finalMessage := formattedSuggestions[0]

if suggestionsFlag {
// Show all suggestions with ranking
color.Blue("\n💡 Ranked Suggestions:")
for i, msg := range formattedSuggestions {
if i == 0 {
color.Green("1. %s (recommended)\n", msg)
} else {
fmt.Printf("%d. %s\n", i+1, msg)
}
}
fmt.Println()
}

if interactiveFlag && len(formattedSuggestions) > 1 {
// TODO: Add interactive selection using a proper terminal UI library
// For now, just show numbered options and read input
color.Blue("\n📝 Choose a commit message:")
for i, msg := range formattedSuggestions {
fmt.Printf("%d. %s\n", i+1, msg)
}
fmt.Printf("\nEnter number (1-%d) [1]: ", len(formattedSuggestions))

var choice string
fmt.Scanln(&choice)

if choice != "" {
var num int
if _, err := fmt.Sscanf(choice, "%d", &num); err == nil && num > 0 && num <= len(formattedSuggestions) {
finalMessage = formattedSuggestions[num-1]
}
}
fmt.Println()

}

// If not in summary mode, show the suggestion and prompt for action
if !summaryFlag {
color.Green("\n💡 Suggested commit message:")
fmt.Printf("%s\n\n", finalMessage)

if !autoFlag && !dryRunFlag {
for {
color.Blue("What would you like to do?")
fmt.Println("y - Accept and commit")
fmt.Println("n - Reject and exit")
fmt.Println("e - Edit message")
fmt.Println("c - Create new message")
fmt.Printf("\nChoice [y/n/e/c]: ")

var choice string
fmt.Scanln(&choice)
fmt.Println()

switch strings.ToLower(choice) {
case "y":
// Commit the message
commitCmd := exec.Command("git", "commit", "-m", finalMessage)
commitCmd.Stdout = os.Stdout
commitCmd.Stderr = os.Stderr
err := commitCmd.Run()
if err != nil {
return fmt.Errorf("error committing changes: %w", err)
}
color.Green("✅ Changes committed successfully.")
history.AddEntry(finalMessage, "") // Save to history
if err := history.SaveHistory(); err != nil {
return err
}
return nil

case "n":
color.Yellow("❌ Commit cancelled.")
return nil

case "e":
color.Blue("📝 Edit the commit message (press Enter when done):")
fmt.Printf("%s", finalMessage)

var editedMessage string
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
editedMessage = scanner.Text()
}

if editedMessage != "" {
finalMessage = editedMessage
// Show the edited message and prompt again
color.Green("\nUpdated commit message:")
fmt.Printf("%s\n\n", finalMessage)
continue
}

case "c":
color.Blue("📝 Enter your commit message:")
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
finalMessage = scanner.Text()
}

if finalMessage != "" {
// Show the new message and prompt again
color.Green("\nNew commit message:")
fmt.Printf("%s\n\n", finalMessage)
continue
}

default:
color.Yellow("Invalid choice. Please try again.")
continue
}
}
}
} else {
color.Green(finalMessage)
fmt.Println("\nCopy the message above and use it to commit.")
fmt.Println(finalMessage)
}

// Handle auto-commit and dry-run cases
if autoFlag && !dryRunFlag {
commitCmd := exec.Command("git", "commit", "-m", finalMessage)
commitCmd.Stdout = os.Stdout
Expand All @@ -111,8 +281,8 @@ func runPropose(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("error committing changes: %w", err)
}
fmt.Println("Changes committed successfully.")
history.AddEntry(finalMessage, initialMessage) // Pass actual template used
color.Green("✅ Changes committed successfully.")
history.AddEntry(finalMessage, "") // Save to history
if err := history.SaveHistory(); err != nil {
return err
}
Expand Down
30 changes: 29 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
package cmd

import (
"os"

"github.com/spf13/cobra"
)

var (
version = "0.1.0"
// Global flags
interactiveFlag bool
suggestionsFlag bool

rootCmd = &cobra.Command{
Use: "gitmit",
Short: "🧠 Smart Git Commit Message Generator",
Long: `Gitmit is a lightweight CLI tool that analyzes your staged changes
and suggests professional commit messages following Conventional Commits format.`,
and suggests professional commit messages following Conventional Commits format.

Examples:
gitmit # Analyze changes and suggest commit message
gitmit propose # Same as above
gitmit propose -i # Interactive mode with multiple suggestions
gitmit propose -s # Show multiple suggestions
gitmit propose --auto # Auto-commit with best suggestion`,
Version: version,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Add global validation or setup here
if suggestionsFlag {
interactiveFlag = true // -s implies -i
}
},
}
)

func init() {
// Global flags available to all commands
rootCmd.PersistentFlags().BoolVarP(&interactiveFlag, "interactive", "i", false, "Interactive mode with multiple suggestions")
rootCmd.PersistentFlags().BoolVarP(&suggestionsFlag, "suggestions", "s", false, "Show multiple ranked suggestions")
}

func Execute() error {

Check failure on line 43 in cmd/root.go

View workflow job for this annotation

GitHub Actions / Build and Test

exported function Execute should have comment or be unexported
// ✅ Added: if no subcommand provided, fallback to "propose"
if len(os.Args) == 1 {
return proposeCmd.RunE(rootCmd, nil)
}
return rootCmd.Execute()
}
Loading
Loading