diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 2f8aaa5..0000000 --- a/.mailmap +++ /dev/null @@ -1,7 +0,0 @@ -# Map all variations to the canonical name -Thomas Vilte butakero -Thomas Vilte ¨thomas¨ -Thomas Vilte Thomas -Thomas Vilte Thomas Vilte <88698264+Tomas-vilte@users.noreply.github.com> -# Map Claude co-author to main author -Thomas Vilte Claude Sonnet 4.5 diff --git a/internal/ai/gemini/helper.go b/internal/ai/gemini/helper.go index b63c467..32ff123 100644 --- a/internal/ai/gemini/helper.go +++ b/internal/ai/gemini/helper.go @@ -2,10 +2,10 @@ package gemini import ( "encoding/json" - "regexp" "strings" "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/regex" "google.golang.org/genai" ) @@ -48,8 +48,7 @@ func GetGenerateConfig(modelName string, responseType string) *genai.GenerateCon func ExtractJSON(text string) string { text = strings.TrimSpace(text) - re := regexp.MustCompile("(?s)```(?:json)?\n?(.*?)```") - matches := re.FindAllStringSubmatch(text, -1) + matches := regex.MarkdownJSONBlock.FindAllStringSubmatch(text, -1) var bestMarkdown string for _, m := range matches { if len(m) > 1 { @@ -138,12 +137,10 @@ func ExtractJSON(text string) string { return SanitizeJSON(text) } -var jsonStringRegex = regexp.MustCompile(`"(?:\\.|[^"\\])*"`) - // SanitizeJSON cleans malformed JSON that LLMs sometimes generate, // such as unescaped newlines within String Literals. func SanitizeJSON(s string) string { - return jsonStringRegex.ReplaceAllStringFunc(s, func(m string) string { + return regex.JSONString.ReplaceAllStringFunc(s, func(m string) string { return strings.ReplaceAll(m, "\n", "\\n") }) } diff --git a/internal/commands/completion/completion.go b/internal/commands/completion/completion.go index 8e049b3..7b5cd6d 100644 --- a/internal/commands/completion/completion.go +++ b/internal/commands/completion/completion.go @@ -81,7 +81,7 @@ func NewCompletionCommand(t *i18n.Translations) *cli.Command { shell := os.Getenv("SHELL") home, err := os.UserHomeDir() if err != nil { - return fmt.Errorf("%s", t.GetMessage("completion.error_home_dir", 0, map[string]interface{}{"Error": err.Error()})) + return fmt.Errorf("%s", t.GetMessage("completion.error_home_dir", 0, struct{ Error string }{err.Error()})) } var configFile string @@ -94,14 +94,14 @@ func NewCompletionCommand(t *i18n.Translations) *cli.Command { configFile = filepath.Join(home, ".bashrc") shellName = "bash" } else { - return fmt.Errorf("%s", t.GetMessage("completion.error_unsupported_shell", 0, map[string]interface{}{"Shell": shell})) + return fmt.Errorf("%s", t.GetMessage("completion.error_unsupported_shell", 0, struct{ Shell string }{shell})) } content := fmt.Sprintf(installInfo, shellName) fileContent, err := os.ReadFile(configFile) if err == nil && strings.Contains(string(fileContent), "# MateCommit Shell Completion") { - fmt.Println(t.GetMessage("completion.already_installed", 0, map[string]interface{}{"File": configFile})) + fmt.Println(t.GetMessage("completion.already_installed", 0, struct{ File string }{configFile})) fmt.Println(t.GetMessage("completion.restart_shell", 0, nil)) fmt.Printf(" source %s\n", configFile) return nil @@ -109,7 +109,7 @@ func NewCompletionCommand(t *i18n.Translations) *cli.Command { f, err := os.OpenFile(configFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) if err != nil { - return fmt.Errorf("%s", t.GetMessage("completion.error_open_config", 0, map[string]interface{}{"Error": err.Error()})) + return fmt.Errorf("%s", t.GetMessage("completion.error_open_config", 0, struct{ Error string }{err.Error()})) } defer func() { if err := f.Close(); err != nil { @@ -118,10 +118,10 @@ func NewCompletionCommand(t *i18n.Translations) *cli.Command { }() if _, err := f.WriteString(content); err != nil { - return fmt.Errorf("%s", t.GetMessage("completion.error_write_config", 0, map[string]interface{}{"Error": err.Error()})) + return fmt.Errorf("%s", t.GetMessage("completion.error_write_config", 0, struct{ Error string }{err.Error()})) } - fmt.Println(t.GetMessage("completion.installed_success", 0, map[string]interface{}{"File": configFile})) + fmt.Println(t.GetMessage("completion.installed_success", 0, struct{ File string }{configFile})) fmt.Println(t.GetMessage("completion.restart_shell", 0, nil)) fmt.Printf(" source %s\n", configFile) diff --git a/internal/commands/config/doctor.go b/internal/commands/config/doctor.go index 4cadc4e..adbb663 100644 --- a/internal/commands/config/doctor.go +++ b/internal/commands/config/doctor.go @@ -8,9 +8,9 @@ import ( "strings" "time" + "github.com/thomas-vilte/matecommit/internal/ai/gemini" "github.com/thomas-vilte/matecommit/internal/config" "github.com/thomas-vilte/matecommit/internal/i18n" - "github.com/thomas-vilte/matecommit/internal/ai/gemini" "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -252,7 +252,7 @@ func (d *DoctorCommand) checkEditor(_ context.Context, t *i18n.Translations, _ * if _, err := exec.LookPath(ed); err == nil { return checkResult{ status: checkStatusWarning, - message: t.GetMessage("doctor.editor_not_set", 0, map[string]interface{}{"Editor": ed}), + message: t.GetMessage("doctor.editor_not_set", 0, struct{ Editor string }{ed}), suggestion: fmt.Sprintf("export EDITOR=%s", ed), } } @@ -268,7 +268,7 @@ func (d *DoctorCommand) checkEditor(_ context.Context, t *i18n.Translations, _ * if _, err := exec.LookPath(editor); err != nil { return checkResult{ status: checkStatusError, - message: t.GetMessage("doctor.editor_not_found", 0, map[string]interface{}{"Editor": editor}), + message: t.GetMessage("doctor.editor_not_found", 0, struct{ Editor string }{editor}), suggestion: t.GetMessage("doctor.set_valid_editor", 0, nil), } } diff --git a/internal/commands/config/edit.go b/internal/commands/config/edit.go index aa51ef1..29066a5 100644 --- a/internal/commands/config/edit.go +++ b/internal/commands/config/edit.go @@ -15,11 +15,11 @@ func (c *ConfigCommandFactory) newEditCommand(t *i18n.Translations, cfg *config. return &cli.Command{ Name: "edit", Usage: t.GetMessage("config_edit_usage", 0, nil), - Action: editConfigAction(cfg), + Action: editConfigAction(cfg, t), } } -func editConfigAction(cfg *config.Config) cli.ActionFunc { +func editConfigAction(cfg *config.Config, t *i18n.Translations) cli.ActionFunc { return func(ctx context.Context, command *cli.Command) error { editor := os.Getenv("EDITOR") if editor == "" { @@ -28,7 +28,7 @@ func editConfigAction(cfg *config.Config) cli.ActionFunc { } else if _, err := exec.LookPath("vim"); err == nil { editor = "vim" } else { - return fmt.Errorf("ningun editor de texto definido. Por favor, configure la variable de entorno $EDITOR") + return fmt.Errorf("%s", t.GetMessage("config_save.error_no_editor", 0, nil)) } } @@ -38,7 +38,7 @@ func editConfigAction(cfg *config.Config) cli.ActionFunc { cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return fmt.Errorf("error opening editor: %w", err) + return fmt.Errorf("%s: %w", t.GetMessage("config_save.error_opening_editor", 0, struct{ Error error }{err}), err) } return nil diff --git a/internal/commands/config/edit_test.go b/internal/commands/config/edit_test.go index 14466fa..9907b4d 100644 --- a/internal/commands/config/edit_test.go +++ b/internal/commands/config/edit_test.go @@ -8,10 +8,10 @@ import ( "strings" "testing" - "github.com/thomas-vilte/matecommit/internal/config" - "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" ) func setupEditTest(t *testing.T) (*config.Config, *i18n.Translations, func()) { @@ -147,7 +147,7 @@ func TestEditCommand(t *testing.T) { // Assert assert.Error(t, err) - assert.Contains(t, err.Error(), "ningun editor de texto definido") + assert.Contains(t, err.Error(), translations.GetMessage("config_save.error_no_editor", 0, nil)) }) t.Run("should return error if editor fails to run", func(t *testing.T) { @@ -166,6 +166,6 @@ func TestEditCommand(t *testing.T) { // Assert assert.Error(t, err) - assert.Contains(t, err.Error(), "error opening editor") + assert.Contains(t, err.Error(), translations.GetMessage("config_save.error_opening_editor", 0, struct{ Error error }{fmt.Errorf("error")})[:10]) }) } diff --git a/internal/commands/config/init.go b/internal/commands/config/init.go index 0dc4925..4d9a6d7 100644 --- a/internal/commands/config/init.go +++ b/internal/commands/config/init.go @@ -9,10 +9,10 @@ import ( "strings" "time" + "github.com/thomas-vilte/matecommit/internal/ai/gemini" "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" "github.com/thomas-vilte/matecommit/internal/config" "github.com/thomas-vilte/matecommit/internal/i18n" - "github.com/thomas-vilte/matecommit/internal/ai/gemini" "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -79,7 +79,7 @@ func runFullSetup(ctx context.Context, command *cli.Command, reader *bufio.Reade return err } if err := config.SaveConfig(cfg); err != nil { - fmt.Println(t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{"Error": err.Error()})) + fmt.Println(t.GetMessage("config_save.error_saving_config", 0, struct{ Error string }{err.Error()})) return fmt.Errorf("error saving configuration: %w", err) } @@ -109,19 +109,13 @@ func configureWelcome(ctx context.Context, reader *bufio.Reader, cfg *config.Con printSection(t.GetMessage("init.section_welcome", 0, nil)) fmt.Println(t.GetMessage("init.welcome", 0, nil)) - fmt.Println(t.GetMessage("init.ai_intro", 0, map[string]interface{}{"Providers": aiProvidersStr})) - - ui.PrintInfo(t.GetMessage("config.api_key_instructions", 0, map[string]interface{}{ - "Provider": "Gemini", - })) - ui.PrintInfo(t.GetMessage("config.get_key_at", 0, map[string]interface{}{ - "URL": "https://makersuite.google.com/app/apikey", - })) + fmt.Println(t.GetMessage("init.ai_intro", 0, struct{ Providers string }{aiProvidersStr})) + + ui.PrintInfo(t.GetMessage("config.api_key_instructions", 0, struct{ Provider string }{"Gemini"})) + ui.PrintInfo(t.GetMessage("config.get_key_at", 0, struct{ URL string }{"https://makersuite.google.com/app/apikey"})) fmt.Println() - fmt.Print(t.GetMessage("init.prompt_ai_api_key", 0, map[string]interface{}{ - "Provider": "Gemini", - })) + fmt.Print(t.GetMessage("init.prompt_ai_api_key", 0, struct{ Provider string }{"Gemini"})) apiKey, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("error reading API_KEY: %w", err) @@ -139,8 +133,8 @@ func configureWelcome(ctx context.Context, reader *bufio.Reader, cfg *config.Con } } - fmt.Println(t.GetMessage("init.model_hint_supported", 0, map[string]interface{}{"Models": geminiModelsStr})) - fmt.Print(t.GetMessage("init.prompt_model_with_default", 0, map[string]interface{}{"Default": geminiDefault})) + fmt.Println(t.GetMessage("init.model_hint_supported", 0, struct{ Models string }{geminiModelsStr})) + fmt.Print(t.GetMessage("init.prompt_model_with_default", 0, struct{ Default string }{geminiDefault})) modelInput, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("error reading model: %w", err) @@ -174,7 +168,7 @@ func configureWelcome(ctx context.Context, reader *bufio.Reader, cfg *config.Con func configureLanguage(reader *bufio.Reader, cfg *config.Config, t *i18n.Translations) error { printSection(t.GetMessage("init.section_language", 0, nil)) - fmt.Println(t.GetMessage("init.language_supported_with_current", 0, map[string]interface{}{"Current": cfg.Language})) + fmt.Println(t.GetMessage("init.language_supported_with_current", 0, struct{ Current string }{cfg.Language})) fmt.Print(t.GetMessage("init.prompt_language_blank_keeps", 0, nil)) lang, err := reader.ReadString('\n') @@ -199,7 +193,7 @@ func configureVCS(reader *bufio.Reader, cfg *config.Config, t *i18n.Translations vcsProvidersStr := strings.Join(vcsProviders, ", ") printSection(t.GetMessage("init.section_vcs", 0, nil)) - fmt.Print(t.GetMessage("init.prompt_vcs_enable_blank_no", 0, map[string]interface{}{"Providers": vcsProvidersStr})) + fmt.Print(t.GetMessage("init.prompt_vcs_enable_blank_no", 0, struct{ Providers string }{vcsProvidersStr})) ansVCS, err := reader.ReadString('\n') if err != nil { @@ -237,7 +231,7 @@ func configureTickets(reader *bufio.Reader, cfg *config.Config, t *i18n.Translat ticketProviders := config.SupportedTicketServices() ticketProvidersStr := strings.Join(ticketProviders, ", ") printSection(t.GetMessage("init.section_tickets", 0, nil)) - fmt.Print(t.GetMessage("init.prompt_ticket_enable_blank_no", 0, map[string]interface{}{"Providers": ticketProvidersStr})) + fmt.Print(t.GetMessage("init.prompt_ticket_enable_blank_no", 0, struct{ Providers string }{ticketProvidersStr})) ansJira, err := reader.ReadString('\n') if err != nil { @@ -319,32 +313,41 @@ func printConfigSummary(cfg *config.Config, t *i18n.Translations) { fmt.Println() fmt.Println(t.GetMessage("init.summary_header", 0, nil)) - langLabel := t.GetMessage("language_label", 0, map[string]interface{}{"Lang": cfg.Language}) + langLabel := t.GetMessage("language_label", 0, struct{ Lang string }{cfg.Language}) activeAI := string(cfg.AIConfig.ActiveAI) - fmt.Println(t.GetMessage("config_models.active_ai_label", 0, map[string]interface{}{"IA": activeAI})) + fmt.Println(t.GetMessage("config_models.active_ai_label", 0, struct{ IA string }{activeAI})) if m, ok := cfg.AIConfig.Models[config.AIGemini]; ok && m != "" { - fmt.Println(t.GetMessage("init.summary_model", 0, map[string]interface{}{"AI": "gemini", "Model": string(m)})) + fmt.Println(t.GetMessage("init.summary_model", 0, struct { + AI string + Model string + }{"gemini", string(m)})) } else { - fmt.Println(t.GetMessage("init.summary_model_none", 0, map[string]interface{}{"AI": "gemini"})) + fmt.Println(t.GetMessage("init.summary_model_none", 0, struct{ AI string }{"gemini"})) } apiMask := "❌" if providerCfg, exists := cfg.AIProviders["gemini"]; exists && providerCfg.APIKey != "" { apiMask = "✅" } - fmt.Println(t.GetMessage("init.summary_api", 0, map[string]interface{}{"AI": "gemini", "Configured": apiMask})) + fmt.Println(t.GetMessage("init.summary_api", 0, struct { + AI string + Configured string + }{"gemini", apiMask})) if cfg.ActiveVCSProvider != "" { - fmt.Println(t.GetMessage("vcs_summary.config_active_vcs_updated", 0, map[string]interface{}{"Provider": cfg.ActiveVCSProvider})) + fmt.Println(t.GetMessage("vcs_summary.config_active_vcs_updated", 0, struct{ Provider string }{cfg.ActiveVCSProvider})) } else { fmt.Println(t.GetMessage("init.summary_vcs_none", 0, nil)) } if cfg.UseTicket && cfg.ActiveTicketService == "jira" { - fmt.Println(t.GetMessage("config_models.ticket_service_enabled", 0, map[string]interface{}{"Service": "jira"})) + fmt.Println(t.GetMessage("config_models.ticket_service_enabled", 0, struct{ Service string }{"jira"})) jiraCfg := cfg.TicketProviders["jira"] - fmt.Println(t.GetMessage("config_models.jira_config_label", 0, map[string]interface{}{"BaseURL": jiraCfg.BaseURL, "Email": jiraCfg.Email})) + fmt.Println(t.GetMessage("config_models.jira_config_label", 0, struct { + BaseURL string + Email string + }{jiraCfg.BaseURL, jiraCfg.Email})) } else { fmt.Println(t.GetMessage("config_models.ticket_service_disabled", 0, nil)) } @@ -385,9 +388,7 @@ func validateGeminiAPIKey(ctx context.Context, apiKey string, t *i18n.Translatio _, err := gemini.NewGeminiCommitSummarizer(testCtx, testCfg, nil) if err != nil { spinner.Error(t.GetMessage("config.api_key_invalid", 0, nil)) - ui.PrintError(os.Stdout, t.GetMessage("config.check_api_key_error", 0, map[string]interface{}{ - "Error": err.Error(), - })) + ui.PrintError(os.Stdout, t.GetMessage("config.check_api_key_error", 0, struct{ Error string }{err.Error()})) return false } diff --git a/internal/commands/config/quick_setup.go b/internal/commands/config/quick_setup.go index 260eb62..503b565 100644 --- a/internal/commands/config/quick_setup.go +++ b/internal/commands/config/quick_setup.go @@ -21,17 +21,11 @@ func runQuickSetup(ctx context.Context, reader *bufio.Reader, cfg *config.Config providerKey := config.AIGemini apiURL := "https://makersuite.google.com/app/apikey" - fmt.Println(t.GetMessage("quick_setup.ai_provider_intro", 0, map[string]interface{}{ - "Provider": provider, - })) - fmt.Println(t.GetMessage("quick_setup.get_api_key_at", 0, map[string]interface{}{ - "URL": apiURL, - })) + fmt.Println(t.GetMessage("quick_setup.ai_provider_intro", 0, struct{ Provider string }{provider})) + fmt.Println(t.GetMessage("quick_setup.get_api_key_at", 0, struct{ URL string }{apiURL})) fmt.Println() - fmt.Print(t.GetMessage("quick_setup.prompt_api_key", 0, map[string]interface{}{ - "Provider": provider, - })) + fmt.Print(t.GetMessage("quick_setup.prompt_api_key", 0, struct{ Provider string }{provider})) apiKey, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("error reading API_KEY: %w", err) diff --git a/internal/commands/config/show.go b/internal/commands/config/show.go index b0a4c78..88da828 100644 --- a/internal/commands/config/show.go +++ b/internal/commands/config/show.go @@ -17,9 +17,9 @@ func (c *ConfigCommandFactory) newShowCommand(t *i18n.Translations, cfg *config. fmt.Println(t.GetMessage("current_config", 0, nil)) fmt.Printf("━━━━━━━━━━━━━━━━━━━━━━━\n") - fmt.Printf("%s\n", t.GetMessage("language_label", 0, map[string]interface{}{"Lang": cfg.Language})) + fmt.Printf("%s\n", t.GetMessage("language_label", 0, struct{ Lang string }{cfg.Language})) - fmt.Printf("%s\n", t.GetMessage("emojis_label", 0, map[string]interface{}{"Emoji": cfg.UseEmoji})) + fmt.Printf("%s\n", t.GetMessage("emojis_label", 0, struct{ Emoji bool }{cfg.UseEmoji})) hasGemini := false if providerCfg, exists := cfg.AIProviders["gemini"]; exists && providerCfg.APIKey != "" { @@ -35,19 +35,22 @@ func (c *ConfigCommandFactory) newShowCommand(t *i18n.Translations, cfg *config. } if cfg.UseTicket { - fmt.Printf("%s\n", t.GetMessage("config_models.ticket_service_enabled", 0, map[string]interface{}{"Service": cfg.ActiveTicketService})) + fmt.Printf("%s\n", t.GetMessage("config_models.ticket_service_enabled", 0, struct{ Service string }{cfg.ActiveTicketService})) if cfg.ActiveTicketService == "jira" { jiraCfg := cfg.TicketProviders["jira"] - fmt.Printf("%s\n", t.GetMessage("config_models.jira_config_label", 0, map[string]interface{}{ - "BaseURL": jiraCfg.BaseURL, - "Email": jiraCfg.Email, + fmt.Printf("%s\n", t.GetMessage("config_models.jira_config_label", 0, struct { + BaseURL string + Email string + }{ + BaseURL: jiraCfg.BaseURL, + Email: jiraCfg.Email, })) } } else { fmt.Println(t.GetMessage("config_models.ticket_service_disabled", 0, nil)) } - fmt.Printf("%s\n", t.GetMessage("config_models.active_ai_label", 0, map[string]interface{}{"IA": cfg.AIConfig.ActiveAI})) + fmt.Printf("%s\n", t.GetMessage("config_models.active_ai_label", 0, struct{ IA config.AI }{cfg.AIConfig.ActiveAI})) if len(cfg.AIConfig.Models) > 0 { fmt.Println(t.GetMessage("config_models.ai_models_label", 0, nil)) diff --git a/internal/commands/handler/suggestions.go b/internal/commands/handler/suggestions.go index 0cc0e0b..764ce25 100644 --- a/internal/commands/handler/suggestions.go +++ b/internal/commands/handler/suggestions.go @@ -6,11 +6,11 @@ import ( "os" "strings" + "github.com/fatih/color" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/ports" - "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/thomas-vilte/matecommit/internal/ui" - "github.com/fatih/color" ) // gitService is a minimal interface for testing purposes @@ -54,7 +54,7 @@ func (h *SuggestionHandler) displaySuggestions(suggestions []models.CommitSugges separator := color.New(color.FgCyan).Sprint("━━━━━━━━━━━━━━━━━━━━━━━") fmt.Printf("\n%s\n", separator) - suggestionHeader := color.New(color.FgMagenta, color.Bold).Sprint(h.t.GetMessage("ui_labels.suggestion_number", 0, map[string]interface{}{"Number": i + 1})) + suggestionHeader := color.New(color.FgMagenta, color.Bold).Sprint(h.t.GetMessage("ui_labels.suggestion_number", 0, struct{ Number int }{i + 1})) fmt.Printf("%s\n\n", suggestionHeader) _, _ = sectionColor.Println(h.t.GetMessage("ui_labels.code_analysis", 0, nil)) @@ -192,7 +192,7 @@ func (h *SuggestionHandler) handleCommitSelection(ctx context.Context, suggestio fmt.Print(prompt + " ") if _, err := fmt.Scan(&selection); err != nil { - msg := h.t.GetMessage("commit.error_reading_selection", 0, map[string]interface{}{"Error": err}) + msg := h.t.GetMessage("commit.error_reading_selection", 0, struct{ Error error }{err}) ui.PrintError(os.Stdout, msg) return fmt.Errorf("%s", msg) } @@ -203,7 +203,7 @@ func (h *SuggestionHandler) handleCommitSelection(ctx context.Context, suggestio } if selection < 1 || selection > len(suggestions) { - msg := h.t.GetMessage("commit.invalid_selection", 0, map[string]interface{}{"Number": len(suggestions)}) + msg := h.t.GetMessage("commit.invalid_selection", 0, struct{ Number int }{len(suggestions)}) ui.PrintError(os.Stdout, msg) return fmt.Errorf("%s", msg) } @@ -216,30 +216,22 @@ func (h *SuggestionHandler) processCommit(ctx context.Context, suggestion models commitTitle := strings.TrimSpace(strings.TrimPrefix(suggestion.CommitTitle, "Commit: ")) fmt.Println() - ui.PrintInfo(h.t.GetMessage("ui_preview.commit_selected", 0, map[string]interface{}{ - "Title": commitTitle, - })) + ui.PrintInfo(h.t.GetMessage("ui_preview.commit_selected", 0, struct{ Title string }{commitTitle})) treeHeader := h.t.GetMessage("ui_preview.modified_files_header", 0, nil) if err := ui.ShowFilesTree(suggestion.Files, treeHeader); err != nil { - ui.PrintInfo(h.t.GetMessage("ui_preview.files_count", 0, map[string]interface{}{ - "Count": len(suggestion.Files), - })) + ui.PrintInfo(h.t.GetMessage("ui_preview.files_count", 0, struct{ Count int }{len(suggestion.Files)})) } statsHeader := h.t.GetMessage("ui_preview.changes_header", 0, nil) if err := ui.ShowDiffStats(suggestion.Files, statsHeader); err != nil { - ui.PrintWarning(h.t.GetMessage("ui_preview.error_showing_stats", 0, map[string]interface{}{ - "Error": err, - })) + ui.PrintWarning(h.t.GetMessage("ui_preview.error_showing_stats", 0, struct{ Error error }{err})) } if ui.AskConfirmation(h.t.GetMessage("ui_preview.ask_show_diff", 0, nil)) { fmt.Println() if err := ui.ShowDiff(suggestion.Files); err != nil { - ui.PrintWarning(h.t.GetMessage("ui_preview.error_showing_diff", 0, map[string]interface{}{ - "Error": err, - })) + ui.PrintWarning(h.t.GetMessage("ui_preview.error_showing_diff", 0, struct{ Error error }{err})) } } @@ -248,9 +240,7 @@ func (h *SuggestionHandler) processCommit(ctx context.Context, suggestion models editorError := h.t.GetMessage("ui_preview.editor_error", 0, nil) editedMessage, err := ui.EditCommitMessage(commitTitle, editorError) if err != nil { - ui.PrintError(os.Stdout, h.t.GetMessage("ui_preview.error_editing_message", 0, map[string]interface{}{ - "Error": err, - })) + ui.PrintError(os.Stdout, h.t.GetMessage("ui_preview.error_editing_message", 0, struct{ Error error }{err})) return err } finalCommitMessage = editedMessage @@ -267,25 +257,21 @@ func (h *SuggestionHandler) processCommit(ctx context.Context, suggestion models for _, file := range suggestion.Files { if err := gitSvc.AddFileToStaging(ctx, file); err != nil { - spinner.Error(h.t.GetMessage("commit.error_add_file_staging", 0, map[string]interface{}{ - "File": file, - "Error": err, - })) + spinner.Error(h.t.GetMessage("commit.error_add_file_staging", 0, struct { + File string + Error error + }{file, err})) return fmt.Errorf("error adding %s: %w", file, err) } } - spinner.Success(h.t.GetMessage("ui.files_added_to_staging", 0, map[string]interface{}{ - "Count": len(suggestion.Files), - })) + spinner.Success(h.t.GetMessage("ui.files_added_to_staging", 0, struct{ Count int }{len(suggestion.Files)})) spinner = ui.NewSmartSpinner(h.t.GetMessage("ui.creating_commit", 0, nil)) spinner.Start() if err := gitSvc.CreateCommit(ctx, finalCommitMessage); err != nil { - spinner.Error(h.t.GetMessage("commit.error_creating_commit", 0, map[string]interface{}{ - "Error": err, - })) + spinner.Error(h.t.GetMessage("commit.error_creating_commit", 0, struct{ Error error }{err})) return fmt.Errorf("error creating commit: %w", err) } diff --git a/internal/commands/issues/issues.go b/internal/commands/issues/issues.go index 39b0546..3689e25 100644 --- a/internal/commands/issues/issues.go +++ b/internal/commands/issues/issues.go @@ -10,8 +10,8 @@ import ( "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" "github.com/thomas-vilte/matecommit/internal/config" - "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -170,9 +170,7 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c var spinnerMsg string if fromPR > 0 { - spinnerMsg = t.GetMessage("issue.analyzing_pr", 0, map[string]interface{}{ - "Number": fromPR, - }) + spinnerMsg = t.GetMessage("issue.analyzing_pr", 0, struct{ Number int }{fromPR}) } else { spinnerMsg = t.GetMessage("issue.analyzing", 0, nil) } @@ -224,9 +222,7 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c ui.PrintWarning(fmt.Sprintf("%s: %v", t.GetMessage("issue.warn_assignee_failed", 0, nil), err)) } else { assignees = []string{username} - ui.PrintInfo(t.GetMessage("issue.will_assign", 0, map[string]interface{}{ - "User": username, - })) + ui.PrintInfo(t.GetMessage("issue.will_assign", 0, struct{ User string }{username})) } } @@ -241,38 +237,34 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c return err } - ui.PrintSuccess(os.Stdout, t.GetMessage("issue.created_successfully", 0, map[string]interface{}{ - "Number": issue.Number, - "URL": issue.URL, - })) + ui.PrintSuccess(os.Stdout, t.GetMessage("issue.created_successfully", 0, struct { + Number int + URL string + }{issue.Number, issue.URL})) if fromPR > 0 { if err := issueService.LinkIssueToPR(ctx, fromPR, issue.Number); err != nil { - ui.PrintWarning(t.GetMessage("issue.link_error", 0, map[string]interface{}{ - "PR": fromPR, - "Error": err, - })) + ui.PrintWarning(t.GetMessage("issue.link_error", 0, struct { + PR int + Error error + }{fromPR, err})) } else { - ui.PrintInfo(t.GetMessage("issue.link_success", 0, map[string]interface{}{ - "PR": fromPR, - "Issue": issue.Number, - })) + ui.PrintInfo(t.GetMessage("issue.link_success", 0, struct { + PR int + Issue int + }{fromPR, issue.Number})) } } if checkoutBranch { branchName := issueService.InferBranchName(issue.Number, result.Labels) - ui.PrintInfo(t.GetMessage("issue.creating_branch", 0, map[string]interface{}{ - "Branch": branchName, - })) + ui.PrintInfo(t.GetMessage("issue.creating_branch", 0, struct{ Branch string }{branchName})) if err := f.checkoutBranch(branchName); err != nil { ui.PrintWarning(fmt.Sprintf("%s: %v", t.GetMessage("issue.warn_checkout_failed", 0, nil), err)) } else { - ui.PrintSuccess(os.Stdout, t.GetMessage("issue.branch_created", 0, map[string]interface{}{ - "Branch": branchName, - })) + ui.PrintSuccess(os.Stdout, t.GetMessage("issue.branch_created", 0, struct{ Branch string }{branchName})) } } @@ -385,10 +377,10 @@ func (f *IssuesCommandFactory) createLinkAction(t *i18n.Translations, _ *config. return err } - spinner := ui.NewSmartSpinner(t.GetMessage("issue.linking", 0, map[string]interface{}{ - "PR": prNumber, - "Issue": issueNumber, - })) + spinner := ui.NewSmartSpinner(t.GetMessage("issue.linking", 0, struct { + PR int + Issue int + }{prNumber, issueNumber})) spinner.Start() err = issueService.LinkIssueToPR(ctx, prNumber, issueNumber) @@ -399,10 +391,10 @@ func (f *IssuesCommandFactory) createLinkAction(t *i18n.Translations, _ *config. return err } - ui.PrintSuccess(os.Stdout, t.GetMessage("issue.link_success", 0, map[string]interface{}{ - "PR": prNumber, - "Issue": issueNumber, - })) + ui.PrintSuccess(os.Stdout, t.GetMessage("issue.link_success", 0, struct { + PR int + Issue int + }{prNumber, issueNumber})) return nil } diff --git a/internal/commands/issues/templates.go b/internal/commands/issues/templates.go index c5c8d17..d3eece2 100644 --- a/internal/commands/issues/templates.go +++ b/internal/commands/issues/templates.go @@ -42,9 +42,7 @@ func (f *IssuesCommandFactory) newTemplateCommand(t *i18n.Translations, _ *confi } templatesDir, _ := templateService.GetTemplatesDir() - ui.PrintSuccess(os.Stdout, t.GetMessage("issue.template_init_success", 0, map[string]interface{}{ - "Dir": templatesDir, - })) + ui.PrintSuccess(os.Stdout, t.GetMessage("issue.template_init_success", 0, struct{ Dir string }{templatesDir})) return nil }, diff --git a/internal/commands/pull_requests/summarize.go b/internal/commands/pull_requests/summarize.go index 962a272..8752ce7 100644 --- a/internal/commands/pull_requests/summarize.go +++ b/internal/commands/pull_requests/summarize.go @@ -6,8 +6,8 @@ import ( "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" cfg "github.com/thomas-vilte/matecommit/internal/config" - "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -54,28 +54,18 @@ func (c *SummarizeCommand) CreateCommand(t *i18n.Translations, _ *cfg.Config) *c return fmt.Errorf("%s", t.GetMessage("error.pr_number_required", 0, nil)) } - spinner := ui.NewSmartSpinner(t.GetMessage("ui.fetching_pr_info", 0, map[string]interface{}{ - "Number": prNumber, - })) + spinner := ui.NewSmartSpinner(t.GetMessage("ui.fetching_pr_info", 0, struct{ Number int }{prNumber})) spinner.Start() summary, err := prService.SummarizePR(ctx, prNumber, func(event models.ProgressEvent) { msg := "" switch event.Type { case models.ProgressIssuesDetected: - issues := event.Data["Issues"].([]string) - msg = t.GetMessage("vcs_summary.issues_detected", 0, map[string]interface{}{ - "PRNumber": event.Data["PRNumber"], - "Issues": fmt.Sprintf("%v", issues), - }) + msg = t.GetMessage("vcs_summary.issues_detected", 0, event.Data) case models.ProgressIssuesClosing: - msg = t.GetMessage("vcs_summary.issues_closing", 0, map[string]interface{}{ - "Count": event.Data["Count"], - }) + msg = t.GetMessage("vcs_summary.issues_closing", 0, event.Data) case models.ProgressBreakingChanges: - msg = t.GetMessage("vcs_summary.breaking_changes_detected", 0, map[string]interface{}{ - "Count": event.Data["Count"], - }) + msg = t.GetMessage("vcs_summary.breaking_changes_detected", 0, event.Data) case models.ProgressTestPlan: msg = t.GetMessage("vcs_summary.test_plan_generated", 0, nil) default: @@ -91,10 +81,10 @@ func (c *SummarizeCommand) CreateCommand(t *i18n.Translations, _ *cfg.Config) *c return fmt.Errorf(t.GetMessage("error.pr_summary_error", 0, nil)+": %w", err) } - spinner.Success(t.GetMessage("ui.pr_updated_successfully", 0, map[string]interface{}{ - "Number": prNumber, - "Title": summary.Title, - })) + spinner.Success(t.GetMessage("ui.pr_updated_successfully", 0, struct { + Number int + Title string + }{prNumber, summary.Title})) if summary.Usage != nil { fmt.Println() diff --git a/internal/commands/release/create.go b/internal/commands/release/create.go index 804447c..50dec0c 100644 --- a/internal/commands/release/create.go +++ b/internal/commands/release/create.go @@ -68,9 +68,7 @@ func createReleaseAction(releaseSvc releaseService, trans *i18n.Translations, re release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, struct{ Error string }{err.Error()})) } if version := cmd.String("version"); version != "" { @@ -78,18 +76,13 @@ func createReleaseAction(releaseSvc releaseService, trans *i18n.Translations, re } if err := releaseSvc.EnrichReleaseContext(ctx, release); err != nil { - fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, map[string]interface{}{ - "Error": err.Error(), - })) + fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, struct{ Error string }{err.Error()})) } notes, err := releaseSvc.GenerateReleaseNotes(ctx, release) if err != nil { ui.HandleAppError(err, trans) - return fmt.Errorf("%s", trans.GetMessage("release.error_generating_notes", 0, - map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_generating_notes", 0, struct{ Error string }{err.Error()})) } updateChangelog := cmd.Bool("changelog") @@ -102,29 +95,25 @@ func createReleaseAction(releaseSvc releaseService, trans *i18n.Translations, re s.Start() if err := releaseSvc.UpdateLocalChangelog(release, notes); err != nil { - s.Error(trans.GetMessage("release.error_updating_changelog", 0, map[string]interface{}{ - "Error": err.Error(), - })) + s.Error(trans.GetMessage("release.error_updating_changelog", 0, struct{ Error string }{err.Error()})) return fmt.Errorf("%w", err) } s.Success(trans.GetMessage("release.changelog_updated", 0, nil)) fmt.Println() - sVersion := ui.NewSmartSpinner(trans.GetMessage("release.app_version_update_started", 0, map[string]interface{}{"Version": release.Version})) + sVersion := ui.NewSmartSpinner(trans.GetMessage("release.app_version_update_started", 0, struct{ Version string }{release.Version})) sVersion.Start() if err := releaseSvc.UpdateAppVersion(release.Version); err != nil { - sVersion.Error(trans.GetMessage("release.error_updating_app_version", 0, map[string]interface{}{"Error": err.Error()})) + sVersion.Error(trans.GetMessage("release.error_updating_app_version", 0, struct{ Error string }{err.Error()})) return fmt.Errorf("error updating app version: %w", err) } - sVersion.Success(trans.GetMessage("release.app_version_updated", 0, map[string]interface{}{"Version": release.Version})) + sVersion.Success(trans.GetMessage("release.app_version_updated", 0, struct{ Version string }{release.Version})) fmt.Println() sCommit := ui.NewSmartSpinner(trans.GetMessage("release.committing_changelog", 0, nil)) sCommit.Start() if err := releaseSvc.CommitChangelog(ctx, release.Version); err != nil { - sCommit.Error(trans.GetMessage("release.error_committing_changelog", 0, map[string]interface{}{ - "Error": err.Error(), - })) + sCommit.Error(trans.GetMessage("release.error_committing_changelog", 0, struct{ Error string }{err.Error()})) return fmt.Errorf("error committing changelog: %w", err) } sCommit.Success(trans.GetMessage("release.changelog_committed", 0, nil)) @@ -133,27 +122,23 @@ func createReleaseAction(releaseSvc releaseService, trans *i18n.Translations, re sPush := ui.NewSmartSpinner(trans.GetMessage("release.pushing_changes", 0, nil)) sPush.Start() if err := releaseSvc.PushChanges(ctx); err != nil { - sPush.Error(trans.GetMessage("release.error_pushing_changes", 0, map[string]interface{}{ - "Error": err.Error(), - })) + sPush.Error(trans.GetMessage("release.error_pushing_changes", 0, struct{ Error string }{err.Error()})) return fmt.Errorf("error pushing changes: %w", err) } sPush.Success(trans.GetMessage("release.changes_pushed", 0, nil)) fmt.Println() } - fmt.Println(trans.GetMessage("release.create_preview", 0, map[string]interface{}{ - "Version": release.Version, - "Bump": release.VersionBump, - })) - fmt.Println(trans.GetMessage("release.create_title", 0, map[string]interface{}{ - "Title": notes.Title, - })) - fmt.Println(trans.GetMessage("release.create_stats", 0, map[string]interface{}{ - "Features": len(release.Features), - "Fixes": len(release.BugFixes), - "Breaking": len(release.Breaking), - })) + fmt.Println(trans.GetMessage("release.create_preview", 0, struct { + Version string + Bump string + }{release.Version, string(release.VersionBump)})) + fmt.Println(trans.GetMessage("release.create_title", 0, struct{ Title string }{notes.Title})) + fmt.Println(trans.GetMessage("release.create_stats", 0, struct { + Features int + Fixes int + Breaking int + }{len(release.Features), len(release.BugFixes), len(release.Breaking)})) fmt.Println() if !cmd.Bool("auto") { @@ -170,14 +155,10 @@ func createReleaseAction(releaseSvc releaseService, trans *i18n.Translations, re message := fmt.Sprintf("%s\n\n%s", notes.Title, notes.Summary) err = releaseSvc.CreateTag(ctx, release.Version, message) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_creating_tag", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_creating_tag", 0, struct{ Error string }{err.Error()})) } - fmt.Println(trans.GetMessage("release.tag_created", 0, map[string]interface{}{ - "Version": release.Version, - })) + fmt.Println(trans.GetMessage("release.tag_created", 0, struct{ Version string }{release.Version})) if cmd.Bool("publish") { notes.Changelog = FormatReleaseMarkdown(release, notes, trans) @@ -186,18 +167,14 @@ func createReleaseAction(releaseSvc releaseService, trans *i18n.Translations, re buildBinaries := cmd.Bool("build-binaries") err := releaseSvc.PublishRelease(ctx, release, notes, cmd.Bool("draft"), buildBinaries) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_publishing_release", 0, map[string]interface{}{"Error": err.Error()})) + return fmt.Errorf("%s", trans.GetMessage("release.error_publishing_release", 0, struct{ Error string }{err.Error()})) } fmt.Println(trans.GetMessage("release.release_published", 0, nil)) } else { fmt.Println() fmt.Println(trans.GetMessage("release.create_next_steps", 0, nil)) - fmt.Println(trans.GetMessage("release.create_review", 0, map[string]interface{}{ - "Version": release.Version, - })) - fmt.Println(trans.GetMessage("release.create_push", 0, map[string]interface{}{ - "Version": release.Version, - })) + fmt.Println(trans.GetMessage("release.create_review", 0, struct{ Version string }{release.Version})) + fmt.Println(trans.GetMessage("release.create_push", 0, struct{ Version string }{release.Version})) fmt.Println(trans.GetMessage("release.create_push_help", 0, nil)) } diff --git a/internal/commands/release/edit.go b/internal/commands/release/edit.go index d3fcfd6..06f8759 100644 --- a/internal/commands/release/edit.go +++ b/internal/commands/release/edit.go @@ -7,8 +7,8 @@ import ( "os/exec" "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" - "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -55,16 +55,12 @@ func editReleaseAction(releaseSvc releaseService, gitSvc gitService, trans *i18n editor := cmd.String("editor") useAI := cmd.Bool("ai") - fmt.Println(trans.GetMessage("release.fetching_release", 0, map[string]interface{}{ - "Version": version, - })) + fmt.Println(trans.GetMessage("release.fetching_release", 0, struct{ Version string }{version})) existingRelease, err := releaseSvc.GetRelease(ctx, version) if err != nil { ui.HandleAppError(err, trans) - return fmt.Errorf("%s", trans.GetMessage("release.error_fetching_release", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_fetching_release", 0, struct{ Error string }{err.Error()})) } content := existingRelease.Body @@ -78,18 +74,14 @@ func editReleaseAction(releaseSvc releaseService, gitSvc gitService, trans *i18n previousVersion, err := getPreviousVersion(version) if err != nil { - fmt.Printf("⚠️ %s\n", trans.GetMessage("release.error_calculating_previous", 0, map[string]interface{}{ - "Error": err.Error(), - })) + fmt.Printf("⚠️ %s\n", trans.GetMessage("release.error_calculating_previous", 0, struct{ Error string }{err.Error()})) if content == "" { content = generateReleaseTemplate(existingRelease.Name, trans) } } else { commits, err := gitSvc.GetCommitsSinceTag(ctx, previousVersion) if err != nil { - fmt.Printf("⚠️ %s\n", trans.GetMessage("release.error_getting_commits", 0, map[string]interface{}{ - "Error": err.Error(), - })) + fmt.Printf("⚠️ %s\n", trans.GetMessage("release.error_getting_commits", 0, struct{ Error string }{err.Error()})) if content == "" { content = generateReleaseTemplate(existingRelease.Name, trans) } @@ -101,16 +93,12 @@ func editReleaseAction(releaseSvc releaseService, gitSvc gitService, trans *i18n } if err := releaseSvc.EnrichReleaseContext(ctx, release); err != nil { - fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, map[string]interface{}{ - "Error": err.Error(), - })) + fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, struct{ Error string }{err.Error()})) } notes, err := releaseSvc.GenerateReleaseNotes(ctx, release) if err != nil { - fmt.Printf("⚠️ %s\n", trans.GetMessage("release.error_generating_for_regen", 0, map[string]interface{}{ - "Error": err.Error(), - })) + fmt.Printf("⚠️ %s\n", trans.GetMessage("release.error_generating_for_regen", 0, struct{ Error string }{err.Error()})) if content == "" { content = generateReleaseTemplate(existingRelease.Name, trans) } @@ -123,9 +111,7 @@ func editReleaseAction(releaseSvc releaseService, gitSvc gitService, trans *i18n } tmpFile, err := os.CreateTemp("", fmt.Sprintf("release-%s-*.md", version)) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_creating_temp", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_creating_temp", 0, struct{ Error string }{err.Error()})) } defer func() { if err := os.Remove(tmpFile.Name()); err != nil { @@ -134,41 +120,27 @@ func editReleaseAction(releaseSvc releaseService, gitSvc gitService, trans *i18n }() _, err = tmpFile.WriteString(content) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_writing_temp", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_writing_temp", 0, struct{ Error string }{err.Error()})) } _ = tmpFile.Close() - fmt.Println(trans.GetMessage("release.opening_editor", 0, map[string]interface{}{ - "Editor": editor, - })) + fmt.Println(trans.GetMessage("release.opening_editor", 0, struct{ Editor string }{editor})) editorCmd := exec.CommandContext(ctx, editor, tmpFile.Name()) editorCmd.Stdin = os.Stdin editorCmd.Stdout = os.Stdout editorCmd.Stderr = os.Stderr if err := editorCmd.Run(); err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_running_editor", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_running_editor", 0, struct{ Error string }{err.Error()})) } editedContent, err := os.ReadFile(tmpFile.Name()) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_reading_temp", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_reading_temp", 0, struct{ Error string }{err.Error()})) } - fmt.Println(trans.GetMessage("release.updating_release", 0, map[string]interface{}{ - "Version": version, - })) + fmt.Println(trans.GetMessage("release.updating_release", 0, struct{ Version string }{version})) err = releaseSvc.UpdateRelease(ctx, version, string(editedContent)) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_updating_release", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_updating_release", 0, struct{ Error string }{err.Error()})) } - fmt.Println(trans.GetMessage("release.edit_success", 0, map[string]interface{}{ - "Version": version, - })) + fmt.Println(trans.GetMessage("release.edit_success", 0, struct{ Version string }{version})) return nil } } diff --git a/internal/commands/release/formatter.go b/internal/commands/release/formatter.go index 86e1e61..8bb3d08 100644 --- a/internal/commands/release/formatter.go +++ b/internal/commands/release/formatter.go @@ -4,11 +4,11 @@ import ( "fmt" "strings" - "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" ) -// FormatReleaseMarkdown generates the full release markdown with all sections +// FormatReleaseMarkdown generates the full release Markdown with all sections func FormatReleaseMarkdown(release *models.Release, notes *models.ReleaseNotes, trans *i18n.Translations) string { content := fmt.Sprintf("# %s\n\n", notes.Title) var md strings.Builder @@ -106,9 +106,7 @@ func FormatReleaseMarkdown(release *models.Release, notes *models.ReleaseNotes, md.WriteString("\n\n") if len(release.NewContributors) > 0 { - md.WriteString(trans.GetMessage("release.new_contributors", 0, map[string]interface{}{ - "Count": len(release.NewContributors), - })) + md.WriteString(trans.GetMessage("release.new_contributors", 0, struct{ Count int }{len(release.NewContributors)})) md.WriteString(" ") for i, contributor := range release.NewContributors { md.WriteString(fmt.Sprintf("@%s", contributor)) diff --git a/internal/commands/release/generate.go b/internal/commands/release/generate.go index 2fb8b86..d9657e0 100644 --- a/internal/commands/release/generate.go +++ b/internal/commands/release/generate.go @@ -43,24 +43,17 @@ func generateReleaseAction(releaseSvc releaseService, trans *i18n.Translations) release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { ui.HandleAppError(err, trans) - return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, struct{ Error string }{err.Error()})) } if err := releaseSvc.EnrichReleaseContext(ctx, release); err != nil { - fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, map[string]interface{}{ - "Error": err.Error(), - })) + fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, struct{ Error string }{err.Error()})) } notes, err := releaseSvc.GenerateReleaseNotes(ctx, release) if err != nil { ui.HandleAppError(err, trans) - return fmt.Errorf("%s", trans.GetMessage("release.error_generating_notes", 0, - map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_generating_notes", 0, struct{ Error string }{err.Error()})) } content := FormatReleaseMarkdown(release, notes, trans) @@ -68,17 +61,11 @@ func generateReleaseAction(releaseSvc releaseService, trans *i18n.Translations) outputFile := cmd.String("output") err = os.WriteFile(outputFile, []byte(content), 0644) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_writing_file", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_writing_file", 0, struct{ Error string }{err.Error()})) } - fmt.Println(trans.GetMessage("release.notes_saved", 0, map[string]interface{}{ - "File": outputFile, - })) - fmt.Println(trans.GetMessage("release.version_label", 0, map[string]interface{}{ - "Version": release.Version, - })) + fmt.Println(trans.GetMessage("release.notes_saved", 0, struct{ File string }{outputFile})) + fmt.Println(trans.GetMessage("release.version_label", 0, struct{ Version string }{release.Version})) if notes.Usage != nil { fmt.Println() diff --git a/internal/commands/release/preview.go b/internal/commands/release/preview.go index 954e977..6792bd2 100644 --- a/internal/commands/release/preview.go +++ b/internal/commands/release/preview.go @@ -33,56 +33,35 @@ func previewReleaseAction(releaseSvc releaseService, trans *i18n.Translations) c release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, struct{ Error string }{err.Error()})) } - fmt.Println(trans.GetMessage("release.previous_version", 0, map[string]interface{}{ - "Version": release.PreviousVersion, - })) - fmt.Println(trans.GetMessage("release.next_version", 0, map[string]interface{}{ - "Version": release.Version, - "Bump": release.VersionBump, - })) + fmt.Println(trans.GetMessage("release.previous_version", 0, struct{ Version string }{release.PreviousVersion})) + fmt.Println(trans.GetMessage("release.next_version", 0, struct { + Version string + Bump string + }{release.Version, string(release.VersionBump)})) fmt.Println() fmt.Println(trans.GetMessage("release.changes_summary", 0, nil)) if len(release.Breaking) > 0 { - fmt.Println(trans.GetMessage("release.breaking_changes", 0, map[string]interface{}{ - "Count": len(release.Breaking), - })) + fmt.Println(trans.GetMessage("release.breaking_changes", 0, struct{ Count int }{len(release.Breaking)})) } - fmt.Println(trans.GetMessage("release.new_features", 0, map[string]interface{}{ - "Count": len(release.Features), - })) - fmt.Println(trans.GetMessage("release.bug_fixes", 0, map[string]interface{}{ - "Count": len(release.BugFixes), - })) - fmt.Println(trans.GetMessage("release.improvements", 0, map[string]interface{}{ - "Count": len(release.Improvements), - })) - fmt.Println(trans.GetMessage("release.documentation", 0, map[string]interface{}{ - "Count": len(release.Documentation), - })) - fmt.Println(trans.GetMessage("release.total_commits", 0, map[string]interface{}{ - "Count": len(release.AllCommits), - })) + fmt.Println(trans.GetMessage("release.new_features", 0, struct{ Count int }{len(release.Features)})) + fmt.Println(trans.GetMessage("release.bug_fixes", 0, struct{ Count int }{len(release.BugFixes)})) + fmt.Println(trans.GetMessage("release.improvements", 0, struct{ Count int }{len(release.Improvements)})) + fmt.Println(trans.GetMessage("release.documentation", 0, struct{ Count int }{len(release.Documentation)})) + fmt.Println(trans.GetMessage("release.total_commits", 0, struct{ Count int }{len(release.AllCommits)})) fmt.Println() if err := releaseSvc.EnrichReleaseContext(ctx, release); err != nil { - fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, map[string]interface{}{ - "Error": err.Error(), - })) + fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, struct{ Error string }{err.Error()})) } notes, err := releaseSvc.GenerateReleaseNotes(ctx, release) if err != nil { ui.HandleAppError(err, trans) - return fmt.Errorf("%s", trans.GetMessage("release.error_generating_notes", 0, - map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_generating_notes", 0, struct{ Error string }{err.Error()})) } fmt.Println(trans.GetMessage("release.separator", 0, nil)) diff --git a/internal/commands/release/publish.go b/internal/commands/release/publish.go index f5ac2e0..a2d59bb 100644 --- a/internal/commands/release/publish.go +++ b/internal/commands/release/publish.go @@ -49,9 +49,7 @@ func publishReleaseAction(releaseSvc releaseService, return func(ctx context.Context, cmd *cli.Command) error { release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, struct{ Error string }{err.Error()})) } if version := cmd.String("version"); version != "" { @@ -59,17 +57,13 @@ func publishReleaseAction(releaseSvc releaseService, } if err := releaseSvc.EnrichReleaseContext(ctx, release); err != nil { - fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, map[string]interface{}{ - "Error": err.Error(), - })) + fmt.Printf("⚠️ %s\n", trans.GetMessage("release.warning_enrich_context", 0, struct{ Error string }{err.Error()})) } notes, err := releaseSvc.GenerateReleaseNotes(ctx, release) if err != nil { ui.HandleAppError(err, trans) - return fmt.Errorf("%s", trans.GetMessage("release.error_generating_notes", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_generating_notes", 0, struct{ Error string }{err.Error()})) } draft := cmd.Bool("draft") @@ -79,21 +73,17 @@ func publishReleaseAction(releaseSvc releaseService, draftText = " " + trans.GetMessage("release.as_draft", 0, nil) } - fmt.Println(trans.GetMessage("release.publishing", 0, map[string]interface{}{ - "Version": release.Version, - "Draft": draftText, - })) + fmt.Println(trans.GetMessage("release.publishing", 0, struct { + Version string + Draft string + }{release.Version, draftText})) err = releaseSvc.PublishRelease(ctx, release, notes, draft, buildBinaries) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_publishing", 0, map[string]interface{}{ - "Error": err.Error(), - })) + return fmt.Errorf("%s", trans.GetMessage("release.error_publishing", 0, struct{ Error string }{err.Error()})) } - fmt.Println(trans.GetMessage("release.publish_success", 0, map[string]interface{}{ - "Version": release.Version, - })) + fmt.Println(trans.GetMessage("release.publish_success", 0, struct{ Version string }{release.Version})) if notes.Usage != nil { fmt.Println() diff --git a/internal/commands/release/push.go b/internal/commands/release/push.go index b59d83e..43790b3 100644 --- a/internal/commands/release/push.go +++ b/internal/commands/release/push.go @@ -39,20 +39,20 @@ func pushReleaseAction(releaseSvc releaseService, trans *i18n.Translations) cli. if version == "" { release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { - return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{"Error": err.Error()})) + return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, struct{ Error string }{err.Error()})) } version = release.Version } - fmt.Println(trans.GetMessage("release.pushing_tag", 0, map[string]interface{}{"Version": version})) + fmt.Println(trans.GetMessage("release.pushing_tag", 0, struct{ Version string }{version})) err := releaseSvc.PushTag(ctx, version) if err != nil { ui.HandleAppError(err, trans) - return fmt.Errorf("%s", trans.GetMessage("release.error_pushing_tag", 0, map[string]interface{}{"Error": err.Error()})) + return fmt.Errorf("%s", trans.GetMessage("release.error_pushing_tag", 0, struct{ Error string }{err.Error()})) } - fmt.Println(trans.GetMessage("release.push_success", 0, map[string]interface{}{"Version": version})) + fmt.Println(trans.GetMessage("release.push_success", 0, struct{ Version string }{version})) return nil } } diff --git a/internal/commands/release/release.go b/internal/commands/release/release.go index 2cd5d70..a5da799 100644 --- a/internal/commands/release/release.go +++ b/internal/commands/release/release.go @@ -78,7 +78,7 @@ func (r *ReleaseCommandFactory) CreateCommand(t *i18n.Translations, _ *cfg.Confi } } -func (r *ReleaseCommandFactory) createReleaseService(ctx context.Context, t *i18n.Translations) (*services.ReleaseService, error) { +func (r *ReleaseCommandFactory) createReleaseService(ctx context.Context, _ *i18n.Translations) (*services.ReleaseService, error) { var vcsClient ports.VCSClient if r.config.ActiveVCSProvider != "" { if vcsConfig, ok := r.config.VCSConfigs[r.config.ActiveVCSProvider]; ok { @@ -93,7 +93,7 @@ func (r *ReleaseCommandFactory) createReleaseService(ctx context.Context, t *i18 if r.config.AIConfig.ActiveAI == "gemini" { owner, repo, _, err := r.gitService.GetRepoInfo(ctx) if err != nil { - return nil, fmt.Errorf("error obteniendo informacion del repositorio: %w", err) + return nil, fmt.Errorf("error retrieving information from repository: %w", err) } gen, err := gemini.NewReleaseNotesGenerator(ctx, r.config, nil, owner, repo) diff --git a/internal/commands/stats/stats.go b/internal/commands/stats/stats.go index af2cfa1..a08cc94 100644 --- a/internal/commands/stats/stats.go +++ b/internal/commands/stats/stats.go @@ -5,10 +5,10 @@ import ( "fmt" "time" + "github.com/fatih/color" "github.com/thomas-vilte/matecommit/internal/config" "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/thomas-vilte/matecommit/internal/services/cost" - "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -107,9 +107,7 @@ func (c *StatsCommand) showMonthlyStats(manager *cost.Manager, t *i18n.Translati } cyan := color.New(color.FgCyan, color.Bold) yellow := color.New(color.FgYellow) - _, _ = cyan.Printf("\n📅 %s\n", t.GetMessage("stats.monthly_title", 0, map[string]interface{}{ - "Month": time.Now().Format("January 2006"), - })) + _, _ = cyan.Printf("\n📅 %s\n", t.GetMessage("stats.monthly_title", 0, struct{ Month string }{time.Now().Format("January 2006")})) fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") if len(monthRecords) == 0 { fmt.Printf("\n%s\n\n", t.GetMessage("stats.no_activity", 0, nil)) diff --git a/internal/commands/suggests_commits/suggests_commits.go b/internal/commands/suggests_commits/suggests_commits.go index 847ffda..dea177f 100644 --- a/internal/commands/suggests_commits/suggests_commits.go +++ b/internal/commands/suggests_commits/suggests_commits.go @@ -8,8 +8,8 @@ import ( "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" "github.com/thomas-vilte/matecommit/internal/config" - "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -87,10 +87,10 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla } count := command.Int("count") if count < 1 || count > 10 { - msg := t.GetMessage("invalid_suggestions_count", 0, map[string]interface{}{ - "Min": 1, - "Max": 10, - }) + msg := t.GetMessage("invalid_suggestions_count", 0, struct { + Min int + Max int + }{1, 10}) ui.PrintError(os.Stdout, msg) return fmt.Errorf("%s", msg) } @@ -121,14 +121,11 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla msg := "" switch event.Type { case models.ProgressIssuesDetected: - title := event.Data["Title"].(string) - id := event.Data["IssueID"].(int) - isAuto := event.Data["IsAuto"].(bool) key := "issue_detected_auto" - if !isAuto { + if !event.Data.IsAuto { key = "issue_using_manual" } - msg = fmt.Sprintf("%s: #%d - %s", key, id, title) // TODO: use translations for key + msg = t.GetMessage(key, 0, event.Data) case models.ProgressGeneric: msg = event.Message default: @@ -145,15 +142,11 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla if err != nil { spinner.Error(t.GetMessage("ui.error_generating_suggestions", 0, nil)) ui.HandleAppError(err, t) - return fmt.Errorf("%s", t.GetMessage("suggestion_generation_error", 0, map[string]interface{}{ - "Error": err, - })) + return fmt.Errorf("%s", t.GetMessage("suggestion_generation_error", 0, struct{ Error error }{err})) } spinner.Stop() - ui.PrintDuration(t.GetMessage("ui.suggestions_generated", 0, map[string]interface{}{ - "Count": len(suggestions), - }), duration) + ui.PrintDuration(t.GetMessage("ui.suggestions_generated", 0, struct{ Count int }{len(suggestions)}), duration) return f.commitHandler.HandleSuggestions(ctx, suggestions) } } diff --git a/internal/commands/suggests_commits/suggests_commits_test.go b/internal/commands/suggests_commits/suggests_commits_test.go index 0e2f383..f49c28a 100644 --- a/internal/commands/suggests_commits/suggests_commits_test.go +++ b/internal/commands/suggests_commits/suggests_commits_test.go @@ -7,11 +7,11 @@ import ( "path/filepath" "testing" - "github.com/thomas-vilte/matecommit/internal/config" - "github.com/thomas-vilte/matecommit/internal/models" - "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" ) type MockCommitService struct { @@ -110,10 +110,10 @@ func TestSuggestCommand(t *testing.T) { // Assert assert.Error(t, err) - assert.Contains(t, err.Error(), translations.GetMessage("invalid_suggestions_count", 0, map[string]interface{}{ - "Min": 1, - "Max": 10, - })) + assert.Contains(t, err.Error(), translations.GetMessage("invalid_suggestions_count", 0, struct { + Min int + Max int + }{1, 10})) mockService.AssertNotCalled(t, "GenerateSuggestions") mockHandler.AssertNotCalled(t, "HandleSuggestions") }) @@ -207,9 +207,7 @@ func TestSuggestCommand(t *testing.T) { // Assert assert.Error(t, err) - assert.Contains(t, err.Error(), translations.GetMessage("suggestion_generation_error", 0, map[string]interface{}{ - "Error": expectedError, - })) + assert.Contains(t, err.Error(), translations.GetMessage("suggestion_generation_error", 0, struct{ Error error }{expectedError})) mockService.AssertExpectations(t) mockHandler.AssertNotCalled(t, "HandleSuggestions") }) diff --git a/internal/dependency/gomod_analyzer.go b/internal/dependency/gomod_analyzer.go index 3ab6845..c288cd1 100644 --- a/internal/dependency/gomod_analyzer.go +++ b/internal/dependency/gomod_analyzer.go @@ -2,13 +2,13 @@ package dependency import ( "context" - "regexp" "strconv" "strings" 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/regex" ) var _ ports.DependencyAnalyzer = (*GoModAnalyzer)(nil) @@ -60,11 +60,6 @@ type goDep struct { func (g *GoModAnalyzer) parseGoMod(content string) map[string]goDep { deps := make(map[string]goDep) - // Regex for lines inside require() - requireRegex := regexp.MustCompile(`^\s+(\S+)\s+v?(\S+)(\s+//\s*indirect)?`) - // Regex for individual require lines - singleRequireRegex := regexp.MustCompile(`^require\s+(\S+)\s+v?(\S+)(\s+//\s*indirect)?`) - inRequire := false lines := strings.Split(content, "\n") @@ -82,7 +77,7 @@ func (g *GoModAnalyzer) parseGoMod(content string) map[string]goDep { } if inRequire { - matches := requireRegex.FindStringSubmatch(line) + matches := regex.GoModRequireBlock.FindStringSubmatch(line) if len(matches) >= 3 { module := matches[1] version := matches[2] @@ -94,7 +89,7 @@ func (g *GoModAnalyzer) parseGoMod(content string) map[string]goDep { } } } else if strings.HasPrefix(trimmedLine, "require ") { - matches := singleRequireRegex.FindStringSubmatch(trimmedLine) + matches := regex.GoModRequireSingle.FindStringSubmatch(trimmedLine) if len(matches) >= 3 { module := matches[1] version := matches[2] diff --git a/internal/git/git_service.go b/internal/git/git_service.go index eab3db8..f3bfe66 100644 --- a/internal/git/git_service.go +++ b/internal/git/git_service.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "os/exec" - "regexp" "strings" "github.com/thomas-vilte/matecommit/internal/errors" "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/regex" ) type GitService struct{} @@ -281,14 +281,11 @@ func (s *GitService) GetTagDate(ctx context.Context, tag string) (string, error) } func parseRepoURL(url string) (string, string, string, error) { - sshRegex := regexp.MustCompile(`git@([^:]+):([^/]+)/(.+)\.git$`) - httpsRegex := regexp.MustCompile(`https://([^/]+)/([^/]+)/(.+?)(?:\.git)?$`) - var matches []string - if sshRegex.MatchString(url) { - matches = sshRegex.FindStringSubmatch(url) - } else if httpsRegex.MatchString(url) { - matches = httpsRegex.FindStringSubmatch(url) + if regex.SSHRepo.MatchString(url) { + matches = regex.SSHRepo.FindStringSubmatch(url) + } else if regex.HTTPSRepo.MatchString(url) { + matches = regex.HTTPSRepo.FindStringSubmatch(url) } if len(matches) >= 4 { diff --git a/internal/github/github_service.go b/internal/github/github_service.go index 6c07046..d1e4ab7 100644 --- a/internal/github/github_service.go +++ b/internal/github/github_service.go @@ -12,10 +12,11 @@ import ( "strings" "time" + "github.com/google/go-github/v80/github" + "github.com/thomas-vilte/matecommit/internal/builder" "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/ports" - "github.com/thomas-vilte/matecommit/internal/builder" - "github.com/google/go-github/v80/github" + "github.com/thomas-vilte/matecommit/internal/regex" "golang.org/x/oauth2" ) @@ -603,17 +604,8 @@ func extractAcceptanceCriteria(body string) []string { var criteria []string lines := strings.Split(body, "\n") - // Regex to capture markdown checkboxes: - // ^ - Start of line (allowing leading spaces) - // [\-\*\+] - Bullet: - or * or + - // \s+ - Required space - // \[([ xX])\] - Checkbox: [ ], [x], [X] - // \s+ - Required space - // (.+) - The criterion text - re := regexp.MustCompile(`^\s*[\-*+]\s+\[([ xX])]\s+(.+)`) - for _, line := range lines { - matches := re.FindStringSubmatch(line) + matches := regex.MarkdownCheckbox.FindStringSubmatch(line) if len(matches) > 2 { criterion := strings.TrimSpace(matches[2]) if criterion != "" { @@ -649,17 +641,13 @@ func (ghc *GitHubClient) GetFileAtTag(ctx context.Context, tag, filepath string) func (ghc *GitHubClient) GetPRIssues(ctx context.Context, branchName string, commits []string, prDescription string) ([]models.Issue, error) { issueNumbers := make(map[int]bool) - - branchPatterns := []string{ - `#(\d+)`, - `issue[/-](\d+)`, - `^(\d+)-`, - `/(\d+)-`, - `-(\d+)-`, - } - - for _, pattern := range branchPatterns { - re := regexp.MustCompile(pattern) + for _, re := range []*regexp.Regexp{ + regex.BranchIssueSharp, + regex.BranchIssueName, + regex.BranchIssueStart, + regex.BranchIssueFolder, + regex.BranchIssueMid, + } { if matches := re.FindStringSubmatch(branchName); len(matches) > 1 { if num, err := strconv.Atoi(matches[1]); err == nil { issueNumbers[num] = true @@ -668,39 +656,46 @@ func (ghc *GitHubClient) GetPRIssues(ctx context.Context, branchName string, com } if prDescription != "" { - descPatterns := []string{ - `(?i)(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)`, - `#(\d+)`, - } - - for _, pattern := range descPatterns { - re := regexp.MustCompile(pattern) - matches := re.FindAllStringSubmatch(prDescription, -1) - for _, match := range matches { - if len(match) > 1 { - if num, err := strconv.Atoi(match[1]); err == nil { - issueNumbers[num] = true - } + matches := regex.GitHubClosedLink.FindAllStringSubmatch(prDescription, -1) + for _, match := range matches { + if len(match) > 1 { + if num, err := strconv.Atoi(match[1]); err == nil { + issueNumbers[num] = true + } + } + } + matchesSharp := regex.BranchIssueSharp.FindAllStringSubmatch(prDescription, -1) + for _, match := range matchesSharp { + if len(match) > 1 { + if num, err := strconv.Atoi(match[1]); err == nil { + issueNumbers[num] = true } } } - } - - commitPatterns := []string{ - `(?i)(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)`, - `\(#(\d+)\)`, - `#(\d+)`, } for _, commit := range commits { - for _, pattern := range commitPatterns { - re := regexp.MustCompile(pattern) - matches := re.FindAllStringSubmatch(commit, -1) - for _, match := range matches { - if len(match) > 1 { - if num, err := strconv.Atoi(match[1]); err == nil { - issueNumbers[num] = true - } + matches := regex.GitHubClosedLink.FindAllStringSubmatch(commit, -1) + for _, match := range matches { + if len(match) > 1 { + if num, err := strconv.Atoi(match[1]); err == nil { + issueNumbers[num] = true + } + } + } + matchesPR := regex.GitHubPR.FindAllStringSubmatch(commit, -1) + for _, match := range matchesPR { + if len(match) > 1 { + if num, err := strconv.Atoi(match[1]); err == nil { + issueNumbers[num] = true + } + } + } + matchesSharp := regex.BranchIssueSharp.FindAllStringSubmatch(commit, -1) + for _, match := range matchesSharp { + if len(match) > 1 { + if num, err := strconv.Atoi(match[1]); err == nil { + issueNumbers[num] = true } } } @@ -743,11 +738,9 @@ func (ghc *GitHubClient) UpdateIssueChecklist(ctx context.Context, issueNumber i body := issue.GetBody() lines := strings.Split(body, "\n") - re := regexp.MustCompile(`^(\s*[\-*+]\s+)\[([ xX])](\s+.+)`) - var checklistLineIndices []int for i, line := range lines { - if re.MatchString(line) { + if regex.MarkdownCheckboxUpdate.MatchString(line) { checklistLineIndices = append(checklistLineIndices, i) } } @@ -758,7 +751,7 @@ func (ghc *GitHubClient) UpdateIssueChecklist(ctx context.Context, issueNumber i lineIdx := checklistLineIndices[idx] line := lines[lineIdx] - if matches := re.FindStringSubmatch(line); len(matches) > 3 { + if matches := regex.MarkdownCheckboxUpdate.FindStringSubmatch(line); len(matches) > 3 { if matches[2] == " " { lines[lineIdx] = matches[1] + "[x]" + matches[3] updated = true diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index 5f98c7e..429e447 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -73,7 +73,7 @@ func (t *Translations) SetLanguage(lang string) error { return fmt.Errorf("unsupported language '%s'", lang) } -func (t *Translations) GetMessage(messageID string, count int, templateData map[string]interface{}) string { +func (t *Translations) GetMessage(messageID string, count int, templateData interface{}) string { localized, err := t.localize.Localize(&i18n.LocalizeConfig{ DefaultMessage: &i18n.Message{ ID: messageID, diff --git a/internal/i18n/locales/active.en.toml b/internal/i18n/locales/active.en.toml index 7938694..2a58554 100644 --- a/internal/i18n/locales/active.en.toml +++ b/internal/i18n/locales/active.en.toml @@ -49,18 +49,18 @@ other = "Manage configuration" [config_init_usage] other = "🚀 Initialize configuration wizard" -[config_init_quick_flag] -other = "Quick setup (only essential configuration)" - -[config_init_full_flag] -other = "Full setup (configure everything)" - [config_edit_usage] other = "✏️ Open configuration file in editor" [config_show_usage] other = "📋 Show current configuration" +[config_init_quick_flag] +other = "Quick setup (only essential configuration)" + +[config_init_full_flag] +other = "Full setup (configure everything)" + [current_config] other = "📋 Current configuration" @@ -111,6 +111,8 @@ issue_updated_successfully = "GitHub issue updated successfully." [config_save] error_saving_config = "Error saving configuration: {{.Error}}" +error_no_editor = "no text editor defined. Please set the $EDITOR environment variable" +error_opening_editor = "error opening editor: {{.Error}}" [ai_service] modified_files_prefix = "📄 Modified files:" @@ -165,6 +167,7 @@ no_ai_models_configured = "No AI models configured" error_invalid_language = "Invalid Language: {{.Language}}" [error] +pr_service_creation_error = "Error creating PR service: {{.Error}}" get_labels = "Error getting labels" create_label = "Error creating label '{{.label}}'" update_pr = "Error updating PR #{{.pr_number}}" @@ -298,6 +301,9 @@ error_generating_notes = "Error generating notes: {{.Error}}" error_creating_tag = "Error creating tag: {{.Error}}" error_writing_file = "Error writing file: {{.Error}}" command_usage = "Manage releases and changelogs" +preview_usage = "Preview the next release without creating it" +create_usage = "Create a new release (tag + notes)" +generate_usage = "Generate release notes and save them to a file" push_usage = "Push a tag to remote" output_flag = "Output file (default: RELEASE_NOTES.md)" auto_flag = "Skip confirmation" @@ -305,6 +311,10 @@ version_flag = "Override version manually (e.g., v1.2.3)" push_version_flag = "Version of tag to push (optional, auto-detects if not specified)" publish_usage = "Publish release to GitHub/GitLab" draft_flag = "Create as draft" +flag_publish_usage = "Publish the release to the VCS provider (e.g., GitHub)" +flag_draft_usage = "Publish as a draft (requires --publish)" +flag_build_binaries_usage = "Build and upload binaries for this release" +build_binaries_flag = "Build and upload binaries" previous_version = "📦 Previous version: {{.Version}}" next_version = "🚀 Next version: {{.Version}} ({{.Bump}})" changes_summary = "📊 Changes summary:" @@ -361,6 +371,23 @@ md_version = "Version" md_previous = "Previous Version" md_summary = "Summary" md_highlights = "Highlights" +md_contributors = "👥 Contributors" +new_contributors = "Welcome {{.Count}} new contributors!" +all_contributors = "All contributors for this release:" +md_stats = "📊 Statistics" +files_changed = "Files changed" +insertions = "Lines added" +deletions = "Lines deleted" +quick_start_title = "Quick Start" +examples_title = "Examples" +breaking_changes_title = "Breaking Changes" +no_breaking_changes = "No breaking changes" +comparison_title = "Before/After Comparison" +resources_title = "Resources" +template_changes = "Changes" +template_tip = "Tip: You can regenerate notes with 'mate-commit release generate'" +template_warning = "This release has no description in GitHub" +preview_title = "Release Preview" error_publishing_release = "Error publishing release: {{.Error}}" flag_changelog_usage = "Update CHANGELOG.md and create commit automatically" @@ -390,13 +417,7 @@ error_analyzing_for_regen = "Could not analyze commits: {{.Error}}" error_generating_for_regen = "Could not generate with AI: {{.Error}}" notes_regenerated = "✅ Release notes regenerated with AI successfully" -# New sections for enhanced release notes -quick_start_title = "Quick Start" -examples_title = "Examples" -breaking_changes_title = "Breaking Changes" -no_breaking_changes = "No breaking changes" -comparison_title = "Before/After Comparison" -resources_title = "Resources" + [git] no_staged_changes = "No staged changes found" @@ -505,6 +526,9 @@ check_ai_key_generic = "AI provider API key" check_github_token = "GitHub token" check_editor = "Editor configured" +gemini_key_invalid = "Invalid Gemini API key or no permissions" +gemini_not_configured = "Gemini API key not configured" + # Results config_not_found = "Configuration file not found" run_config_init = "Run: matecommit config init" @@ -727,9 +751,12 @@ budget_exceeded_excess = " Excess: ${{.Excess}}" # Smart Routing routing_suggestion = "💡 Suggestion: {{.Rationale}}" routing_suggested_model = " Suggested model: {{.Suggested}} (currently using: {{.Current}})" + [routing] reason_small = "Small operation (< 1k tokens), sufficient economical model" reason_high_quality = "High quality operation, requires better writing" reason_large = "Large operation (> 10k tokens), requires better context handling" reason_balance = "Optimal balance between cost and quality" reason_default = "Default model" + + diff --git a/internal/i18n/locales/active.es.toml b/internal/i18n/locales/active.es.toml index 83f2a47..6c6e8c0 100644 --- a/internal/i18n/locales/active.es.toml +++ b/internal/i18n/locales/active.es.toml @@ -58,11 +58,15 @@ other = "Configuración completa (configurar todo)" [config_edit_usage] other = "✏️ Abrir archivo de configuración en el editor" +[config_show_usage] +other = "📋 Mostrar configuración actual" + + + + [language_set_success] other = "✅ Idioma configurado a: {{.Lang}}" -[config_show_usage] -other = "📋 Mostrar configuración actual" [current_config] other = "📋 Configuración actual" @@ -73,6 +77,8 @@ other = "🌍 Idioma: {{.Lang}}" [emojis_label] other = "😊 Emojis: {{.Emoji}}" + + [invalid_selection] other = "Selección inválida: tiene que estar entre 1 y {{.Max}}" @@ -120,6 +126,8 @@ improvement_suggestions_prefix = "Sugerencias de Mejora:" [config_save] error_saving_config = "Error al guardar la configuración: {{.Error}}" +error_no_editor = "ningún editor de texto definido. Por favor, configura la variable de entorno $EDITOR" +error_opening_editor = "error al abrir el editor: {{.Error}}" [ai_service] modified_files_prefix = "📄 Archivos modificados:" @@ -170,9 +178,9 @@ jira_config_label = "Configuración de Jira - BaseURL: {{.BaseURL}}, Email: {{.E active_ai_label = "IA Activa: {{.IA}}" ai_models_label = "Modelos de IA configurados:" no_ai_models_configured = "No hay modelos de IA configurados" -error_invalid_language = "Lenguaje invalido: {{.Language}}" - [error] +pr_service_creation_error = "Error al crear el servicio de PR: {{.Error}}" +pr_summary_error = "Error al generar el resumen del PR: {{.Error}}" get_labels = "Error al obtener las etiquetas" create_label = "Error al crear la etiqueta '{{.label}}'" update_pr = "Error al actualizar el PR #{{.pr_number}}" @@ -198,7 +206,6 @@ Para actualizar PRs en repositorios personales, tu token necesita estos scopes: Nota: Los tokens de organizaciones pueden tener diferentes requisitos de permisos.""" add_labels = "Error al añadir las etiquetas al repo #{{.pr_number}}" invalid_repo_format = "Formato de repositorio inválido" -pr_summary_error = "Error generando el resumen del PR" no_repo_configured = "No se ha configurado ningún repositorio. Usa --repo o configura un proveedor VCS activo" vcs_provider_not_configured = "El proveedor VCS '{{.Provider}}' no está configurado" vcs_provider_auto_detected_not_configured = "Proveedor de VCS '%s' detectado automáticamente pero no configurado. Use 'matecommit config set-vcs --provider %s --token ' para configurarlo" @@ -258,19 +265,19 @@ error_update_pr = "Error al actualizar el PR: {{.Error}}" section_welcome = "1. Bienvenida e IA" welcome = "👋 ¡Bienvenido al asistente de configuración de MateCommit!" ai_intro = "Primero, configuremos la IA. (Proveedores soportados: {{.Providers}})" -prompt_ai_api_key = "> Introduce tu API Key de {{.Provider}} (Enter para omitir): " -prompt_ai_api_key_generic = "> Introduce tu API Key de IA (Enter para omitir): " -model_hint_supported = "> Puedes especificar un modelo (soportados: {{.Models}})." +prompt_ai_api_key = "> Ingresá tu API Key de {{.Provider}} (Enter para omitir): " +prompt_ai_api_key_generic = "> Ingresá tu API Key de IA (Enter para omitir): " +model_hint_supported = "> Podés especificar un modelo (soportados: {{.Models}})." prompt_model_with_default = "> Modelo a usar (por defecto: {{.Default}}): " section_language = "2. Idioma" language_supported_with_current = "Ahora, el idioma. (Soportados: en, es) Actual: {{.Current}}" -prompt_language_blank_keeps = "> Elige tu idioma preferido (en/es). Enter mantiene el actual: " -error_invalid_language = "Idioma inválido. Por favor ingresa 'en' o 'es'." +prompt_language_blank_keeps = "> Elegí tu idioma preferido (en/es). Enter mantiene el actual: " +error_invalid_language = "Idioma inválido. Por favor ingresá 'en' o 'es'." section_vcs = "3. Control de Versiones (VCS)" -prompt_vcs_enable_blank_no = "¿Configurar VCS para funciones como resúmenes de PRs? (Soportado: {{.Providers}}) (s/n, Enter = no): " -prompt_github_token_blank_skip = "> Introduce tu token de acceso de GitHub (Enter para omitir): " +prompt_vcs_enable_blank_no = "¿Configuramos VCS para funciones como resúmenes de PRs? (Soportado: {{.Providers}}) (s/n, Enter = no): " +prompt_github_token_blank_skip = "> Ingresá tu token de acceso de GitHub (Enter para omitir): " info_vcs_skipped = "Se omitió la configuración de VCS." section_tickets = "4. Gestor de Tickets" @@ -350,6 +357,11 @@ generate_usage = "Generar release notes y guardarlas en un archivo" generating = "📝 Generando notas del release..." notes_saved = "✅ Release notes guardadas en: {{.File}}" version_label = "📦 Versión: {{.Version}}" +flag_publish_usage = "Publicar release en el proveedor VCS (ej: GitHub)" +flag_draft_usage = "Publicar como borrador (requiere --publish)" +flag_changelog_usage = "Actualizar CHANGELOG.md y crear commit automáticamente" +flag_build_binaries_usage = "Construir y subir binarios para este release" +build_binaries_flag = "Construir y subir binarios" # Create create_usage = "Crear un nuevo release (tag + notes)" @@ -377,6 +389,8 @@ changelog_committed = "✅ Changelog confirmado con éxito" pushing_changes = "Subiendo cambios al repositorio remoto..." error_pushing_changes = "Error al subir cambios: {{.Error}}" changes_pushed = "✅ Cambios subidos con éxito" +committing_changelog = "💾 Guardando cambios en el changelog..." +error_updating_changelog = "Error al actualizar el changelog: {{.Error}}" highlights_section = "### Highlights" # Generate Markdown @@ -389,9 +403,6 @@ pushing_tag = "🚀 Subiendo tag {{.Version}}..." error_pushing_tag = "Error subiendo tag: {{.Error}}" push_success = "✅ Tag {{.Version}} subido exitosamente" -flag_publish_usage = "Publicar release en el proveedor VCS (ej: GitHub)" -flag_draft_usage = "Publicar como borrador (requiere --publish)" -flag_changelog_usage = "Actualizar CHANGELOG.md y crear commit automáticamente" publishing_release = "🚀 Publicando release..." release_published = "✅ Release publicado exitosamente" error_publishing_release = "Error al publicar la release: {{.Error}}" @@ -408,18 +419,18 @@ ai_editing = "🤖 Generando release notes mejoradas con IA..." error_creating_temp = "Error al crear archivo temporal: {{.Error}}" error_writing_temp = "Error al escribir archivo temporal: {{.Error}}" opening_editor = "✏️ Abriendo {{.Editor}}..." -error_running_editor = "Error al ejecutar editor: {{.Error}}" -error_reading_temp = "Error al leer archivo temporal: {{.Error}}" +error_running_editor = "Error al ejecutar el editor: {{.Error}}" +error_reading_temp = "Error al leer el archivo temporal: {{.Error}}" updating_release = "💾 Actualizando release {{.Version}}..." error_updating_release = "Error al actualizar release: {{.Error}}" -edit_success = "✅ Release {{.Version}} actualizado exitosamente" -empty_body_regenerating = "⚠️ Release sin contenido. Regenerando con IA..." -regenerating_with_ai = "🤖 Regenerando release notes con IA..." -error_calculating_previous = "Error al calcular versión anterior: {{.Error}}" +edit_success = "✅ Release {{.Version}} actualizado con éxito" +empty_body_regenerating = "⚠️ La release no tiene contenido. Regenerando con IA..." +regenerating_with_ai = "🤖 Regenerando notas del release con IA..." +error_calculating_previous = "Error al calcular la versión anterior: {{.Error}}" error_getting_commits = "Error al obtener commits: {{.Error}}" -error_analyzing_for_regen = "No se pudo analizar commits: {{.Error}}" +error_analyzing_for_regen = "No se pudieron analizar los commits: {{.Error}}" error_generating_for_regen = "No se pudo generar con IA: {{.Error}}" -notes_regenerated = "✅ Release notes regeneradas con IA exitosamente" +notes_regenerated = "✅ Notas del release regeneradas con IA con éxito" # New sections for enhanced release notes quick_start_title = "Inicio Rápido" @@ -536,6 +547,9 @@ check_ai_key_generic = "API key del proveedor de IA" check_github_token = "Token de GitHub" check_editor = "Editor configurado" +gemini_key_invalid = "API key de Gemini inválida o sin permisos" +gemini_not_configured = "API key de Gemini no configurada" + # Results config_not_found = "Archivo de configuración no encontrado" run_config_init = "Ejecuta: matecommit config init" @@ -754,6 +768,7 @@ budget_exceeded_total = " Total sería: ${{.Total}}" budget_exceeded_limit = " Límite diario: ${{.Limit}}" budget_exceeded_excess = " Exceso: ${{.Excess}}" + # Smart Routing routing_suggestion = "💡 Sugerencia: {{.Rationale}}" routing_suggested_model = " Modelo sugerido: {{.Suggested}} (actualmente usando: {{.Current}})" diff --git a/internal/jira/jira_service.go b/internal/jira/jira_service.go index dcee14f..cab8f0b 100644 --- a/internal/jira/jira_service.go +++ b/internal/jira/jira_service.go @@ -5,11 +5,11 @@ import ( "encoding/json" "fmt" "net/http" - "regexp" "strings" domainErrors "github.com/thomas-vilte/matecommit/internal/errors" "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/regex" ) // Constants for acceptance criteria patterns @@ -246,7 +246,7 @@ func extractCriteriaFromCustomField(fields map[string]CustomField, fieldID strin if strings.HasPrefix(criterion, "- ") || strings.HasPrefix(criterion, "* ") { criterion = strings.TrimPrefix(criterion, "- ") criterion = strings.TrimPrefix(criterion, "* ") - } else if matches := regexp.MustCompile(`^\d+\.\s*`).FindStringSubmatch(criterion); len(matches) > 0 { + } else if matches := regex.NumberedList.FindStringSubmatch(criterion); len(matches) > 0 { criterion = strings.TrimPrefix(criterion, matches[0]) } filteredCriteria = append(filteredCriteria, criterion) @@ -355,15 +355,8 @@ func extractAndRemoveCriteria(text string) ([]string, string) { // removeCriteriaFromDescription removes acceptance criteria from the description. func removeCriteriaFromDescription(description string) string { - patterns := []string{ - "Acceptance criteria:.*(\n.*)*", // Removes everything following "Acceptance criteria:" - "Criterio de aceptacion:.*(\n.*)*", // Removes everything following "Criterio de aceptacion:" - } - - for _, pattern := range patterns { - re := regexp.MustCompile(pattern) - description = re.ReplaceAllString(description, "") - } + description = regex.AcceptanceCriteriaEN.ReplaceAllString(description, "") + description = regex.AcceptanceCriteriaES.ReplaceAllString(description, "") return strings.TrimSpace(description) } diff --git a/internal/models/pr.go b/internal/models/pr.go index 9121170..4a1f2c3 100644 --- a/internal/models/pr.go +++ b/internal/models/pr.go @@ -4,17 +4,26 @@ type ProgressEventType string const ( ProgressIssuesDetected ProgressEventType = "issues_detected" - ProgressStatsCalculated ProgressEventType = "stats_calculated" ProgressIssuesClosing ProgressEventType = "issues_closing" ProgressBreakingChanges ProgressEventType = "breaking_changes" ProgressTestPlan ProgressEventType = "test_plan_generated" ProgressGeneric ProgressEventType = "generic_info" ) +type ProgressData struct { + Issues []string + PRNumber int + Count int + Title string + Number int + IsAuto bool + Error string +} + type ProgressEvent struct { Type ProgressEventType Message string - Data map[string]interface{} + Data *ProgressData } type ( diff --git a/internal/regex/regex.go b/internal/regex/regex.go new file mode 100644 index 0000000..891bf56 --- /dev/null +++ b/internal/regex/regex.go @@ -0,0 +1,49 @@ +package regex + +import "regexp" + +var ( + // Commit and Release patterns + ConventionalCommit = regexp.MustCompile(`^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(([^)]+)\))?(!)?:\s*(.+)`) + BreakingChange = regexp.MustCompile(`BREAKING[ -]CHANGE:\s*(.+)`) + SemVer = regexp.MustCompile(`v?(\d+)\.(\d+)\.(\d+)`) + GitHubPR = regexp.MustCompile(`\(#(\d+)\)`) + + // Issue and Ticket patterns + JiraTicket = regexp.MustCompile(`([A-Za-z]+-\d+)`) + NumberedList = regexp.MustCompile(`^\d+\.\s*`) + MarkdownCheckbox = regexp.MustCompile(`^\s*[\-*+]\s+\[([ xX])]\s+(.+)`) + MarkdownCheckboxUpdate = regexp.MustCompile(`^(\s*[\-*+]\s+)\[([ xX])](\s+.+)`) + + // GitHub linkage patterns + GitHubClosedLink = regexp.MustCompile(`(?i)(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)`) + + // Branch patterns for issue detection + BranchIssueSharp = regexp.MustCompile(`#(\d+)`) + BranchIssueName = regexp.MustCompile(`issue[/-](\d+)`) + BranchIssueStart = regexp.MustCompile(`^(\d+)-`) + BranchIssueFolder = regexp.MustCompile(`/(\d+)-`) + BranchIssueMid = regexp.MustCompile(`-(\d+)-`) + + // Service specific patterns + FixKeywords = regexp.MustCompile(`(?i)(fix|bug|resolve|close)`) + FeatKeywords = regexp.MustCompile(`(?i)(feat|feature|add|implement)`) + RefactorKeywords = regexp.MustCompile(`(?i)(refactor|restructure|reorganize)`) + + // Git and Repo patterns + SSHRepo = regexp.MustCompile(`git@([^:]+):([^/]+)/(.+)\.git$`) + HTTPSRepo = regexp.MustCompile(`https://([^/]+)/([^/]+)/(.+?)(?:\.git)?$`) + + // Jira Acceptance Criteria cleanup + AcceptanceCriteriaEN = regexp.MustCompile(`(?i)Acceptance criteria:.*(\n.*)*`) + AcceptanceCriteriaES = regexp.MustCompile(`(?i)Criterio de aceptacion:.*(\n.*)*`) + + // AI and JSON parsing + MarkdownJSONBlock = regexp.MustCompile("(?s)```(?:json)?\n?(.*?)```") + JSONString = regexp.MustCompile(`"(?:\\.|[^"\\])*"`) + QuotedString = regexp.MustCompile(`"(.*)"`) + + // Dependency management + GoModRequireBlock = regexp.MustCompile(`^\s+(\S+)\s+v?(\S+)(\s+//\s*indirect)?`) + GoModRequireSingle = regexp.MustCompile(`^require\s+(\S+)\s+v?(\S+)(\s+//\s*indirect)?`) +) diff --git a/internal/services/commit_service.go b/internal/services/commit_service.go index 8feb687..4301b67 100644 --- a/internal/services/commit_service.go +++ b/internal/services/commit_service.go @@ -9,9 +9,10 @@ import ( "github.com/thomas-vilte/matecommit/internal/config" domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/github" "github.com/thomas-vilte/matecommit/internal/models" "github.com/thomas-vilte/matecommit/internal/ports" - "github.com/thomas-vilte/matecommit/internal/github" + "github.com/thomas-vilte/matecommit/internal/regex" ) // commitGitService defines only the methods needed by CommitService. @@ -137,9 +138,9 @@ func (s *CommitService) buildCommitInfo(ctx context.Context, issueNumber int, pr if progress != nil { progress(models.ProgressEvent{ Type: models.ProgressGeneric, - Data: map[string]interface{}{ - "Error": err.Error(), - "IssueID": detectedIssue, + Data: &models.ProgressData{ + Error: err.Error(), + Number: detectedIssue, }, }) } @@ -147,10 +148,10 @@ func (s *CommitService) buildCommitInfo(ctx context.Context, issueNumber int, pr if progress != nil { progress(models.ProgressEvent{ Type: models.ProgressIssuesDetected, - Data: map[string]interface{}{ - "IssueID": detectedIssue, - "Title": issueInfo.Title, - "IsAuto": issueNumber == 0, + Data: &models.ProgressData{ + Number: detectedIssue, + Title: issueInfo.Title, + IsAuto: issueNumber == 0, }, }) } @@ -229,16 +230,13 @@ func (s *CommitService) detectIssueFromBranch(ctx context.Context) int { return 0 } - patterns := []string{ - `#(\d+)`, // #123 - `issue[/-](\d+)`, // issue-123, issue/123 - `^(\d+)-`, // 123-feature - `/(\d+)-`, // feature/123-desc - `-(\d+)-`, // bugfix-123-description - } - - for _, pattern := range patterns { - re := regexp.MustCompile(pattern) + for _, re := range []*regexp.Regexp{ + regex.BranchIssueSharp, + regex.BranchIssueName, + regex.BranchIssueStart, + regex.BranchIssueFolder, + regex.BranchIssueMid, + } { if match := re.FindStringSubmatch(branchName); len(match) > 1 { if num, err := strconv.Atoi(match[1]); err == nil { return num @@ -258,20 +256,10 @@ func (s *CommitService) detectIssueFromCommits(ctx context.Context) int { } // https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue - keywords := []string{ - "fix", "fixes", "fixed", - "close", "closes", "closed", - "resolve", "resolves", "resolved", - } - - for _, keyword := range keywords { - pattern := fmt.Sprintf(`(?i)\b%s\s+#(\d+)\b`, keyword) - re := regexp.MustCompile(pattern) - for _, msg := range commitMessages { - if match := re.FindStringSubmatch(msg); len(match) > 1 { - if num, err := strconv.Atoi(match[1]); err == nil { - return num - } + for _, msg := range commitMessages { + if match := regex.GitHubClosedLink.FindStringSubmatch(msg); len(match) > 1 { + if num, err := strconv.Atoi(match[1]); err == nil { + return num } } } @@ -285,8 +273,7 @@ func (s *CommitService) getTicketIDFromBranch(ctx context.Context) (string, erro return "", domainErrors.NewAppError(domainErrors.TypeGit, "error getting branch name", err) } - re := regexp.MustCompile(`([A-Za-z]+-\d+)`) - match := re.FindString(branchName) + match := regex.JiraTicket.FindString(branchName) if match == "" { return "", domainErrors.NewAppError(domainErrors.TypeGit, "ticket ID not found in branch", nil) } diff --git a/internal/services/issue_generator_service.go b/internal/services/issue_generator_service.go index 4b5794f..b35b739 100644 --- a/internal/services/issue_generator_service.go +++ b/internal/services/issue_generator_service.go @@ -3,13 +3,13 @@ package services import ( "context" "fmt" - "regexp" "strings" "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/regex" ) // issueGitService defines only the methods needed by IssueGeneratorService. @@ -379,17 +379,13 @@ func (s *IssueGeneratorService) analyzeDiff(diff string, changedFiles []string) } } - fixPattern := regexp.MustCompile(`(?i)(fix|bug|resolve|close)`) - featPattern := regexp.MustCompile(`(?i)(feat|feature|add|implement)`) - refactorPattern := regexp.MustCompile(`(?i)(refactor|restructure|reorganize)`) - - if fixPattern.MatchString(diff) { + if regex.FixKeywords.MatchString(diff) { analysis.Keywords["fix"] = true } - if featPattern.MatchString(diff) { + if regex.FeatKeywords.MatchString(diff) { analysis.Keywords["feat"] = true } - if refactorPattern.MatchString(diff) { + if regex.RefactorKeywords.MatchString(diff) { analysis.Keywords["refactor"] = true } diff --git a/internal/services/pull_request_service.go b/internal/services/pull_request_service.go index c1f2324..dc30941 100644 --- a/internal/services/pull_request_service.go +++ b/internal/services/pull_request_service.go @@ -5,10 +5,10 @@ import ( "fmt" "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/ai" ) // prVCSClient defines the methods needed by PRService from a VCS provider. @@ -84,9 +84,9 @@ func (s *PRService) SummarizePR(ctx context.Context, prNumber int, progress func if progress != nil { progress(models.ProgressEvent{ Type: models.ProgressIssuesDetected, - Data: map[string]interface{}{ - "PRNumber": prNumber, - "Issues": issueNums, + Data: &models.ProgressData{ + PRNumber: prNumber, + Issues: issueNums, }, }) } @@ -104,8 +104,8 @@ func (s *PRService) SummarizePR(ctx context.Context, prNumber int, progress func if progress != nil { progress(models.ProgressEvent{ Type: models.ProgressIssuesClosing, - Data: map[string]interface{}{ - "Count": len(prData.RelatedIssues), + Data: &models.ProgressData{ + Count: len(prData.RelatedIssues), }, }) } @@ -116,8 +116,8 @@ func (s *PRService) SummarizePR(ctx context.Context, prNumber int, progress func if progress != nil { progress(models.ProgressEvent{ Type: models.ProgressBreakingChanges, - Data: map[string]interface{}{ - "Count": len(breakingChanges), + Data: &models.ProgressData{ + Count: len(breakingChanges), }, }) } diff --git a/internal/services/release_service.go b/internal/services/release_service.go index 9db9f57..06afd35 100644 --- a/internal/services/release_service.go +++ b/internal/services/release_service.go @@ -9,16 +9,11 @@ import ( "strings" "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/dependency" 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/dependency" -) - -var ( - conventionalRegex = regexp.MustCompile(`^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(([^)]+)\))?(!)?:\s*(.+)`) - breakingRegex = regexp.MustCompile(`BREAKING[ -]CHANGE:\s*(.+)`) - versionRegex = regexp.MustCompile(`v?(\d+)\.(\d+)\.(\d+)`) + "github.com/thomas-vilte/matecommit/internal/regex" ) // releaseGitService defines only the methods needed by ReleaseService. @@ -244,8 +239,7 @@ func (s *ReleaseService) categorizeCommits(release *models.Release) { lines := strings.Split(msg, "\n") firstLine := lines[0] - prRegex := regexp.MustCompile(`\(#(\d+)\)`) - prMatch := prRegex.FindStringSubmatch(firstLine) + prMatch := regex.GitHubPR.FindStringSubmatch(firstLine) prNumber := "" if len(prMatch) > 1 { prNumber = prMatch[1] @@ -253,13 +247,13 @@ func (s *ReleaseService) categorizeCommits(release *models.Release) { hasBreaking := false for _, line := range lines[1:] { - if breakingRegex.MatchString(line) { + if regex.BreakingChange.MatchString(line) { hasBreaking = true break } } - matches := conventionalRegex.FindStringSubmatch(firstLine) + matches := regex.ConventionalCommit.FindStringSubmatch(firstLine) if len(matches) > 0 { commitType := matches[1] scope := matches[3] @@ -303,7 +297,7 @@ func (s *ReleaseService) categorizeCommits(release *models.Release) { // calculateVersion calculates the new version based on semantic versioning func (s *ReleaseService) calculateVersion(currentTag string, release *models.Release) (string, models.VersionBump) { - matches := versionRegex.FindStringSubmatch(currentTag) + matches := regex.SemVer.FindStringSubmatch(currentTag) major, minor, patch := 0, 0, 0 if len(matches) >= 4 { @@ -529,8 +523,7 @@ func (s *ReleaseService) UpdateAppVersion(version string) error { match := re.FindString(currentContent) - valueRe := regexp.MustCompile(`"(.*)"`) - valMatch := valueRe.FindStringIndex(match) + valMatch := regex.QuotedString.FindStringIndex(match) if valMatch == nil { return domainErrors.NewAppError(domainErrors.TypeInternal, "could not find quoted string in matching pattern", nil) diff --git a/internal/services/version_checker.go b/internal/services/version_checker.go index 3c5ef74..18633d0 100644 --- a/internal/services/version_checker.go +++ b/internal/services/version_checker.go @@ -154,7 +154,7 @@ func (v *VersionUpdater) updateViaBrew(ctx context.Context) error { func (v *VersionUpdater) updateViaBinary(ctx context.Context) error { client := github.NewClient(nil) - release, _, err := client.Repositories.GetLatestRelease(ctx, "Tomas-vilte", "MateCommit") + release, _, err := client.Repositories.GetLatestRelease(ctx, "thomas-vilte", "matecommit") if err != nil { return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to get latest release info", err) }