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
8 changes: 4 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"os"
"strings"

"github.com/fatih/color"
"github.com/thomas-vilte/matecommit/internal/ai"
"github.com/thomas-vilte/matecommit/internal/commands/cache"
"github.com/thomas-vilte/matecommit/internal/commands/completion"
"github.com/thomas-vilte/matecommit/internal/commands/config"
Expand All @@ -18,15 +20,13 @@ import (
"github.com/thomas-vilte/matecommit/internal/commands/suggests_commits"
"github.com/thomas-vilte/matecommit/internal/commands/update"
cfg "github.com/thomas-vilte/matecommit/internal/config"
"github.com/thomas-vilte/matecommit/internal/ports"
"github.com/thomas-vilte/matecommit/internal/i18n"
"github.com/thomas-vilte/matecommit/internal/ai"
"github.com/thomas-vilte/matecommit/internal/git"
"github.com/thomas-vilte/matecommit/internal/i18n"
"github.com/thomas-vilte/matecommit/internal/ports"
"github.com/thomas-vilte/matecommit/internal/providers"
"github.com/thomas-vilte/matecommit/internal/services"
"github.com/thomas-vilte/matecommit/internal/ui"
"github.com/thomas-vilte/matecommit/internal/version"
"github.com/fatih/color"
"github.com/urfave/cli/v3"
)

Expand Down
14 changes: 7 additions & 7 deletions internal/ai/cost_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"fmt"
"time"

"github.com/thomas-vilte/matecommit/internal/cache"
"github.com/thomas-vilte/matecommit/internal/errors"
"github.com/thomas-vilte/matecommit/internal/models"
"github.com/thomas-vilte/matecommit/internal/ports"
"github.com/thomas-vilte/matecommit/internal/cache"
cost2 "github.com/thomas-vilte/matecommit/internal/services/cost"
"github.com/thomas-vilte/matecommit/internal/services/cost"
"github.com/thomas-vilte/matecommit/internal/services/routing"
)

