Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .mailmap

This file was deleted.

9 changes: 3 additions & 6 deletions internal/ai/gemini/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
})
}
Expand Down
12 changes: 6 additions & 6 deletions internal/commands/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -94,22 +94,22 @@ 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
}

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 {
Expand All @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions internal/commands/config/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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),
}
}
Expand All @@ -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),
}
}
Expand Down
8 changes: 4 additions & 4 deletions internal/commands/config/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand All @@ -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))
}
}

Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions internal/commands/config/edit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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])
})
}
59 changes: 30 additions & 29 deletions internal/commands/config/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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')
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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
}

Expand Down
12 changes: 3 additions & 9 deletions internal/commands/config/quick_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 10 additions & 7 deletions internal/commands/config/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand All @@ -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))
Expand Down
Loading
Loading