diff --git a/cmd/main.go b/cmd/main.go index 24ad80f..7f15e97 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,11 +5,13 @@ import ( "fmt" "github.com/Tomas-vilte/MateCommit/internal/cli/command/config" "github.com/Tomas-vilte/MateCommit/internal/cli/command/handler" + "github.com/Tomas-vilte/MateCommit/internal/cli/command/pr" "github.com/Tomas-vilte/MateCommit/internal/cli/command/suggest" "github.com/Tomas-vilte/MateCommit/internal/cli/registry" cfg "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai/gemini" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/factory" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/git" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/tickets/jira" "github.com/Tomas-vilte/MateCommit/internal/services" @@ -57,6 +59,11 @@ func initializeApp() (*cli.Command, error) { log.Fatalf("Error initializing AI service: %v", err) } + aiSummarizer, err := gemini.NewGeminiPRSummarizer(context.Background(), cfgApp, translations) + if err != nil { + log.Fatalf("Error al crear el servicio: %v", err) + } + ticketService := jira.NewJiraService(cfgApp, &http.Client{}) commitService := services.NewCommitService(gitService, aiProvider, ticketService, cfgApp) @@ -65,6 +72,15 @@ func initializeApp() (*cli.Command, error) { registerCommand := registry.NewRegistry(cfgApp, translations) + prServiceFactory := factory.NewPrServiceFactory(cfgApp, translations, aiSummarizer) + prService, err := prServiceFactory.CreatePRService() + if err != nil { + log.Printf("Warning: %v", err) + log.Println("Algunos comandos estan desactivados, configura el vcs") + } + + prCommand := pr.NewSummarizeCommand(prService) + if err := registerCommand.Register("suggest", suggest.NewSuggestCommandFactory(commitService, commitHandler)); err != nil { log.Fatalf("Error al registrar el comando 'suggest': %v", err) } @@ -73,6 +89,10 @@ func initializeApp() (*cli.Command, error) { log.Fatalf("Error al registrar el comando 'config': %v", err) } + if err := registerCommand.Register("summarize-pr", prCommand); err != nil { + log.Fatalf("Error al registrar el comando 'summarize-pr': %v", err) + } + return &cli.Command{ Name: "mate-commit", Usage: translations.GetMessage("app_usage", 0, nil), diff --git a/internal/cli/command/config/config.go b/internal/cli/command/config/config.go index ca350e4..bfccc5f 100644 --- a/internal/cli/command/config/config.go +++ b/internal/cli/command/config/config.go @@ -26,6 +26,8 @@ func (c *ConfigCommandFactory) CreateCommand(t *i18n.Translations, cfg *config.C c.newSetTicketCommand(t, cfg), c.newSetAIActiveCommand(t, cfg), c.newSetAIModelCommand(t, cfg), + c.newSetActiveVCSCommand(t, cfg), + c.newSetVCSConfigCommand(t, cfg), }, } } diff --git a/internal/cli/command/config/set_vcs_active.go b/internal/cli/command/config/set_vcs_active.go new file mode 100644 index 0000000..5220eaf --- /dev/null +++ b/internal/cli/command/config/set_vcs_active.go @@ -0,0 +1,48 @@ +package config + +import ( + "context" + "fmt" + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/urfave/cli/v3" +) + +func (c *ConfigCommandFactory) newSetActiveVCSCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "set-active-vcs", + Usage: t.GetMessage("vcs_summary.config_set_active_vcs_usage", 0, nil), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "provider", + Aliases: []string{"p"}, + Usage: t.GetMessage("vcs_summary.config_set_active_vcs_provider_usage", 0, nil), + Required: true, + }, + }, + Action: func(ctx context.Context, command *cli.Command) error { + provider := command.String("provider") + + if _, exists := cfg.VCSConfigs[provider]; !exists { + msg := t.GetMessage("error.vcs_provider_not_configured", 0, map[string]interface{}{ + "Provider": provider, + }) + return fmt.Errorf("%s", msg) + } + + cfg.ActiveVCSProvider = provider + + if err := config.SaveConfig(cfg); err != nil { + msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ + "Error": err.Error(), + }) + return fmt.Errorf("%s", msg) + } + + fmt.Println(t.GetMessage("vcs_summary.config_active_vcs_updated", 0, map[string]interface{}{ + "Provider": provider, + })) + return nil + }, + } +} diff --git a/internal/cli/command/config/set_vcs_active_test.go b/internal/cli/command/config/set_vcs_active_test.go new file mode 100644 index 0000000..3301273 --- /dev/null +++ b/internal/cli/command/config/set_vcs_active_test.go @@ -0,0 +1,96 @@ +package config + +import ( + "context" + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestSetActiveVCSCommand(t *testing.T) { + t.Run("should successfully set active VCS provider", func(t *testing.T) { + // Arrange + cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) + defer cleanup() + + cfg.VCSConfigs = map[string]config.VCSConfig{ + "github": {Provider: "github"}, + "gitlab": {Provider: "gitlab"}, + } + cfg.ActiveVCSProvider = "gitlab" + assert.NoError(t, config.SaveConfig(cfg)) + + factory := NewConfigCommandFactory() + cmd := factory.newSetActiveVCSCommand(translations, cfg) + + app := cmd + ctx := context.Background() + + // Act + err := app.Run(ctx, []string{"set-active-vcs", "--provider", "github"}) + + // Assert + assert.NoError(t, err) + loadedCfg, err := config.LoadConfig(tmpConfigPath) + assert.NoError(t, err) + assert.Equal(t, "github", loadedCfg.ActiveVCSProvider) + }) + + t.Run("should fail with non-existent provider", func(t *testing.T) { + // Arrange + cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) + defer cleanup() + + cfg.VCSConfigs = map[string]config.VCSConfig{ + "github": {Provider: "github"}, + } + cfg.ActiveVCSProvider = "github" + assert.NoError(t, config.SaveConfig(cfg)) + + factory := NewConfigCommandFactory() + cmd := factory.newSetActiveVCSCommand(translations, cfg) + + app := cmd + ctx := context.Background() + + // Act + err := app.Run(ctx, []string{"set-active-vcs", "--provider", "bitbucket"}) + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), translations.GetMessage("error.vcs_provider_not_configured", 0, map[string]interface{}{ + "Provider": "bitbucket", + })) + loadedCfg, err := config.LoadConfig(tmpConfigPath) + assert.NoError(t, err) + assert.Equal(t, "github", loadedCfg.ActiveVCSProvider) + }) + + t.Run("config save error", func(t *testing.T) { + // Arrange + cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) + defer cleanup() + + err := os.Mkdir(tmpConfigPath, 0755) + assert.NoError(t, err) + + cfg.VCSConfigs = map[string]config.VCSConfig{ + "github": {Provider: "github"}, + } + + factory := NewConfigCommandFactory() + cmd := factory.newSetActiveVCSCommand(translations, cfg) + + app := cmd + ctx := context.Background() + + // Act + err = app.Run(ctx, []string{"set-active-vcs", "--provider", "github"}) + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "error al guardar la configuración") + assert.Equal(t, "github", cfg.ActiveVCSProvider) + }) +} diff --git a/internal/cli/command/config/set_vcs_config.go b/internal/cli/command/config/set_vcs_config.go new file mode 100644 index 0000000..aa4d516 --- /dev/null +++ b/internal/cli/command/config/set_vcs_config.go @@ -0,0 +1,78 @@ +package config + +import ( + "context" + "fmt" + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/urfave/cli/v3" +) + +func (c *ConfigCommandFactory) newSetVCSConfigCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "set-vcs", + Usage: t.GetMessage("vcs_summary.config_set_vcs_usage", 0, nil), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "provider", + Aliases: []string{"p"}, + Usage: t.GetMessage("vcs_summary.config_set_vcs_provider_usage", 0, nil), + Required: true, + }, + &cli.StringFlag{ + Name: "token", + Aliases: []string{"t"}, + Usage: t.GetMessage("vcs_summary.config_set_vcs_token_usage", 0, nil), + Required: false, + }, + &cli.StringFlag{ + Name: "owner", + Aliases: []string{"o"}, + Usage: t.GetMessage("vcs_summary.config_set_vcs_owner_usage", 0, nil), + Required: false, + }, + &cli.StringFlag{ + Name: "repo", + Aliases: []string{"r"}, + Usage: t.GetMessage("vcs_summary.config_set_vcs_repo_usage", 0, nil), + Required: false, + }, + }, + Action: func(ctx context.Context, command *cli.Command) error { + provider := command.String("provider") + + if cfg.VCSConfigs == nil { + cfg.VCSConfigs = make(map[string]config.VCSConfig) + } + + vcsConfig, exists := cfg.VCSConfigs[provider] + if !exists { + vcsConfig = config.VCSConfig{Provider: provider} + } + + if token := command.String("token"); token != "" { + vcsConfig.Token = token + } + if owner := command.String("owner"); owner != "" { + vcsConfig.Owner = owner + } + if repo := command.String("repo"); repo != "" { + vcsConfig.Repo = repo + } + + cfg.VCSConfigs[provider] = vcsConfig + + if err := config.SaveConfig(cfg); err != nil { + msg := t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{ + "Error": err.Error(), + }) + return fmt.Errorf("%s", msg) + } + + fmt.Println(t.GetMessage("vcs_summary.config_vcs_updated", 0, map[string]interface{}{ + "Provider": provider, + })) + return nil + }, + } +} diff --git a/internal/cli/command/config/set_vcs_config_test.go b/internal/cli/command/config/set_vcs_config_test.go new file mode 100644 index 0000000..556e15c --- /dev/null +++ b/internal/cli/command/config/set_vcs_config_test.go @@ -0,0 +1,134 @@ +package config + +import ( + "context" + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestSetVCSConfigCommand(t *testing.T) { + t.Run("should successfully set new VCS config", func(t *testing.T) { + // Arrange + cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) + defer cleanup() + + assert.NoError(t, config.SaveConfig(cfg)) + + factory := NewConfigCommandFactory() + cmd := factory.newSetVCSConfigCommand(translations, cfg) + + app := cmd + ctx := context.Background() + + // Act + err := app.Run(ctx, []string{"set-vcs", "--provider", "github", "--token", "abc123", "--owner", "testuser", "--repo", "testrepo"}) + + // Assert + assert.NoError(t, err) + loadedCfg, err := config.LoadConfig(tmpConfigPath) + assert.NoError(t, err) + + vcsConfig, exists := loadedCfg.VCSConfigs["github"] + assert.True(t, exists) + assert.Equal(t, "github", vcsConfig.Provider) + assert.Equal(t, "abc123", vcsConfig.Token) + assert.Equal(t, "testuser", vcsConfig.Owner) + assert.Equal(t, "testrepo", vcsConfig.Repo) + }) + + t.Run("should update existing VCS config", func(t *testing.T) { + // Arrange + cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) + defer cleanup() + + cfg.VCSConfigs = map[string]config.VCSConfig{ + "github": { + Provider: "github", + Token: "old-token", + Owner: "old-owner", + Repo: "old-repo", + }, + } + assert.NoError(t, config.SaveConfig(cfg)) + + factory := NewConfigCommandFactory() + cmd := factory.newSetVCSConfigCommand(translations, cfg) + + app := cmd + ctx := context.Background() + + // Act + err := app.Run(ctx, []string{"set-vcs", "--provider", "github", "--owner", "new-owner"}) + + // Assert + assert.NoError(t, err) + loadedCfg, err := config.LoadConfig(tmpConfigPath) + assert.NoError(t, err) + + vcsConfig, exists := loadedCfg.VCSConfigs["github"] + assert.True(t, exists) + assert.Equal(t, "github", vcsConfig.Provider) + assert.Equal(t, "old-token", vcsConfig.Token) + assert.Equal(t, "new-owner", vcsConfig.Owner) + assert.Equal(t, "old-repo", vcsConfig.Repo) + }) + + t.Run("should create VCSConfigs map if nil", func(t *testing.T) { + // Arrange + cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) + defer cleanup() + + cfg.VCSConfigs = nil + assert.NoError(t, config.SaveConfig(cfg)) + + factory := NewConfigCommandFactory() + cmd := factory.newSetVCSConfigCommand(translations, cfg) + + app := cmd + ctx := context.Background() + + // Act + err := app.Run(ctx, []string{"set-vcs", "--provider", "github", "--token", "abc123"}) + + // Assert + assert.NoError(t, err) + loadedCfg, err := config.LoadConfig(tmpConfigPath) + assert.NoError(t, err) + + vcsConfig, exists := loadedCfg.VCSConfigs["github"] + assert.True(t, exists) + assert.Equal(t, "github", vcsConfig.Provider) + assert.Equal(t, "abc123", vcsConfig.Token) + }) + + t.Run("config save error", func(t *testing.T) { + // Arrange + cfg, translations, tmpConfigPath, cleanup := setupConfigTest(t) + defer cleanup() + + err := os.Mkdir(tmpConfigPath, 0755) + assert.NoError(t, err) + + factory := NewConfigCommandFactory() + cmd := factory.newSetVCSConfigCommand(translations, cfg) + + app := cmd + ctx := context.Background() + + // Act + err = app.Run(ctx, []string{"set-vcs", "--provider", "github", "--token", "abc123"}) + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), "Error al guardar la configuración") + + if cfg.VCSConfigs != nil { + vcsConfig, exists := cfg.VCSConfigs["github"] + assert.True(t, exists) + assert.Equal(t, "github", vcsConfig.Provider) + assert.Equal(t, "abc123", vcsConfig.Token) + } + }) +} diff --git a/internal/cli/command/config/show.go b/internal/cli/command/config/show.go index 114f36c..8f7530a 100644 --- a/internal/cli/command/config/show.go +++ b/internal/cli/command/config/show.go @@ -28,7 +28,6 @@ func (c *ConfigCommandFactory) newShowCommand(t *i18n.Translations, cfg *config. fmt.Println(t.GetMessage("api.key_set", 0, nil)) } - // Imprimir el servicio de tickets activo if cfg.UseTicket { fmt.Printf("%s\n", t.GetMessage("config_models.ticket_service_enabled", 0, map[string]interface{}{"Service": cfg.ActiveTicketService})) if cfg.ActiveTicketService == "jira" { @@ -41,10 +40,8 @@ func (c *ConfigCommandFactory) newShowCommand(t *i18n.Translations, cfg *config. fmt.Println(t.GetMessage("config_models.ticket_service_disabled", 0, nil)) } - // Imprimir la IA activa fmt.Printf("%s\n", t.GetMessage("config_models.active_ai_label", 0, map[string]interface{}{"IA": cfg.AIConfig.ActiveAI})) - // Imprimir los modelos configurados para cada IA if len(cfg.AIConfig.Models) > 0 { fmt.Println(t.GetMessage("config_models.ai_models_label", 0, nil)) for ai, model := range cfg.AIConfig.Models { diff --git a/internal/cli/command/pr/summarize.go b/internal/cli/command/pr/summarize.go new file mode 100644 index 0000000..4c97000 --- /dev/null +++ b/internal/cli/command/pr/summarize.go @@ -0,0 +1,81 @@ +package pr + +import ( + "context" + "fmt" + cfg "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/urfave/cli/v3" + "strings" +) + +type SummarizeCommand struct { + prService ports.PRService +} + +func NewSummarizeCommand(prService ports.PRService) *SummarizeCommand { + return &SummarizeCommand{ + prService: prService, + } +} + +func (c *SummarizeCommand) CreateCommand(t *i18n.Translations, cfg *cfg.Config) *cli.Command { + var defaultRepo string + if cfg.ActiveVCSProvider != "" { + if vcsConfig, exists := cfg.VCSConfigs[cfg.ActiveVCSProvider]; exists { + defaultRepo = fmt.Sprintf("%s/%s", vcsConfig.Owner, vcsConfig.Repo) + } + } + + return &cli.Command{ + Name: "summarize-pr", + Aliases: []string{"spr"}, + Usage: t.GetMessage("vcs_summary.pr_summary_usage", 0, nil), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo", + Aliases: []string{"r"}, + Usage: t.GetMessage("vcs_summary.repo_flag_usage", 0, nil), + Value: defaultRepo, + }, + &cli.IntFlag{ + Name: "pr-number", + Aliases: []string{"n"}, + Usage: t.GetMessage("vcs_summary.pr_number_usage", 0, nil), + Required: true, + }, + }, + Action: func(ctx context.Context, command *cli.Command) error { + prNumber := command.Int("pr-number") + repo := command.String("repo") + + if repo == "" || prNumber == 0 { + return fmt.Errorf("%s", t.GetMessage("error.no_repo_configured", 0, nil)) + } + + if _, _, err := parseRepo(repo); err != nil { + return fmt.Errorf(t.GetMessage("error.invalid_repo_format", 0, nil)+": %w", err) + } + + summary, err := c.prService.SummarizePR(ctx, int(prNumber)) + if err != nil { + return fmt.Errorf(t.GetMessage("error.pr_summary_error", 0, nil)+": %w", err) + } + + fmt.Println(t.GetMessage("vcs_summary.pr_summary_success", 0, map[string]interface{}{ + "PRNumber": prNumber, + "Title": summary.Title, + })) + return nil + }, + } +} + +func parseRepo(repo string) (owner string, repoName string, err error) { + parts := strings.Split(repo, "/") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("formato invalido, debe ser owner/repo") + } + return parts[0], parts[1], nil +} diff --git a/internal/cli/command/pr/summarize_test.go b/internal/cli/command/pr/summarize_test.go new file mode 100644 index 0000000..4fc7284 --- /dev/null +++ b/internal/cli/command/pr/summarize_test.go @@ -0,0 +1,122 @@ +package pr + +import ( + "context" + "fmt" + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "testing" +) + +type MockPRService struct { + mock.Mock +} + +func (m *MockPRService) SummarizePR(ctx context.Context, prNumber int) (models.PRSummary, error) { + args := m.Called(ctx, prNumber) + return args.Get(0).(models.PRSummary), args.Error(1) +} + +func setupSummarizeTest(t *testing.T) (*MockPRService, *i18n.Translations, *config.Config) { + mockPRService := new(MockPRService) + + cfg := &config.Config{ + ActiveVCSProvider: "github", + VCSConfigs: map[string]config.VCSConfig{ + "github": { + Owner: "testowner", + Repo: "testrepo", + }, + }, + } + + translations, err := i18n.NewTranslations("es", "../../../i18n/locales") + require.NoError(t, err) + + return mockPRService, translations, cfg +} + +func TestSummarizeCommand(t *testing.T) { + t.Run("should successfully summarize PR", func(t *testing.T) { + // Arrange + mockPRService, translations, cfg := setupSummarizeTest(t) + + prNumber := 123 + summary := models.PRSummary{ + Title: "Test PR", + } + + mockPRService.On("SummarizePR", mock.Anything, prNumber).Return(summary, nil) + + cmd := NewSummarizeCommand(mockPRService).CreateCommand(translations, cfg) + app := cmd + + ctx := context.Background() + + err := app.Run(ctx, []string{"summarize-pr", "--pr-number", "123"}) + + // Assert + assert.NoError(t, err) + mockPRService.AssertExpectations(t) + }) + + t.Run("should fail when repo is not configured", func(t *testing.T) { + // Arrange + mockPRService, translations, cfg := setupSummarizeTest(t) + cfg.ActiveVCSProvider = "" + cfg.VCSConfigs = nil + + cmd := NewSummarizeCommand(mockPRService).CreateCommand(translations, cfg) + app := cmd + ctx := context.Background() + + // Act + err := app.Run(ctx, []string{"summarize-pr", "--pr-number", "123"}) + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), translations.GetMessage("error.no_repo_configured", 0, nil)) + }) + + t.Run("should fail with invalid repo format", func(t *testing.T) { + // Arrange + mockPRService, translations, cfg := setupSummarizeTest(t) + + cmd := NewSummarizeCommand(mockPRService).CreateCommand(translations, cfg) + app := cmd + ctx := context.Background() + + // Act + err := app.Run(ctx, []string{"summarize-pr", "--pr-number", "123", "--repo", "invalid-format"}) + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), translations.GetMessage("error.invalid_repo_format", 0, nil)) + }) + + t.Run("should fail when PR service returns error", func(t *testing.T) { + // Arrange + mockPRService, translations, cfg := setupSummarizeTest(t) + + prNumber := 123 + mockError := fmt.Errorf("service error") + + mockPRService.On("SummarizePR", mock.Anything, prNumber).Return(models.PRSummary{}, mockError) + + cmd := NewSummarizeCommand(mockPRService).CreateCommand(translations, cfg) + app := cmd + ctx := context.Background() + + // Act + err := app.Run(ctx, []string{"summarize-pr", "--pr-number", "123"}) + + // Assert + assert.Error(t, err) + assert.Contains(t, err.Error(), translations.GetMessage("error.pr_summary_error", 0, nil)) + mockPRService.AssertExpectations(t) + }) +} diff --git a/internal/config/config.go b/internal/config/config.go index 278a67a..1259cae 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,7 +21,8 @@ type ( UseTicket bool `json:"use_ticket,omitempty"` AIConfig AIConfig `json:"ai_config"` - GitHubConfig GitHubConfig `json:"github_config"` + VCSConfigs map[string]VCSConfig `json:"vcs_configs"` + ActiveVCSProvider string `json:"active_vcs_provider,omitempty"` } JiraConfig struct { @@ -34,11 +35,12 @@ type ( ActiveAI AI `json:"active_ai"` Models map[AI]Model `json:"models"` } - - GitHubConfig struct { - GitHubToken string `json:"github_token,omitempty"` - GitHubOwner string `json:"github_owner,omitempty"` - GitHubRepo string `json:"github_repo,omitempty"` + + VCSConfig struct { + Provider string `json:"provider"` // github o gitlab lo que se te cante + Token string `json:"token,omitempty"` + Owner string `json:"owner,omitempty"` + Repo string `json:"repo,omitempty"` } ) diff --git a/internal/i18n/locales/active.en.toml b/internal/i18n/locales/active.en.toml index 3f4919a..39de2be 100644 --- a/internal/i18n/locales/active.en.toml +++ b/internal/i18n/locales/active.en.toml @@ -164,6 +164,9 @@ technical_analysis_section = "💭 Technical Analysis:" improvement_suggestions_label = "Suggested Improvements:" criteria_status_full = "⚠️ Criteria Status: {{.Status}}" missing_criteria_none = "✅ Missing Criteria: None" +pr_title_section = "PR Title" +pr_labels_section = "Suggested Tags" +pr_changes_section = "Key Changes" [suggestion_header] other = "=========[ Suggestion {{.Number}} ]=========" @@ -181,6 +184,12 @@ error_missing_model = "You must specify a model" error_invalid_model = "Invalid Model {{.Model}}" config_current_model_for_ai = "Current model for {{.AI}}: {{.Model}}" config_no_model_selected_for_ai = "No model selected for {{.AI}}" +ticket_service_enabled = "Ticket service enabled: {{.Service}}" +ticket_service_disabled = "Ticket service disabled" +jira_config_label = "Jira Settings - BaseURL: {{.BaseURL}}, Email: {{.Email}}" +active_ai_label = "Active AI: {{.IA}}" +ai_models_label = "Configured AI models:" +no_ai_models_configured = "No AI models configured" error_invalid_language = "Invalid Language: {{.Language}}" [error] @@ -192,6 +201,10 @@ get_commits = "Error getting commits for PR #{{.pr_number}}" get_diff = "Error getting diff for PR #{{.pr_number}}" get_repo_labels = "Error getting repository labels" add_labels = "Error adding labels to PR #{{.pr_number}}" +invalid_repo_format = "Invalid repository format" +pr_summary_error = "Error generating PR summary" +no_repo_configured = "No repository configured. Use --repo or configure an active VCS provider" +vcs_provider_not_configured = "VCS provider '{{.Provider}}' is not configured" [label] feature = "New features" @@ -201,4 +214,20 @@ refactor = "Code refactoring" test = "Testing and coverage" infra = "Infrastructure and DevOps" hotfix = "Critical fixes" -default = "Label: {{.label}}" \ No newline at end of file +default = "Label: {{.label}}" + + +[vcs_summary] +pr_summary_usage = "Generate an automatic summary for a Pull Request" +repo_flag_usage = "Specifies the repository in owner/repo format" +pr_number_usage = "Pull Request number to summarize" +pr_summary_success = "✅ PR #{{.PRNumber}} updated: {{.Title}}" +config_set_vcs_usage = "Configure a version control system (VCS) provider" +config_set_vcs_provider_usage = "Name of the VCS provider (github, gitlab, etc.)" +config_set_vcs_token_usage = "Authentication token for the VCS provider" +config_set_vcs_owner_usage = "Owner or user of the repository" +config_set_vcs_repo_usage = "Name of the repository" +config_vcs_updated = "VCS provider '{{.Provider}}' configuration updated successfully" +config_set_active_vcs_usage = "Set the active VCS provider" +config_set_active_vcs_provider_usage = "Name of the VCS provider to set as active" +config_active_vcs_updated = "Active VCS provider set to '{{.Provider}}'" \ No newline at end of file diff --git a/internal/i18n/locales/active.es.toml b/internal/i18n/locales/active.es.toml index 9d6fb06..e430f3d 100644 --- a/internal/i18n/locales/active.es.toml +++ b/internal/i18n/locales/active.es.toml @@ -172,6 +172,9 @@ primary_purpose_prefix = "- Propósito Principal:" technical_impact_prefix = "- Impacto Técnico:" suggestion_prefix = "=========\\[ Sugerencia\\s*\\d*\\s*\\]=========" technical_analysis_section = "💭 Analisis Tecnico:" +pr_title_section = "Titulo del PR" +pr_labels_section = "Etiquetas sugeridas" +pr_changes_section = "Cambios Clave" [suggestion_header] other = "=========[ Sugerencias {{.Number}} ]=========" @@ -206,6 +209,10 @@ get_commits = "Error al obtener los commits del PR #{{.pr_number}}" get_diff = "Error al obtener el diff del PR #{{.pr_number}}" get_repo_labels = "Error al obtener las etiquetas del repo" 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" [label] feature = "Nuevas funcionalidades" @@ -215,4 +222,19 @@ refactor = "Reestructuración de código" test = "Pruebas y coverage" infra = "Infraestructura y DevOps" hotfix = "Correcciones críticas" -default = "Etiqueta: {{.label}}" \ No newline at end of file +default = "Etiqueta: {{.label}}" + +[vcs_summary] +pr_summary_usage = "Genera un resumen automático para un Pull Request" +repo_flag_usage = "Especifica el repositorio en formato owner/repo" +pr_number_usage = "Número del Pull Request a resumir" +pr_summary_success = "✅ PR #{{.PRNumber}} actualizado: {{.Title}}" +config_set_vcs_usage = "Configurar un proveedor de control de versiones (VCS)" +config_set_vcs_provider_usage = "Nombre del proveedor VCS (github, gitlab, etc.)" +config_set_vcs_token_usage = "Token de autenticación para el proveedor VCS" +config_set_vcs_owner_usage = "Propietario o usuario del repositorio" +config_set_vcs_repo_usage = "Nombre del repositorio" +config_vcs_updated = "Configuración del proveedor VCS '{{.Provider}}' actualizada correctamente" +config_set_active_vcs_usage = "Establecer el proveedor VCS activo" +config_set_active_vcs_provider_usage = "Nombre del proveedor VCS a establecer como activo" +config_active_vcs_updated = "Proveedor VCS activo establecido a '{{.Provider}}'" \ No newline at end of file diff --git a/internal/infrastructure/ai/gemini/gemini_pr_summarizer_service.go b/internal/infrastructure/ai/gemini/gemini_pr_summarizer_service.go index f68df60..31fb7b9 100644 --- a/internal/infrastructure/ai/gemini/gemini_pr_summarizer_service.go +++ b/internal/infrastructure/ai/gemini/gemini_pr_summarizer_service.go @@ -60,7 +60,7 @@ func (gps *GeminiPRSummarizer) GeneratePRSummary(ctx context.Context, prompt str return models.PRSummary{}, fmt.Errorf("respuesta vacía de la IA") } - return parseSummary(rawSummary) + return gps.parseSummary(rawSummary) } func (gps *GeminiPRSummarizer) generatePRPrompt(prContent string) string { @@ -68,14 +68,16 @@ func (gps *GeminiPRSummarizer) generatePRPrompt(prContent string) string { return fmt.Sprintf(template, prContent) } -func parseSummary(raw string) (models.PRSummary, error) { +func (gps *GeminiPRSummarizer) parseSummary(raw string) (models.PRSummary, error) { summary := models.PRSummary{} raw = strings.ReplaceAll(raw, "## ", "##") sections := strings.Split(raw, "##") + titleKey := gps.trans.GetMessage("gemini_service.pr_title_section", 0, nil) + labelsKey := gps.trans.GetMessage("gemini_service.pr_labels_section", 0, nil) + changesKey := gps.trans.GetMessage("gemini_service.pr_changes_section", 0, nil) - // Extraer título for _, sec := range sections { - if strings.HasPrefix(sec, "Título del PR") { + if strings.HasPrefix(sec, titleKey) { lines := strings.SplitN(sec, "\n", 2) if len(lines) > 1 { summary.Title = strings.TrimSpace(lines[1]) @@ -84,9 +86,8 @@ func parseSummary(raw string) (models.PRSummary, error) { } } - // Extraer etiquetas for _, sec := range sections { - if strings.HasPrefix(sec, "Etiquetas sugeridas") { + if strings.HasPrefix(sec, labelsKey) { lines := strings.SplitN(sec, "\n", 2) if len(lines) > 1 { labels := strings.Split(lines[1], ",") @@ -100,10 +101,9 @@ func parseSummary(raw string) (models.PRSummary, error) { } } - // Extraer body var bodyParts []string for _, sec := range sections { - if strings.HasPrefix(sec, "Cambios clave") { + if strings.HasPrefix(sec, changesKey) { lines := strings.SplitN(sec, "\n", 2) if len(lines) > 1 { bodyParts = append(bodyParts, strings.TrimSpace(lines[1])) diff --git a/internal/infrastructure/ai/prompts.go b/internal/infrastructure/ai/prompts.go index b513e4b..60c2f00 100644 --- a/internal/infrastructure/ai/prompts.go +++ b/internal/infrastructure/ai/prompts.go @@ -2,16 +2,23 @@ package ai // Templates para Pull Requests const ( - prPromptTemplateEN = `Please generate a concise and clear summary of this Pull Request. - The summary should include: - - The main changes made - - The purpose of the changes - - Any significant technical impact + prPromptTemplateEN = `Hey, could you whip up a summary for this PR with: + + ## PR Title + A short title (max 80 chars). Example: "fix: Image loading error" + + ## Key Changes + - The 3 main changes + - Purpose of each one + - Technical impact if applicable + + ## Suggested Tags + Comma-separated. Options: feature, fix, refactor, docs, infra, test. Example: fix,infra PR Content: %s - Please structure the summary in a professional and clear manner.` + Thanks a bunch, you rock!` prPromptTemplateES = `Che, armame un resumen de este PR con: diff --git a/internal/infrastructure/factory/pr_service_factory.go b/internal/infrastructure/factory/pr_service_factory.go new file mode 100644 index 0000000..687aed8 --- /dev/null +++ b/internal/infrastructure/factory/pr_service_factory.go @@ -0,0 +1,47 @@ +package factory + +import ( + "fmt" + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" + "github.com/Tomas-vilte/MateCommit/internal/services" +) + +type PRServiceFactory struct { + config *config.Config + aiService ports.PRSummarizer + trans *i18n.Translations +} + +func NewPrServiceFactory(cfg *config.Config, trans *i18n.Translations, aiService ports.PRSummarizer) *PRServiceFactory { + return &PRServiceFactory{ + config: cfg, + trans: trans, + aiService: aiService, + } +} + +func (f *PRServiceFactory) CreatePRService() (ports.PRService, error) { + if f.config.ActiveVCSProvider == "" { + return nil, fmt.Errorf("provedor vcs no configurado") + } + + vcsConfig, exists := f.config.VCSConfigs[f.config.ActiveVCSProvider] + if !exists { + return nil, fmt.Errorf("configuración para el proveedor de VCS '%s' no encontrada", f.config.ActiveVCSProvider) + } + + var vcsClient ports.VCSClient + + switch vcsConfig.Provider { + case "github": + vcsClient = github.NewGitHubClient(vcsConfig.Owner, vcsConfig.Repo, vcsConfig.Token, f.trans) + case "?": // bueno aca iria los otros provedores como gitlab o bitbucket, mas adelante se agregara + default: + return nil, fmt.Errorf("proveedor de VCS no compatible: %s", vcsConfig.Provider) + } + + return services.NewPRService(vcsClient, f.aiService), nil +}