Expand All @@ -29,8 +29,8 @@ type ConfirmationResult struct {

type CostAwareWrapper struct {
provider ports.CostAwareAIProvider
calculator *cost2.Calculator
manager *cost2.Manager
calculator *cost.Calculator
manager *cost.Manager
cache *cache.Cache
modelSelector *routing.ModelSelector
estimatedOutputTokens int
Expand All @@ -48,7 +48,7 @@ type WrapperConfig struct {

// NewCostAwareWrapper creates a provider-agnostic wrapper
func NewCostAwareWrapper(cfg WrapperConfig) (*CostAwareWrapper, error) {
manager, err := cost2.NewManager(cfg.BudgetDaily)
manager, err := cost.NewManager(cfg.BudgetDaily)
if err != nil {
return nil, fmt.Errorf("error creating cost manager: %w", err)
}
Expand All @@ -60,7 +60,7 @@ func NewCostAwareWrapper(cfg WrapperConfig) (*CostAwareWrapper, error) {

return &CostAwareWrapper{
provider: cfg.Provider,
calculator: cost2.NewCalculator(),
calculator: cost.NewCalculator(),
manager: manager,
cache: cacheService,
modelSelector: routing.NewModelSelector(),
Expand Down Expand Up @@ -159,7 +159,7 @@ func (w *CostAwareWrapper) WrapGenerate(
usage.DurationMs = time.Since(startTime).Milliseconds()
usage.CacheHit = false

_ = w.manager.SaveActivity(cost2.ActivityRecord{
_ = w.manager.SaveActivity(cost.ActivityRecord{
Timestamp: time.Now(),
Command: command,
Provider: providerName,
Expand Down
10 changes: 7 additions & 3 deletions internal/ai/gemini/commit_summarizer_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (
"regexp"
"strings"

"github.com/thomas-vilte/matecommit/internal/ai"
"github.com/thomas-vilte/matecommit/internal/config"
domainErrors "github.com/thomas-vilte/matecommit/internal/errors"
"github.com/thomas-vilte/matecommit/internal/models"
"github.com/thomas-vilte/matecommit/internal/ports"
"github.com/thomas-vilte/matecommit/internal/ai"
"google.golang.org/genai"
)

Expand Down Expand Up @@ -274,16 +274,20 @@ func formatCriteria(criteria []string) string {
return strings.Join(formattedCriteria, "\n")
}

// formatResponse formats the Gemini API response into a string.
// formatResponse formats the Gemini API response into a string, filtering out thinking parts.
func formatResponse(resp *genai.GenerateContentResponse) string {
if resp == nil || len(resp.Candidates) == 0 {
return ""
}

var formattedContent strings.Builder
for _, cand := range resp.Candidates {
if cand.Content != nil {
for _, part := range cand.Content.Parts {
// Skip thinking parts - only get actual content
if part.Thought {
// This is a thinking part, skip it
continue
}
if part.Text != "" {
formattedContent.WriteString(part.Text)
}
Expand Down
39 changes: 39 additions & 0 deletions internal/commands/config/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func (d *DoctorCommand) runHealthCheck(ctx context.Context, t *i18n.Translations
{name: "doctor.check_config_file", fn: d.checkConfigFile},
{name: "doctor.check_git_repo", fn: d.checkGitRepo},
{name: "doctor.check_git_installed", fn: d.checkGitInstalled},
{name: "doctor.check_git_user_name", fn: d.checkGitUserName},
{name: "doctor.check_git_user_email", fn: d.checkGitUserEmail},
{name: "doctor.check_gemini_key", fn: func(ctx context.Context, t *i18n.Translations, cfg *config.Config) checkResult {
return d.checkGeminiAPIKey(ctx, t, cfg)
}},
Expand Down Expand Up @@ -289,3 +291,40 @@ func (d *DoctorCommand) printCommandStatus(command string, available bool, t *i1

fmt.Printf(" %s matecommit %-15s %s\n", status, command, statusMsg)
}

func (d *DoctorCommand) checkGitUserName(ctx context.Context, t *i18n.Translations, _ *config.Config) checkResult {
cmd := exec.CommandContext(ctx, "git", "config", "user.name")
output, err := cmd.Output()
userName := strings.TrimSpace(string(output))

if err != nil || userName == "" {
return checkResult{
status: checkStatusError,
message: t.GetMessage("doctor.git_user_not_set", 0, nil),
suggestion: "git config --global user.name \"Your name\"",
}
}

return checkResult{
status: checkStatusOK,
message: fmt.Sprintf("(%s)", userName),
}
}
func (d *DoctorCommand) checkGitUserEmail(ctx context.Context, t *i18n.Translations, _ *config.Config) checkResult {
cmd := exec.CommandContext(ctx, "git", "config", "user.email")
output, err := cmd.Output()
userEmail := strings.TrimSpace(string(output))

if err != nil || userEmail == "" {
return checkResult{
status: checkStatusError,
message: t.GetMessage("doctor.git_email_not_set", 0, nil),
suggestion: "git config --global user.email \"your@email.com\"",
}
}

return checkResult{
status: checkStatusOK,
message: fmt.Sprintf("(%s)", userEmail),
}
}
7 changes: 7 additions & 0 deletions internal/commands/suggests_commits/suggests_commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/thomas-vilte/matecommit/internal/commands/completion_helper"
"github.com/thomas-vilte/matecommit/internal/config"
"github.com/thomas-vilte/matecommit/internal/git"
"github.com/thomas-vilte/matecommit/internal/i18n"
"github.com/thomas-vilte/matecommit/internal/models"
"github.com/thomas-vilte/matecommit/internal/ui"
Expand Down Expand Up @@ -108,6 +109,12 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla

ui.PrintSectionBanner(t.GetMessage("ui.generating_suggestions_banner", 0, nil))

gitSvc := git.NewGitService()
if err := gitSvc.ValidateGitConfig(ctx); err != nil {
ui.HandleAppError(err, t)
return err
}

spinner := ui.NewSmartSpinner(t.GetMessage("analyzing_changes", 0, nil))
spinner.Start()

Expand Down
21 changes: 20 additions & 1 deletion internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ var (
ErrNoDiff = NewAppError(TypeGit, "no differences detected", nil)
)

// Git configuration errors
var (
ErrGitUserNotConfigured = NewAppError(TypeGit, "git user.name not configured", nil)
ErrGitEmailNotConfigured = NewAppError(TypeGit, "git user.email not configured", nil)
ErrNotInGitRepo = NewAppError(TypeGit, "not in a git repository", nil)
)

// Configuration errors
var (
ErrAPIKeyMissing = NewAppError(TypeConfiguration, "AI API key is missing", nil)
Expand All @@ -76,13 +83,26 @@ var (
ErrUploadAsset = NewAppError(TypeVCS, "failed to upload release asset", nil)
)

// GitHub/VCS specific errors
var (
ErrGitHubTokenInvalid = NewAppError(TypeVCS, "GitHub token is invalid or expired", nil)
ErrGitHubInsufficientPerms = NewAppError(TypeVCS, "GitHub token has insufficient permissions", nil)
ErrGitHubRateLimit = NewAppError(TypeVCS, "GitHub API rate limit exceeded", nil)
)

// AI errors
var (
ErrQuotaExceeded = NewAppError(TypeAI, "AI quota exceeded or rate limited", nil)
ErrAIGeneration = NewAppError(TypeAI, "AI generation failed", nil)
ErrInvalidAIOutput = NewAppError(TypeAI, "invalid AI output format", nil)
)

// Gemini/AI specific errors
var (
ErrGeminiAPIKeyInvalid = NewAppError(TypeAI, "Gemini API key is invalid", nil)
ErrGeminiQuotaExceeded = NewAppError(TypeAI, "Gemini API quota exceeded", nil)
)

// Internal errors
var (
ErrNetwork = NewAppError(TypeInternal, "network error occurred", nil)
Expand All @@ -93,7 +113,6 @@ var (
var (
ErrUpdateFailed = NewAppError(TypeUpdate, "failed to update application", nil)
)

var (
ErrBuildNoVersion = NewAppError(TypeInternal, "build version not specified", nil)
ErrBuildNoCommit = NewAppError(TypeInternal, "build commit not specified", nil)
Expand Down
61 changes: 60 additions & 1 deletion internal/git/git_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,26 @@ func (s *GitService) CreateCommit(ctx context.Context, message string) error {
}

cmd := exec.CommandContext(ctx, "git", "commit", "-m", message)
var stderr strings.Builder
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
return fmt.Errorf("%w: %v", errors.ErrCreateCommit, err)
stderrStr := strings.TrimSpace(stderr.String())

if strings.Contains(stderrStr, "Please tell me who you are") ||
(strings.Contains(stderrStr, "user.name")) &&
strings.Contains(stderrStr, "user.email") {
return errors.ErrGitUserNotConfigured
}
if strings.Contains(stderrStr, "user.name") {
return errors.ErrGitUserNotConfigured
}
if strings.Contains(stderrStr, "user.email") {
return errors.ErrGitEmailNotConfigured
}

fullErr := fmt.Sprintf("%v: %s", err, stderrStr)
return fmt.Errorf("%w: %s", errors.ErrCreateCommit, fullErr)
}
return nil
}
Expand Down Expand Up @@ -280,6 +298,47 @@ func (s *GitService) GetTagDate(ctx context.Context, tag string) (string, error)
return dateStr, nil
}

// ValidateGitConfig checks if git user.name and user.email are configured
func (s *GitService) ValidateGitConfig(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--git-dir")
if err := cmd.Run(); err != nil {
return errors.ErrNotInGitRepo
}

cmd = exec.CommandContext(ctx, "git", "config", "user.name")
output, err := cmd.Output()
if err != nil || strings.TrimSpace(string(output)) == "" {
return errors.ErrGitUserNotConfigured
}

cmd = exec.CommandContext(ctx, "git", "config", "user.email")
output, err = cmd.Output()
if err != nil || strings.TrimSpace(string(output)) == "" {
return errors.ErrGitEmailNotConfigured
}
return nil
}

// GetGitUserName returns the configured git user.name
func (s *GitService) GetGitUserName(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, "git", "config", "user.name")
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}

// GetGitUserEmail returns the configured git user.email
func (s *GitService) GetGitUserEmail(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, "git", "config", "user.email")
output, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}

func parseRepoURL(url string) (string, string, string, error) {
var matches []string
if regex.SSHRepo.MatchString(url) {
Expand Down
3 changes: 2 additions & 1 deletion internal/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"embed"
"fmt"
"os"
"path"
"path/filepath"

"github.com/BurntSushi/toml"
Expand Down Expand Up @@ -43,7 +44,7 @@ func NewTranslations(defaultLang string, localesPath string) (*Translations, err
for _, file := range files {
var data []byte
if localesPath == "" {
data, err = localesFS.ReadFile(filepath.Join("locales", file.Name()))
data, err = localesFS.ReadFile(path.Join("locales", file.Name()))
} else {
data, err = os.ReadFile(filepath.Join(localesPath, file.Name()))
}
Expand Down
28 changes: 27 additions & 1 deletion internal/i18n/locales/active.en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ explanation_prefix = "💡"
select_option_prompt = "\nSelect an option:"
option_commit = "1-N: Use the corresponding suggestion"
option_exit = "0: Exit without committing"
error_creating_commit = "Error creating the commit: {{.Error}}"
error_creating_commit = "Error creating the commit: {{.Error}}"
factory_already_registered = "❌ The factory with the name '{{.FactoryName}}' is already registered"
ask_update_issue_criteria = "💡 AI identified {{.Count}} criteria completed in issue #{{.Number}}. Do you want to update the issue?"
updating_issue = "Updating GitHub issue..."
Expand Down Expand Up @@ -462,6 +462,28 @@ ensure_modified_files = "Make sure you have modified files before generating sug
run_config_init = "Run: matecommit config init"
internal_error = "Internal system error"
error_saving_config = "Error saving configuration"
git_user_not_configured = "Git user.name is not configured"
git_email_not_configured = "Git user.email is not configured"
git_config_user_suggestion = "Run: git config --global user.name \"Your Name\""
git_config_email_suggestion = "Run: git config --global user.email \"your@email.com\""
not_in_git_repo = "You are not in a Git repository"
git_init_suggestion = "Run 'git init' to create a new repository or navigate to an existing one"

# GitHub/VCS Errors
github_token_invalid = "GitHub token is invalid or expired"
github_token_suggestion = "Check your token at: matecommit config init"
github_insufficient_perms = "GitHub token has insufficient permissions"
github_perms_suggestion = "Ensure your token has the scopes: repo, write:org"
github_rate_limit = "GitHub API rate limit exceeded"
github_rate_limit_suggestion = "Wait a few minutes before trying again"
vcs_error = "VCS provider error"

# Gemini/AI Errors
gemini_api_key_invalid = "Gemini API key is invalid"
gemini_api_key_suggestion = "Check your API key at: https://makersuite.google.com/app/apikey"
gemini_quota_exceeded = "Gemini API quota exceeded"
gemini_quota_suggestion = "Wait a few minutes or consider using a cheaper model"
update_failed = "Failed to update application"

# UI - Preview and confirmations
[ui_preview]
Expand Down Expand Up @@ -516,6 +538,8 @@ has_errors = "Incomplete setup (errors found)"
available_commands = "Available commands:"
command_ready = "(ready)"
command_unavailable = "(unavailable)"
git_user_not_set = "Git user.name is not configured"
git_email_not_set = "Git user.email is not configured"

# Checks
check_config_file = "Configuration file"
Expand All @@ -525,6 +549,8 @@ check_ai_key = "{{.Provider}} API key"
check_ai_key_generic = "AI provider API key"
check_github_token = "GitHub token"
check_editor = "Editor configured"
check_git_user_name = "Checking Git user.name"
check_git_user_email = "Checking Git user.email"

gemini_key_invalid = "Invalid Gemini API key or no permissions"
gemini_not_configured = "Gemini API key not configured"
Expand Down
Loading
Loading