Hypercode/alex/hypercodePublic

Code

  1. hypercode
  2. commands
  3. commit.go
commit.go312 lines
package commands

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

	"github.com/charmbracelet/huh"
	"github.com/urfave/cli/v3"
)

// CommitCommand creates the commit command for creating conventional commits
func CommitCommand() *cli.Command {
	return NewCommandWithFlags(
		"commit",
		"Create a conventional commit",
		`Interactive tool to help you create conventional commits.

Conventional Commits format: (): 

Types:
  feat     - A new feature
  fix      - A bug fix
  docs     - Documentation only changes
  style    - Changes that don't affect code meaning (formatting, etc)
  refactor - Code change that neither fixes a bug nor adds a feature
  perf     - Performance improvements
  test     - Adding or updating tests
  build    - Changes to build system or dependencies
  ci       - Changes to CI configuration
  chore    - Other changes that don't modify src or test files
  revert   - Reverts a previous commit`,
		[]cli.Flag{
			&cli.BoolFlag{
				Name:    "all",
				Aliases: []string{"a"},
				Usage:   "Stage all changes before committing (git add -A)",
			},
			&cli.BoolFlag{
				Name:    "push",
				Aliases: []string{"p"},
				Usage:   "Push after committing",
			},
		},
		runCommit,
	)
}

func runCommit(ctx context.Context, cmd *cli.Command) error {
	// Check if we're in a git repository
	if !isGitRepo() {
		return fmt.Errorf("not a git repository")
	}

	// Stage all changes if -a flag is set
	if cmd.Bool("all") {
		if err := gitAddAll(); err != nil {
			return fmt.Errorf("failed to stage changes: %w", err)
		}
		fmt.Println("✓ Staged all changes")
	}

	// Check if there are staged changes
	hasChanges, err := hasStagedChanges()
	if err != nil {
		return fmt.Errorf("failed to check for staged changes: %w", err)
	}
	if !hasChanges {
		return fmt.Errorf("no changes staged for commit. Use 'git add' or the -a flag")
	}

	// Collect commit information via interactive form
	var (
		commitType        string
		scope             string
		description       string
		body              string
		breaking          bool
		breakingDesc      string
		includeBodyPrompt bool
	)

	// Type selection
	typeForm := huh.NewForm(
		huh.NewGroup(
			huh.NewSelect[string]().
				Title("Select commit type").
				Options(
					huh.NewOption("feat - A new feature", "feat"),
					huh.NewOption("fix - A bug fix", "fix"),
					huh.NewOption("docs - Documentation changes", "docs"),
					huh.NewOption("style - Code style changes (formatting)", "style"),
					huh.NewOption("refactor - Code refactoring", "refactor"),
					huh.NewOption("perf - Performance improvements", "perf"),
					huh.NewOption("test - Add or update tests", "test"),
					huh.NewOption("build - Build system or dependencies", "build"),
					huh.NewOption("ci - CI configuration changes", "ci"),
					huh.NewOption("chore - Other changes", "chore"),
					huh.NewOption("revert - Revert a previous commit", "revert"),
				).
				Value(&commitType),
		),
	)

	if err := typeForm.Run(); err != nil {
		return fmt.Errorf("form cancelled: %w", err)
	}

	// Scope and description
	mainForm := huh.NewForm(
		huh.NewGroup(
			huh.NewInput().
				Title("Scope (optional)").
				Description("Component or module affected (e.g., api, ui, auth)").
				Placeholder("Leave empty if not applicable").
				Value(&scope),

			huh.NewInput().
				Title("Short description").
				Description("A brief description of the change").
				Placeholder("e.g., add user authentication").
				CharLimit(72).
				Validate(func(s string) error {
					if strings.TrimSpace(s) == "" {
						return fmt.Errorf("description is required")
					}
					return nil
				}).
				Value(&description),

			huh.NewConfirm().
				Title("Add detailed description?").
				Description("Include a longer description of the changes").
				Value(&includeBodyPrompt),
		),
	)

	if err := mainForm.Run(); err != nil {
		return fmt.Errorf("form cancelled: %w", err)
	}

	// Body (if requested)
	if includeBodyPrompt {
		bodyForm := huh.NewForm(
			huh.NewGroup(
				huh.NewText().
					Title("Detailed description").
					Description("Explain what and why (press ESC then Enter when done)").
					Placeholder("Describe the changes in detail...").
					CharLimit(500).
					Value(&body),
			),
		)

		if err := bodyForm.Run(); err != nil {
			return fmt.Errorf("form cancelled: %w", err)
		}
	}

	// Breaking changes
	breakingForm := huh.NewForm(
		huh.NewGroup(
			huh.NewConfirm().
				Title("Does this introduce breaking changes?").
				Description("Changes that require updates to dependent code").
				Value(&breaking),
		),
	)

	if err := breakingForm.Run(); err != nil {
		return fmt.Errorf("form cancelled: %w", err)
	}

	if breaking {
		breakingDescForm := huh.NewForm(
			huh.NewGroup(
				huh.NewText().
					Title("Describe the breaking changes").
					Description("Explain what breaks and how to migrate").
					Placeholder("BREAKING CHANGE: ...").
					CharLimit(500).
					Validate(func(s string) error {
						if strings.TrimSpace(s) == "" {
							return fmt.Errorf("breaking change description is required")
						}
						return nil
					}).
					Value(&breakingDesc),
			),
		)

		if err := breakingDescForm.Run(); err != nil {
			return fmt.Errorf("form cancelled: %w", err)
		}
	}

	// Build the commit message
	commitMsg := buildCommitMessage(commitType, scope, description, body, breaking, breakingDesc)

	// Show preview and confirm
	fmt.Println("\n" + strings.Repeat("─", 50))
	fmt.Println("Commit message preview:")
	fmt.Println(strings.Repeat("─", 50))
	fmt.Println(commitMsg)
	fmt.Println(strings.Repeat("─", 50))

	var confirm bool
	confirmForm := huh.NewForm(
		huh.NewGroup(
			huh.NewConfirm().
				Title("Create this commit?").
				Affirmative("Yes").
				Negative("No").
				Value(&confirm),
		),
	)

	if err := confirmForm.Run(); err != nil {
		return fmt.Errorf("form cancelled: %w", err)
	}

	if !confirm {
		fmt.Println("Commit cancelled")
		return nil
	}

	// Create the commit
	if err := gitCommit(commitMsg); err != nil {
		return fmt.Errorf("failed to create commit: %w", err)
	}

	fmt.Println("✓ Commit created successfully")

	// Push if -p flag is set
	if cmd.Bool("push") {
		fmt.Println("Pushing to remote...")
		if err := gitPush(); err != nil {
			return fmt.Errorf("failed to push: %w", err)
		}
		fmt.Println("✓ Pushed successfully")
	}

	return nil
}

