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
14 changes: 14 additions & 0 deletions internal/commands/release/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,20 @@ func createReleaseAction(releaseSvc releaseService, trans *i18n.Translations, re

fmt.Println(trans.GetMessage("release.publishing_release", 0, nil))
buildBinaries := cmd.Bool("build-binaries")
mainPath := cmd.String("main-path")
if mainPath == "" && config != nil && config.MainPath != "" {
mainPath = config.MainPath
}
if mainPath == "" {
mainPath = "./cmd/main.go"
}

if buildBinaries {
if _, err := os.Stat(mainPath); os.IsNotExist(err) {
log.Warn("main entrypoint not found, smartly skipping build binaries during publish", "file", mainPath)
buildBinaries = false
}
}

if buildBinaries {
progressCh := make(chan models.BuildProgress, 10)
Expand Down
6 changes: 3 additions & 3 deletions internal/commands/release/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func TestCreateCommand_WithPublish(t *testing.T) {
mockService.On("GenerateReleaseNotes", mock.Anything, release).Return(notes, nil)
mockService.On("CreateTag", mock.Anything, "v1.0.0", mock.Anything).Return(nil)

mockService.On("PublishRelease", mock.Anything, release, notes, false, true, mock.Anything).Return(nil)
mockService.On("PublishRelease", mock.Anything, release, notes, false, false, mock.Anything).Return(nil)

err := runCreateTest(t, "y\n", []string{"--publish"}, mockService)
assert.NoError(t, err)
Expand All @@ -205,7 +205,7 @@ func TestCreateCommand_WithPublishDraft(t *testing.T) {
mockService.On("GenerateReleaseNotes", mock.Anything, release).Return(notes, nil)
mockService.On("CreateTag", mock.Anything, "v1.0.0", mock.Anything).Return(nil)

mockService.On("PublishRelease", mock.Anything, release, notes, true, true, mock.Anything).Return(nil)
mockService.On("PublishRelease", mock.Anything, release, notes, true, false, mock.Anything).Return(nil)

err := runCreateTest(t, "y\n", []string{"--publish", "--draft"}, mockService)
assert.NoError(t, err)
Expand All @@ -224,7 +224,7 @@ func TestCreateCommand_PublishError(t *testing.T) {
mockService.On("GenerateReleaseNotes", mock.Anything, release).Return(notes, nil)
mockService.On("CreateTag", mock.Anything, "v1.0.0", mock.Anything).Return(nil)

mockService.On("PublishRelease", mock.Anything, release, notes, false, true, mock.Anything).Return(errors.New("publish error"))
mockService.On("PublishRelease", mock.Anything, release, notes, false, false, mock.Anything).Return(errors.New("publish error"))

err := runCreateTest(t, "y\n", []string{"--publish"}, mockService)
assert.Error(t, err)
Expand Down
46 changes: 40 additions & 6 deletions internal/commands/stats/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,13 @@ func (c *StatsCommand) showDailyStats(manager *cost.Manager, t *i18n.Translation
for _, record := range todayRecords {
cacheIndicator := ""
if record.CacheHit {
cacheIndicator = green.Sprint(" [CACHE]")
cacheIndicator = green.Sprint(t.GetMessage("stats.cache_indicator", 0, nil))
}

tokensInfo := dim.Sprintf("(%d→%d tok)", record.TokensInput, record.TokensOutput)
tokensInfo := dim.Sprint(t.GetMessage("stats.tokens_label", 0, struct {
Input int
Output int
}{record.TokensInput, record.TokensOutput}))

fmt.Printf("%s - %s: %s %s%s\n",
record.Timestamp.Format("15:04"),
Expand All @@ -119,7 +122,7 @@ func (c *StatsCommand) showDailyStats(manager *cost.Manager, t *i18n.Translation
fmt.Println()
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
_, _ = cyan.Printf("%s: ", t.GetMessage("stats.total_today", 0, nil))
_, _ = yellow.Printf("$%.4f USD\n", total)
_, _ = yellow.Println(t.GetMessage("stats.total_today_value", 0, struct{ Total float64 }{total}))
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Println()

Expand Down Expand Up @@ -154,7 +157,11 @@ func (c *StatsCommand) showMonthlyStats(manager *cost.Manager, t *i18n.Translati
dim := color.New(color.FgHiBlack)
green := color.New(color.FgGreen)

_, _ = cyan.Printf("\n📅 %s\n", t.GetMessage("stats.monthly_title", 0, struct{ Month string }{time.Now().Format("January 2006")}))
now := time.Now()
monthName := getMonthName(t, now.Month())
monthYear := fmt.Sprintf("%s %d", monthName, now.Year())

_, _ = cyan.Printf("\n📅 %s\n", t.GetMessage("stats.monthly_title", 0, struct{ Month string }{monthYear}))
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Println()

Expand Down Expand Up @@ -201,7 +208,7 @@ func (c *StatsCommand) showMonthlyStats(manager *cost.Manager, t *i18n.Translati
fmt.Println()
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
_, _ = cyan.Printf("%s: ", t.GetMessage("stats.total_month", 0, nil))
_, _ = yellow.Printf("$%.4f USD\n", total)
_, _ = yellow.Println(t.GetMessage("stats.total_month_value", 0, struct{ Total float64 }{total}))

daysWithActivity := len(dailyTotals)
if daysWithActivity > 0 {
Expand Down Expand Up @@ -259,7 +266,11 @@ func (c *StatsCommand) showBreakdown(manager *cost.Manager, t *i18n.Translations
dim := color.New(color.FgHiBlack)
green := color.New(color.FgGreen)

_, _ = cyan.Printf("\n📊 Usage Breakdown - %s\n", time.Now().Format("January 2006"))
now := time.Now()
monthName := getMonthName(t, now.Month())
monthYear := fmt.Sprintf("%s %d", monthName, now.Year())

_, _ = cyan.Printf("\n📊 %s\n", t.GetMessage("stats.usage_breakdown_title", 0, struct{ Month string }{monthYear}))
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
fmt.Println()

Expand Down Expand Up @@ -331,3 +342,26 @@ func (c *StatsCommand) showBreakdown(manager *cost.Manager, t *i18n.Translations

return nil
}

func getMonthName(t *i18n.Translations, month time.Month) string {
months := map[time.Month]string{
time.January: "months.january",
time.February: "months.february",
time.March: "months.march",
time.April: "months.april",
time.May: "months.may",
time.June: "months.june",
time.July: "months.july",
time.August: "months.august",
time.September: "months.september",
time.October: "months.october",
time.November: "months.november",
time.December: "months.december",
}

key, ok := months[month]
if !ok {
return month.String()
}
return t.GetMessage(key, 0, nil)
}
2 changes: 1 addition & 1 deletion internal/commands/stats/stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func TestShowMonthlyStats_WithActivity(t *testing.T) {
CostUSD: 0.0010,
},
{
Timestamp: now.AddDate(0, -1, 0),
Timestamp: time.Date(now.Year(), now.Month()-1, 15, 0, 0, 0, 0, now.Location()),
Command: "suggest",
CostUSD: 0.0050,
},
Expand Down
44 changes: 40 additions & 4 deletions internal/commands/suggests_commits/suggests_commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

// commitService is a minimal interface for testing purposes
type commitService interface {
GenerateSuggestions(ctx context.Context, count int, issueNumber int, progress func(models.ProgressEvent)) ([]models.CommitSuggestion, error)
GenerateSuggestions(ctx context.Context, count int, issueNumber int, files []string, progress func(models.ProgressEvent)) ([]models.CommitSuggestion, error)
}

// commitHandler is a minimal interface for testing purposes
Expand All @@ -31,6 +31,7 @@ type gitService interface {
ValidateGitConfig(ctx context.Context) error
GetChangedFiles(ctx context.Context) ([]string, error)
GetDiff(ctx context.Context) (string, error)
GetDiffForFiles(ctx context.Context, files []string) (string, error)
}

type SuggestCommandFactory struct {
Expand Down Expand Up @@ -81,10 +82,14 @@ func (f *SuggestCommandFactory) createFlags(cfg *config.Config, t *i18n.Translat
},
&cli.IntFlag{
Name: "issue",
Aliases: []string{"i"},
Usage: t.GetMessage("suggest_issue_flag_usage", 0, nil),
Value: 0,
},
&cli.BoolFlag{
Name: "interactive",
Aliases: []string{"i"},
Usage: "Interactively select which files to include in the AI summary",
},
&cli.BoolFlag{
Name: "dry-run",
Aliases: []string{"d"},
Expand All @@ -102,13 +107,19 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla
lang := command.String("lang")
noEmoji := command.Bool("no-emoji")
dryRun := command.Bool("dry-run")
interactive := command.Bool("interactive")

if interactive && !command.IsSet("count") {
count = 1
}

log.Info("executing suggest command",
"count", count,
"issue_number", issueNumber,
"language", lang,
"no_emoji", noEmoji,
"dry_run", dryRun)
"dry_run", dryRun,
"interactive", interactive)

if noEmoji {
cfg.UseEmoji = false
Expand Down Expand Up @@ -142,6 +153,31 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla
return err
}

var selectedFiles []string
if interactive {
changedFiles, err := f.gitService.GetChangedFiles(ctx)
if err != nil {
ui.HandleAppError(err, t)
return err
}

if len(changedFiles) == 0 {
ui.PrintWarning("No changed files to select.")
return nil
}

selectedFiles, err = ui.PromptMultiSelect(t, t.GetMessage("ui.multi_select_default_msg", 0, nil), changedFiles)
if err != nil {
ui.HandleAppError(err, t)
return err
}

if len(selectedFiles) == 0 {
ui.PrintWarning("No files selected. Operation cancelled.")
return nil
}
}

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

Expand All @@ -150,7 +186,7 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla

start := time.Now()

suggestions, err = f.commitService.GenerateSuggestions(ctx, count, issueNumber, func(event models.ProgressEvent) {
suggestions, err = f.commitService.GenerateSuggestions(ctx, count, issueNumber, selectedFiles, func(event models.ProgressEvent) {
msg := ""
switch event.Type {
case models.ProgressIssuesDetected:
Expand Down
17 changes: 11 additions & 6 deletions internal/commands/suggests_commits/suggests_commits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type MockCommitService struct {
mock.Mock
}

func (m *MockCommitService) GenerateSuggestions(ctx context.Context, count int, issueNumber int, progress func(models.ProgressEvent)) ([]models.CommitSuggestion, error) {
args := m.Called(ctx, count, issueNumber, progress)
func (m *MockCommitService) GenerateSuggestions(ctx context.Context, count int, issueNumber int, files []string, progress func(models.ProgressEvent)) ([]models.CommitSuggestion, error) {
args := m.Called(ctx, count, issueNumber, files, progress)
return args.Get(0).([]models.CommitSuggestion), args.Error(1)
}

Expand Down Expand Up @@ -56,6 +56,11 @@ func (m *MockGitService) GetDiff(ctx context.Context) (string, error) {
return args.String(0), args.Error(1)
}

func (m *MockGitService) GetDiffForFiles(ctx context.Context, files []string) (string, error) {
args := m.Called(ctx, files)
return args.String(0), args.Error(1)
}

func setupTestEnv(t *testing.T) (*config.Config, *i18n.Translations, func()) {
tmpDir, err := os.MkdirTemp("", "matecommit-test-*")
if err != nil {
Expand Down Expand Up @@ -102,7 +107,7 @@ func TestSuggestCommand(t *testing.T) {
}

mockGit.On("ValidateGitConfig", mock.Anything).Return(nil)
mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, mock.Anything).Return(suggestions, nil)
mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, []string(nil), mock.Anything).Return(suggestions, nil)
mockHandler.On("HandleSuggestions", mock.Anything, suggestions).Return(nil)

factory := NewSuggestCommandFactory(mockService, mockHandler, mockGit)
Expand Down Expand Up @@ -162,7 +167,7 @@ func TestSuggestCommand(t *testing.T) {
}

mockGit.On("ValidateGitConfig", mock.Anything).Return(nil)
mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, mock.Anything).Return(suggestions, nil)
mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, []string(nil), mock.Anything).Return(suggestions, nil)
mockHandler.On("HandleSuggestions", mock.Anything, suggestions).Return(nil)

factory := NewSuggestCommandFactory(mockService, mockHandler, mockGit)
Expand Down Expand Up @@ -198,7 +203,7 @@ func TestSuggestCommand(t *testing.T) {
}

mockGit.On("ValidateGitConfig", mock.Anything).Return(nil)
mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, mock.Anything).Return(suggestions, nil)
mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, []string(nil), mock.Anything).Return(suggestions, nil)
mockHandler.On("HandleSuggestions", mock.Anything, suggestions).Return(nil)

factory := NewSuggestCommandFactory(mockService, mockHandler, mockGit)
Expand Down Expand Up @@ -227,7 +232,7 @@ func TestSuggestCommand(t *testing.T) {

expectedError := fmt.Errorf("service error")
mockGit.On("ValidateGitConfig", mock.Anything).Return(nil)
mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, mock.Anything).Return([]models.CommitSuggestion{}, expectedError)
mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, []string(nil), mock.Anything).Return([]models.CommitSuggestion{}, expectedError)

factory := NewSuggestCommandFactory(mockService, mockHandler, mockGit)
command := factory.CreateCommand(translations, cfg)
Expand Down
69 changes: 61 additions & 8 deletions internal/git/git_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package git
import (
"context"
"fmt"
"os"
"os/exec"
"regexp"
"sort"
Expand Down Expand Up @@ -145,6 +144,66 @@ func (s *GitService) GetDiff(ctx context.Context) (string, error) {
return combinedDiff, nil
}

func (s *GitService) GetDiffForFiles(ctx context.Context, files []string) (string, error) {
log := logger.FromContext(ctx)

if len(files) == 0 {
return "", errors.ErrNoDiff
}

log.Debug("executing git diff for specific files", "count", len(files))

argsStaged := append([]string{"diff", "--cached", "--"}, files...)
stagedCmd := exec.CommandContext(ctx, "git", argsStaged...)
stagedOutput, err := stagedCmd.Output()
if err != nil {
log.Error("git diff --cached failed for files", "error", err)
return "", errors.ErrGetDiff.WithError(err).WithContext("diff_type", "staged_files")
}

argsUnstaged := append([]string{"diff", "--"}, files...)
unstagedCmd := exec.CommandContext(ctx, "git", argsUnstaged...)
unstageOutput, err := unstagedCmd.Output()
if err != nil {
log.Error("git diff failed for files", "error", err)
return "", errors.ErrGetDiff.WithError(err).WithContext("diff_type", "unstaged_files")
}

combinedDiff := string(stagedOutput) + string(unstageOutput)

if combinedDiff == "" {
// Fallback check for untracked files in our selection
untrackedCmd := exec.CommandContext(ctx, "git", "ls-files", "--others", "--exclude-standard")
untrackedFiles, err := untrackedCmd.Output()
if err == nil && len(untrackedFiles) > 0 {
untrackedList := strings.Split(string(untrackedFiles), "\n")

// Quick map for O(1) lookups
selectedMap := make(map[string]bool)
for _, f := range files {
selectedMap[f] = true
}

for _, file := range untrackedList {
if file != "" && selectedMap[file] {
fileContentCmd := exec.CommandContext(ctx, "git", "show", ":"+file)
content, err := fileContentCmd.Output()
if err != nil {
combinedDiff += "\n=== New file" + " " + file + "===\n"
combinedDiff += string(content)
}
}
}
}

if combinedDiff == "" {
return "", errors.ErrNoDiff
}
}

return combinedDiff, nil
}

func (s *GitService) CreateCommit(ctx context.Context, message string) error {
log := logger.FromContext(ctx)

Expand Down Expand Up @@ -205,13 +264,7 @@ func (s *GitService) AddFileToStaging(ctx context.Context, file string) error {
"file", file,
"repo_root", repoRoot)

fullPath := repoRoot + "/" + file
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
log.Error("file does not exist",
"file", file,
"full_path", fullPath)
return errors.ErrAddFile.WithError(err).WithContext("file", file).WithContext("reason", "file_not_found")
}


cmd := exec.CommandContext(ctx, "git", "add", "--", file)
cmd.Dir = repoRoot
Expand Down
1 change: 1 addition & 0 deletions internal/git/git_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ func TestAddFileToStaging(t *testing.T) {
expectedMessages := []string{
"did not match any files",
"no concordó con ningún archivo",
"no such file or directory",
}

match := false
Expand Down
Loading
Loading