func buildCommitMessage(commitType, scope, description, body string, breaking bool, breakingDesc string) string {
	var msg strings.Builder

	// Header
	if scope != "" {
		scope = strings.TrimSpace(scope)
		msg.WriteString(fmt.Sprintf("%s(%s): %s", commitType, scope, description))
	} else {
		msg.WriteString(fmt.Sprintf("%s: %s", commitType, description))
	}

	// Body
	if body != "" {
		msg.WriteString("\n\n")
		msg.WriteString(strings.TrimSpace(body))
	}

	// Breaking changes footer
	if breaking && breakingDesc != "" {
		msg.WriteString("\n\n")
		msg.WriteString("BREAKING CHANGE: ")
		msg.WriteString(strings.TrimSpace(breakingDesc))
	}

	return msg.String()
}

func isGitRepo() bool {
	cmd := exec.Command("git", "rev-parse", "--git-dir")
	return cmd.Run() == nil
}

func hasStagedChanges() (bool, error) {
	cmd := exec.Command("git", "diff", "--cached", "--quiet")
	err := cmd.Run()
	if err == nil {
		return false, nil // No changes
	}
	if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
		return true, nil // Has changes
	}
	return false, err
}

func gitAddAll() error {
	cmd := exec.Command("git", "add", "-A")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func gitCommit(message string) error {
	cmd := exec.Command("git", "commit", "-m", message)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func gitPush() error {
	cmd := exec.Command("git", "push")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}