From 4f60d066cd359a76327926c9e429f7789f7e0f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8thomas=C2=A8?= Date: Sun, 21 Dec 2025 02:01:08 -0300 Subject: [PATCH 1/2] refactor all --- .github/ISSUE_TEMPLATE/bug_report.yml | 101 +++--- .github/ISSUE_TEMPLATE/feature_request.yml | 67 ++-- .github/PULL_REQUEST_TEMPLATE.md | 26 ++ .github/workflows/golangci-lint.yml | 22 ++ CHANGELOG.md | 61 ++-- COMMANDS.md | 309 +++++----------- CONTRIBUTING.md | 81 +++-- README.md | 110 +++--- cmd/main.go | 250 ++++++++----- docs/es/CHANGELOG.md | 76 ++++ docs/es/COMMANDS.md | 139 ++++++++ docs/es/CONTRIBUTING.md | 40 +++ docs/es/README.md | 79 +++++ internal/cli/command/config/config_test.go | 6 +- internal/cli/command/config/doctor.go | 6 +- internal/cli/command/config/edit.go | 2 +- internal/cli/command/config/edit_test.go | 2 +- internal/cli/command/config/init.go | 26 +- internal/cli/command/config/init_test.go | 24 +- internal/cli/command/config/quick_setup.go | 9 +- internal/cli/command/config/show_test.go | 4 +- internal/cli/command/handler/suggestions.go | 38 +- .../cli/command/handler/suggestions_test.go | 6 +- internal/cli/command/issues/issues.go | 101 +++--- internal/cli/command/issues/issues_test.go | 3 +- internal/cli/command/issues/templates.go | 9 +- .../cli/command/pull_requests/summarize.go | 49 ++- .../command/pull_requests/summarize_test.go | 53 ++- internal/cli/command/release/create.go | 28 +- internal/cli/command/release/create_test.go | 2 +- internal/cli/command/release/edit.go | 15 +- internal/cli/command/release/formatter.go | 2 +- internal/cli/command/release/generate.go | 11 +- internal/cli/command/release/generate_test.go | 2 +- internal/cli/command/release/mocks.go | 8 +- internal/cli/command/release/preview.go | 10 +- internal/cli/command/release/preview_test.go | 2 +- internal/cli/command/release/publish.go | 12 +- internal/cli/command/release/push.go | 9 +- internal/cli/command/release/push_test.go | 2 +- internal/cli/command/release/release.go | 60 +++- .../cli/command/release/version_helper.go | 14 +- internal/cli/command/stats/stats.go | 2 +- internal/cli/command/stats/stats_test.go | 83 +++-- .../suggests_commits/suggests_commits.go | 95 +++-- .../suggests_commits/suggests_commits_test.go | 15 +- internal/cli/command/update/cmd.go | 4 +- internal/cli/completion_helper/helper.go | 4 +- internal/cli/registry/registry.go | 45 --- internal/cli/registry/registry_test.go | 108 ------ internal/config/ai.go | 2 +- internal/config/config.go | 30 +- internal/config/config_test.go | 102 +++--- internal/domain/errors/errors.go | 172 +++++---- internal/domain/errors/errors_test.go | 110 ------ internal/domain/models/issue_generation.go | 40 +-- internal/domain/models/issue_template.go | 16 +- internal/domain/models/pr.go | 23 +- internal/domain/models/release.go | 44 +-- internal/domain/ports/ai_provider.go | 12 +- internal/domain/ports/ai_service.go | 16 +- internal/domain/ports/binary_builder.go | 17 - internal/domain/ports/commit_handler.go | 11 - internal/domain/ports/commit_service.go | 15 - internal/domain/ports/dependency_analyzer.go | 8 +- internal/domain/ports/git_service.go | 42 --- .../domain/ports/issue_generator_service.go | 34 -- .../domain/ports/issue_template_service.go | 26 -- internal/domain/ports/pr_service.go | 12 - internal/domain/ports/release_service.go | 38 -- internal/domain/ports/vcs_client.go | 40 +-- internal/i18n/i18n.go | 8 +- internal/i18n/i18n_test.go | 62 ++-- internal/i18n/locales/active.en.toml | 263 +------------- internal/i18n/locales/active.es.toml | 266 +------------- internal/infrastructure/ai/cost_wrapper.go | 146 +++----- .../infrastructure/ai/cost_wrapper_test.go | 97 ++--- internal/infrastructure/ai/gemini/base.go | 10 +- .../ai/gemini/commit_summarizer_service.go | 86 ++--- .../gemini/commit_summarizer_service_test.go | 108 +++--- .../ai/gemini/gemini_factory.go | 64 ---- .../ai/gemini/gemini_factory_test.go | 67 ---- internal/infrastructure/ai/gemini/helper.go | 12 +- .../ai/gemini/issue_content_generator.go | 47 +-- .../ai/gemini/issue_content_generator_test.go | 17 +- .../pull_requests_summarizer_service.go | 37 +- .../pull_requests_summarizer_service_test.go | 30 +- .../ai/gemini/release_generator.go | 55 ++- .../ai/gemini/release_generator_test.go | 22 +- internal/infrastructure/ai/prompts.go | 163 +++++---- .../ai/registry/ai_provider_registry.go | 89 ----- .../ai/registry/ai_provider_registry_test.go | 121 ------- .../infrastructure/builder/binary_builder.go | 76 +++- .../builder/binary_builder_test.go | 25 +- internal/infrastructure/cache/cache.go | 26 +- .../dependency/gomod_analyzer.go | 20 +- .../dependency/gomod_analyzer_test.go | 4 +- .../dependency/package_json_analyzer.go | 19 +- .../dependency/package_json_analyzer_test.go | 8 +- .../infrastructure/dependency/registry.go | 6 +- internal/infrastructure/di/container.go | 224 ------------ internal/infrastructure/di/container_test.go | 332 ------------------ .../factory/pr_service_factory.go | 63 ---- internal/infrastructure/git/git_service.go | 104 ++---- .../infrastructure/git/git_service_test.go | 78 ++-- .../infrastructure/httpclient/httpclient.go | 7 - .../tickets/jira/jira_factory.go | 47 --- .../tickets/jira/jira_service.go | 89 ++--- .../tickets/jira/jira_service_test.go | 10 +- .../registry/ticket_provider_registry.go | 111 ------ .../registry/ticket_provider_registry_test.go | 228 ------------ .../vcs/github/github_factory.go | 40 --- .../vcs/github/github_service.go | 170 +++++---- .../vcs/github/github_service_test.go | 49 ++- internal/infrastructure/vcs/github/mocks.go | 11 +- .../vcs/registry/vcs_provider_registry.go | 122 ------- internal/providers/ai.go | 68 ++++ internal/providers/tickets.go | 30 ++ internal/providers/vcs.go | 35 ++ internal/services/commit_service.go | 182 +++++----- internal/services/commit_service_test.go | 198 ++++++----- internal/services/cost/calculator.go | 10 +- internal/services/cost/calculator_test.go | 4 +- internal/services/cost/manager.go | 114 +++--- internal/services/cost/manager_test.go | 27 +- internal/services/issue_generator_service.go | 123 ++++--- .../services/issue_generator_service_test.go | 14 +- internal/services/issue_template_service.go | 121 ++++--- .../services/issue_template_service_test.go | 17 +- internal/services/mocks.go | 8 +- internal/services/pull_request_service.go | 138 +++++--- .../services/pull_request_service_test.go | 105 +++--- internal/services/release_service.go | 112 +++--- internal/services/release_service_test.go | 85 ++--- internal/services/routing/model_selector.go | 16 +- internal/services/version_checker.go | 146 +++----- internal/services/version_checker_test.go | 42 +-- internal/ui/ui.go | 138 ++++++-- internal/version/version.go | 6 +- 139 files changed, 3201 insertions(+), 5219 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 docs/es/CHANGELOG.md create mode 100644 docs/es/COMMANDS.md create mode 100644 docs/es/CONTRIBUTING.md create mode 100644 docs/es/README.md delete mode 100644 internal/cli/registry/registry.go delete mode 100644 internal/cli/registry/registry_test.go delete mode 100644 internal/domain/errors/errors_test.go delete mode 100644 internal/domain/ports/binary_builder.go delete mode 100644 internal/domain/ports/commit_handler.go delete mode 100644 internal/domain/ports/commit_service.go delete mode 100644 internal/domain/ports/git_service.go delete mode 100644 internal/domain/ports/issue_generator_service.go delete mode 100644 internal/domain/ports/issue_template_service.go delete mode 100644 internal/domain/ports/pr_service.go delete mode 100644 internal/domain/ports/release_service.go delete mode 100644 internal/infrastructure/ai/gemini/gemini_factory.go delete mode 100644 internal/infrastructure/ai/gemini/gemini_factory_test.go delete mode 100644 internal/infrastructure/ai/registry/ai_provider_registry.go delete mode 100644 internal/infrastructure/ai/registry/ai_provider_registry_test.go delete mode 100644 internal/infrastructure/di/container.go delete mode 100644 internal/infrastructure/di/container_test.go delete mode 100644 internal/infrastructure/factory/pr_service_factory.go delete mode 100644 internal/infrastructure/httpclient/httpclient.go delete mode 100644 internal/infrastructure/tickets/jira/jira_factory.go delete mode 100644 internal/infrastructure/tickets/registry/ticket_provider_registry.go delete mode 100644 internal/infrastructure/tickets/registry/ticket_provider_registry_test.go delete mode 100644 internal/infrastructure/vcs/github/github_factory.go delete mode 100644 internal/infrastructure/vcs/registry/vcs_provider_registry.go create mode 100644 internal/providers/ai.go create mode 100644 internal/providers/tickets.go create mode 100644 internal/providers/vcs.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 32146de..a8ddeae 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,49 +1,54 @@ +name: Bug Report +description: Create a report to help us improve MateCommit +labels: ["bug"] body: - - attributes: - value: Gracias por reportar este problema, tu feedback ayuda a mejorar el proyecto. - type: markdown - - attributes: - description: Contame con tus palabras qué problema encontraste - label: ¿Qué pasó? - placeholder: Cuando intento guardar los cambios, la aplicación deja de responder... - id: description - type: textarea - validations: - required: true - - attributes: - description: Detallá los pasos para que pueda reproducir el error - label: ¿Cómo lo reproduzco? - placeholder: "1. \n2. \n3. " - id: steps - type: textarea - validations: - required: true - - attributes: - label: ¿Qué esperabas que pasara? - placeholder: Esperaba que se guardaran los cambios sin problemas - id: expected - type: textarea - validations: - required: true - - attributes: - label: ¿Qué pasó en realidad? - placeholder: La aplicación se quedó trabada y tuve que cerrarla - id: actual - type: textarea - validations: - required: true - - attributes: - label: Versión - placeholder: v1.0.0 - id: version - type: input - - attributes: - description: Screenshots, logs, o cualquier detalle que pueda ayudar a entender el problema - label: Información extra - id: additional - type: textarea -description: Crea un reporte para ayudarnos a mejorar -labels: - - bug -name: Reporte de Error -title: '[BUG] ' + - type: markdown + attributes: + value: | + Thanks for reporting this issue! Your feedback helps make MateCommit better for everyone. + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. + placeholder: I was trying to... and then... + validations: + required: true + - type: textarea + id: steps + attributes: + label: Reproduction Steps + description: Tell us how to reproduce the error. + placeholder: | + 1. Run `matecommit suggest` + 2. ... + 3. ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + placeholder: I expected the commit message to be... + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? + placeholder: The CLI crashed with error... + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Version of MateCommit (run `matecommit --version`) + placeholder: v1.4.0 + - type: textarea + id: additional + attributes: + label: Additional Context + description: Screenshots, logs, or any other information that might help. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 26a87db..23fcb57 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,35 +1,34 @@ +name: Feature Request +description: Suggest an idea for MateCommit +labels: ["enhancement"] body: - - attributes: - value: Gracias por contribuir con ideas para mejorar el proyecto. - type: markdown - - attributes: - description: Contame qué necesidad o dificultad tenés actualmente - label: ¿Qué problema querés resolver? - placeholder: Me cuesta mucho hacer X porque no hay forma de Y... - id: problem - type: textarea - validations: - required: true - - attributes: - description: Describí cómo te imaginás que debería funcionar - label: ¿Qué solución proponés? - placeholder: Estaría bueno poder hacer X desde Y para que sea más rápido... - id: solution - type: textarea - validations: - required: true - - attributes: - description: Si consideraste alternativas, contame cuáles - label: ¿Pensaste en otras opciones? - id: alternatives - type: textarea - - attributes: - description: Screenshots, ejemplos, o links que puedan ayudar a entender la propuesta - label: Contexto adicional - id: additional - type: textarea -description: Sugiere una idea para este proyecto -labels: - - enhancement -name: Solicitud de Funcionalidad -title: '[FEATURE] ' + - type: markdown + attributes: + value: | + Thanks for contributing ideas to improve the project! + - type: textarea + id: problem + attributes: + label: Problem Statement + description: Is your feature request related to a problem? Please describe. + placeholder: I'm always frustrated when... + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: A clear and concise description of what you want to happen. + placeholder: It would be great if MateCommit could... + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + id: additional + attributes: + label: Additional Context + description: Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1d44608 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +## Description +Please include a summary of the change and which issue is fixed. Also include relevant motivation and context. + +Fixes # (issue) + +## Type of Change +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update + +## How Has This Been Tested? +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. + +- [ ] Unit tests +- [ ] Integration tests +- [ ] Manual test: (describe below) + +## Checklist +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..ef59317 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,22 @@ +name: golangci-lint +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] +permissions: + contents: read +jobs: + golangci-lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3459859..4b6baad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,71 +6,70 @@ All notable changes to this project will be documented in this file. [v1.4.0]: https://github.com/Tomas-vilte/MateCommit/compare/v1.3.0...v1.4.0 -En esta versión nos enfocamos en transformar tu interacción con la herramienta, mejorando drásticamente la experiencia de usuario con feedback visual en tiempo real y optimizando la automatización de tus procesos de release. Además, potenciamos la inteligencia artificial para un análisis más profundo y contextualizado. +In this release, we focused on transforming your interaction with the tool by drastically improving the user experience with real-time visual feedback and optimizing release process automation. Additionally, we've enhanced the AI for deeper and more contextualized analysis. -### ✨ Highlights +### Highlights -- **Experiencia de Usuario Renovada:** Implementamos spinners, colores y una previsualización de cambios (diff) para un feedback visual en tiempo real. Además, agregamos un comando `doctor` para validar tu clave de API de Gemini y mejoramos la previsualización de commits, permitiéndote editar el mensaje antes de confirmar. -- **Automatización Integral de Releases:** Simplificamos y automatizamos la generación de notas de release para `CHANGELOG.md`, la actualización de la versión de la aplicación y el commit automático del changelog. Ahora también podés editar releases existentes, agilizando tu flujo de trabajo. -- **Inteligencia Artificial Contextualizada:** Mejoramos la capacidad de la IA para entender el contexto de tus commits y solicitudes de incorporación (PRs). Ahora detecta automáticamente issues, breaking changes y planes de prueba, enriqueciendo los resúmenes de PR y las notas de release con información más relevante. -- **Análisis de Dependencias Multi-lenguaje:** Agregamos la capacidad de analizar cambios en las dependencias de tus proyectos, incluso en entornos multi-lenguaje, brindando una visión más completa y detallada de cada release. -- **Mejoras en la Interfaz de Línea de Comandos (CLI):** Implementamos autocompletado para comandos y flags, haciendo tu experiencia en la terminal mucho más fluida y eficiente. -- **Correcciones y Estabilidad:** Aseguramos que el resumidor de PRs utilice correctamente la plantilla para las instrucciones de formato JSON, garantizando la consistencia en la generación de resúmenes. +- **Renovated User Experience:** Implemented spinners, colors, and change previews (diff) for real-time visual feedback. Added a `doctor` command to validate your Gemini API key and improved commit previews, allowing message editing before confirmation. +- **Comprehensive Release Automation:** Simplified and automated release note generation for `CHANGELOG.md`, version updates, and automatic changelog commits. You can now also edit existing releases, streamlining your workflow. +- **Contextualized AI:** Enhanced the AI's ability to understand the context of your commits and Pull Requests (PRs). It now automatically detects issues, breaking changes, and test plans, enriching PR summaries and release notes with more relevant information. +- **Multi-language Dependency Analysis:** Added the ability to analyze dependency changes in your projects, even in multi-language environments, providing a more complete and detailed view of each release. +- **CLI Improvements:** Implemented autocompletion for commands and flags, making your terminal experience smoother and more efficient. +- **Fixes and Stability:** Ensured the PR summarizer correctly uses the JSON format template, guaranteeing consistency in summary generation. ## [v1.3.0] - 2025-12-09 [v1.3.0]: https://github.com/Tomas-vilte/MateCommit/compare/v1.2.0...v1.3.0 -En esta versión, nos enfocamos en simplificar y automatizar aún más sus flujos de trabajo, desde la gestión de releases hasta la interacción con Pull Requests. También mejoramos la experiencia de configuración y la estabilidad general de la aplicación. +In this release, we focused on further simplifying and automating your workflows, from release management to Pull Request interaction. We also improved the configuration experience and general application stability. -### ✨ Highlights +### Highlights -- Simplificamos la gestión de releases y la publicación: Ahora pueden generar y publicar nuevas versiones de forma más fluida con comandos dedicados y un asistente de prompts que utiliza un tono natural y en primera persona, haciendo el proceso más intuitivo y automatizado. -- Renovamos la configuración y la asistencia en la CLI: Introdujimos el comando `config init` para guiarlos en la configuración inicial, un comando `edit` para ajustar fácilmente los parámetros, y un comando `help` para que siempre tengan la información a mano. También optimizamos la guía para la configuración de VCS y agregamos una etiqueta de 'performance' para la IA. -- Potenciamos la interacción con Pull Requests: Ahora detectamos automáticamente la información del repositorio para los comandos de PR, validamos y normalizamos las etiquetas, y manejamos mejor los diffs grandes con un sistema de fallback, asegurando un flujo de trabajo más robusto. -- Mejoramos la localización y claridad de los mensajes: Agregamos mensajes internacionalizados para errores de permisos de token de GitHub y para el procesamiento de diffs grandes en PRs, brindando un feedback más claro y útil en español. -- Actualizamos nuestros modelos de IA: Migramos a los modelos Gemini v2.5, lo que nos permite ofrecer respuestas más precisas y eficientes. También mejoramos la configuración de la IA para que puedan ajustarla a sus necesidades. -- Correcciones de Estabilidad y Calidad: Solucionamos un problema que causaba errores en el servicio de IA en ciertas situaciones, mejoramos la precisión de `git add` y corregimos un error de ortografía en nuestros mensajes en español para una experiencia más pulcra. +- **Simplified Release Management:** You can now generate and publish new versions more smoothly with dedicated commands and a prompt assistant using a natural first-person tone. +- **Renewed CLI Configuration & Assistance:** Introduced `config init` to guide you through initial setup, an `edit` command to easily adjust parameters, and a `help` command. Optimized VCS configuration guides and added a 'performance' tag for AI. +- **Enhanced PR Interaction:** Automatically detect repository information for PR commands, validate and normalize tags, and handle large diffs better with a fallback system. +- **Improved Localization:** Added internationalized messages for GitHub token permission errors and large PR diff processing, providing clearer feedback in both English and Spanish. +- **AI Model Updates:** Migrated to Gemini v1.5/2.0 models for more accurate and efficient responses. +- **Stability Fixes:** Resolved AI service errors in specific scenarios, improved `git add` precision, and corrected spelling in Spanish messages. ## [v1.2.0] - 2025-02-18 [v1.2.0]: https://github.com/Tomas-vilte/MateCommit/compare/v1.1.1...v1.2.0 -En esta versión de MateCommit, nos complace presentar una nueva funcionalidad que te va a ahorrar tiempo: la capacidad de resumir Pull Requests. Además, aprovechamos para fortalecer la aplicación mejorando el manejo de errores y su adaptabilidad, para que tu experiencia sea aún más fluida y confiable. +In this version of MateCommit, we're excited to introduce a time-saving feature: Pull Request Summarization. We've also strengthened the application by improving error handling and adaptability. -### ✨ Highlights +### Highlights -- Resumen de Pull Requests: Agregamos el comando `summarize-pr`, una herramienta potente que te permite obtener resúmenes concisos de tus Pull Requests directamente desde la terminal. Para hacer esto posible, implementamos un cliente de GitHub robusto que facilita la interacción con tus repositorios. -- Estabilidad y Adaptabilidad Mejoradas: Optimizamos significativamente el manejo de errores para que la aplicación sea más robusta frente a situaciones inesperadas. También ampliamos la internacionalización, haciendo a MateCommit más adaptable y fácil de usar en distintos contextos. +- **Pull Request Summary:** Added the `summarize-pr` command to get concise PR summaries directly from the terminal. Implemented a robust GitHub client for smoother repository interaction. +- **Improved Stability & Adaptability:** Significantly optimized error handling and expanded internationalization for a more adaptable and reliable experience. ## [v1.1.1] - 2025-02-06 [v1.1.1]: https://github.com/Tomas-vilte/MateCommit/compare/v1.1.0...v1.1.1 -En esta versión nos enfocamos en fortalecer la robustez de la aplicación. Implementamos mejoras clave en el manejo de errores para que las operaciones sean más estables y el feedback, más preciso. +Focused on strengthening application robustness with key error handling improvements for more stable operations and precise feedback. -### ✨ Highlights +### Highlights -- Manejo de Errores Mejorado: Reforzamos la gestión de errores al agregar archivos al staging. Esto significa que la aplicación es más robusta y te brindará información más clara si surge algún problema durante esta operación. +- **Enhanced Error Handling:** Reinforced error management when staging files, providing clearer information if issues arise. ## [v1.1.0] - 2025-02-05 [v1.1.0]: https://github.com/Tomas-vilte/MateCommit/compare/v1.0.0...v1.1.0 -En esta versión de MateCommit, nos enfocamos en expandir las capacidades de nuestra interfaz de línea de comandos. Ahora, los usuarios podrán configurar la inteligencia artificial directamente desde la CLI, lo que facilita una mayor personalización y control sobre sus flujos de trabajo. +Expanded CLI capabilities, allowing users to configure AI settings directly from the terminal for greater personalization and control. -### ✨ Highlights +### Highlights -- Configuración de IA en la CLI: Agregamos una nueva funcionalidad que permite configurar la inteligencia artificial directamente desde la línea de comandos, ofreciendo una experiencia más integrada y un control detallado para los usuarios avanzados. +- **AI Configuration via CLI:** New functionality to configure AI settings directly from the command line, offering an integrated experience for advanced users. ## [v1.0.0] - 2025-01-15 [v1.0.0]: https://github.com/Tomas-vilte/MateCommit/releases/tag/v1.0.0 -En esta primera versión de MateCommit, nos enfocamos en potenciar tu flujo de trabajo con la integración de inteligencia artificial. Ahora, generar mensajes de commit descriptivos y profesionales es más fácil que nunca, y te damos una bienvenida más cálida al iniciar la herramienta. +The first version of MateCommit focuses on boosting your workflow with AI integration. Generating descriptive and professional commit messages is now easier than ever. -### ✨ Highlights - -- **Sugerencias de Mensajes de Commit con IA:** Implementamos la integración con modelos de IA (como Gemini) para que MateCommit te ofrezca sugerencias inteligentes y relevantes para tus mensajes de commit. Esto te ayuda a mantener un historial de cambios claro y consistente sin esfuerzo. -- **Bienvenida Mejorada en la CLI:** Agregamos un mensaje de saludo al iniciar la aplicación, haciendo la experiencia de usuario más amigable y dándote la bienvenida a MateCommit. +### Highlights +- **AI-Powered Commit Suggestions:** Integration with AI models (like Gemini) to offer intelligent and relevant commit message suggestions. +- **Improved CLI Welcome:** Added a greeting message to make the user experience friendlier from the start. diff --git a/COMMANDS.md b/COMMANDS.md index f802a18..b353bdf 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -1,270 +1,139 @@ -*** +# MateCommit CLI Reference -# Guía de la CLI de MateCommit +**Technical Manual & Usage Guide** -Hola, te dejo esta guía para que le saques el jugo a la CLI de MateCommit. Acá vas a encontrar todo lo necesario para empezar a usarla y configurarla a tu gusto. +This document serves as the authoritative reference for the MateCommit CLI. It details execution flows, configuration parameters, and internal behaviors to help you integrate the tool seamlessly into your development pipeline. -## Índice -- [Empezando](#empezando) -- [Comandos Principales](#comandos-principales) - - [Sugerencias de Commits](#sugerencias-de-commits) - - [Configuración](#configuracion) - - [Idiomas](#idiomas) - - [Gestión de Releases](#gestion-de-releases) -- [Ejemplos de uso](#ejemplos-de-uso) -- [Tips y consejos](#tips-y-consejos) +--- -## Empezando +## 1. Commit Intelligence -MateCommit es bastante simple. La idea es que te ayude a armar commits prolijos sin que tengas que dar muchas vueltas pensando el mensaje. - -### Instalación Básica - -Para arrancar, lo mejor es correr la configuración interactiva: - -```bash -# Configuración interactiva completa (recomendado) -matecommit config init - -# Si querés ver qué tenés configurado actualmente -matecommit config show -``` - -### Autocompletado (Shell Completion) - -Para activar el autocompletado en tu terminal (Bash o Zsh) y que la herramienta te sugiera comandos y flags, ejecutá: - -```bash -matecommit completion install -``` - -Esto va a detectar tu shell y agregar la configuración necesaria en tu `.bashrc` o `.zshrc`. -Una vez hecho esto, **reiniciá tu terminal** o ejecutá `source ~/.bashrc` (o el archivo que corresponda). - -Ahora probá escribir `matecommit serv` y apretá `TAB`. ¡Magia! ✨ -También funciona para flags: `matecommit suggest --`. - -## Comandos Principales - -### Sugerencias de Commits - -El comando que más vas a usar es `suggest` (o `s` si no querés escribir tanto). Básicamente analiza tus cambios y te tira opciones. - -```bash -# Generar 3 sugerencias (el default) -matecommit suggest - -# Si querés más variedad (ej: 5 opciones) -matecommit s -n 5 - -# Si necesitás los mensajes en inglés -matecommit s -l en - -# Si preferís un output limpio sin emojis en el commit -matecommit s --no-emoji -``` - -### Configuración - -Tenés varias formas de configurar la herramienta, dependiendo de qué tanto quieras personalizar. - -#### Setup Rápido (Para arrancar ya) - -Si estás apurado y solo querés que ande, usá el flag `--quick`. Solo te va a pedir la API Key de Gemini. - -```bash -matecommit config init --quick -# o más corto -matecommit config init -q -``` - -**Lo que hace este comando:** -- Te pide la API key de Gemini. -- Configura el modelo recomendado (gemini-2.5-flash) por defecto. -- Deja todo listo para usar en menos de un minuto. - -#### Setup Completo (Para tener el control total) - -Si preferís revisar cada detalle, mandale el init completo. Es un wizard interactivo. +### `suggest` / `s` +Analyzes staged changes (`git diff --cached`) and prompts the configured AI model to generate conventional commit messages. +**Command:** ```bash -matecommit config init --full -# o simplemente -matecommit config init +matecommit suggest [flags] ``` -**Acá vas a poder configurar:** +**Technical Details:** +* **Context Window**: The tool sends the diff summary, file names, and (optionally) linked issue context to the LLM. Large diffs are automatically truncated to fit within the model's token limit while preserving critical logic changes. +* **Precedence**: Flags override `config.yaml` settings. For example, passing `--lang en` overrides a global `es` configuration. -1. **IA (Gemini):** API key y qué modelo específico querés usar (flash, pro, etc.). -2. **Idioma:** Si querés que te hable en español o inglés por defecto. -3. **VCS (GitHub):** Token de GitHub. Esto es clave si querés usar las funciones de resumen de PRs o releases. -4. **Tickets (Jira):** Si usás Jira, podés configurar la URL y credenciales para que te linkee los tickets solo. -5. **Releases:** Opciones como actualizar automáticamente el `CHANGELOG.md` (`update_changelog`). - -#### Ver configuración actual - -Para chequear cómo quedó todo configurado: - -```bash -matecommit config show -``` - -#### Editar configuración manualmente - -Si sos de los que prefieren tocar los archivos directamente: +**Flags:** +| Flag | Short | Type | Description | +| :--- | :--- | :--- | :--- | +| `--count` | `-n` | `int` | Quantity of suggestions to generate (1-10). | +| `--lang` | `-l` | `string` | Output language (ISO code, e.g., `en`, `es`, `pt`). | +| `--issue` | `-i` | `int` | Fetches issue title/description from the VCS provider to enrich AI context. | +| `--no-emoji` | `-ne` | `bool` | Strips emojis from the suggestion output for strict convention adherence. | +**Advanced Example:** ```bash -matecommit config edit +# Generate 5 suggestions, forcing English, using Issue #42 for context +matecommit suggest -n 5 -l en -i 42 --no-emoji ``` -Esto te abre el archivo `config.yaml` en tu editor predeterminado. +--- -#### Diagnóstico (Doctor) +## 2. Pull Request Management -Si algo no te anda, tirá este comando para ver qué pasa: +### `summarize-pr` / `spr` +Generates a structured Summary, Test Plan, and Breaking Changes warning for an existing Pull Request. +**Command:** ```bash -matecommit doctor +matecommit spr --pr-number ``` -Verifica que tengas conexión, que las keys sean válidas, que git esté instalado, etc. - -#### Cómo obtener las API Keys +**Workflow:** +1. **Fetch**: Retries PR metadata (commits, diffs, linked issues) via GitHub API. +2. **Analyze**: Uses Gemini to synthesize the changes into a cohesive narrative. +3. **Update**: Patches the PR body directly on GitHub. -**Gemini (Requerido):** -1. Entrá a Google AI Studio. -2. Logueate y generá una API Key nueva. -3. Copiala (empieza con `AIza...`). +**Requirements:** +* `GITHUB_TOKEN` must be set in your environment or config. +* Token scopes: `repo` (for private repos) or `public_repo` (for public). -**GitHub (Opcional, pero recomendado):** -1. Andá a Settings > Developer settings > Personal access tokens > Tokens (classic). -2. Generá un token nuevo con scope `repo` y `read:org`. -3. Usá ese token (empieza con `ghp_...`). +--- -**Importante:** Usá los "Classic tokens", suelen dar menos problemas de permisos que los fine-grained para este tipo de herramientas. +## 3. Issue Lifecycle -### Idiomas - -Podés configurar el idioma base en el `config init`, pero si justo necesitás un commit en otro idioma, podés forzarlo con el flag `-l`: +### `issue generate` / `g` +Creates a GitHub Issue using AI to format vague inputs into professional reports. +**Command:** ```bash -matecommit s -l en # Genera sugerencias en inglés -matecommit s -l es # Genera sugerencias en español +matecommit issue generate [source-flags] [options] ``` -## Gestión de Releases - -MateCommit trae un gestor de releases integrado. Automatiza el versionado, genera el changelog con IA y publica en GitHub. - -### Comandos de Release - -El comando base es `release` (o `r`). - -#### Vista previa (Preview) -Antes de romper nada, fijate qué cambios entrarían en el release: - -```bash -matecommit release preview -# o el alias -matecommit r p -``` -Te muestra la versión actual, la siguiente sugerida y un borrador de las notas. +**Source Flags (Mutually Exclusive):** +* `--from-diff` / `-d`: Uses current staged changes as the basis for the issue (ideal for "I fixed this, now I need a ticket"). +* `--from-pr` / `-p`: Uses a PR's title/body to create a tracking issue. +* `--description` / `-m`: Uses a raw text string as input. -#### Generar notas -Si solo querés el changelog en un archivo: +**Options:** +* `--template` / `-t`: Target specific template keys (e.g., `bug_report`). Matches filename in `.github/ISSUE_TEMPLATE/`. +* `--checkout` / `-c`: Automates branch creation (`git checkout -b issue/123-title`) after generation. +* `--dry-run`: Prints the generated Markdown to stdout without calling the GitHub API. +**Scenario:** +*You just hacked together a fix but didn't open a ticket.* ```bash -matecommit release generate -# Guardar en un archivo específico -matecommit r g -o CHANGELOG.md +git add . +matecommit issue generate --from-diff --template bug_report --assign-me --checkout ``` +*Result: Creates issue, assigns you, and switches branch.* -#### Crear release (Tag local) -Esto crea el tag de git en tu máquina. +--- -```bash -# Con confirmación -matecommit release create - -# Directo sin preguntar (útil para scripts) -matecommit r c --auto - -# Crear y subir a GitHub de una -matecommit r c --publish - -# Actualizar CHANGELOG.md localmente y crear release -matecommit r c --changelog -``` - -**Nota sobre `--changelog`:** -Este flag genera el contenido del changelog, actualiza tu archivo `CHANGELOG.md` local (haciendo prepend), y realiza automáticamente un `git add CHANGELOG.md` y un `git commit` antes de crear el tag. Esto asegura que el changelog actualizado sea parte de la versión liberada. +## 4. Release Automation -Podés configurar esto para que sea el comportamiento por defecto editando tu config (`matecommit config edit`) y seteando `update_changelog: true`. +### `release` / `r` +Standardizes the release process following [Semantic Versioning](https://semver.org/). -#### Publicar en GitHub -Si ya tenés el tag local y querés armar el release en GitHub: +**Subcommands:** -```bash -matecommit release publish -# Publicar como draft (para revisar antes de hacer público) -matecommit r pub --draft -``` +#### `preview` / `p` +Dry-run of the release. Calculates the next version (e.g., `v1.0.0` -> `v1.1.0`) based on commit history (Conventional Commits analysis) and generates the draft changelog. -#### Editar release existente -Si el release ya está creado pero querés mejorar las notas: +#### `create` / `c` +Executes the release locally. +1. Updates `CHANGELOG.md` (prepends new entry). +2. Creates a git tag. +3. (Optional) Pushes changes. -```bash -matecommit release edit -v v1.2.3 -# Regenerar las notas con IA -matecommit r e -v v1.2.3 --ai -``` +**Flags:** +* `--auto`: Non-interactive mode (for CI/CD scripts). +* `--changelog`: Forces the commit of the updated changelog file. +* `--publish`: Triggers `git push origin ` immediately. -### Flujo de trabajo sugerido +#### `publish` / `pub` +Synchronizes a local tag with GitHub Releases. Creates the release entry on GitHub with the AI-generated notes. -1. **Revisión:** Ejecutá `matecommit r p` para ver qué se viene. -2. **Creación:** Si está todo ok, mandale `matecommit r c --publish`. -3. **Drafts:** Si no estás seguro, usá el flag `--draft` al publicar para revisarlo en la web de GitHub antes de soltarlo. +--- -## Ejemplos de uso +## 5. System & Config -### Flujo básico -```bash -# 1. Pedís sugerencias -matecommit s - -# Output: -# Analizando cambios... -# Sugerencias: -# 1. feat: agrega soporte para múltiples idiomas -# Archivos: translations/es.json, translations/en.json -# Explicación: Agregué archivos de traducción base. -# -# Seleccioná una opción: 1 -# Commit creado con éxito. -``` +### `config` +**File Location**: `~/.config/matecommit/config.yaml` (Linux) or standard OS config paths. -### Integración con Jira -Si tenés Jira configurado, la herramienta detecta el contexto y formatea el commit acorde: +* `init`: Interactive wizard. +* `doctor`: Connectivity check (Gemini API, GitHub API, Git binary path). -```bash -matecommit s +### `stats` +Displays cost estimation based on token usage. +* **Note**: Costs are estimates based on standard Gemini pricing. Actual billing may vary. -# Output: -# feat(PROJ-123): implementa nuevo endpoint de usuarios -``` +### `update` +Self-updater using GitHub Releases. Replaces the current binary with the latest stable version. -### Resumen de PR -Si tenés el token de GitHub, podés pedir un resumen de un Pull Request: +--- -```bash -matecommit spr -n 42 -# Output: -# PR #42 actualizado: Implementación de repository pattern -``` +## Environment Variables -## Tips y consejos +MateCommit respects the following environment variables, which override config file values: -1. **Usá los alias:** No escribas `matecommit suggest` cada vez. Usá `matecommit s`. Lo mismo para `r` (release) o `spr` (resumen de PR). -2. **Feedback:** Si ninguna sugerencia te convence, seleccioná la opción 0 para generar nuevas o editar una manualmente. -3. **Repos de Organización:** Si laburás en una organización de GitHub, acordate de usar el Personal Access Token (Classic) y autorizarlo en la organización (SSO), si no te va a tirar error de permisos. -4. **Release rápido:** Si confiás en la herramienta, `matecommit r c --publish --auto` hace todo el trabajo sucio (tag, notas, push y release) en un solo paso. \ No newline at end of file +* `GEMINI_API_KEY`: Google AI Studio Key. +* `GITHUB_TOKEN`: GitHub Personal Access Token. +* `MATECOMMIT_LANG`: Default language override. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a587ba5..d4efab6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,49 +1,60 @@ -# Cómo contribuir a MateCommit +# Contributing to MateCommit -¡Gracias por querer aportar a MateCommit! Soy yo, el único que está trabajando en este proyecto por ahora, pero si en algún momento alguien más se suma, estas son las reglas básicas para mantener todo en orden. +Thank you for your interest in contributing to MateCommit! We welcome contributions of all kinds, from bug fixes and new features to documentation improvements. -## Reglas generales +While the project is currently maintained by a small team, we strive to follow best practices to ensure code quality and project sustainability. -1. **Creación de tags**: - - Los tags los manejo yo (el único admin por ahora jeje), nadie más puede crear, actualizar o borrar tags - - Los tags se ponen solo después de que todo pase las verificaciones de estado, o sea, si las pruebas y el CI/CD no se quejan. +--- -2. **Verificación del código**: - - Antes de mandar un PR, asegurate de que tu código no esté todo roto. Si no pasa las pruebas, no voy a aceptar el PR xd, hasta que esté todo en ok. +## Getting Started + +1. **Fork the repository** on GitHub. +2. **Clone your fork** locally: + ```bash + git clone https://github.com/YOUR_USERNAME/MateCommit.git + ``` +3. **Sync dependencies**: + ```bash + go mod tidy + ``` +4. **Create a feature branch**: + ```bash + git checkout -b feature/your-awesome-feature + ``` -3. **Cómo manejar los PRs**: - - Los PRs tienen que estar bien hechos. - - No seas vago, poné una descripción clara de qué hiciste y por qué lo hiciste. Nada de "cambié el código", explicá qué cosas cambiaron, para que el que vea sepa qué hiciste y tenga más contexto. +--- -## Cómo hacer un Pull Request (PR) +## Contribution Guidelines -Si querés sumar tu cambio, hacé lo siguiente: +### 1. Code Quality & Standards +- Ensure your code follows standard Go formatting (`go fmt`). +- Run tests before submitting a Pull Request: + ```bash + go test ./... + ``` +- If you're adding a new feature, please include corresponding tests. -1. **Forkea el repo**. -2. **Clonalo a tu máquina**. -3. **Creá una rama** con un nombre que se entienda qué vas a hacer (tipo `feature/agregar-nueva-funcionalidad` o `bugfix/corregir-error`). -4. **Hacés los cambios** y asegurate de que no estés rompiendo todo. ¡Testea bien, no seas pajero! -5. **Hacé un commit** con un mensaje claro. Usá este formato: - - `tipo: descripción corta` (Ejemplo: `feat: agregar nueva funcionalidad` o `fix: corregir bug en validación`). - - Si hace falta, explicá un toque más en el cuerpo del mensaje. -6. **Mandá el PR** desde tu rama hacia `master`. - - Revisá que no haya conflictos y que el código esté bien. - - Yo lo reviso, y si todo está ok, lo apruebo. +### 2. Commit Messages +We practice what we preach! Please use clear and descriptive commit messages. We recommend the [Conventional Commits](https://www.conventionalcommits.org/) format: +- `feat: add support for new AI model` +- `fix: resolve issue with Jira integration` +- `docs: update installation instructions` -## Buenas prácticas +### 3. Pull Request Process +- Provide a clear description of the changes and the problem they solve. +- Link any related issues in the PR description. +- Ensure all CI checks pass. +- A maintainer will review your PR as soon as possible. -- **Mantené tu fork actualizado**: Antes de ponerte a cambiar cosas, asegurate de que tu fork esté sincronizado con el repo principal. -- **Escribí pruebas**: Si estás agregando algo nuevo, escribí pruebas para eso. Si corregís un bug, agregá una prueba para que no vuelva a pasar. -- **Documentá tus cambios**: Si hacés un cambio importante, ponélo en la documentación para que no dejemos a nadie perdido. +--- -## Resumen +## Best Practices +- **Update Documentation**: If your change affects how the tool is used, please update `README.md` or `COMMANDS.md`. +- **Stay Updated**: Before starting work, ensure your fork is synced with the main repository's `master` branch. -1. **Los tags son exclusivos de los admins**. Si no sos admin, no toques nada de eso. -2. **Tu código tiene que pasar las verificaciones antes de ser aprobado** Sino tabla. -3. **Seguí las buenas prácticas al hacer un PR**. +--- -¡Gracias por querer sumar! +## Admin & Releases +*Note: Tagging and release management are currently handled by the core maintainers to ensure stable versions are delivered.* ---- -PD: perdón que sea tan rompe bolas con esto, pero aprendemos a usar buenas prácticas y aprender obviamente. -**MateCommit - Por ahora, soy yo. Si algún día alguien más se suma, les cuento las reglas también.** +Thank you for helping us make MateCommit even better! Happy coding. diff --git a/README.md b/README.md index 761be40..32b4c25 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,79 @@ -*** -# MateCommit +
+ MateCommit Logo -![Build Status](https://github.com/Tomas-vilte/MateCommit/workflows/Go%20CI/badge.svg) ![Report Card](https://goreportcard.com/badge/github.com/Tomas-vilte/MateCommit) [![codecov](https://codecov.io/gh/Tomas-vilte/MateCommit/branch/master/graph/badge.svg?token=6O798E12DC)](https://codecov.io/gh/Tomas-vilte/MateCommit) + # MateCommit -Bienvenido a **MateCommit**. Este proyecto nació para simplificar el flujo de trabajo con Git y sacar un poco de la fricción del día a día. + **Effortless Git Intelligence** -## ¿De qué se trata? + MateCommit leverage the power of Gemini AI to transform your raw code changes into meaningful, descriptive commit history. Stop struggling with naming; focus on building. -Básicamente, si te da fiaca pensar nombres descriptivos para los commits o sentís que perdés tiempo en eso, esta herramienta te da una mano. Analiza los cambios que tenés en staging y te sugiere títulos coherentes usando IA, para que vos te ocupes del código (y del mate). + [Go Report Card](https://goreportcard.com/report/github.com/Tomas-vilte/MateCommit) | [License](https://opensource.org/licenses/MIT) | [Build Status](https://github.com/Tomas-vilte/MateCommit/actions) -### Características principales -- **Sugerencias inteligentes**: Analiza el diff y te propone mensajes de commit con sentido. -- **Integración con GitHub**: Se lleva bien con tu flujo de trabajo actual. -- **Motor de IA**: Funciona con Gemini y soporta varios de sus modelos (Flash, Pro, etc.). -- **Idiomas**: Podés pedirle las sugerencias tanto en español como en inglés. -- **Resumen de Pull Requests**: Te arma una descripción del PR basándose en todos los commits y cambios que hiciste. -- **Gestión de Releases**: Automatiza el versionado y la generación del changelog. +
-## Instalación +--- -Tenés dos formas de instalarlo, elegí la que te quede más cómoda. +### Languages +* [Official Documentation (English)](#) +* [Traducción al Español (🇦🇷)](./docs/es/README.md) -### Opción 1: Usando el binario (Recomendado) +--- -1. **Descargá el ejecutable** desde la sección de releases para tu sistema operativo (Linux, Windows o Mac). +## The Value Proposition +Writing high-quality commit messages is a critical yet time-consuming task. MateCommit eliminates this friction by analyzing your `git diff` and suggesting context-aware messages that follow conventional standards. -2. **Dale permisos de ejecución** (si estás en Linux o Mac): - ```bash - chmod +x matecommit-linux-amd64 - ``` +* **Intelligent Suggestions**: Contextual analysis of logic changes, not just file names. +* **Gemini Power**: Optimized for Flash 1.5 and 2.0 models for speed and accuracy. +* **Issue Life-cycle**: Generate GitHub issues from code, PRs, or descriptions. +* **Unified Releases**: Automated changelog generation, tagging, and GitHub publishing. +* **PR Intelligence**: Instant summaries for complex Pull Requests. +* **Usage Insights**: Track AI costs and usage statistics directly in your terminal. +* **Efficiency Tools**: Shell autocompletion, local caching, and diagnostic tools. -3. **Movelo a tu PATH** para poder ejecutarlo desde cualquier lado: - ```bash - sudo mv matecommit-linux-amd64 /usr/local/bin/matecommit - ``` -4. **Configuración inicial**: - Corré el comando de inicialización para dejar todo listo (API keys, preferencias, etc.): - ```bash - matecommit config init - ``` +--- -### Opción 2: Desde el código fuente +## Quick Start -Si preferís compilarlo vos mismo: +### 1. Installation +Install the latest binary using Go: -1. **Cloná el repositorio**: - ```bash - git clone https://github.com/Tomas-vilte/MateCommit.git - ``` +```bash +go install github.com/Tomas-vilte/MateCommit/cmd/matecommit@latest +``` -2. **Bajá las dependencias**: - ```bash - cd MateCommit - go mod tidy - ``` +### 2. Initial Setup +Run the guided configuration to set up your API keys and preferences: -3. **Compilá el binario**: - ```bash - go build -o matecommit ./cmd/main.go - ``` +```bash +matecommit config init +``` -## Documentación +### 3. Usage +Stage your files and generate suggestions: -Para no hacer este README eterno, separé la guía detallada de uso en otro archivo. En [COMMANDS.md](COMMANDS.md) vas a encontrar: +```bash +git add . +matecommit suggest +``` -- Cómo hacer la configuración completa paso a paso. -- Explicación de todos los comandos (`suggest`, `release`, etc.). -- Cómo integrar la herramienta con Jira. -- Ejemplos de output y algunos trucos. +--- -## Modelos de IA +## Advanced Features +MateCommit is built for professional workflows, including: -Actualmente la herramienta funciona con **Gemini** (Google). Probamos y soporta bien las versiones `1.5-flash`, `1.5-pro` y `2.0-flash`. +* **Jira Integration**: Automatic ticket linking based on branch context. +* **PR Summarization**: Generates standardized Pull Request bodies. +* **Release Automation**: Updates changelogs and pushes tags in one motion. -Tengo en el roadmap integrar **GPT-4** y **Claude** más adelante para dar más opciones. +For a full technical overview of all commands, see [COMMANDS.md](./COMMANDS.md). -## Licencia +--- -El código es abierto bajo licencia MIT. Fijate el archivo [LICENSE](./LICENSE) para más detalles. +## Contributing +We value high-quality contributions. If you are interested in improving the codebase or documentation, please review our [Contribution Guidelines](./CONTRIBUTING.md). -## Contribuciones +--- -Si querés sumar algo, arreglar un bug o mejorar la documentación, sos más que bienvenido. Pegale una mirada a la guía de contribución en `CONTRIBUTING.md` para ver cómo nos manejamos. \ No newline at end of file +## License +Distributed under the MIT License. See [LICENSE](./LICENSE) for more information. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index a244c68..1a61919 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "strings" "github.com/Tomas-vilte/MateCommit/internal/cli/command/cache" "github.com/Tomas-vilte/MateCommit/internal/cli/command/completion" @@ -16,25 +17,23 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/cli/command/stats" "github.com/Tomas-vilte/MateCommit/internal/cli/command/suggests_commits" "github.com/Tomas-vilte/MateCommit/internal/cli/command/update" - "github.com/Tomas-vilte/MateCommit/internal/cli/registry" 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/Tomas-vilte/MateCommit/internal/infrastructure/ai/gemini" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/di" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/factory" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/git" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/tickets/jira" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" + "github.com/Tomas-vilte/MateCommit/internal/providers" "github.com/Tomas-vilte/MateCommit/internal/services" + "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/Tomas-vilte/MateCommit/internal/version" + "github.com/fatih/color" "github.com/urfave/cli/v3" ) func main() { app, err := initializeApp() if err != nil { - log.Fatalf("Error iniciando la cli: %v", err) + log.Fatalf("Error starting the CLI: %v", err) } if err := app.Run(context.Background(), os.Args); err != nil { @@ -45,7 +44,7 @@ func main() { func initializeApp() (*cli.Command, error) { homeDir, err := os.UserHomeDir() if err != nil { - return nil, fmt.Errorf("no se pudo obtener el directorio del usuario: %w", err) + return nil, fmt.Errorf("could not get user home directory: %w", err) } cfgApp, err := cfg.LoadConfig(homeDir) @@ -55,102 +54,118 @@ func initializeApp() (*cli.Command, error) { translations, err := i18n.NewTranslations(cfgApp.Language, "") if err != nil { - log.Fatalf("Error al cargar las traducciones: %v", err) + log.Fatalf("Error loading translations: %v", err) } - err = cfg.SaveConfig(cfgApp) - if err != nil { + if err = cfg.SaveConfig(cfgApp); err != nil { return nil, err } - container := di.NewContainer(cfgApp, translations) - - if err := container.RegisterAIProvider("gemini", gemini.NewGeminiProviderFactory()); err != nil { - log.Printf("Warning: no se pudo registrar el proveedor Gemini: %v", err) - } - - if err := container.RegisterVCSProvider("github", github.NewGitHubProviderFactory()); err != nil { - log.Printf("Warning: no se pudo registrar el proveedor de GitHub: %v", err) - } - - if err := container.RegisterTicketProvider("jira", jira.NewJiraProviderFactory()); err != nil { - log.Printf("Warning: no se pudo registrar el proveedor Jira: %v", err) - } - - gitService := git.NewGitService(translations) - container.SetGitService(gitService) - ctx := context.Background() - commitService, err := container.GetCommitService(ctx) - if err != nil { - log.Printf("Warning: la inicialización del servicio de confirmación falló: %v", err) - log.Println("La IA no está configurada. Podés configurarla con 'matecommit config init'") - } - - var vcsClient = container.GetVCSRegistry() - vcsClientInstance, _ := vcsClient.CreateClientFromConfig(ctx, gitService, cfgApp, translations) - commitHandler := handler.NewSuggestionHandler(gitService, vcsClientInstance, translations) - aiSummarizer, err := container.GetPRSummarizer(ctx) - if err != nil { - log.Printf("Warning: no se pudo crear el servicio de IA para PRs: %v", err) - aiSummarizer = nil - } - - prServiceFactory := factory.NewPrServiceFactory(cfgApp, translations, aiSummarizer, gitService) - prCommand := pull_requests.NewSummarizeCommand(prServiceFactory) - registerCommand := registry.NewRegistry(cfgApp, translations) + gitService := git.NewGitService() - if err := registerCommand.Register("suggest", suggests_commits.NewSuggestCommandFactory(commitService, commitHandler)); err != nil { - log.Fatalf("Error al registrar el comando 'suggest': %v", err) - } - - issueServiceProvider := func(ctx context.Context) (ports.IssueGeneratorService, error) { - return container.GetIssueGeneratorService(ctx) - } - - if err := registerCommand.Register("issue", issues.NewIssuesCommandFactory(issueServiceProvider, container.GetIssueTemplateService())); err != nil { - log.Fatalf("Error al registrar el comando 'issue': %v", err) - } + var commitAI ports.CommitSummarizer + var prAI ports.PRSummarizer + var issueAI ports.IssueContentGenerator - if err := registerCommand.Register("config", config.NewConfigCommandFactory()); err != nil { - log.Fatalf("Error al registrar el comando 'config': %v", err) - } + if cfgApp.AIConfig.ActiveAI != "" { + onConfirmation := createConfirmationCallback(translations) - if err := registerCommand.Register("doctor", config.NewDoctorCommand()); err != nil { - log.Fatalf("Error al registrar el comando 'doctor': %v", err) - } + commitAI, err = providers.NewCommitSummarizer(ctx, cfgApp, onConfirmation) + if err != nil { + log.Printf("Warning: could not create CommitSummarizer: %v", err) + log.Println("AI is not configured. You can configure it with 'mate-commit config init'") + } - if err := registerCommand.Register("summarize-pr", prCommand); err != nil { - log.Fatalf("Error al registrar el comando 'summarize-pr': %v", err) - } + prAI, err = providers.NewPRSummarizer(ctx, cfgApp, onConfirmation) + if err != nil { + log.Printf("Warning: could not create PRSummarizer: %v", err) + prAI = nil + } - if err := registerCommand.Register("release", release.NewReleaseCommandFactory(gitService, cfgApp)); err != nil { - log.Fatalf("Error al registrar el comando 'release': %v", err) + issueAI, err = providers.NewIssueContentGenerator(ctx, cfgApp, onConfirmation) + if err != nil { + log.Printf("Warning: could not create IssueContentGenerator: %v", err) + issueAI = nil + } } - if err := registerCommand.Register("update", update.NewUpdateCommandFactory("v1.4.0")); err != nil { - log.Fatalf("Error al registrar el comando 'update': %v", err) + vcsClient, err := providers.NewVCSClient(ctx, gitService, cfgApp) + if err != nil { + log.Printf("Warning: could not create VCS client: %v", err) + vcsClient = nil } - commands := registerCommand.CreateCommands() - commands = append(commands, completion.NewCompletionCommand(translations)) - commands = append(commands, stats.NewStatsCommand().CreateCommand(translations, cfgApp)) - commands = append(commands, cache.NewCacheCommand().CreateCommand(translations, cfgApp)) - - helpCommand := &cli.Command{ + ticketMgr, err := providers.NewTicketManager(ctx, cfgApp) + if err != nil { + log.Printf("Warning: could not create Ticket manager: %v", err) + ticketMgr = nil + } + + commitService := services.NewCommitService( + gitService, + commitAI, + services.WithTicketManager(ticketMgr), + services.WithVCSClient(vcsClient), + services.WithConfig(cfgApp), + ) + + prService := services.NewPRService( + services.WithPRVCSClient(vcsClient), + services.WithPRAIProvider(prAI), + services.WithPRConfig(cfgApp), + ) + + templateService := services.NewIssueTemplateService( + services.WithTemplateConfig(cfgApp), + ) + + issueService := services.NewIssueGeneratorService( + gitService, + issueAI, + services.WithIssueVCSClient(vcsClient), + services.WithIssueTemplateService(templateService), + services.WithIssueConfig(cfgApp), + ) + + commitHandler := handler.NewSuggestionHandler(gitService, vcsClient, translations) + + issueProvider := func(ctx context.Context) (issues.IssueGeneratorService, error) { + return issueService, nil + } + commands := []*cli.Command{ + suggests_commits.NewSuggestCommandFactory(commitService, commitHandler).CreateCommand(translations, cfgApp), + issues.NewIssuesCommandFactory( + issueProvider, + templateService, + ).CreateCommand(translations, cfgApp), + pull_requests.NewSummarizeCommand(func(ctx context.Context) (pull_requests.PRService, error) { + return prService, nil + }).CreateCommand(translations, cfgApp), + release.NewReleaseCommandFactory(gitService, cfgApp).CreateCommand(translations, cfgApp), + config.NewConfigCommandFactory().CreateCommand(translations, cfgApp), + config.NewDoctorCommand().CreateCommand(translations, cfgApp), + update.NewUpdateCommandFactory("v1.4.0").CreateCommand(translations, cfgApp), + completion.NewCompletionCommand(translations), + stats.NewStatsCommand().CreateCommand(translations, cfgApp), + cache.NewCacheCommand().CreateCommand(translations, cfgApp), + } + + commands = append(commands, &cli.Command{ Name: "help", Aliases: []string{"h"}, Usage: translations.GetMessage("help_command_usage", 0, nil), Action: func(ctx context.Context, cmd *cli.Command) error { return cli.ShowAppHelp(cmd) }, - } - commands = append(commands, helpCommand) + }) go func() { - checker := services.NewVersionUpdater(version.FullVersion(), translations) - checker.CheckForUpdates(context.Background()) + checker := services.NewVersionUpdater( + services.WithCurrentVersion(version.FullVersion()), + ) + _, _ = checker.CheckForUpdates(context.Background()) }() return &cli.Command{ @@ -162,3 +177,78 @@ func initializeApp() (*cli.Command, error) { EnableShellCompletion: true, }, nil } + +func createConfirmationCallback(t *i18n.Translations) ai.ConfirmationCallback { + return func(result ai.ConfirmationResult) (string, bool) { + ui.SuspendActiveSpinner() + defer ui.ResumeSuspendedSpinner() + + cyan := color.New(color.FgCyan, color.Bold) + yellow := color.New(color.FgYellow) + hasSuggestion := result.SuggestedModel != "" && result.SuggestedModel != result.CurrentModel + + fmt.Println() + _, _ = cyan.Println(t.GetMessage("cost.confirmation_separator", 0, nil)) + _, _ = cyan.Println(t.GetMessage("cost.confirmation_header", 0, nil)) + _, _ = cyan.Println(t.GetMessage("cost.confirmation_separator", 0, nil)) + + if result.RationaleKey != "" { + rationale := t.GetMessage(result.RationaleKey, 0, nil) + _, _ = yellow.Println(t.GetMessage("cost.routing_suggestion", 0, map[string]interface{}{ + "Rationale": rationale, + })) + _, _ = yellow.Println(t.GetMessage("cost.routing_suggested_model", 0, map[string]interface{}{ + "Suggested": result.SuggestedModel, + "Current": result.CurrentModel, + })) + } + + fmt.Println(t.GetMessage("cost.confirmation_input_tokens", 0, map[string]interface{}{ + "Tokens": yellow.Sprintf("%d", result.InputTokens), + })) + fmt.Println(t.GetMessage("cost.confirmation_output_tokens", 0, map[string]interface{}{ + "Tokens": yellow.Sprintf("%d", result.OutputTokens), + })) + + costLabel := t.GetMessage("cost.confirmation_estimated_cost", 0, map[string]interface{}{ + "Cost": yellow.Sprintf("$%.4f", result.EstimatedCost), + }) + if hasSuggestion { + fmt.Printf("%s (%s)\n", costLabel, result.SuggestedModel) + } else { + fmt.Println(costLabel) + } + + _, _ = cyan.Println(t.GetMessage("cost.confirmation_separator", 0, nil)) + + if hasSuggestion { + fmt.Printf("%s ", t.GetMessage("cost.confirmation_use_suggested", 0, nil)) + fmt.Printf("%s\n", color.HiBlackString(t.GetMessage("cost.confirmation_use_suggested_help", 0, nil))) + } else { + fmt.Printf("%s ", t.GetMessage("cost.confirmation_prompt", 0, nil)) + } + + var response string + _, _ = fmt.Scanln(&response) + response = strings.TrimSpace(strings.ToLower(response)) + + if !hasSuggestion { + proceed := response == "" || response == "y" || response == "yes" + if proceed { + return "original", true + } + return "", false + } + + switch response { + case "", "y", "yes": + return "suggested", true + case "m", "stay": + return "original", true + case "c", "cancel", "n", "no": + return "", false + default: + return "", false + } + } +} diff --git a/docs/es/CHANGELOG.md b/docs/es/CHANGELOG.md new file mode 100644 index 0000000..81b4ecc --- /dev/null +++ b/docs/es/CHANGELOG.md @@ -0,0 +1,76 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [v1.4.0] + +[v1.4.0]: https://github.com/Tomas-vilte/MateCommit/compare/v1.3.0...v1.4.0 + +En esta versión nos enfocamos en transformar tu interacción con la herramienta, mejorando drásticamente la experiencia de usuario con feedback visual en tiempo real y optimizando la automatización de tus procesos de release. Además, potenciamos la inteligencia artificial para un análisis más profundo y contextualizado. + +### Highlights + +- **Experiencia de Usuario Renovada:** Implementamos spinners, colores y una previsualización de cambios (diff) para un feedback visual en tiempo real. Además, agregamos un comando `doctor` para validar tu clave de API de Gemini y mejoramos la previsualización de commits, permitiéndote editar el mensaje antes de confirmar. +- **Automatización Integral de Releases:** Simplificamos y automatizamos la generación de notas de release para `CHANGELOG.md`, la actualización de la versión de la aplicación y el commit automático del changelog. Ahora también podés editar releases existentes, agilizando tu flujo de trabajo. +- **Inteligencia Artificial Contextualizada:** Mejoramos la capacidad de la IA para entender el contexto de tus commits y solicitudes de incorporación (PRs). Ahora detecta automáticamente issues, breaking changes y planes de prueba, enriqueciendo los resúmenes de PR y las notas de release con información más relevante. +- **Análisis de Dependencias Multi-lenguaje:** Agregamos la capacidad de analizar cambios en las dependencias de tus proyectos, incluso en entornos multi-lenguaje, brindando una visión más completa y detallada de cada release. +- **Mejoras en la Interfaz de Línea de Comandos (CLI):** Implementamos autocompletado para comandos y flags, haciendo tu experiencia en la terminal mucho más fluida y eficiente. +- **Correcciones y Estabilidad:** Aseguramos que el resumidor de PRs utilice correctamente la plantilla para las instrucciones de formato JSON, garantizando la consistencia en la generación de resúmenes. + +## [v1.3.0] - 2025-12-09 + +[v1.3.0]: https://github.com/Tomas-vilte/MateCommit/compare/v1.2.0...v1.3.0 + +En esta versión, nos enfocamos en simplificar y automatizar aún más sus flujos de trabajo, desde la gestión de releases hasta la interacción con Pull Requests. También mejoramos la experiencia de configuración y la estabilidad general de la aplicación. + +### Highlights + +- Simplificamos la gestión de releases y la publicación: Ahora pueden generar y publicar nuevas versiones de forma más fluida con comandos dedicados y un asistente de prompts que utiliza un tono natural y en primera persona, haciendo el proceso más intuitivo y automatizado. +- Renovamos la configuración y la asistencia en la CLI: Introdujimos el comando `config init` para guiarlos en la configuración inicial, un comando `edit` para ajustar fácilmente los parámetros, y un comando `help` para que siempre tengan la información a mano. También optimizamos la guía para la configuración de VCS y agregamos una etiqueta de 'performance' para la IA. +- Potenciamos la interacción con Pull Requests: Ahora detectamos automáticamente la información del repositorio para los comandos de PR, validamos y normalizamos las etiquetas, y manejamos mejor los diffs grandes con un sistema de fallback, asegurando un flujo de trabajo más robusto. +- Mejoramos la localización y claridad de los mensajes: Agregamos mensajes internacionalizados para errores de permisos de token de GitHub y para el procesamiento de diffs grandes en PRs, brindando un feedback más claro y útil en español. +- Actualizamos nuestros modelos de IA: Migramos a los modelos Gemini v2.5, lo que nos permite ofrecer respuestas más precisas y eficientes. También mejoramos la configuración de la IA para que puedan ajustarla a sus necesidades. +- Correcciones de Estabilidad y Calidad: Solucionamos un problema que causaba errores en el servicio de IA en ciertas situaciones, mejoramos la precisión de `git add` y corregimos un error de ortografía en nuestros mensajes en español para una experiencia más pulcra. + +## [v1.2.0] - 2025-02-18 + +[v1.2.0]: https://github.com/Tomas-vilte/MateCommit/compare/v1.1.1...v1.2.0 + +En esta versión de MateCommit, nos complace presentar una nueva funcionalidad que te va a ahorrar tiempo: la capacidad de resumir Pull Requests. Además, aprovechamos para fortalecer la aplicación mejorando el manejo de errores y su adaptabilidad, para que tu experiencia sea aún más fluida y confiable. + +### Highlights + +- Resumen de Pull Requests: Agregamos el comando `summarize-pr`, una herramienta potente que te permite obtener resúmenes concisos de tus Pull Requests directamente desde la terminal. Para hacer esto posible, implementamos un cliente de GitHub robusto que facilita la interacción con tus repositorios. +- Estabilidad y Adaptabilidad Mejoradas: Optimizamos significativamente el manejo de errores para que la aplicación sea más robusta frente a situaciones inesperadas. También ampliamos la internacionalización, haciendo a MateCommit más adaptable y fácil de usar en distintos contextos. + +## [v1.1.1] - 2025-02-06 + +[v1.1.1]: https://github.com/Tomas-vilte/MateCommit/compare/v1.1.0...v1.1.1 + +En esta versión nos enfocamos en fortalecer la robustez de la aplicación. Implementamos mejoras clave en el manejo de errores para que las operaciones sean más estables y el feedback, más preciso. + +### Highlights + +- Manejo de Errores Mejorado: Reforzamos la gestión de errores al agregar archivos al staging. Esto significa que la aplicación es más robusta y te brindará información más clara si surge algún problema durante esta operación. + +## [v1.1.0] - 2025-02-05 + +[v1.1.0]: https://github.com/Tomas-vilte/MateCommit/compare/v1.0.0...v1.1.0 + +En esta versión de MateCommit, nos enfocamos en expandir las capacidades de nuestra interfaz de línea de comandos. Ahora, los usuarios podrán configurar la inteligencia artificial directamente desde la CLI, lo que facilita una mayor personalización y control sobre sus flujos de trabajo. + +### Highlights + +- Configuración de IA en la CLI: Agregamos una nueva funcionalidad que permite configurar la inteligencia artificial directamente desde la línea de comandos, ofreciendo una experiencia más integrada y un control detallado para los usuarios avanzados. + +## [v1.0.0] - 2025-01-15 + +[v1.0.0]: https://github.com/Tomas-vilte/MateCommit/releases/tag/v1.0.0 + +En esta primera versión de MateCommit, nos enfocamos en potenciar tu flujo de trabajo con la integración de inteligencia artificial. Ahora, generar mensajes de commit descriptivos y profesionales es más fácil que nunca, y te damos una bienvenida más cálida al iniciar la herramienta. + +### Highlights + +- **Sugerencias de Mensajes de Commit con IA:** Implementamos la integración con modelos de IA (como Gemini) para que MateCommit te ofrezca sugerencias inteligentes y relevantes para tus mensajes de commit. Esto te ayuda a mantener un historial de cambios claro y consistente sin esfuerzo. +- **Bienvenida Mejorada en la CLI:** Agregamos un mensaje de saludo al iniciar la aplicación, haciendo la experiencia de usuario más amigable y dándote la bienvenida a MateCommit. + diff --git a/docs/es/COMMANDS.md b/docs/es/COMMANDS.md new file mode 100644 index 0000000..b87b89b --- /dev/null +++ b/docs/es/COMMANDS.md @@ -0,0 +1,139 @@ +# Referencia de CLI MateCommit + +**Manual Técnico y Guía de Uso** + +Este documento es la referencia definitiva para exprimir al máximo MateCommit. Acá vas a encontrar el detalle fino de cómo funciona cada comando, qué banderas ("flags") podés usar y cómo integrar la herramienta en tu flujo de trabajo diario. + +--- + +## 1. Inteligencia en Commits + +### `suggest` / `s` +Este comando analiza tus cambios en staging (`git diff --cached`) y le pide al modelo de IA que genere mensajes siguiendo Conventional Commits. + +**Comando:** +```bash +matecommit suggest [flags] +``` + +**Detalles Técnicos:** +* **Context Window**: La herramienta envía el resumen del diff, nombres de archivos y (si lo activás) el contexto del issue linkeado. Si tu diff es gigante, lo truncamos inteligentemente para que entre en el límite de tokens del modelo sin perder la lógica principal. +* **Precedencia**: Los flags que pases por consola siempre le ganan a lo que tengas en `config.yaml`. (ej: si tenés configurado inglés pero tirás `--lang es`, sale en español). + +**Flags:** +| Flag | Corto | Tipo | Descripción | +| :--- | :--- | :--- | :--- | +| `--count` | `-n` | `int` | Cantidad de opciones a generar (1-10). | +| `--lang` | `-l` | `string` | Idioma de salida (ej: `es`, `en`, `pt`). | +| `--issue` | `-i` | `int` | Busca el título/descripción del issue en GitHub para darle contexto a la IA. | +| `--no-emoji` | `-ne` | `bool` | Vuela los emojis del título para mantener un historial sobrio. | + +**Ejemplo Avanzado:** +```bash +# Dame 5 sugerencias en español, leé el contexto del issue #42 y sacame los emojis +matecommit suggest -n 5 -l es -i 42 --no-emoji +``` + +--- + +## 2. Gestión de Pull Requests + +### `summarize-pr` / `spr` +Genera un Resumen, Plan de Pruebas y Alerta de Breaking Changes para un PR existente. + +**Comando:** +```bash +matecommit spr --pr-number +``` + +**Workflow:** +1. **Fetch**: Traemos la metadata del PR (commits, diffs, issues linkeados) vía API de GitHub. +2. **Análisis**: Gemini sintetiza los cambios en una narrativa coherente. +3. **Update**: Parcheamos el cuerpo del PR directamente en GitHub. + +**Requisitos:** +* Necesitás el `GITHUB_TOKEN` configurado. +* Scopes del token: `repo` (para repos privados) o `public_repo` (para públicos). + +--- + +## 3. Ciclo de Vida de Issues + +### `issue generate` / `g` +Crea issues en GitHub usando IA para transformar inputs vagos en reportes profesionales. + +**Comando:** +```bash +matecommit issue generate [source-flags] [opciones] +``` + +**Fuentes (Elegí una):** +* `--from-diff` / `-d`: Usa tus cambios en staging como base (Ideal para: "Ya arreglé esto, ahora necesito el ticket"). +* `--from-pr` / `-p`: Usa el título/body de un PR para crear un issue de seguimiento. +* `--description` / `-m`: Usa un texto plano que le pases como input. + +**Opciones:** +* `--template` / `-t`: Apunta a un template específico (ej: `bug_report`). Matchea con los archivos en `.github/ISSUE_TEMPLATE/`. +* `--checkout` / `-c`: Automatiza la creación de la rama (`git checkout -b issue/123-titulo`) post-generación. +* `--dry-run`: Te imprime el Markdown en consola sin tocar la API de GitHub. + +**Escenario Real:** +*Hiciste un fix rápido pero te colgaste en crear el ticket.* +```bash +git add . +matecommit issue generate --from-diff --template bug_report --assign-me --checkout +``` +*Resultado: Crea el issue, te lo asigna a vos y te cambia a la rama correcta.* + +--- + +## 4. Automatización de Releases + +### `release` / `r` +Estandarizamos el proceso de release siguiendo [Semantic Versioning](https://semver.org/). + +**Subcomandos:** + +#### `preview` / `p` +Un "dry-run". Calculamos la próxima versión (ej: `v1.0.0` -> `v1.1.0`) basándonos en el historial de commits y te mostramos el borrador del changelog. + +#### `create` / `c` +Ejecuta el release localmente. +1. Actualiza `CHANGELOG.md` (agrega lo nuevo arriba). +2. Crea el tag de git. +3. (Opcional) Pushea los cambios. + +**Flags:** +* `--auto`: Modo no-interactivo (clave para scripts de CI/CD). +* `--changelog`: Fuerza el commit del archivo de changelog actualizado. +* `--publish`: Dispara el `git push origin ` inmediatamente. + +#### `publish` / `pub` +Sincroniza el tag local con GitHub Releases. Crea la entrada en GitHub con las notas generadas por la IA. + +--- + +## 5. Sistema y Configuración + +### `config` +**Ubicación**: `~/.config/matecommit/config.yaml` (o la ruta estándar de tu SO). + +* `init`: El asistente interactivo. +* `doctor`: Chequeo de salud (Conectividad con Gemini, GitHub, path de Git). + +### `stats` +Te muestra una estimación de costos basada en el uso de tokens. +* **Nota**: Son estimaciones basadas en el precio público de Gemini. La facturación real puede variar. + +### `update` +Auto-updater usando GitHub Releases. Reemplaza tu binario actual por la última versión estable. + +--- + +## Variables de Entorno + +MateCommit respeta estas variables, que tienen prioridad sobre el archivo de configuración (ideal para CI): + +* `GEMINI_API_KEY`: Tu Key de Google AI Studio. +* `GITHUB_TOKEN`: Tu Personal Access Token de GitHub. +* `MATECOMMIT_LANG`: Override del idioma por defecto. \ No newline at end of file diff --git a/docs/es/CONTRIBUTING.md b/docs/es/CONTRIBUTING.md new file mode 100644 index 0000000..073bd82 --- /dev/null +++ b/docs/es/CONTRIBUTING.md @@ -0,0 +1,40 @@ +# Guía de Contribución para MateCommit + +¡Gracias por tu interés en contribuir a MateCommit! Valoramos mucho el tiempo y esfuerzo dedicados a mejorar este proyecto. Para mantener la calidad del código y la coherencia técnica, solicitamos seguir estas pautas generales. + +--- + +## Proceso de Desarrollo + +### 1. Preparación +* Realiza un **Fork** del repositorio en GitHub. +* Clona tu fork localmente. +* Asegúrate de tener las dependencias al día ejecutando `go mod tidy`. +* Crea una rama descriptiva para tu cambio: `git checkout -b feature/nombre-de-la-funcionalidad`. + +### 2. Estándares de Código +* **Formato**: El código debe seguir las convenciones estándar de Go (`go fmt`). +* **Pruebas**: No se aceptarán cambios que rompan los tests existentes. Si agregas una funcionalidad, se espera que incluyas pruebas unitarias o de integración que la validen. +* **Documentación**: Si tu cambio afecta el uso de la herramienta, actualiza los archivos `README.md` o `COMMANDS.md` correspondientes. + +### 3. Mensajes de Commit +Practicamos lo que predicamos. Por favor, utiliza el formato de [Conventional Commits](https://www.conventionalcommits.org/): +* `feat: descripción de nueva funcionalidad` +* `fix: corrección de un error` +* `refactor: mejora del código sin cambios lógicos` + +--- + +## Envío de Pull Requests (PR) + +Al abrir un PR, asegúrate de proporcionar el contexto suficiente: +* **Descripción**: Explica qué cambios realizaste y por qué son necesarios. Evita descripciones genéricas. +* **Issues**: Vincula cualquier issue relacionado utilizando palabras clave como "Fixes #34". +* **Verificación**: Asegúrate de que todas las comprobaciones del CI (Integración Continua) pasen correctamente. + +--- + +## Administración y Versiones +La gestión de etiquetas (tags) y lanzamientos de versiones (releases) es responsabilidad exclusiva de los mantenedores del proyecto para garantizar la estabilidad de las versiones distribuidas. + +¡Esperamos con interés tus propuestas y mejoras! Gracias por ayudarnos a hacer de MateCommit una herramienta profesional para la comunidad. diff --git a/docs/es/README.md b/docs/es/README.md new file mode 100644 index 0000000..a8dda1c --- /dev/null +++ b/docs/es/README.md @@ -0,0 +1,79 @@ +
+ MateCommit Logo + + # MateCommit + + **Inteligencia Artificial para tu flujo de trabajo en Git** + + MateCommit elimina la fricción de escribir mensajes de commit. Utiliza el poder de Gemini AI para analizar tus cambios y sugerir títulos coherentes y profesionales, permitiéndote enfocarte en lo que realmente importa: tu código. + + [Go Report Card](https://goreportcard.com/report/github.com/Tomas-vilte/MateCommit) | [Licencia](https://opensource.org/licenses/MIT) | [Estado del Build](https://github.com/Tomas-vilte/MateCommit/actions) + +
+ +--- + +### Idiomas +* [Documentación Oficial (Inglés)](../../README.md) +* [Traducción al Español (🇦🇷)](#) + +--- + +## Qué ofrece MateCommit +Escribir buenos nombres para los commits es fundamental pero consume tiempo. Esta herramienta automatiza esa tarea analizando el `diff` de tus archivos en staging. + +* **Sugerencias Inteligentes**: Análisis contextual de lógica, no solo nombres de archivos. +* **Potencia de Gemini**: Optimizado para modelos Flash 1.5 y 2.0 para máxima precisión. +* **Ciclo de vida de Issues**: Generá issues de GitHub desde código, PRs o descripciones. +* **Releases Unificados**: Automatización de changelogs, tagging y publicación en un solo paso. +* **Inteligencia en PRs**: Resúmenes instantáneos para Pull Requests complejos. +* **Control de Costos**: Seguimiento de gastos y estadísticas de uso de IA en tiempo real. +* **Herramientas de Eficiencia**: Autocompletado, caché local y herramientas de diagnóstico. + + +--- + +## Inicio Rápido + +### 1. Instalación +La forma más rápida de instalarlo es a través de Go: + +```bash +go install github.com/Tomas-vilte/MateCommit/cmd/matecommit@latest +``` + +### 2. Configuración +Corré el asistente interactivo para configurar tus API Keys: + +```bash +matecommit config init +``` + +### 3. Uso +Agregá tus cambios y pedí sugerencias: + +```bash +git add . +matecommit suggest +``` + +--- + +## Funcionalidad Avanzada +Diseñado para entornos profesionales: + +* **Integración con Jira**: Linkeo automático de tickets basado en el contexto. +* **Resúmenes de PR**: Generación automática de descripciones para Pull Requests. +* **Automatización de Releases**: Actualización de changelogs y versionado en un solo paso. + +Para una guía detallada de comandos, consultá [COMMANDS.md](../../COMMANDS.md). + +--- + +## Contribuciones +Valoramos las contribuciones de calidad. Si querés mejorar el proyecto, revisá nuestra [Guía de Contribución](../../CONTRIBUTING.md). + +--- + +## Licencia +Código abierto bajo licencia MIT. Consultá [LICENSE](../../LICENSE) para más detalles. \ No newline at end of file diff --git a/internal/cli/command/config/config_test.go b/internal/cli/command/config/config_test.go index 4738f81..b9777d5 100644 --- a/internal/cli/command/config/config_test.go +++ b/internal/cli/command/config/config_test.go @@ -18,15 +18,15 @@ func setupConfigTest(t *testing.T) (*config.Config, *i18n.Translations, string, cfg := &config.Config{ PathFile: tmpConfigPath, - Language: "es", + Language: "en", } - translations, err := i18n.NewTranslations("es", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") assert.NoError(t, err) cleanup := func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Logf("Error al limpiar directorio temporal: %v", err) + t.Logf("Error cleaning temporary directory: %v", err) } } diff --git a/internal/cli/command/config/doctor.go b/internal/cli/command/config/doctor.go index 59eb175..68d0d0d 100644 --- a/internal/cli/command/config/doctor.go +++ b/internal/cli/command/config/doctor.go @@ -85,11 +85,11 @@ func (d *DoctorCommand) runHealthCheck(ctx context.Context, t *i18n.Translations ui.PrintSectionBanner(t.GetMessage("doctor.summary", 0, nil)) if allPassed && len(warnings) == 0 { - ui.PrintSuccess(t.GetMessage("doctor.all_good", 0, nil)) + ui.PrintSuccess(os.Stdout, t.GetMessage("doctor.all_good", 0, nil)) } else if len(errors) == 0 { ui.PrintWarning(t.GetMessage("doctor.has_warnings", 0, nil)) } else { - ui.PrintError(t.GetMessage("doctor.has_errors", 0, nil)) + ui.PrintError(os.Stdout, t.GetMessage("doctor.has_errors", 0, nil)) } fmt.Println() @@ -206,7 +206,7 @@ func (d *DoctorCommand) checkGeminiAPIKey(ctx context.Context, t *i18n.Translati testCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - service, err := gemini.NewGeminiCommitSummarizer(testCtx, cfg, t) + service, err := gemini.NewGeminiCommitSummarizer(testCtx, cfg, nil) if err != nil { return checkResult{ status: checkStatusError, diff --git a/internal/cli/command/config/edit.go b/internal/cli/command/config/edit.go index b4d966c..ef5eac4 100644 --- a/internal/cli/command/config/edit.go +++ b/internal/cli/command/config/edit.go @@ -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 al abrir el editor: %w", err) + return fmt.Errorf("error opening editor: %w", err) } return nil diff --git a/internal/cli/command/config/edit_test.go b/internal/cli/command/config/edit_test.go index aa7941a..6989f33 100644 --- a/internal/cli/command/config/edit_test.go +++ b/internal/cli/command/config/edit_test.go @@ -166,6 +166,6 @@ func TestEditCommand(t *testing.T) { // Assert assert.Error(t, err) - assert.Contains(t, err.Error(), "error al abrir el editor") + assert.Contains(t, err.Error(), "error opening editor") }) } diff --git a/internal/cli/command/config/init.go b/internal/cli/command/config/init.go index 8da0742..97525a8 100644 --- a/internal/cli/command/config/init.go +++ b/internal/cli/command/config/init.go @@ -80,7 +80,7 @@ func runFullSetup(ctx context.Context, command *cli.Command, reader *bufio.Reade } if err := config.SaveConfig(cfg); err != nil { fmt.Println(t.GetMessage("config_save.error_saving_config", 0, map[string]interface{}{"Error": err.Error()})) - return fmt.Errorf("error guardando configuracion: %w", err) + return fmt.Errorf("error saving configuration: %w", err) } printConfigSummary(cfg, t) @@ -89,7 +89,7 @@ func runFullSetup(ctx context.Context, command *cli.Command, reader *bufio.Reade fmt.Print(t.GetMessage("init.prompt_run_again", 0, nil)) runAgain, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error leyendo input: %w", err) + return fmt.Errorf("error reading input: %w", err) } if isYes(runAgain) { @@ -124,7 +124,7 @@ func configureWelcome(ctx context.Context, reader *bufio.Reader, cfg *config.Con })) apiKey, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error leyendo API_KEY: %w", err) + return fmt.Errorf("error reading API_KEY: %w", err) } apiKey = strings.TrimSpace(apiKey) @@ -143,7 +143,7 @@ func configureWelcome(ctx context.Context, reader *bufio.Reader, cfg *config.Con fmt.Print(t.GetMessage("init.prompt_model_with_default", 0, map[string]interface{}{"Default": geminiDefault})) modelInput, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error leyendo modelo: %w", err) + return fmt.Errorf("error reading model: %w", err) } modelInput = strings.TrimSpace(modelInput) @@ -203,7 +203,7 @@ func configureVCS(reader *bufio.Reader, cfg *config.Config, t *i18n.Translations ansVCS, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error al leer la respuesta de VCS: %w", err) + return fmt.Errorf("error reading VCS response: %w", err) } ansVCS = strings.TrimSpace(strings.ToLower(ansVCS)) @@ -212,7 +212,7 @@ func configureVCS(reader *bufio.Reader, cfg *config.Config, t *i18n.Translations fmt.Print(t.GetMessage("init.prompt_github_token_blank_skip", 0, nil)) token, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error al leer el token de GitHub: %w", err) + return fmt.Errorf("error reading GitHub token: %w", err) } token = strings.TrimSpace(token) @@ -241,7 +241,7 @@ func configureTickets(reader *bufio.Reader, cfg *config.Config, t *i18n.Translat ansJira, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error al leer la respuesta de Jira: %w", err) + return fmt.Errorf("error reading Jira response: %w", err) } ansJira = strings.TrimSpace(strings.ToLower(ansJira)) @@ -256,7 +256,7 @@ func configureTickets(reader *bufio.Reader, cfg *config.Config, t *i18n.Translat fmt.Print(t.GetMessage("init.prompt_jira_base_url_blank_cancel", 0, nil)) jiraURL, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error al leer la URL de Jira: %w", err) + return fmt.Errorf("error reading Jira URL: %w", err) } jiraURL = strings.TrimSpace(jiraURL) @@ -273,7 +273,7 @@ func configureTickets(reader *bufio.Reader, cfg *config.Config, t *i18n.Translat fmt.Print(t.GetMessage("init.prompt_jira_email_blank_cancel", 0, nil)) jiraEmail, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error al leer el correo electrónico de Jira: %w", err) + return fmt.Errorf("error reading Jira email: %w", err) } jiraEmail = strings.TrimSpace(jiraEmail) @@ -290,7 +290,7 @@ func configureTickets(reader *bufio.Reader, cfg *config.Config, t *i18n.Translat fmt.Print(t.GetMessage("init.prompt_jira_api_token_blank_cancel", 0, nil)) jiraToken, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error al leer el token de Jira: %w", err) + return fmt.Errorf("error reading Jira token: %w", err) } jiraToken = strings.TrimSpace(jiraToken) @@ -382,10 +382,10 @@ func validateGeminiAPIKey(ctx context.Context, apiKey string, t *i18n.Translatio testCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - _, err := gemini.NewGeminiCommitSummarizer(testCtx, testCfg, t) + _, err := gemini.NewGeminiCommitSummarizer(testCtx, testCfg, nil) if err != nil { spinner.Error(t.GetMessage("config.api_key_invalid", 0, nil)) - ui.PrintError(t.GetMessage("config.check_api_key_error", 0, map[string]interface{}{ + ui.PrintError(os.Stdout, t.GetMessage("config.check_api_key_error", 0, map[string]interface{}{ "Error": err.Error(), })) return false @@ -402,7 +402,7 @@ func disableTickets(cfg *config.Config) { func isYes(s string) bool { switch strings.ToLower(strings.TrimSpace(s)) { - case "y", "yes", "s", "si", "sí": + case "y", "yes", "s": return true default: return false diff --git a/internal/cli/command/config/init_test.go b/internal/cli/command/config/init_test.go index d0de39d..b2e3fd5 100644 --- a/internal/cli/command/config/init_test.go +++ b/internal/cli/command/config/init_test.go @@ -22,10 +22,10 @@ func runInitCommandTest(t *testing.T, userInput string, fullMode bool) (output s fakeConfigPath := filepath.Join(tempDir, "config.yaml") cfg := &config.Config{ PathFile: fakeConfigPath, - Language: "es", + Language: "en", } - translations, err := i18n.NewTranslations("es", "../../../i18n/locales") + translations, err := i18n.NewTranslations("en", "../../../i18n/locales") require.NoError(t, err) originalStdin := os.Stdin @@ -92,10 +92,10 @@ func TestInitCommand(t *testing.T) { "https://myjira.atlassian.net", "user@example.com", "my-jira-token", "no", }, "\n") + "\n" output, finalCfg := runInitCommandTest(t, userInput, true) - assert.Contains(t, output, "Introduce tu API Key de") - assert.Contains(t, output, "Introduce tu token de acceso de GitHub") - assert.Contains(t, output, "URL base de tu instancia de Jira") - assert.Contains(t, output, "Resumen de configuración") + assert.Contains(t, output, "Enter your Gemini API Key") + assert.Contains(t, output, "Enter your GitHub Personal Access Token") + assert.Contains(t, output, "Base URL of your Jira instance") + assert.Contains(t, output, "Configuration summary") if finalCfg.AIProviders != nil && finalCfg.AIProviders["gemini"].APIKey != "" { assert.Equal(t, "my-gemini-api-key", finalCfg.AIProviders["gemini"].APIKey) } @@ -109,8 +109,8 @@ func TestInitCommand(t *testing.T) { }, "\n") + "\n" output, finalCfg := runInitCommandTest(t, userInput, true) - assert.Contains(t, output, "Servicio de tickets deshabilitado") - assert.NotContains(t, output, "Introduce tu token de acceso de GitHub") + assert.Contains(t, output, "Ticket service disabled") + assert.NotContains(t, output, "Enter your GitHub Personal Access Token") if finalCfg.AIProviders != nil && finalCfg.AIProviders["gemini"].APIKey != "" { assert.Equal(t, "test-api-key", finalCfg.AIProviders["gemini"].APIKey) @@ -125,8 +125,8 @@ func TestInitCommand(t *testing.T) { "", "", "fr", "no", "no", "no", }, "\n") + "\n" output, finalCfg := runInitCommandTest(t, userInput, true) - assert.Contains(t, output, "Idioma inválido. Por favor ingresa 'en' o 'es'.") - assert.Equal(t, "es", finalCfg.Language) + assert.Contains(t, output, "Invalid language. Please enter 'en' or 'es'.") + assert.Equal(t, "en", finalCfg.Language) }) t.Run("should run configuration again if user enters yes", func(t *testing.T) { @@ -136,8 +136,8 @@ func TestInitCommand(t *testing.T) { }, "\n") + "\n" output, finalCfg := runInitCommandTest(t, userInput, true) - assert.Equal(t, 2, strings.Count(output, "Introduce tu API Key de")) - assert.Equal(t, 2, strings.Count(output, "Resumen de configuración")) + assert.Equal(t, 2, strings.Count(output, "Enter your Gemini API Key")) + assert.Equal(t, 2, strings.Count(output, "Configuration summary")) if finalCfg.AIProviders != nil && finalCfg.AIProviders["gemini"].APIKey != "" { assert.Equal(t, "second-run-key", finalCfg.AIProviders["gemini"].APIKey) } diff --git a/internal/cli/command/config/quick_setup.go b/internal/cli/command/config/quick_setup.go index 1bf6167..b1e1d7b 100644 --- a/internal/cli/command/config/quick_setup.go +++ b/internal/cli/command/config/quick_setup.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "os" "strings" "github.com/Tomas-vilte/MateCommit/internal/config" @@ -33,7 +34,7 @@ func runQuickSetup(ctx context.Context, reader *bufio.Reader, cfg *config.Config })) apiKey, err := reader.ReadString('\n') if err != nil { - return fmt.Errorf("error leyendo la API_KEY: %w", err) + return fmt.Errorf("error reading API_KEY: %w", err) } apiKey = strings.TrimSpace(apiKey) @@ -44,7 +45,7 @@ func runQuickSetup(ctx context.Context, reader *bufio.Reader, cfg *config.Config fmt.Println(t.GetMessage("quick_setup.validating_key", 0, nil)) if validateGeminiAPIKey(ctx, apiKey, t) { - ui.PrintSuccess(t.GetMessage("quick_setup.key_validated", 0, nil)) + ui.PrintSuccess(os.Stdout, t.GetMessage("quick_setup.key_validated", 0, nil)) } else { ui.PrintWarning(t.GetMessage("quick_setup.key_validation_failed", 0, nil)) } @@ -71,11 +72,11 @@ func runQuickSetup(ctx context.Context, reader *bufio.Reader, cfg *config.Config cfg.AIConfig.Models[providerKey] = config.ModelGeminiV25Flash if err := config.SaveConfig(cfg); err != nil { - return fmt.Errorf("error guardando configuracion: %w", err) + return fmt.Errorf("error saving configuration: %w", err) } fmt.Println() - ui.PrintSuccess(t.GetMessage("quick_setup.config_saved", 0, nil)) + ui.PrintSuccess(os.Stdout, t.GetMessage("quick_setup.config_saved", 0, nil)) fmt.Println() ui.PrintInfo(t.GetMessage("quick_setup.try_now", 0, nil)) ui.PrintInfo(t.GetMessage("quick_setup.full_setup_hint", 0, nil)) diff --git a/internal/cli/command/config/show_test.go b/internal/cli/command/config/show_test.go index 40fd0a7..dad974a 100644 --- a/internal/cli/command/config/show_test.go +++ b/internal/cli/command/config/show_test.go @@ -108,8 +108,8 @@ func TestShowCommand(t *testing.T) { // assert assert.NoError(t, err) - assert.Contains(t, output, "Servicio de tickets habilitado: jira") - assert.Contains(t, output, "Configuración de Jira - BaseURL: https://example.atlassian.net, Email: user@example.com") + assert.Contains(t, output, "Ticket service enabled: jira") + assert.Contains(t, output, "Jira Settings - BaseURL: https://example.atlassian.net, Email: user@example.com") assert.Contains(t, output, "gemini: gemini-1.5-flash") assert.Contains(t, output, "openai: gpt-4o") diff --git a/internal/cli/command/handler/suggestions.go b/internal/cli/command/handler/suggestions.go index 1c9bd40..b8d659a 100644 --- a/internal/cli/command/handler/suggestions.go +++ b/internal/cli/command/handler/suggestions.go @@ -3,6 +3,7 @@ package handler import ( "context" "fmt" + "os" "strings" "github.com/Tomas-vilte/MateCommit/internal/domain/models" @@ -12,17 +13,26 @@ import ( "github.com/fatih/color" ) -var _ ports.CommitHandler = (*SuggestionHandler)(nil) +// gitService is a minimal interface for testing purposes +type gitService interface { + GetChangedFiles(ctx context.Context) ([]string, error) + GetDiff(ctx context.Context) (string, error) + HasStagedChanges(ctx context.Context) bool + CreateCommit(ctx context.Context, message string) error + AddFileToStaging(ctx context.Context, file string) error + GetCurrentBranch(ctx context.Context) (string, error) + GetRepoInfo(ctx context.Context) (string, string, string, error) +} type SuggestionHandler struct { - gitService ports.GitService + gitService gitService vcsClient ports.VCSClient t *i18n.Translations } -func NewSuggestionHandler(git ports.GitService, vcs ports.VCSClient, t *i18n.Translations) *SuggestionHandler { +func NewSuggestionHandler(gitSvc gitService, vcs ports.VCSClient, t *i18n.Translations) *SuggestionHandler { return &SuggestionHandler{ - gitService: git, + gitService: gitSvc, vcsClient: vcs, t: t, } @@ -44,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).Sprintf("📝 Sugerencia #%d", i+1) + suggestionHeader := color.New(color.FgMagenta, color.Bold).Sprint(h.t.GetMessage("ui_labels.suggestion_number", 0, map[string]interface{}{"Number": i + 1})) fmt.Printf("%s\n\n", suggestionHeader) _, _ = sectionColor.Println(h.t.GetMessage("ui_labels.code_analysis", 0, nil)) @@ -183,7 +193,7 @@ func (h *SuggestionHandler) handleCommitSelection(ctx context.Context, suggestio if _, err := fmt.Scan(&selection); err != nil { msg := h.t.GetMessage("commit.error_reading_selection", 0, map[string]interface{}{"Error": err}) - ui.PrintError(msg) + ui.PrintError(os.Stdout, msg) return fmt.Errorf("%s", msg) } @@ -194,7 +204,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)}) - ui.PrintError(msg) + ui.PrintError(os.Stdout, msg) return fmt.Errorf("%s", msg) } @@ -202,7 +212,7 @@ func (h *SuggestionHandler) handleCommitSelection(ctx context.Context, suggestio } func (h *SuggestionHandler) processCommit(ctx context.Context, suggestion models.CommitSuggestion, - gitService ports.GitService) error { + gitSvc gitService) error { commitTitle := strings.TrimSpace(strings.TrimPrefix(suggestion.CommitTitle, "Commit: ")) fmt.Println() @@ -238,13 +248,13 @@ 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(h.t.GetMessage("ui_preview.error_editing_message", 0, map[string]interface{}{ + ui.PrintError(os.Stdout, h.t.GetMessage("ui_preview.error_editing_message", 0, map[string]interface{}{ "Error": err, })) return err } finalCommitMessage = editedMessage - ui.PrintSuccess(h.t.GetMessage("ui_preview.message_updated", 0, nil)) + ui.PrintSuccess(os.Stdout, h.t.GetMessage("ui_preview.message_updated", 0, nil)) } if !ui.AskConfirmation(h.t.GetMessage("ui_preview.ask_confirm_commit", 0, nil)) { @@ -256,11 +266,11 @@ func (h *SuggestionHandler) processCommit(ctx context.Context, suggestion models spinner.Start() for _, file := range suggestion.Files { - if err := gitService.AddFileToStaging(ctx, file); err != nil { + if err := gitSvc.AddFileToStaging(ctx, file); err != nil { spinner.Error(h.t.GetMessage("error_adding_file", 0, map[string]interface{}{ "File": file, })) - return fmt.Errorf("error al agregar %s: %w", file, err) + return fmt.Errorf("error adding %s: %w", file, err) } } @@ -271,9 +281,9 @@ func (h *SuggestionHandler) processCommit(ctx context.Context, suggestion models spinner = ui.NewSmartSpinner(h.t.GetMessage("ui.creating_commit", 0, nil)) spinner.Start() - if err := gitService.CreateCommit(ctx, finalCommitMessage); err != nil { + if err := gitSvc.CreateCommit(ctx, finalCommitMessage); err != nil { spinner.Error(h.t.GetMessage("error_creating_commit", 0, nil)) - return fmt.Errorf("error al crear el commit: %w", err) + return fmt.Errorf("error creating commit: %w", err) } spinner.Success(h.t.GetMessage("ui.commit_created_successfully", 0, nil)) diff --git a/internal/cli/command/handler/suggestions_test.go b/internal/cli/command/handler/suggestions_test.go index 7594cee..ad91d3e 100644 --- a/internal/cli/command/handler/suggestions_test.go +++ b/internal/cli/command/handler/suggestions_test.go @@ -18,9 +18,9 @@ type mockGitService struct { mock.Mock } -func (m *mockGitService) GetChangedFiles(ctx context.Context) ([]models.GitChange, error) { +func (m *mockGitService) GetChangedFiles(ctx context.Context) ([]string, error) { args := m.Called(ctx) - return args.Get(0).([]models.GitChange), args.Error(1) + return args.Get(0).([]string), args.Error(1) } func (m *mockGitService) GetDiff(ctx context.Context) (string, error) { @@ -216,7 +216,7 @@ func TestSuggestionHandler_HandleCommitSelection(t *testing.T) { mockGit.On("AddFileToStaging", mock.Anything, "test.go").Return(nil) mockGit.On("CreateCommit", mock.Anything, "feat: test feature").Return(nil) - // Act - simula: 1 (selección), n (no ver diff), n (no editar mensaje), y (confirmar commit) + // Act - simulates: 1 (selection), n (no diff), n (no message edit), y (confirm commit) simulateInput("1\nn\nn\ny\n", func() { err = handler.handleCommitSelection(context.Background(), suggestions) }) diff --git a/internal/cli/command/issues/issues.go b/internal/cli/command/issues/issues.go index 41fee67..5d46c3d 100644 --- a/internal/cli/command/issues/issues.go +++ b/internal/cli/command/issues/issues.go @@ -11,29 +11,47 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" ) -type IssueServiceProvider func(ctx context.Context) (ports.IssueGeneratorService, error) +// IssueGeneratorService is a minimal interface for testing purposes +type IssueGeneratorService interface { + GenerateFromDiff(ctx context.Context, hint string, skipLabels bool) (*models.IssueGenerationResult, error) + GenerateFromDescription(ctx context.Context, description string, skipLabels bool) (*models.IssueGenerationResult, error) + GenerateFromPR(ctx context.Context, prNumber int, hint string, skipLabels bool) (*models.IssueGenerationResult, error) + GenerateWithTemplate(ctx context.Context, templateName string, hint string, fromDiff bool, description string, skipLabels bool) (*models.IssueGenerationResult, error) + CreateIssue(ctx context.Context, result *models.IssueGenerationResult, assignees []string) (*models.Issue, error) + GetAuthenticatedUser(ctx context.Context) (string, error) + InferBranchName(issueNumber int, labels []string) string + LinkIssueToPR(ctx context.Context, prNumber int, issueNumber int) error +} + +// IssueTemplateService is a minimal interface for testing purposes +type IssueTemplateService interface { + InitializeTemplates(force bool) error + GetTemplatesDir() (string, error) + ListTemplates() ([]models.TemplateMetadata, error) +} -// IssuesCommandFactory es el factory para crear el comando de issues. +type IssueServiceProvider func(ctx context.Context) (IssueGeneratorService, error) + +// IssuesCommandFactory is the factory to create the issues command. type IssuesCommandFactory struct { issueServiceProvider IssueServiceProvider - templateService ports.IssueTemplateService + templateService IssueTemplateService } -// NewIssuesCommandFactory crea una nueva instancia del factory. -func NewIssuesCommandFactory(issueServiceProvider IssueServiceProvider, templateService ports.IssueTemplateService) *IssuesCommandFactory { +// NewIssuesCommandFactory creates a new instance of the factory. +func NewIssuesCommandFactory(issueServiceProvider IssueServiceProvider, templateService IssueTemplateService) *IssuesCommandFactory { return &IssuesCommandFactory{ issueServiceProvider: issueServiceProvider, templateService: templateService, } } -// CreateCommand crea el comando principal de issues con sus subcomandos. +// CreateCommand creates the main issues command with its subcommands. func (f *IssuesCommandFactory) CreateCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { return &cli.Command{ Name: "issue", @@ -47,7 +65,7 @@ func (f *IssuesCommandFactory) CreateCommand(t *i18n.Translations, cfg *config.C } } -// newGenerateCommand crea el subcomando 'generate'. +// newGenerateCommand creates the 'generate' subcommand. func (f *IssuesCommandFactory) newGenerateCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { return &cli.Command{ Name: "generate", @@ -59,7 +77,7 @@ func (f *IssuesCommandFactory) newGenerateCommand(t *i18n.Translations, cfg *con } } -// createGenerateFlags define los flags para el comando generate. +// createGenerateFlags defines the flags for the generate command. func (f *IssuesCommandFactory) createGenerateFlags(t *i18n.Translations) []cli.Flag { return []cli.Flag{ &cli.BoolFlag{ @@ -133,12 +151,12 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c } if sourcesCount == 0 { - ui.PrintError(t.GetMessage("issue.error_no_input", 0, nil)) + ui.PrintError(os.Stdout, t.GetMessage("issue.error_no_input", 0, nil)) return fmt.Errorf("%s", t.GetMessage("issue.error_no_input", 0, nil)) } if sourcesCount > 1 { - ui.PrintError(t.GetMessage("issue.error_multiple_sources", 0, nil)) + ui.PrintError(os.Stdout, t.GetMessage("issue.error_multiple_sources", 0, nil)) return fmt.Errorf("%s", t.GetMessage("issue.error_multiple_sources", 0, nil)) } @@ -146,7 +164,7 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c issueService, err := f.issueServiceProvider(ctx) if err != nil { - ui.PrintError(fmt.Sprintf("%s: %v", t.GetMessage("issue.error_generating", 0, nil), err)) + ui.PrintError(os.Stdout, fmt.Sprintf("%s: %v", t.GetMessage("issue.error_generating", 0, nil), err)) return err } @@ -177,7 +195,7 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c spinner.Stop() if err != nil { - f.handleGenerationError(err, t) + ui.HandleAppError(err, t) return err } @@ -219,11 +237,11 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c spinner.Stop() if err != nil { - f.handleCreationError(err, t) + ui.HandleAppError(err, t) return err } - ui.PrintSuccess(t.GetMessage("issue.created_successfully", 0, map[string]interface{}{ + ui.PrintSuccess(os.Stdout, t.GetMessage("issue.created_successfully", 0, map[string]interface{}{ "Number": issue.Number, "URL": issue.URL, })) @@ -252,7 +270,7 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c 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(t.GetMessage("issue.branch_created", 0, map[string]interface{}{ + ui.PrintSuccess(os.Stdout, t.GetMessage("issue.branch_created", 0, map[string]interface{}{ "Branch": branchName, })) } @@ -262,35 +280,9 @@ func (f *IssuesCommandFactory) createGenerateAction(t *i18n.Translations, cfg *c } } -// handleGenerationError maneja errores durante la generacion de contenido. -func (f *IssuesCommandFactory) handleGenerationError(err error, t *i18n.Translations) { - errStr := err.Error() - - if strings.Contains(strings.ToLower(errStr), "api key") || strings.Contains(errStr, "not configured") { - ui.PrintErrorWithSuggestion( - t.GetMessage("issue.error_generating", 0, nil)+": "+errStr, - t.GetMessage("ui_error.run_config_init", 0, nil), - ) - } else { - ui.PrintError(fmt.Sprintf("%s: %v", t.GetMessage("issue.error_generating", 0, nil), err)) - } -} - -// handleCreationError maneja errores durante la creacion de la issue. -func (f *IssuesCommandFactory) handleCreationError(err error, t *i18n.Translations) { - errStr := err.Error() - - if strings.Contains(strings.ToLower(errStr), "token") { - ui.PrintErrorWithSuggestion( - t.GetMessage("issue.error_creating", 0, nil)+": "+errStr, - t.GetMessage("ui_error.run_config_init", 0, nil), - ) - } else { - ui.PrintError(fmt.Sprintf("%s: %v", t.GetMessage("issue.error_creating", 0, nil), err)) - } -} +// handleCreationError is now replaced by ui.HandleAppError -// printPreview muestra un preview de la issue que se va a crear. +// printPreview shows a preview of the issue to be created. func (f *IssuesCommandFactory) printPreview(result *models.IssueGenerationResult, t *i18n.Translations, cfg *config.Config) { separator := strings.Repeat("\u2500", 60) @@ -320,12 +312,12 @@ func (f *IssuesCommandFactory) printPreview(result *models.IssueGenerationResult fmt.Println() } -// promptConfirmation solicita confirmacion al usuario para crear la issue. +// promptConfirmation requests confirmation from the user to create the issue. func (f *IssuesCommandFactory) promptConfirmation(t *i18n.Translations) bool { reader := bufio.NewReader(os.Stdin) prompt := t.GetMessage("issue.confirm_prompt", 0, nil) - fmt.Printf("%s (Y/n): ", prompt) + fmt.Printf("%s: ", prompt) response, err := reader.ReadString('\n') if err != nil { @@ -334,15 +326,14 @@ func (f *IssuesCommandFactory) promptConfirmation(t *i18n.Translations) bool { response = strings.ToLower(strings.TrimSpace(response)) - return response == "" || response == "y" || response == "yes" || - response == "s" || response == "si" + return response == "" || response == "y" || response == "yes" } func (f *IssuesCommandFactory) checkoutBranch(branchName string) error { cmd := exec.Command("git", "checkout", "-b", branchName) output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("git checkout fallo: %w (output: %s)", err, string(output)) + return fmt.Errorf("git checkout failed: %w (output: %s)", err, string(output)) } return nil } @@ -370,19 +361,19 @@ func (f *IssuesCommandFactory) newLinkCommand(t *i18n.Translations, cfg *config. } } -// createLinkAction crea la acción para linkear un PR a una issue. +// createLinkAction creates the action to link a PR to an issue. func (f *IssuesCommandFactory) createLinkAction(t *i18n.Translations, _ *config.Config) cli.ActionFunc { return func(ctx context.Context, command *cli.Command) error { prNumber := command.Int("pr") issueNumber := command.Int("issue") if prNumber <= 0 { - ui.PrintError(t.GetMessage("issue.error_invalid_pr", 0, nil)) + ui.PrintError(os.Stdout, t.GetMessage("issue.error_invalid_pr", 0, nil)) return fmt.Errorf("invalid PR number") } if issueNumber <= 0 { - ui.PrintError(t.GetMessage("issue.error_invalid_issue", 0, nil)) + ui.PrintError(os.Stdout, t.GetMessage("issue.error_invalid_issue", 0, nil)) return fmt.Errorf("invalid issue number") } @@ -390,7 +381,7 @@ func (f *IssuesCommandFactory) createLinkAction(t *i18n.Translations, _ *config. issueService, err := f.issueServiceProvider(ctx) if err != nil { - ui.PrintError(fmt.Sprintf("%s: %v", t.GetMessage("issue.error_linking", 0, nil), err)) + ui.HandleAppError(err, t) return err } @@ -404,11 +395,11 @@ func (f *IssuesCommandFactory) createLinkAction(t *i18n.Translations, _ *config. spinner.Stop() if err != nil { - ui.PrintError(fmt.Sprintf("%s: %v", t.GetMessage("issue.error_linking", 0, nil), err)) + ui.PrintError(os.Stdout, fmt.Sprintf("%s: %v", t.GetMessage("issue.error_linking", 0, nil), err)) return err } - ui.PrintSuccess(t.GetMessage("issue.link_success", 0, map[string]interface{}{ + ui.PrintSuccess(os.Stdout, t.GetMessage("issue.link_success", 0, map[string]interface{}{ "PR": prNumber, "Issue": issueNumber, })) diff --git a/internal/cli/command/issues/issues_test.go b/internal/cli/command/issues/issues_test.go index c77a5b6..2bae32f 100644 --- a/internal/cli/command/issues/issues_test.go +++ b/internal/cli/command/issues/issues_test.go @@ -7,7 +7,6 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -19,7 +18,7 @@ func setupIssuesTest(t *testing.T) (*MockIssueGeneratorService, *MockIssueTempla mockGen := &MockIssueGeneratorService{} mockTemp := &MockIssueTemplateService{} - provider := func(ctx context.Context) (ports.IssueGeneratorService, error) { + provider := func(ctx context.Context) (IssueGeneratorService, error) { return mockGen, nil } diff --git a/internal/cli/command/issues/templates.go b/internal/cli/command/issues/templates.go index 46f146d..eb6bf12 100644 --- a/internal/cli/command/issues/templates.go +++ b/internal/cli/command/issues/templates.go @@ -3,6 +3,7 @@ package issues import ( "context" "fmt" + "os" "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/i18n" @@ -10,7 +11,7 @@ import ( "github.com/urfave/cli/v3" ) -// newTemplateCommand crea el subcomando 'template' para gestión de templates. +// newTemplateCommand creates the 'template' subcommand for template management. func (f *IssuesCommandFactory) newTemplateCommand(t *i18n.Translations, _ *config.Config) *cli.Command { templateService := f.templateService @@ -36,12 +37,12 @@ func (f *IssuesCommandFactory) newTemplateCommand(t *i18n.Translations, _ *confi ui.PrintInfo(t.GetMessage("issue.template_init_info", 0, nil)) if err := templateService.InitializeTemplates(force); err != nil { - ui.PrintError(fmt.Sprintf("%s: %v", t.GetMessage("issue.template_init_error", 0, nil), err)) + ui.PrintError(os.Stdout, fmt.Sprintf("%s: %v", t.GetMessage("issue.template_init_error", 0, nil), err)) return err } templatesDir, _ := templateService.GetTemplatesDir() - ui.PrintSuccess(t.GetMessage("issue.template_init_success", 0, map[string]interface{}{ + ui.PrintSuccess(os.Stdout, t.GetMessage("issue.template_init_success", 0, map[string]interface{}{ "Dir": templatesDir, })) @@ -55,7 +56,7 @@ func (f *IssuesCommandFactory) newTemplateCommand(t *i18n.Translations, _ *confi Action: func(ctx context.Context, cmd *cli.Command) error { templates, err := templateService.ListTemplates() if err != nil { - ui.PrintError(fmt.Sprintf("%s: %v", t.GetMessage("issue.template_list_error", 0, nil), err)) + ui.PrintError(os.Stdout, fmt.Sprintf("%s: %v", t.GetMessage("issue.template_list_error", 0, nil), err)) return err } diff --git a/internal/cli/command/pull_requests/summarize.go b/internal/cli/command/pull_requests/summarize.go index 86493d3..740d46d 100644 --- a/internal/cli/command/pull_requests/summarize.go +++ b/internal/cli/command/pull_requests/summarize.go @@ -6,20 +6,27 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" cfg "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/factory" - "github.com/Tomas-vilte/MateCommit/internal/ui" - + "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" ) +// PRService is a minimal interface for testing purposes +type PRService interface { + SummarizePR(ctx context.Context, prNumber int, progress func(models.ProgressEvent)) (models.PRSummary, error) +} + +// PRServiceProvider is a function that returns a PRService on demand +type PRServiceProvider func(ctx context.Context) (PRService, error) + type SummarizeCommand struct { - prFactory factory.PRServiceFactoryInterface + prProvider PRServiceProvider } -func NewSummarizeCommand(prFactory factory.PRServiceFactoryInterface) *SummarizeCommand { +func NewSummarizeCommand(prProvider PRServiceProvider) *SummarizeCommand { return &SummarizeCommand{ - prFactory: prFactory, + prProvider: prProvider, } } @@ -38,7 +45,7 @@ func (c *SummarizeCommand) CreateCommand(t *i18n.Translations, _ *cfg.Config) *c }, ShellComplete: completion_helper.DefaultFlagComplete, Action: func(ctx context.Context, cmd *cli.Command) error { - prService, err := c.prFactory.CreatePRService(ctx) + prService, err := c.prProvider(ctx) if err != nil { return fmt.Errorf(t.GetMessage("error.pr_service_creation_error", 0, nil)+": %w", err) } @@ -52,11 +59,35 @@ func (c *SummarizeCommand) CreateCommand(t *i18n.Translations, _ *cfg.Config) *c })) spinner.Start() - summary, err := prService.SummarizePR(ctx, prNumber, func(msg string) { - spinner.Log(msg) + 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), + }) + case models.ProgressIssuesClosing: + msg = t.GetMessage("vcs_summary.issues_closing", 0, map[string]interface{}{ + "Count": event.Data["Count"], + }) + case models.ProgressBreakingChanges: + msg = t.GetMessage("vcs_summary.breaking_changes_detected", 0, map[string]interface{}{ + "Count": event.Data["Count"], + }) + case models.ProgressTestPlan: + msg = t.GetMessage("vcs_summary.test_plan_generated", 0, nil) + default: + msg = event.Message + } + if msg != "" { + spinner.Log(msg) + } }) if err != nil { spinner.Error(t.GetMessage("ui.error_generating_pr_summary", 0, nil)) + ui.HandleAppError(err, t) return fmt.Errorf(t.GetMessage("error.pr_summary_error", 0, nil)+": %w", err) } diff --git a/internal/cli/command/pull_requests/summarize_test.go b/internal/cli/command/pull_requests/summarize_test.go index 684a947..18590fd 100644 --- a/internal/cli/command/pull_requests/summarize_test.go +++ b/internal/cli/command/pull_requests/summarize_test.go @@ -7,7 +7,6 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -18,49 +17,38 @@ type MockPRService struct { mock.Mock } -func (m *MockPRService) SummarizePR(ctx context.Context, prNumber int, progress func(string)) (models.PRSummary, error) { +func (m *MockPRService) SummarizePR(ctx context.Context, prNumber int, progress func(models.ProgressEvent)) (models.PRSummary, error) { args := m.Called(ctx, prNumber, progress) return args.Get(0).(models.PRSummary), args.Error(1) } -type MockPRServiceFactory struct { - mock.Mock -} - -func (m *MockPRServiceFactory) CreatePRService(ctx context.Context) (ports.PRService, error) { - args := m.Called(ctx) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(ports.PRService), args.Error(1) -} - -func setupSummarizeTest(t *testing.T) (*MockPRService, *MockPRServiceFactory, *i18n.Translations, *config.Config) { +func setupSummarizeTest(t *testing.T) (*MockPRService, *i18n.Translations, *config.Config) { mockPRService := new(MockPRService) - mockFactory := new(MockPRServiceFactory) cfg := &config.Config{} - translations, err := i18n.NewTranslations("es", "../../../i18n/locales") + translations, err := i18n.NewTranslations("en", "../../../i18n/locales") require.NoError(t, err) - return mockPRService, mockFactory, translations, cfg + return mockPRService, translations, cfg } func TestSummarizeCommand(t *testing.T) { t.Run("should successfully summarize PR", func(t *testing.T) { // Arrange - mockPRService, mockFactory, translations, cfg := setupSummarizeTest(t) + mockPRService, translations, cfg := setupSummarizeTest(t) prNumber := 123 summary := models.PRSummary{ Title: "Test PR", } - mockFactory.On("CreatePRService", mock.Anything).Return(mockPRService, nil) mockPRService.On("SummarizePR", mock.Anything, prNumber, mock.Anything).Return(summary, nil) - prCommand := NewSummarizeCommand(mockFactory) + prProvider := func(ctx context.Context) (PRService, error) { + return mockPRService, nil + } + prCommand := NewSummarizeCommand(prProvider) cmd := prCommand.CreateCommand(translations, cfg) // Act @@ -68,18 +56,19 @@ func TestSummarizeCommand(t *testing.T) { // Assert assert.NoError(t, err) - mockFactory.AssertExpectations(t) + mockPRService.AssertExpectations(t) }) t.Run("should fail when factory returns error", func(t *testing.T) { // Arrange - _, mockFactory, translations, cfg := setupSummarizeTest(t) + _, translations, cfg := setupSummarizeTest(t) mockError := fmt.Errorf("factory error") - mockFactory.On("CreatePRService", mock.Anything).Return(nil, mockError) - - prCommand := NewSummarizeCommand(mockFactory) + prProvider := func(ctx context.Context) (PRService, error) { + return nil, mockError + } + prCommand := NewSummarizeCommand(prProvider) cmd := prCommand.CreateCommand(translations, cfg) // Act @@ -88,20 +77,22 @@ func TestSummarizeCommand(t *testing.T) { // Assert assert.Error(t, err) assert.Contains(t, err.Error(), translations.GetMessage("error.pr_service_creation_error", 0, nil)) - mockFactory.AssertExpectations(t) + }) t.Run("should fail when PR service returns error", func(t *testing.T) { // Arrange - mockPRService, mockFactory, translations, cfg := setupSummarizeTest(t) + mockPRService, translations, cfg := setupSummarizeTest(t) prNumber := 123 mockError := fmt.Errorf("service error") - mockFactory.On("CreatePRService", mock.Anything).Return(mockPRService, nil) mockPRService.On("SummarizePR", mock.Anything, prNumber, mock.Anything).Return(models.PRSummary{}, mockError) - prCommand := NewSummarizeCommand(mockFactory) + prProvider := func(ctx context.Context) (PRService, error) { + return mockPRService, nil + } + prCommand := NewSummarizeCommand(prProvider) cmd := prCommand.CreateCommand(translations, cfg) // Act @@ -110,7 +101,7 @@ func TestSummarizeCommand(t *testing.T) { // Assert assert.Error(t, err) assert.Contains(t, err.Error(), translations.GetMessage("error.pr_summary_error", 0, nil)) - mockFactory.AssertExpectations(t) + mockPRService.AssertExpectations(t) }) } diff --git a/internal/cli/command/release/create.go b/internal/cli/command/release/create.go index 837b1d3..10d60bd 100644 --- a/internal/cli/command/release/create.go +++ b/internal/cli/command/release/create.go @@ -9,7 +9,6 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" 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/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" @@ -62,12 +61,12 @@ func (r *ReleaseCommandFactory) newCreateCommand(t *i18n.Translations) *cli.Comm } } -func createReleaseAction(releaseService ports.ReleaseService, trans *i18n.Translations, reader *bufio.Reader, config *cfg.Config) cli.ActionFunc { +func createReleaseAction(releaseSvc releaseService, trans *i18n.Translations, reader *bufio.Reader, config *cfg.Config) cli.ActionFunc { return func(ctx context.Context, cmd *cli.Command) error { fmt.Println(trans.GetMessage("release.creating", 0, nil)) fmt.Println() - release, err := releaseService.AnalyzeNextRelease(ctx) + release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{ "Error": err.Error(), @@ -78,14 +77,15 @@ func createReleaseAction(releaseService ports.ReleaseService, trans *i18n.Transl release.Version = version } - if err := releaseService.EnrichReleaseContext(ctx, release); err != nil { + 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(), })) } - notes, err := releaseService.GenerateReleaseNotes(ctx, release) + 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(), @@ -101,7 +101,7 @@ func createReleaseAction(releaseService ports.ReleaseService, trans *i18n.Transl s := ui.NewSmartSpinner(trans.GetMessage("release.changelog_update_started", 0, nil)) s.Start() - if err := releaseService.UpdateLocalChangelog(release, notes); err != nil { + if err := releaseSvc.UpdateLocalChangelog(release, notes); err != nil { s.Error(trans.GetMessage("release.error_updating_changelog", 0, map[string]interface{}{ "Error": err.Error(), })) @@ -112,31 +112,31 @@ func createReleaseAction(releaseService ports.ReleaseService, trans *i18n.Transl sVersion := ui.NewSmartSpinner(trans.GetMessage("release.app_version_update_started", 0, map[string]interface{}{"Version": release.Version})) sVersion.Start() - if err := releaseService.UpdateAppVersion(release.Version); err != nil { + if err := releaseSvc.UpdateAppVersion(release.Version); err != nil { sVersion.Error(trans.GetMessage("release.error_updating_app_version", 0, map[string]interface{}{"Error": err.Error()})) - return fmt.Errorf("error al actualizar la version de la app: %w", err) + return fmt.Errorf("error updating app version: %w", err) } sVersion.Success(trans.GetMessage("release.app_version_updated", 0, map[string]interface{}{"Version": release.Version})) fmt.Println() sCommit := ui.NewSmartSpinner(trans.GetMessage("release.committing_changelog", 0, nil)) sCommit.Start() - if err := releaseService.CommitChangelog(ctx, release.Version); err != nil { + if err := releaseSvc.CommitChangelog(ctx, release.Version); err != nil { sCommit.Error(trans.GetMessage("release.error_committing_changelog", 0, map[string]interface{}{ "Error": err.Error(), })) - return fmt.Errorf("error comiteando changelog: %w", err) + return fmt.Errorf("error committing changelog: %w", err) } sCommit.Success(trans.GetMessage("release.changelog_committed", 0, nil)) fmt.Println() sPush := ui.NewSmartSpinner(trans.GetMessage("release.pushing_changes", 0, nil)) sPush.Start() - if err := releaseService.PushChanges(ctx); err != nil { + if err := releaseSvc.PushChanges(ctx); err != nil { sPush.Error(trans.GetMessage("release.error_pushing_changes", 0, map[string]interface{}{ "Error": err.Error(), })) - return fmt.Errorf("error pusheando cambios: %w", err) + return fmt.Errorf("error pushing changes: %w", err) } sPush.Success(trans.GetMessage("release.changes_pushed", 0, nil)) fmt.Println() @@ -168,7 +168,7 @@ func createReleaseAction(releaseService ports.ReleaseService, trans *i18n.Transl } message := fmt.Sprintf("%s\n\n%s", notes.Title, notes.Summary) - err = releaseService.CreateTag(ctx, release.Version, message) + 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(), @@ -184,7 +184,7 @@ func createReleaseAction(releaseService ports.ReleaseService, trans *i18n.Transl fmt.Println(trans.GetMessage("release.publishing_release", 0, nil)) buildBinaries := cmd.Bool("build-binaries") - err := releaseService.PublishRelease(ctx, release, notes, cmd.Bool("draft"), buildBinaries) + 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()})) } diff --git a/internal/cli/command/release/create_test.go b/internal/cli/command/release/create_test.go index 6ef0e84..bec909d 100644 --- a/internal/cli/command/release/create_test.go +++ b/internal/cli/command/release/create_test.go @@ -18,7 +18,7 @@ import ( func runCreateTest(t *testing.T, userInput string, args []string, mockService *MockReleaseService) error { trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") if err != nil { - t.Logf("Advertencia: usando traducciones vacías por error: %v", err) + t.Logf("Warning: using empty translations by error: %v", err) trans = &i18n.Translations{} } diff --git a/internal/cli/command/release/edit.go b/internal/cli/command/release/edit.go index 2c99edb..9e32005 100644 --- a/internal/cli/command/release/edit.go +++ b/internal/cli/command/release/edit.go @@ -8,8 +8,8 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -49,7 +49,7 @@ func (r *ReleaseCommandFactory) newEditCommand(trans *i18n.Translations) *cli.Co } } -func editReleaseAction(releaseService ports.ReleaseService, gitService ports.GitService, trans *i18n.Translations) cli.ActionFunc { +func editReleaseAction(releaseSvc releaseService, gitSvc gitService, trans *i18n.Translations) cli.ActionFunc { return func(ctx context.Context, cmd *cli.Command) error { version := cmd.String("version") editor := cmd.String("editor") @@ -59,8 +59,9 @@ func editReleaseAction(releaseService ports.ReleaseService, gitService ports.Git "Version": version, })) - existingRelease, err := releaseService.GetRelease(ctx, 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(), })) @@ -84,7 +85,7 @@ func editReleaseAction(releaseService ports.ReleaseService, gitService ports.Git content = generateReleaseTemplate(existingRelease.Name, trans) } } else { - commits, err := gitService.GetCommitsSinceTag(ctx, previousVersion) + 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(), @@ -99,13 +100,13 @@ func editReleaseAction(releaseService ports.ReleaseService, gitService ports.Git AllCommits: commits, } - if err := releaseService.EnrichReleaseContext(ctx, release); err != nil { + 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(), })) } - notes, err := releaseService.GenerateReleaseNotes(ctx, release) + 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(), @@ -159,7 +160,7 @@ func editReleaseAction(releaseService ports.ReleaseService, gitService ports.Git fmt.Println(trans.GetMessage("release.updating_release", 0, map[string]interface{}{ "Version": version, })) - err = releaseService.UpdateRelease(ctx, version, string(editedContent)) + 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(), diff --git a/internal/cli/command/release/formatter.go b/internal/cli/command/release/formatter.go index 84aa98a..5049269 100644 --- a/internal/cli/command/release/formatter.go +++ b/internal/cli/command/release/formatter.go @@ -8,7 +8,7 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/i18n" ) -// FormatReleaseMarkdown genera el markdown completo de una release con todas las secciones +// 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 diff --git a/internal/cli/command/release/generate.go b/internal/cli/command/release/generate.go index 61b8bd5..267ccfd 100644 --- a/internal/cli/command/release/generate.go +++ b/internal/cli/command/release/generate.go @@ -6,7 +6,6 @@ import ( "os" "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" @@ -36,26 +35,28 @@ func (r *ReleaseCommandFactory) newGenerateCommand(trans *i18n.Translations) *cl } } -func generateReleaseAction(releaseService ports.ReleaseService, trans *i18n.Translations) cli.ActionFunc { +func generateReleaseAction(releaseSvc releaseService, trans *i18n.Translations) cli.ActionFunc { return func(ctx context.Context, cmd *cli.Command) error { fmt.Println(trans.GetMessage("release.generating", 0, nil)) fmt.Println() - release, err := releaseService.AnalyzeNextRelease(ctx) + 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(), })) } - if err := releaseService.EnrichReleaseContext(ctx, release); err != nil { + 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(), })) } - notes, err := releaseService.GenerateReleaseNotes(ctx, release) + 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(), diff --git a/internal/cli/command/release/generate_test.go b/internal/cli/command/release/generate_test.go index cc45581..54656af 100644 --- a/internal/cli/command/release/generate_test.go +++ b/internal/cli/command/release/generate_test.go @@ -18,7 +18,7 @@ import ( func runGenerateTest(t *testing.T, args []string, mockService *MockReleaseService) error { trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") if err != nil { - t.Logf("Advertencia: usando traducciones vacías: %v", err) + t.Logf("Warning: using empty translations: %v", err) trans = &i18n.Translations{} } diff --git a/internal/cli/command/release/mocks.go b/internal/cli/command/release/mocks.go index fc40532..ed2d2f7 100644 --- a/internal/cli/command/release/mocks.go +++ b/internal/cli/command/release/mocks.go @@ -84,9 +84,9 @@ type MockGitService struct { mock.Mock } -func (m *MockGitService) GetChangedFiles(ctx context.Context) ([]models.GitChange, error) { +func (m *MockGitService) GetChangedFiles(ctx context.Context) ([]string, error) { args := m.Called(ctx) - return args.Get(0).([]models.GitChange), args.Error(1) + return args.Get(0).([]string), args.Error(1) } func (m *MockGitService) GetDiff(ctx context.Context) (string, error) { @@ -165,7 +165,7 @@ func (m *MockGitService) Push(ctx context.Context) error { return args.Error(0) } -func (m *MockGitService) GetRecentCommitMessages(ctx context.Context, count int) (string, error) { +func (m *MockGitService) GetRecentCommitMessages(ctx context.Context, count int) ([]string, error) { args := m.Called(ctx, count) - return args.String(0), args.Error(1) + return args.Get(0).([]string), args.Error(1) } diff --git a/internal/cli/command/release/preview.go b/internal/cli/command/release/preview.go index 98687d9..fcc2746 100644 --- a/internal/cli/command/release/preview.go +++ b/internal/cli/command/release/preview.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" @@ -27,12 +26,12 @@ func (r *ReleaseCommandFactory) newPreviewCommand(trans *i18n.Translations) *cli } } -func previewReleaseAction(releaseService ports.ReleaseService, trans *i18n.Translations) cli.ActionFunc { +func previewReleaseAction(releaseSvc releaseService, trans *i18n.Translations) cli.ActionFunc { return func(ctx context.Context, cmd *cli.Command) error { fmt.Println(trans.GetMessage("release.analyzing", 0, nil)) fmt.Println() - release, err := releaseService.AnalyzeNextRelease(ctx) + release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{ "Error": err.Error(), @@ -71,14 +70,15 @@ func previewReleaseAction(releaseService ports.ReleaseService, trans *i18n.Trans })) fmt.Println() - if err := releaseService.EnrichReleaseContext(ctx, release); err != nil { + 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(), })) } - notes, err := releaseService.GenerateReleaseNotes(ctx, release) + 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(), diff --git a/internal/cli/command/release/preview_test.go b/internal/cli/command/release/preview_test.go index e062edd..d4c1d14 100644 --- a/internal/cli/command/release/preview_test.go +++ b/internal/cli/command/release/preview_test.go @@ -16,7 +16,7 @@ import ( func runPreviewTest(t *testing.T, args []string, mockService *MockReleaseService) error { trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") if err != nil { - t.Logf("Advertencia: usando traducciones vacías: %v", err) + t.Logf("Warning: using empty translations: %v", err) trans = &i18n.Translations{} } diff --git a/internal/cli/command/release/publish.go b/internal/cli/command/release/publish.go index 6e70b07..6637215 100644 --- a/internal/cli/command/release/publish.go +++ b/internal/cli/command/release/publish.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" @@ -45,10 +44,10 @@ func (r *ReleaseCommandFactory) newPublishCommand(trans *i18n.Translations) *cli } } -func publishReleaseAction(releaseService ports.ReleaseService, +func publishReleaseAction(releaseSvc releaseService, trans *i18n.Translations) cli.ActionFunc { return func(ctx context.Context, cmd *cli.Command) error { - release, err := releaseService.AnalyzeNextRelease(ctx) + release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{ "Error": err.Error(), @@ -59,14 +58,15 @@ func publishReleaseAction(releaseService ports.ReleaseService, release.Version = version } - if err := releaseService.EnrichReleaseContext(ctx, release); err != nil { + 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(), })) } - notes, err := releaseService.GenerateReleaseNotes(ctx, release) + 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(), })) @@ -84,7 +84,7 @@ func publishReleaseAction(releaseService ports.ReleaseService, "Draft": draftText, })) - err = releaseService.PublishRelease(ctx, release, notes, draft, buildBinaries) + 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(), diff --git a/internal/cli/command/release/push.go b/internal/cli/command/release/push.go index 9614b59..edffb86 100644 --- a/internal/cli/command/release/push.go +++ b/internal/cli/command/release/push.go @@ -5,8 +5,8 @@ import ( "fmt" "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -32,12 +32,12 @@ func (r *ReleaseCommandFactory) newPushCommand(trans *i18n.Translations) *cli.Co } } -func pushReleaseAction(releaseService ports.ReleaseService, trans *i18n.Translations) cli.ActionFunc { +func pushReleaseAction(releaseSvc releaseService, trans *i18n.Translations) cli.ActionFunc { return func(ctx context.Context, cmd *cli.Command) error { version := cmd.String("version") if version == "" { - release, err := releaseService.AnalyzeNextRelease(ctx) + release, err := releaseSvc.AnalyzeNextRelease(ctx) if err != nil { return fmt.Errorf("%s", trans.GetMessage("release.error_analyzing", 0, map[string]interface{}{"Error": err.Error()})) } @@ -46,8 +46,9 @@ func pushReleaseAction(releaseService ports.ReleaseService, trans *i18n.Translat fmt.Println(trans.GetMessage("release.pushing_tag", 0, map[string]interface{}{"Version": version})) - err := releaseService.PushTag(ctx, 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()})) } diff --git a/internal/cli/command/release/push_test.go b/internal/cli/command/release/push_test.go index bfaa462..6e094d1 100644 --- a/internal/cli/command/release/push_test.go +++ b/internal/cli/command/release/push_test.go @@ -16,7 +16,7 @@ import ( func runPushTest(t *testing.T, args []string, mockService *MockReleaseService, mockGit *MockGitService) error { trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") if err != nil { - t.Logf("Advertencia: usando traducciones vacías: %v", err) + t.Logf("Warning: using empty translations: %v", err) trans = &i18n.Translations{} } diff --git a/internal/cli/command/release/release.go b/internal/cli/command/release/release.go index 12d4178..e06702f 100644 --- a/internal/cli/command/release/release.go +++ b/internal/cli/command/release/release.go @@ -5,22 +5,59 @@ import ( "fmt" cfg "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" - geminiAI "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai/gemini" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/git" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" + "github.com/Tomas-vilte/MateCommit/internal/providers" "github.com/Tomas-vilte/MateCommit/internal/services" "github.com/urfave/cli/v3" ) +// releaseService is a minimal interface for testing purposes +type releaseService interface { + AnalyzeNextRelease(ctx context.Context) (*models.Release, error) + GenerateReleaseNotes(ctx context.Context, release *models.Release) (*models.ReleaseNotes, error) + PublishRelease(ctx context.Context, release *models.Release, notes *models.ReleaseNotes, draft bool, buildBinaries bool) error + CreateTag(ctx context.Context, version, message string) error + PushTag(ctx context.Context, version string) error + GetRelease(ctx context.Context, version string) (*models.VCSRelease, error) + UpdateRelease(ctx context.Context, version, body string) error + EnrichReleaseContext(ctx context.Context, release *models.Release) error + UpdateLocalChangelog(release *models.Release, notes *models.ReleaseNotes) error + CommitChangelog(ctx context.Context, version string) error + PushChanges(ctx context.Context) error + UpdateAppVersion(version string) error +} + +// gitService is a minimal interface for testing purposes +type gitService interface { + GetChangedFiles(ctx context.Context) ([]string, error) + GetDiff(ctx context.Context) (string, error) + GetRecentCommitMessages(ctx context.Context, limit int) ([]string, error) + GetRepoInfo(ctx context.Context) (string, string, string, error) + GetCurrentBranch(ctx context.Context) (string, error) + GetLastTag(ctx context.Context) (string, error) + GetCommitCount(ctx context.Context) (int, error) + GetCommitsSinceTag(ctx context.Context, tag string) ([]models.Commit, error) + CreateTag(ctx context.Context, version, message string) error + PushTag(ctx context.Context, version string) error + GetTagDate(ctx context.Context, version string) (string, error) + AddFileToStaging(ctx context.Context, file string) error + HasStagedChanges(ctx context.Context) bool + CreateCommit(ctx context.Context, message string) error + Push(ctx context.Context) error +} + type ReleaseCommandFactory struct { - gitService ports.GitService + gitService gitService config *cfg.Config } -func NewReleaseCommandFactory(gitService ports.GitService, config *cfg.Config) *ReleaseCommandFactory { +func NewReleaseCommandFactory(gitSvc *git.GitService, config *cfg.Config) *ReleaseCommandFactory { return &ReleaseCommandFactory{ - gitService: gitService, + gitService: gitSvc, config: config, } } @@ -41,30 +78,35 @@ func (r *ReleaseCommandFactory) CreateCommand(t *i18n.Translations, _ *cfg.Confi } } -func (r *ReleaseCommandFactory) createReleaseService(ctx context.Context, t *i18n.Translations) (ports.ReleaseService, error) { +func (r *ReleaseCommandFactory) createReleaseService(ctx context.Context, t *i18n.Translations) (*services.ReleaseService, error) { var vcsClient ports.VCSClient if r.config.ActiveVCSProvider != "" { if vcsConfig, ok := r.config.VCSConfigs[r.config.ActiveVCSProvider]; ok { owner, repo, _, err := r.gitService.GetRepoInfo(ctx) if err == nil && vcsConfig.Token != "" { - vcsClient = github.NewGitHubClient(owner, repo, vcsConfig.Token, t) + vcsClient = github.NewGitHubClient(owner, repo, vcsConfig.Token) } } } var notesGen ports.ReleaseNotesGenerator - if providerCfg, exists := r.config.AIProviders["gemini"]; exists && providerCfg.APIKey != "" { + if r.config.AIConfig.ActiveAI != "" { owner, repo, _, err := r.gitService.GetRepoInfo(ctx) if err != nil { return nil, fmt.Errorf("error obteniendo informacion del repositorio: %w", err) } - gen, err := geminiAI.NewReleaseNotesGenerator(ctx, r.config, t, owner, repo) + gen, err := providers.NewReleaseNotesGenerator(ctx, r.config, nil, owner, repo) if err != nil { return nil, err } notesGen = gen } - return services.NewReleaseService(r.gitService, vcsClient, notesGen, t, r.config), nil + return services.NewReleaseService( + r.gitService, + services.WithReleaseVCSClient(vcsClient), + services.WithReleaseNotesGenerator(notesGen), + services.WithReleaseConfig(r.config), + ), nil } diff --git a/internal/cli/command/release/version_helper.go b/internal/cli/command/release/version_helper.go index 82fd4b1..e37e03c 100644 --- a/internal/cli/command/release/version_helper.go +++ b/internal/cli/command/release/version_helper.go @@ -6,30 +6,30 @@ import ( "strings" ) -// getPreviousVersion Calcula la versión anterior a partir de una versión dada -// por ej: v1.3.0 -> v1.2.0 +// getPreviousVersion calculates the previous version from a given version +// e.g.: v1.3.0 -> v1.2.0 func getPreviousVersion(version string) (string, error) { version = strings.TrimPrefix(version, "v") parts := strings.Split(version, ".") if len(parts) != 3 { - return "", fmt.Errorf("formato de versión no válido: %s", version) + return "", fmt.Errorf("invalid version format: %s", version) } // Parse parts major, err := strconv.Atoi(parts[0]) if err != nil { - return "", fmt.Errorf("versión major no válida: %s", parts[0]) + return "", fmt.Errorf("invalid major version: %s", parts[0]) } minor, err := strconv.Atoi(parts[1]) if err != nil { - return "", fmt.Errorf("versión minor no válida: %s", parts[1]) + return "", fmt.Errorf("invalid minor version: %s", parts[1]) } patch, err := strconv.Atoi(parts[2]) if err != nil { - return "", fmt.Errorf("versión de parche no válida: %s", parts[2]) + return "", fmt.Errorf("invalid patch version: %s", parts[2]) } if patch > 0 { @@ -42,7 +42,7 @@ func getPreviousVersion(version string) (string, error) { minor = 0 patch = 0 } else { - return "", fmt.Errorf("no se puede decrementar la versión v0.0.0") + return "", fmt.Errorf("cannot decrement version v0.0.0") } return fmt.Sprintf("v%d.%d.%d", major, minor, patch), nil diff --git a/internal/cli/command/stats/stats.go b/internal/cli/command/stats/stats.go index e5892b0..5077439 100644 --- a/internal/cli/command/stats/stats.go +++ b/internal/cli/command/stats/stats.go @@ -31,7 +31,7 @@ func (c *StatsCommand) CreateCommand(t *i18n.Translations, _ *config.Config) *cl }, }, Action: func(ctx context.Context, cmd *cli.Command) error { - manager, err := cost.NewManager(0, t) + manager, err := cost.NewManager(0) if err != nil { return fmt.Errorf(t.GetMessage("stats.error_init", 0, nil)+": %w", err) } diff --git a/internal/cli/command/stats/stats_test.go b/internal/cli/command/stats/stats_test.go index 1fe8af7..f2f3414 100644 --- a/internal/cli/command/stats/stats_test.go +++ b/internal/cli/command/stats/stats_test.go @@ -19,8 +19,8 @@ func TestNewStatsCommand(t *testing.T) { cmd := NewStatsCommand() // Assert - assert.NotNil(t, cmd, "NewStatsCommand debería retornar una instancia no nula") - assert.IsType(t, &StatsCommand{}, cmd, "debería retornar un puntero a StatsCommand") + assert.NotNil(t, cmd, "NewStatsCommand should return a non-nil instance") + assert.IsType(t, &StatsCommand{}, cmd, "should return a pointer to StatsCommand") } func TestShowDailyStats_NoActivity(t *testing.T) { @@ -45,9 +45,9 @@ func TestShowDailyStats_NoActivity(t *testing.T) { output := buf.String() // Assert - assert.NoError(t, err, "showDailyStats no debería retornar error con datos vacíos") - assert.Contains(t, output, "No activity recorded", "debería indicar que no hay actividad") - assert.Contains(t, output, "━", "debería contener el separador de la tabla") + assert.NoError(t, err, "showDailyStats should not return an error with empty data") + assert.Contains(t, output, "No activity recorded", "should indicate that there is no activity") + assert.Contains(t, output, "━", "should contain the table separator") } func TestShowDailyStats_WithActivity(t *testing.T) { @@ -68,7 +68,7 @@ func TestShowDailyStats_WithActivity(t *testing.T) { CacheHit: false, }, { - Timestamp: now.Add(-1 * time.Hour), + Timestamp: now, Command: "summarize-pr", Provider: "gemini", Model: "gemini-3.0-flash", @@ -110,16 +110,16 @@ func TestShowDailyStats_WithActivity(t *testing.T) { outputStr := buf.String() // Assert - assert.NoError(t, err, "showDailyStats no debería retornar error") - assert.Contains(t, outputStr, "suggest", "debería mostrar el comando suggest") - assert.Contains(t, outputStr, "summarize-pr", "debería mostrar el comando summarize-pr") - assert.Contains(t, outputStr, "$0.0015", "debería mostrar el costo del primer comando") - assert.Contains(t, outputStr, "$0.0085", "debería mostrar el costo del segundo comando") - assert.Contains(t, outputStr, "━", "debería contener separadores visuales") + assert.NoError(t, err, "showDailyStats should not return an error") + assert.Contains(t, outputStr, "suggest", "should show the suggest command") + assert.Contains(t, outputStr, "summarize-pr", "should show the summarize-pr command") + assert.Contains(t, outputStr, "$0.0015", "should show the cost of the first command") + assert.Contains(t, outputStr, "$0.0085", "should show the cost of the second command") + assert.Contains(t, outputStr, "━", "should contain visual separators") total, err := manager.GetDailyTotal() assert.NoError(t, err) - assert.Equal(t, 0.0100, total, "el total calculado debería ser 0.0100") + assert.Equal(t, 0.0100, total, "the calculated total should be 0.0100") } func TestShowDailyStats_WithCacheHits(t *testing.T) { @@ -140,7 +140,7 @@ func TestShowDailyStats_WithCacheHits(t *testing.T) { CacheHit: true, }, { - Timestamp: now.Add(-30 * time.Minute), + Timestamp: now, Command: "suggest", Provider: "gemini", Model: "gemini-2.5-flash", @@ -171,9 +171,9 @@ func TestShowDailyStats_WithCacheHits(t *testing.T) { // Assert assert.NoError(t, err) - assert.Contains(t, outputStr, "[CACHE]", "debería indicar cuándo hay un cache hit") - assert.Contains(t, outputStr, "$0.0000", "debería mostrar costo cero para cache hit") - assert.Contains(t, outputStr, "$0.0015", "debería mostrar el total correcto") + assert.Contains(t, outputStr, "[CACHE]", "should indicate when there is a cache hit") + assert.Contains(t, outputStr, "$0.0000", "should show zero cost for cache hit") + assert.Contains(t, outputStr, "$0.0015", "should show the correct total") } func TestShowMonthlyStats_NoActivity(t *testing.T) { @@ -199,8 +199,8 @@ func TestShowMonthlyStats_NoActivity(t *testing.T) { // Assert assert.NoError(t, err) - assert.Contains(t, output, "No activity recorded", "debería indicar que no hay actividad") - assert.Contains(t, output, "━", "debería contener separadores visuales") + assert.Contains(t, output, "No activity recorded", "should indicate that there is no activity") + assert.Contains(t, output, "━", "should contain visual separators") } func TestShowMonthlyStats_WithActivity(t *testing.T) { @@ -209,7 +209,7 @@ func TestShowMonthlyStats_WithActivity(t *testing.T) { now := time.Now() records := []cost.ActivityRecord{ - // Día 1 + // Day 1 { Timestamp: time.Date(now.Year(), now.Month(), 1, 10, 0, 0, 0, time.Local), Command: "suggest", @@ -220,7 +220,7 @@ func TestShowMonthlyStats_WithActivity(t *testing.T) { Command: "summarize-pr", CostUSD: 0.0025, }, - // Día 2 + // Day 2 { Timestamp: time.Date(now.Year(), now.Month(), 2, 10, 0, 0, 0, time.Local), Command: "suggest", @@ -257,15 +257,15 @@ func TestShowMonthlyStats_WithActivity(t *testing.T) { day1 := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.Local).Format("2006-01-02") day2 := time.Date(now.Year(), now.Month(), 2, 0, 0, 0, 0, time.Local).Format("2006-01-02") - assert.Contains(t, outputStr, day1, "debería mostrar el día 1") - assert.Contains(t, outputStr, day2, "debería mostrar el día 2") - assert.Contains(t, outputStr, "$0.0040", "debería mostrar el total del día 1 (0.0015 + 0.0025)") - assert.Contains(t, outputStr, "$0.0010", "debería mostrar el total del día 2") - assert.Contains(t, outputStr, "━", "debería contener separadores visuales") + assert.Contains(t, outputStr, day1, "should show day 1") + assert.Contains(t, outputStr, day2, "should show day 2") + assert.Contains(t, outputStr, "$0.0040", "should show the total for day 1 (0.0015 + 0.0025)") + assert.Contains(t, outputStr, "$0.0010", "should show the total for day 2") + assert.Contains(t, outputStr, "━", "should contain visual separators") total, err := manager.GetMonthlyTotal() assert.NoError(t, err) - assert.Equal(t, 0.0050, total, "el total mensual debería ser 0.0050") + assert.Equal(t, 0.0050, total, "the monthly total should be 0.0050") } func TestShowMonthlyStats_GroupsByDay(t *testing.T) { @@ -302,8 +302,8 @@ func TestShowMonthlyStats_GroupsByDay(t *testing.T) { assert.NoError(t, err) expectedDay := sameDay.Format("2006-01-02") - assert.Contains(t, outputStr, expectedDay, "debería mostrar el día agrupado") - assert.Contains(t, outputStr, "$0.0065", "debería sumar todos los costos del día (0.0010+0.0020+0.0030+0.0005)") + assert.Contains(t, outputStr, expectedDay, "should show the grouped day") + assert.Contains(t, outputStr, "$0.0065", "should sum all costs for the day (0.0010+0.0020+0.0030+0.0005)") } func TestShowDailyStats_FormatsTime(t *testing.T) { @@ -341,8 +341,8 @@ func TestShowDailyStats_FormatsTime(t *testing.T) { // Assert assert.NoError(t, err) - assert.Contains(t, buf.String(), "14:30", "debería formatear la hora como HH:MM") - assert.Contains(t, buf.String(), "suggest", "debería mostrar el comando") + assert.Contains(t, buf.String(), "14:30", "should format the time as HH:MM") + assert.Contains(t, buf.String(), "suggest", "should show the command") } func setupTestManager(t *testing.T, tempDir string, records []cost.ActivityRecord) *cost.Manager { @@ -354,13 +354,12 @@ func setupTestManager(t *testing.T, tempDir string, records []cost.ActivityRecor _ = os.Setenv("HOME", originalHome) }) - trans := setupTestTranslations(t) - manager, err := cost.NewManager(0, trans) - require.NoError(t, err, "no debería fallar al crear el manager de prueba") + manager, err := cost.NewManager(0) + require.NoError(t, err, "should not fail to create the test manager") for _, record := range records { err := manager.SaveActivity(record) - require.NoError(t, err, "no debería fallar al guardar actividad de prueba") + require.NoError(t, err, "should not fail to save test activity") } return manager @@ -371,14 +370,14 @@ func setupTestTranslations(t *testing.T) *i18n.Translations { localesPath := filepath.Join("..", "..", "..", "i18n", "locales") trans, err := i18n.NewTranslations("en", localesPath) - require.NoError(t, err, "no debería fallar al crear traducciones de prueba") + require.NoError(t, err, "should not fail to create test translations") return trans } func TestStatsCommand_Integration(t *testing.T) { if testing.Short() { - t.Skip("Saltando test de integración en modo short") + t.Skip("Skipping integration test in short mode") } // Arrange @@ -390,7 +389,7 @@ func TestStatsCommand_Integration(t *testing.T) { }) trans := setupTestTranslations(t) - manager, err := cost.NewManager(10.0, trans) + manager, err := cost.NewManager(0) require.NoError(t, err) now := time.Now() @@ -430,7 +429,7 @@ func TestStatsCommand_Integration(t *testing.T) { // Assert assert.NoError(t, errDaily) - assert.NotEmpty(t, dailyBuf.String(), "debería generar output para estadísticas diarias") + assert.NotEmpty(t, dailyBuf.String(), "should generate output for daily statistics") // Act - Monthly Stats var monthlyBuf bytes.Buffer @@ -445,7 +444,7 @@ func TestStatsCommand_Integration(t *testing.T) { // Assert assert.NoError(t, errMonthly) - assert.NotEmpty(t, monthlyBuf.String(), "debería generar output para estadísticas mensuales") + assert.NotEmpty(t, monthlyBuf.String(), "should generate output for monthly statistics") } func TestShowDailyStats_HandlesManagerErrors(t *testing.T) { @@ -464,7 +463,7 @@ func TestShowDailyStats_HandlesManagerErrors(t *testing.T) { err = cmd.showDailyStats(manager, trans) // Assert - assert.Error(t, err, "debería retornar error cuando el historial está corrupto") + assert.Error(t, err, "should return an error when the history is corrupt") } func TestShowMonthlyStats_HandlesManagerErrors(t *testing.T) { @@ -483,5 +482,5 @@ func TestShowMonthlyStats_HandlesManagerErrors(t *testing.T) { err = cmd.showMonthlyStats(manager, trans) // Assert - assert.Error(t, err, "debería retornar error cuando el historial está corrupto") + assert.Error(t, err, "should return an error when the history is corrupt") } diff --git a/internal/cli/command/suggests_commits/suggests_commits.go b/internal/cli/command/suggests_commits/suggests_commits.go index ce772ed..543ee2a 100644 --- a/internal/cli/command/suggests_commits/suggests_commits.go +++ b/internal/cli/command/suggests_commits/suggests_commits.go @@ -3,27 +3,36 @@ package suggests_commits import ( "context" "fmt" - "strings" + "os" "time" "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" ) +// commitService is a minimal interface for testing purposes +type commitService interface { + GenerateSuggestions(ctx context.Context, count int, issueNumber int, progress func(models.ProgressEvent)) ([]models.CommitSuggestion, error) +} + +// commitHandler is a minimal interface for testing purposes +type commitHandler interface { + HandleSuggestions(ctx context.Context, suggestions []models.CommitSuggestion) error +} + type SuggestCommandFactory struct { - commitService ports.CommitService - commitHandler ports.CommitHandler + commitService commitService + commitHandler commitHandler } -func NewSuggestCommandFactory(commitService ports.CommitService, commitHandler ports.CommitHandler) *SuggestCommandFactory { +func NewSuggestCommandFactory(commitSvc commitService, commitHdlr commitHandler) *SuggestCommandFactory { return &SuggestCommandFactory{ - commitService: commitService, - commitHandler: commitHandler, + commitService: commitSvc, + commitHandler: commitHdlr, } } @@ -82,7 +91,7 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla "Min": 1, "Max": 10, }) - ui.PrintError(msg) + ui.PrintError(os.Stdout, msg) return fmt.Errorf("%s", msg) } @@ -93,8 +102,8 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla } if err := config.SaveConfig(cfg); err != nil { - ui.PrintError(t.GetMessage("ui_error.error_saving_config", 0, nil)) - return fmt.Errorf("error al guardar la configuración: %w", err) + ui.PrintError(os.Stdout, t.GetMessage("ui_error.error_saving_config", 0, nil)) + return fmt.Errorf("error saving configuration: %w", err) } ui.PrintSectionBanner(t.GetMessage("ui.generating_suggestions_banner", 0, nil)) @@ -108,54 +117,34 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla start := time.Now() - if issueNumber > 0 { - spinner.UpdateMessage(t.GetMessage("ui.including_issue_context", 0, map[string]interface{}{ - "Number": issueNumber, - })) - time.Sleep(200 * time.Millisecond) - - spinner.Stop() - ui.PrintInfo(t.GetMessage("ui.detected_issue", 0, map[string]interface{}{ - "Number": issueNumber, - })) - - spinner = ui.NewSmartSpinner(t.GetMessage("ui.generating_with_issue", 0, nil)) - spinner.Start() + suggestions, err = f.commitService.GenerateSuggestions(ctx, count, issueNumber, func(event models.ProgressEvent) { + 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 { + key = "issue_using_manual" + } + msg = fmt.Sprintf("%s: #%d - %s", key, id, title) // TODO: use translations for key + case models.ProgressGeneric: + msg = event.Message + default: + msg = event.Message + } - suggestions, err = f.commitService.GenerateSuggestionsWithIssue(ctx, count, issueNumber, func(msg string) { - spinner.Log(msg) - }) - } else { - spinner.UpdateMessage(t.GetMessage("ui.generating_with_ai", 0, nil)) - suggestions, err = f.commitService.GenerateSuggestions(ctx, count, func(msg string) { + if msg != "" { spinner.Log(msg) - }) - } + } + }) duration := time.Since(start) if err != nil { spinner.Error(t.GetMessage("ui.error_generating_suggestions", 0, nil)) - - errStr := err.Error() - if containsStr(errStr, "GEMINI_API_KEY") || containsStr(errStr, "API key") { - ui.PrintErrorWithSuggestion( - t.GetMessage("ui_error.ai_api_key_missing_generic", 0, nil), - t.GetMessage("ui_error.run_config_init", 0, nil), - ) - return err - } else if containsStr(errStr, "GITHUB_TOKEN") { - ui.PrintErrorWithSuggestion( - t.GetMessage("ui_error.github_token_missing", 0, nil), - t.GetMessage("ui_error.run_config_init", 0, nil), - ) - } else if containsStr(errStr, "no differences detected") || containsStr(errStr, "no se detectaron cambios") { - ui.PrintWarning(t.GetMessage("ui_error.no_changes_detected", 0, nil)) - ui.PrintInfo(t.GetMessage("ui_error.ensure_modified_files", 0, nil)) - } else { - ui.PrintError(errStr) - } - + ui.HandleAppError(err, t) return fmt.Errorf("%s", t.GetMessage("suggestion_generation_error", 0, map[string]interface{}{ "Error": err, })) @@ -168,7 +157,3 @@ func (f *SuggestCommandFactory) createAction(cfg *config.Config, t *i18n.Transla return f.commitHandler.HandleSuggestions(ctx, suggestions) } } - -func containsStr(s, substr string) bool { - return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) -} diff --git a/internal/cli/command/suggests_commits/suggests_commits_test.go b/internal/cli/command/suggests_commits/suggests_commits_test.go index cb0e91c..6c74e67 100644 --- a/internal/cli/command/suggests_commits/suggests_commits_test.go +++ b/internal/cli/command/suggests_commits/suggests_commits_test.go @@ -18,12 +18,7 @@ type MockCommitService struct { mock.Mock } -func (m *MockCommitService) GenerateSuggestions(ctx context.Context, count int, progress func(string)) ([]models.CommitSuggestion, error) { - args := m.Called(ctx, count, progress) - return args.Get(0).([]models.CommitSuggestion), args.Error(1) -} - -func (m *MockCommitService) GenerateSuggestionsWithIssue(ctx context.Context, count int, issueNumber int, progress func(string)) ([]models.CommitSuggestion, error) { +func (m *MockCommitService) GenerateSuggestions(ctx context.Context, count int, issueNumber int, progress func(models.ProgressEvent)) ([]models.CommitSuggestion, error) { args := m.Called(ctx, count, issueNumber, progress) return args.Get(0).([]models.CommitSuggestion), args.Error(1) } @@ -82,7 +77,7 @@ func TestSuggestCommand(t *testing.T) { }, } - mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, mock.Anything).Return(suggestions, nil) + mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, mock.Anything).Return(suggestions, nil) mockHandler.On("HandleSuggestions", mock.Anything, suggestions).Return(nil) factory := NewSuggestCommandFactory(mockService, mockHandler) @@ -139,7 +134,7 @@ func TestSuggestCommand(t *testing.T) { }, } - mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, mock.Anything).Return(suggestions, nil) + mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, mock.Anything).Return(suggestions, nil) mockHandler.On("HandleSuggestions", mock.Anything, suggestions).Return(nil) factory := NewSuggestCommandFactory(mockService, mockHandler) @@ -173,7 +168,7 @@ func TestSuggestCommand(t *testing.T) { }, } - mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, mock.Anything).Return(suggestions, nil) + mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, mock.Anything).Return(suggestions, nil) mockHandler.On("HandleSuggestions", mock.Anything, suggestions).Return(nil) factory := NewSuggestCommandFactory(mockService, mockHandler) @@ -200,7 +195,7 @@ func TestSuggestCommand(t *testing.T) { mockHandler := new(MockCommitHandler) expectedError := fmt.Errorf("service error") - mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, mock.Anything).Return([]models.CommitSuggestion{}, expectedError) + mockService.On("GenerateSuggestions", mock.Anything, cfg.SuggestionsCount, 0, mock.Anything).Return([]models.CommitSuggestion{}, expectedError) factory := NewSuggestCommandFactory(mockService, mockHandler) command := factory.CreateCommand(translations, cfg) diff --git a/internal/cli/command/update/cmd.go b/internal/cli/command/update/cmd.go index d77ebcc..ad7dbcd 100644 --- a/internal/cli/command/update/cmd.go +++ b/internal/cli/command/update/cmd.go @@ -7,6 +7,7 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/config" "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/services" + "github.com/Tomas-vilte/MateCommit/internal/ui" "github.com/urfave/cli/v3" ) @@ -25,10 +26,11 @@ func (f *UpdateCommandFactory) CreateCommand(trans *i18n.Translations, _ *config Name: "update", Usage: trans.GetMessage("update.usage", 0, nil), Action: func(ctx context.Context, command *cli.Command) error { - updater := services.NewVersionUpdater(f.currentVersion, trans) + updater := services.NewVersionUpdater(services.WithCurrentVersion(f.currentVersion)) fmt.Println(trans.GetMessage("update.updating", 0, nil)) if err := updater.UpdateCLI(ctx); err != nil { + ui.HandleAppError(err, trans) return err } diff --git a/internal/cli/completion_helper/helper.go b/internal/cli/completion_helper/helper.go index 3577c36..1c14f54 100644 --- a/internal/cli/completion_helper/helper.go +++ b/internal/cli/completion_helper/helper.go @@ -7,8 +7,8 @@ import ( "github.com/urfave/cli/v3" ) -// DefaultFlagComplete imprime todos los flags del comando actual para facilitar la finalización del shell. -// Esto se utiliza para garantizar que se sugieran flags incluso cuando la finalización predeterminada de urfave/cli pueda fallar. +// DefaultFlagComplete prints all flags of the current command to facilitate shell completion. +// This is used to ensure flags are suggested even when the default urfave/cli completion might fail. func DefaultFlagComplete(_ context.Context, cmd *cli.Command) { for _, f := range cmd.Flags { for _, name := range f.Names() { diff --git a/internal/cli/registry/registry.go b/internal/cli/registry/registry.go deleted file mode 100644 index 76fa3fe..0000000 --- a/internal/cli/registry/registry.go +++ /dev/null @@ -1,45 +0,0 @@ -package registry - -import ( - "fmt" - cfg "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/urfave/cli/v3" -) - -type CommandFactory interface { - CreateCommand(t *i18n.Translations, cfg *cfg.Config) *cli.Command -} - -type Registry struct { - factories map[string]CommandFactory - config *cfg.Config - t *i18n.Translations -} - -func NewRegistry(cfg *cfg.Config, t *i18n.Translations) *Registry { - return &Registry{ - factories: make(map[string]CommandFactory), - config: cfg, - t: t, - } -} - -func (r *Registry) Register(name string, factory CommandFactory) error { - if _, exists := r.factories[name]; exists { - msg := r.t.GetMessage("factory_already_registered", 0, map[string]interface{}{ - "FactoryName": name, - }) - return fmt.Errorf("%s", msg) - } - r.factories[name] = factory - return nil -} - -func (r *Registry) CreateCommands() []*cli.Command { - commands := make([]*cli.Command, 0, len(r.factories)) - for _, factory := range r.factories { - commands = append(commands, factory.CreateCommand(r.t, r.config)) - } - return commands -} diff --git a/internal/cli/registry/registry_test.go b/internal/cli/registry/registry_test.go deleted file mode 100644 index 41020f1..0000000 --- a/internal/cli/registry/registry_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package registry - -import ( - "testing" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v3" -) - -type mockCommandFactory struct { -} - -func (m *mockCommandFactory) CreateCommand(t *i18n.Translations, cfg *config.Config) *cli.Command { - return &cli.Command{ - Name: "mock-command", - } -} - -func TestRegistry_Register(t *testing.T) { - t.Run("should register new factory successfully", func(t *testing.T) { - cfg := &config.Config{} - translations, err := i18n.NewTranslations("en", "../../i18n/locales") - assert.NoError(t, err) - registry := NewRegistry(cfg, translations) - factory := &mockCommandFactory{} - - // act - err = registry.Register("test-command", factory) - - // assert - assert.NoError(t, err) - assert.Len(t, registry.factories, 1) - assert.Contains(t, registry.factories, "test-command") - }) - - t.Run("should return error when registering duplicate factory", func(t *testing.T) { - // arrange - cfg := &config.Config{} - translations, err := i18n.NewTranslations("en", "../../i18n/locales") - assert.NoError(t, err) - registry := NewRegistry(cfg, translations) - factory := &mockCommandFactory{} - - // act - _ = registry.Register("test-command", factory) - err = registry.Register("test-command", factory) - - // assert - assert.Error(t, err) - assert.Len(t, registry.factories, 1) - }) -} - -func TestRegistry_CreateCommands(t *testing.T) { - t.Run("should create commands from registered factories", func(t *testing.T) { - // Arrange - cfg := &config.Config{} - translations, err := i18n.NewTranslations("en", "../../i18n/locales") - assert.NoError(t, err) - registry := NewRegistry(cfg, translations) - factory1 := &mockCommandFactory{} - factory2 := &mockCommandFactory{} - - _ = registry.Register("command1", factory1) - _ = registry.Register("command2", factory2) - - // Act - commands := registry.CreateCommands() - - // Assert - assert.Len(t, commands, 2) - assert.Equal(t, "mock-command", commands[0].Name) - assert.Equal(t, "mock-command", commands[1].Name) - }) - - t.Run("should return empty slice when no factories registered", func(t *testing.T) { - // Arrange - cfg := &config.Config{} - translations, err := i18n.NewTranslations("en", "../../i18n/locales") - assert.NoError(t, err) - registry := NewRegistry(cfg, translations) - - // Act - commands := registry.CreateCommands() - - // Assert - assert.Empty(t, commands) - }) -} -func TestNewRegistry(t *testing.T) { - t.Run("should create new registry with empty factories", func(t *testing.T) { - // Arrange - cfg := &config.Config{} - translations, err := i18n.NewTranslations("en", "../../i18n/locales") - assert.NoError(t, err) - - // Act - registry := NewRegistry(cfg, translations) - - // Assert - assert.NotNil(t, registry) - assert.Empty(t, registry.factories) - assert.Equal(t, cfg, registry.config) - assert.Equal(t, translations, registry.t) - }) -} diff --git a/internal/config/ai.go b/internal/config/ai.go index fccefff..dd1ce1d 100644 --- a/internal/config/ai.go +++ b/internal/config/ai.go @@ -16,7 +16,7 @@ const ( ModelGeminiV3Pro Model = "gemini-3-pro-preview" ModelGeminiV3Flash Model = "gemini-3-flash-preview" - // TODO: Agregar mas modelos para openai o otros... + // TODO: Add more models for OpenAI or others... ModelGPTV4o Model = "gpt-4o" ) diff --git a/internal/config/config.go b/internal/config/config.go index b7937ee..7bce3b5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,7 +51,7 @@ type ( } VCSConfig struct { - Provider string `json:"provider"` // github o gitlab lo que se te cante + Provider string `json:"provider"` // github or gitlab or whatever you want Token string `json:"token,omitempty"` Owner string `json:"owner,omitempty"` Repo string `json:"repo,omitempty"` @@ -75,7 +75,7 @@ func LoadConfig(path string) (*Config, error) { if _, err := os.Stat(configDir); os.IsNotExist(err) { if err := os.MkdirAll(configDir, 0755); err != nil { - return nil, fmt.Errorf("error al crear el directorio de configuración: %w", err) + return nil, fmt.Errorf("error creating configuration directory: %w", err) } } } @@ -86,16 +86,16 @@ func LoadConfig(path string) (*Config, error) { data, err := os.ReadFile(configPath) if err != nil { - return nil, fmt.Errorf("error al leer el archivo de configuración: %w", err) + return nil, fmt.Errorf("error reading configuration file: %w", err) } var config Config if err := json.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("error al decodificar el archivo JSON: %w", err) + return nil, fmt.Errorf("error decoding JSON file: %w", err) } if err := validateConfig(&config); err != nil { - return nil, fmt.Errorf("la configuración cargada no es válida: %w", err) + return nil, fmt.Errorf("the loaded configuration is invalid: %w", err) } config.PathFile = configPath @@ -126,16 +126,16 @@ func createDefaultConfig(path string) (*Config, error) { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { - return nil, fmt.Errorf("error al crear el directorio de configuración: %w", err) + return nil, fmt.Errorf("error creating configuration directory: %w", err) } data, err := json.MarshalIndent(config, "", " ") if err != nil { - return nil, fmt.Errorf("error al codificar la configuración por defecto: %w", err) + return nil, fmt.Errorf("error encoding default configuration: %w", err) } if err := os.WriteFile(path, data, 0644); err != nil { - return nil, fmt.Errorf("error al guardar la configuración por defecto: %w", err) + return nil, fmt.Errorf("error saving default configuration: %w", err) } return config, nil @@ -143,20 +143,20 @@ func createDefaultConfig(path string) (*Config, error) { func SaveConfig(config *Config) error { if err := validateConfig(config); err != nil { - return fmt.Errorf("la configuración a guardar no es válida: %w", err) + return fmt.Errorf("the configuration to save is invalid: %w", err) } if config.PathFile == "" { - return errors.New("la ruta del archivo de configuración no está definida") + return errors.New("the configuration file path is not defined") } data, err := json.MarshalIndent(config, "", " ") if err != nil { - return fmt.Errorf("error al codificar la configuración: %w", err) + return fmt.Errorf("error encoding configuration: %w", err) } if err := os.WriteFile(config.PathFile, data, 0644); err != nil { - return fmt.Errorf("error al guardar la configuración: %w", err) + return fmt.Errorf("error saving configuration: %w", err) } return nil @@ -164,17 +164,17 @@ func SaveConfig(config *Config) error { func validateConfig(config *Config) error { if config.Language == "" { - return errors.New("language no puede estar vacío") + return errors.New("language cannot be empty") } if config.ActiveTicketService != "" { if config.TicketProviders != nil { if ticketCfg, exists := config.TicketProviders[config.ActiveTicketService]; exists { if ticketCfg.BaseURL == "" { - return fmt.Errorf("%s base URL no está configurada", config.ActiveTicketService) + return fmt.Errorf("%s base URL is not configured", config.ActiveTicketService) } if ticketCfg.APIKey == "" { - return fmt.Errorf("%s API key no está configurada", config.ActiveTicketService) + return fmt.Errorf("%s API key is not configured", config.ActiveTicketService) } } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e5feac6..fc66910 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -8,7 +8,7 @@ import ( ) func TestLoadConfig(t *testing.T) { - t.Run("debería manejar error al verificar existencia del archivo", func(t *testing.T) { + t.Run("should handle error when checking file existence", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) @@ -19,38 +19,38 @@ func TestLoadConfig(t *testing.T) { defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() _, err := LoadConfig(tmpDir) if err == nil { - t.Error("se esperaba un error al verificar existencia del archivo") + t.Error("expected an error when checking file existence") } err = os.Chmod(configDir, 0755) if err != nil { - t.Fatal("No se pudo cambiar los permisos del directorio") + t.Fatal("Could not change directory permissions") } _, err = LoadConfig(tmpDir) if err != nil { - t.Errorf("No se esperaba un error, pero ocurrió: %v", err) + t.Errorf("Did not expect an error, but one occurred: %v", err) } }) - t.Run("debería manejar configuración inválida", func(t *testing.T) { + t.Run("should handle invalid configuration", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) - // Crear directorio de configuración + // Create configuration directory configDir := filepath.Join(tmpDir, ".mate-commit") err := os.MkdirAll(configDir, 0755) if err != nil { t.Fatal(err) } - // Crear configuración inválida + // Create invalid configuration config := &Config{ Language: "", } @@ -63,36 +63,36 @@ func TestLoadConfig(t *testing.T) { defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() - _, err = LoadConfig(tmpDir) // Cargar desde el directorio base + _, err = LoadConfig(tmpDir) // Load from base directory if err == nil { - t.Error("se esperaba un error debido a configuración inválida") + t.Error("expected an error due to invalid configuration") } }) - t.Run("debería manejar errores al guardar configuración", func(t *testing.T) { + t.Run("should handle errors when saving configuration", func(t *testing.T) { config := &Config{ Language: "", } err := SaveConfig(config) if err == nil { - t.Error("se esperaba un error al guardar configuración inválida, pero no ocurrió") + t.Error("expected an error when saving invalid configuration, but none occurred") } }) - t.Run("debería manejar JSON malformado", func(t *testing.T) { + t.Run("should handle malformed JSON", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) - // Crear directorio de configuración + // Create configuration directory configDir := filepath.Join(tmpDir, ".mate-commit") _ = os.MkdirAll(configDir, 0755) - // Crear archivo con JSON malformado + // Create file with malformed JSON err := os.WriteFile(filepath.Join(configDir, "config.json"), []byte("{malformed json"), 0644) if err != nil { t.Fatal(err) @@ -100,27 +100,27 @@ func TestLoadConfig(t *testing.T) { defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() - _, err = LoadConfig(tmpDir) // Cargar desde el directorio base + _, err = LoadConfig(tmpDir) // Load from base directory if err == nil { - t.Error("se esperaba un error al cargar JSON malformado") + t.Error("expected an error when loading malformed JSON") } }) } func TestSaveConfig(t *testing.T) { - t.Run("debería crear config.json en directorio .mate-commit si no existe", func(t *testing.T) { + t.Run("should create config.json in .mate-commit directory if it doesn't exist", func(t *testing.T) { tmpDir := t.TempDir() expectedConfigDir := filepath.Join(tmpDir, ".mate-commit") expectedConfigPath := filepath.Join(expectedConfigDir, "config.json") defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() @@ -133,11 +133,11 @@ func TestSaveConfig(t *testing.T) { } if _, err := os.Stat(expectedConfigDir); os.IsNotExist(err) { - t.Error("El directorio .mate-commit no fue creado") + t.Error(".mate-commit directory was not created") } if _, err := os.Stat(expectedConfigPath); os.IsNotExist(err) { - t.Error("El archivo config.json no fue creado") + t.Error("config.json file was not created") } if loadedConfig.Language != defaultLang { @@ -149,13 +149,13 @@ func TestSaveConfig(t *testing.T) { } }) - t.Run("debería manejar path directo a archivo json", func(t *testing.T) { + t.Run("should handle direct path to json file", func(t *testing.T) { // Arrange tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "direct-config.json") initialConfig := &Config{ - Language: "es", + Language: "en", UseEmoji: true, PathFile: configPath, AIProviders: map[string]AIProviderConfig{ @@ -170,7 +170,7 @@ func TestSaveConfig(t *testing.T) { defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() @@ -188,7 +188,7 @@ func TestSaveConfig(t *testing.T) { // Assert if err != nil { - t.Errorf("LoadConfig() con path directo error = %v", err) + t.Errorf("LoadConfig() with direct path error = %v", err) } if providerCfg, exists := loadedConfig.AIProviders["gemini"]; !exists || providerCfg.APIKey != "test-key" { @@ -199,7 +199,7 @@ func TestSaveConfig(t *testing.T) { t.Errorf("PathFile = %v, want %v", loadedConfig.PathFile, configPath) } }) - t.Run("debería manejar error al obtener home directory", func(t *testing.T) { + t.Run("should handle error when getting home directory", func(t *testing.T) { t.Setenv("HOME", "") config := &Config{ @@ -208,11 +208,11 @@ func TestSaveConfig(t *testing.T) { err := SaveConfig(config) if err == nil { - t.Error("se esperaba un error al no poder obtener el home directory") + t.Error("expected an error when home directory could not be obtained") } }) - t.Run("debería manejar error al escribir archivo", func(t *testing.T) { + t.Run("should handle error when writing file", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) @@ -223,7 +223,7 @@ func TestSaveConfig(t *testing.T) { defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() @@ -233,22 +233,22 @@ func TestSaveConfig(t *testing.T) { err := SaveConfig(config) if err == nil { - t.Error("se esperaba un error al no poder escribir el archivo") + t.Error("expected an error when file could not be written") } }) - t.Run("debería validar la configuración antes de guardar", func(t *testing.T) { + t.Run("should validate configuration before saving", func(t *testing.T) { config := &Config{ Language: "", } err := SaveConfig(config) if err == nil { - t.Error("se esperaba un error al guardar configuración inválida") + t.Error("expected an error when saving invalid configuration") } }) - t.Run("debería guardar la configuración correctamente", func(t *testing.T) { + t.Run("should save configuration correctly", func(t *testing.T) { // Arrange tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) @@ -261,7 +261,7 @@ func TestSaveConfig(t *testing.T) { defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() @@ -312,7 +312,7 @@ func TestSaveConfig(t *testing.T) { } func TestCreateDefaultConfig(t *testing.T) { - t.Run("debería crear configuración por defecto correctamente", func(t *testing.T) { + t.Run("should create default configuration correctly", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.json") @@ -320,7 +320,7 @@ func TestCreateDefaultConfig(t *testing.T) { defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() @@ -336,24 +336,24 @@ func TestCreateDefaultConfig(t *testing.T) { } }) - t.Run("debería manejar error al crear el directorio", func(t *testing.T) { + t.Run("should handle error when creating directory", func(t *testing.T) { invalidDir := filepath.Join(string([]byte{0}), "config.json") _, err := createDefaultConfig(invalidDir) if err == nil { - t.Error("se esperaba un error al crear el directorio, pero no ocurrió") + t.Error("expected an error when creating the directory, but none occurred") } }) - t.Run("debería manejar error al escribir archivo", func(t *testing.T) { + t.Run("should handle error when writing file", func(t *testing.T) { tmpDir := t.TempDir() if err := os.Chmod(tmpDir, 0000); err != nil { t.Fatal(err) } defer func() { if err := os.Chmod(tmpDir, 0755); err != nil { - t.Fatal("No se pudo cambiar los permisos del directorio") + t.Fatal("Could not change directory permissions") } }() @@ -361,11 +361,11 @@ func TestCreateDefaultConfig(t *testing.T) { _, err := createDefaultConfig(configPath) if err == nil { - t.Error("se esperaba un error al escribir el archivo") + t.Error("expected an error when writing file") } }) - t.Run("debería manejar error al escribir archivo", func(t *testing.T) { + t.Run("should handle error when writing file", func(t *testing.T) { tmpDir := t.TempDir() configDir := filepath.Join(tmpDir, "readonly") @@ -379,13 +379,13 @@ func TestCreateDefaultConfig(t *testing.T) { defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("Error al eliminar el archivo: %v", err) + t.Errorf("Error deleting file: %v", err) } }() configPath := filepath.Join(configDir, "config.json") config := &Config{ - Language: "es", + Language: "en", PathFile: configPath, } @@ -394,11 +394,11 @@ func TestCreateDefaultConfig(t *testing.T) { // Assert if err == nil { - t.Error("se esperaba un error al escribir en directorio sin permisos") + t.Error("expected an error when writing in directory without permissions") } if err := os.Chmod(configDir, 0755); err != nil { - t.Fatal("No se pudo restaurar los permisos del directorio") + t.Fatal("Could not restore directory permissions") } }) } @@ -410,7 +410,7 @@ func TestValidateConfig(t *testing.T) { wantErr bool }{ { - name: "configuración válida", + name: "valid configuration", config: &Config{ Language: "en", }, @@ -418,14 +418,14 @@ func TestValidateConfig(t *testing.T) { }, { - name: "DefaultLang vacío", + name: "empty language", config: &Config{ Language: "", }, wantErr: true, }, { - name: "múltiples campos inválidos", + name: "multiple invalid fields", config: &Config{ Language: "", }, diff --git a/internal/domain/errors/errors.go b/internal/domain/errors/errors.go index 2f6c21b..3fd67cc 100644 --- a/internal/domain/errors/errors.go +++ b/internal/domain/errors/errors.go @@ -2,106 +2,102 @@ package errors import "fmt" -// ConfigError representa un error de configuración -type ConfigError struct { - Field string +// ErrorType defines the category of the error +type ErrorType string + +const ( + TypeConfiguration ErrorType = "CONFIGURATION" + TypeAI ErrorType = "AI" + TypeVCS ErrorType = "VCS" + TypeGit ErrorType = "GIT" + TypeInternal ErrorType = "INTERNAL" + TypeUpdate ErrorType = "UPDATE" +) + +// AppError represents a domain-level error with a type and an underlying error +type AppError struct { + Type ErrorType Message string + Context map[string]interface{} Err error } -func (e *ConfigError) Error() string { +func (e *AppError) Error() string { if e.Err != nil { - return fmt.Sprintf("config error [%s]: %s: %v", e.Field, e.Message, e.Err) + return fmt.Sprintf("%s: %s (%v)", e.Type, e.Message, e.Err) } - return fmt.Sprintf("config error [%s]: %s", e.Field, e.Message) + return fmt.Sprintf("%s: %s", e.Type, e.Message) } -func (e *ConfigError) Unwrap() error { +func (e *AppError) Unwrap() error { return e.Err } -// NewConfigError crea un nuevo error de configuración -func NewConfigError(field, message string, err error) *ConfigError { - return &ConfigError{ - Field: field, - Message: message, +// NewAppError creates a new AppError +func NewAppError(t ErrorType, msg string, err error) *AppError { + return &AppError{ + Type: t, + Message: msg, Err: err, } } -// AIProviderNotFoundError indica que un proveedor de IA no fue encontrado -type AIProviderNotFoundError struct { - Provider string -} - -func (e *AIProviderNotFoundError) Error() string { - return fmt.Sprintf("Proveedor de IA '%s' no encontrado en el registro", e.Provider) -} - -// NewAIProviderNotFoundError crea un nuevo error de proveedor no encontrado -func NewAIProviderNotFoundError(provider string) *AIProviderNotFoundError { - return &AIProviderNotFoundError{Provider: provider} -} - -// AIProviderNotConfiguredError indica que un proveedor de IA no está configurado -type AIProviderNotConfiguredError struct { - Provider string - Reason string -} - -func (e *AIProviderNotConfiguredError) Error() string { - if e.Reason != "" { - return fmt.Sprintf("Proveedor IA '%s' no configurado: %s", e.Provider, e.Reason) - } - return fmt.Sprintf("Proveedor IA '%s' no configurado: %s", e.Provider, e.Reason) -} - -// NewAIProviderNotConfiguredError crea un nuevo error de proveedor no configurado -func NewAIProviderNotConfiguredError(provider, reason string) *AIProviderNotConfiguredError { - return &AIProviderNotConfiguredError{ - Provider: provider, - Reason: reason, - } -} - -// VCSConfigNotFoundError indica que la configuración de VCS no fue encontrada -type VCSConfigNotFoundError struct { - Provider string -} - -func (e *VCSConfigNotFoundError) Error() string { - return fmt.Sprintf("Configuracion VCS para proveedor '%s' no encontrado", e.Provider) -} - -// NewVCSConfigNotFoundError crea un nuevo error de config VCS no encontrada -func NewVCSConfigNotFoundError(provider string) *VCSConfigNotFoundError { - return &VCSConfigNotFoundError{Provider: provider} -} - -// VCSProviderNotConfiguredError indica que un proveedor VCS no está configurado -type VCSProviderNotConfiguredError struct { - Provider string -} - -func (e *VCSProviderNotConfiguredError) Error() string { - return fmt.Sprintf("Proveedor VCS '%s' detectado pero no configurado", e.Provider) -} - -// NewVCSProviderNotConfiguredError crea un nuevo error de proveedor VCS no configurado -func NewVCSProviderNotConfiguredError(provider string) *VCSProviderNotConfiguredError { - return &VCSProviderNotConfiguredError{Provider: provider} -} - -// VCSProviderNotSupportedError indica que un proveedor VCS no es soportado -type VCSProviderNotSupportedError struct { - Provider string -} - -func (e *VCSProviderNotSupportedError) Error() string { - return fmt.Sprintf("Proveedor VCS '%s' no es soportado", e.Provider) -} - -// NewVCSProviderNotSupportedError crea un nuevo error de proveedor no soportado -func NewVCSProviderNotSupportedError(provider string) *VCSProviderNotSupportedError { - return &VCSProviderNotSupportedError{Provider: provider} -} +// Git errors +var ( + ErrNoChanges = NewAppError(TypeGit, "no staged changes detected", nil) + ErrGetBranch = NewAppError(TypeGit, "failed to get current branch", nil) + ErrNoBranch = NewAppError(TypeGit, "no branch detected", nil) + ErrGetRepoRoot = NewAppError(TypeGit, "failed to get repository root", nil) + ErrGetRepoURL = NewAppError(TypeGit, "failed to get repository URL", nil) + ErrGetCommits = NewAppError(TypeGit, "failed to get commits", nil) + ErrAddFile = NewAppError(TypeGit, "failed to add file to staging", nil) + ErrExtractRepoInfo = NewAppError(TypeGit, "failed to extract repository info", nil) + ErrCreateTag = NewAppError(TypeGit, "failed to create tag", nil) + ErrPushTag = NewAppError(TypeGit, "failed to push tag", nil) + ErrCreateCommit = NewAppError(TypeGit, "failed to create commit", nil) + ErrGetDiff = NewAppError(TypeGit, "failed to get diff", nil) + ErrNoDiff = NewAppError(TypeGit, "no differences detected", nil) +) + +// Configuration errors +var ( + ErrAPIKeyMissing = NewAppError(TypeConfiguration, "AI API key is missing", nil) + ErrTokenMissing = NewAppError(TypeConfiguration, "VCS token is missing", nil) + ErrConfigMissing = NewAppError(TypeConfiguration, "configuration is missing", nil) +) + +// VCS errors +var ( + ErrRepositoryNotFound = NewAppError(TypeVCS, "repository not found", nil) + ErrVCSNotSupported = NewAppError(TypeVCS, "VCS provider not supported", nil) + ErrCreateRelease = NewAppError(TypeVCS, "failed to create release", nil) + ErrUpdateRelease = NewAppError(TypeVCS, "failed to update release", nil) + ErrGetRelease = NewAppError(TypeVCS, "failed to get release", nil) + ErrUploadAsset = NewAppError(TypeVCS, "failed to upload release asset", nil) +) + +// AI errors +var ( + ErrQuotaExceeded = NewAppError(TypeAI, "AI quota exceeded or rate limited", nil) + ErrAIGeneration = NewAppError(TypeAI, "AI generation failed", nil) + ErrInvalidAIOutput = NewAppError(TypeAI, "invalid AI output format", nil) +) + +// Internal errors +var ( + ErrNetwork = NewAppError(TypeInternal, "network error occurred", nil) + ErrBuild = NewAppError(TypeInternal, "build operation failed", nil) +) + +// Update errors +var ( + ErrUpdateFailed = NewAppError(TypeUpdate, "failed to update application", nil) +) + +var ( + ErrBuildNoVersion = NewAppError(TypeInternal, "build version not specified", nil) + ErrBuildNoCommit = NewAppError(TypeInternal, "build commit not specified", nil) + ErrBuildNoBuildDir = NewAppError(TypeInternal, "build directory not specified", nil) + ErrBuildNoDate = NewAppError(TypeInternal, "build date not specified", nil) + ErrBuildFailed = NewAppError(TypeInternal, "build operation failed", nil) +) diff --git a/internal/domain/errors/errors_test.go b/internal/domain/errors/errors_test.go deleted file mode 100644 index a4f2578..0000000 --- a/internal/domain/errors/errors_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package errors - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestConfigError(t *testing.T) { - t.Run("with wrapped error", func(t *testing.T) { - innerErr := errors.New("inner error") - err := NewConfigError("api_key", "invalid key", innerErr) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "api_key") - assert.Contains(t, err.Error(), "invalid key") - assert.Contains(t, err.Error(), "inner error") - - assert.Equal(t, innerErr, errors.Unwrap(err)) - }) - - t.Run("without wrapped error", func(t *testing.T) { - err := NewConfigError("api_key", "missing key", nil) - - assert.Error(t, err) - assert.Contains(t, err.Error(), "api_key") - assert.Contains(t, err.Error(), "missing key") - assert.Nil(t, errors.Unwrap(err)) - }) -} - -func TestAIProviderNotFoundError(t *testing.T) { - err := NewAIProviderNotFoundError("openai") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "openai") - assert.Contains(t, err.Error(), "no encontrado") - - var aiErr *AIProviderNotFoundError - assert.True(t, errors.As(err, &aiErr)) - assert.Equal(t, "openai", aiErr.Provider) -} - -func TestAIProviderNotConfiguredError(t *testing.T) { - t.Run("with reason", func(t *testing.T) { - err := NewAIProviderNotConfiguredError("gemini", "API key missing") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "no configurado") - assert.Contains(t, err.Error(), "API key missing") - }) - - t.Run("without reason", func(t *testing.T) { - err := NewAIProviderNotConfiguredError("gemini", "") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "gemini") - assert.Contains(t, err.Error(), "no configurado") - }) -} - -func TestVCSConfigNotFoundError(t *testing.T) { - err := NewVCSConfigNotFoundError("github") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "github") - assert.Contains(t, err.Error(), "no encontrado") - - var vcsErr *VCSConfigNotFoundError - assert.True(t, errors.As(err, &vcsErr)) - assert.Equal(t, "github", vcsErr.Provider) -} - -func TestVCSProviderNotConfiguredError(t *testing.T) { - err := NewVCSProviderNotConfiguredError("gitlab") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "gitlab") - assert.Contains(t, err.Error(), "detectado pero no configurado") -} - -func TestVCSProviderNotSupportedError(t *testing.T) { - err := NewVCSProviderNotSupportedError("bitbucket") - - assert.Error(t, err) - assert.Contains(t, err.Error(), "bitbucket") - assert.Contains(t, err.Error(), "no es soportado") -} - -func TestErrorTypeAssertions(t *testing.T) { - aiNotFound := NewAIProviderNotFoundError("test") - aiNotConfigured := NewAIProviderNotConfiguredError("test", "reason") - vcsNotFound := NewVCSConfigNotFoundError("test") - - // AIProviderNotFoundError - var aiNotFoundErr *AIProviderNotFoundError - assert.True(t, errors.As(aiNotFound, &aiNotFoundErr)) - assert.False(t, errors.As(aiNotConfigured, &aiNotFoundErr)) - - // AIProviderNotConfiguredError - var aiNotConfiguredErr *AIProviderNotConfiguredError - assert.True(t, errors.As(aiNotConfigured, &aiNotConfiguredErr)) - assert.False(t, errors.As(aiNotFound, &aiNotConfiguredErr)) - - // VCSConfigNotFoundError - var vcsNotFoundErr *VCSConfigNotFoundError - assert.True(t, errors.As(vcsNotFound, &vcsNotFoundErr)) - assert.False(t, errors.As(aiNotFound, &vcsNotFoundErr)) -} diff --git a/internal/domain/models/issue_generation.go b/internal/domain/models/issue_generation.go index 1cbf280..053cb42 100644 --- a/internal/domain/models/issue_generation.go +++ b/internal/domain/models/issue_generation.go @@ -1,59 +1,59 @@ package models -// IssueGenerationRequest contiene la información necesaria para generar una issue. -// Soporta múltiples fuentes de contexto: descripción manual, diff de git, o ambos. +// IssueGenerationRequest contains the information needed to generate an issue. +// Supports multiple context sources: manual description, git diff, or both. type IssueGenerationRequest struct { - // Description es la descripción manual proporcionada por el usuario (opcional) + // Description is the manual description provided by the user (optional) Description string - // Diff contiene los cambios locales de git (opcional) + // Diff contains local git changes (optional) Diff string - // ChangedFiles es la lista de archivos modificados (opcional) + // ChangedFiles is the list of modified files (optional) ChangedFiles []string - // Hint es contexto adicional proporcionado por el usuario para guiar la generación (opcional) + // Hint is additional context provided by the user to guide generation (optional) Hint string - // Language es el idioma para la generación de contenido (ej: "es", "en") + // Language is the language for content generation (e.g.: "es", "en") Language string } -// IssueGenerationResult contiene el resultado de la generación de contenido de una issue. +// IssueGenerationResult contains the result of an issue's content generation. type IssueGenerationResult struct { - // Title es el título generado para la issue + // Title is the generated title for the issue Title string - // Description es la descripción completa generada para la issue + // Description is the full generated description for the issue Description string - // Labels son las etiquetas sugeridas para la issue + // Labels are the suggested labels for the issue Labels []string - // Assignees son los responsables sugeridos para la issue + // Assignees are the suggested assignees for the issue Assignees []string - // Usage contiene los metadatos de uso de tokens de la IA + // Usage contains metadata on token usage by the AI Usage *TokenUsage } -// DiffAnalysis contiene el análisis estructurado del diff para inferencia de labels. +// DiffAnalysis contains the structured analysis of the diff for label inference. type DiffAnalysis struct { - // HasGoFiles indica si el diff incluye archivos .go + // HasGoFiles indicates if the diff includes .go files HasGoFiles bool - // HasTestFiles indica si el diff incluye archivos de test + // HasTestFiles indicates if the diff includes test files HasTestFiles bool - // HasDocFiles indica si el diff incluye archivos de documentación + // HasDocFiles indicates if the diff includes documentation files HasDocFiles bool - // HasConfigFiles indica si el diff incluye archivos de configuración + // HasConfigFiles indicates if the diff includes configuration files HasConfigFiles bool - // HasUIFiles indica si el diff incluye archivos de UI (CSS, HTML, JSX, etc) + // HasUIFiles indicates if the diff includes UI files (CSS, HTML, JSX, etc) HasUIFiles bool - // Keywords contiene palabras clave encontradas en el diff (fix, feat, refactor, etc) + // Keywords contains keywords found in the diff (fix, feat, refactor, etc) Keywords map[string]bool } diff --git a/internal/domain/models/issue_template.go b/internal/domain/models/issue_template.go index 92cee07..fd93277 100644 --- a/internal/domain/models/issue_template.go +++ b/internal/domain/models/issue_template.go @@ -1,8 +1,8 @@ package models -// IssueTemplate representa un template de issue con su metadata. +// IssueTemplate represents an issue template with its metadata. type IssueTemplate struct { - // Metadata del frontmatter YAML + // YAML frontmatter metadata Name string `yaml:"name"` About string `yaml:"about,omitempty"` Description string `yaml:"description,omitempty"` @@ -10,17 +10,17 @@ type IssueTemplate struct { Labels []string `yaml:"labels"` Assignees []string `yaml:"assignees,omitempty"` - // Contenido del template - // Para .md: string con markdown - // Para .yml (GitHub Issue Forms): array de campos del formulario + // Template content + // For .md: Markdown string + // For .yml (GitHub Issue Forms): array of form fields Body interface{} `yaml:"body,omitempty"` - BodyContent string `yaml:"-"` // Para retrocompatibilidad con .md + BodyContent string `yaml:"-"` // For backward compatibility with .md - // Path al archivo del template + // Path to the template file FilePath string `yaml:"-"` } -// GetAbout retorna la descripción del template (usa 'description' o 'about') +// GetAbout returns the template description (uses 'description' or 'about') func (t *IssueTemplate) GetAbout() string { if t.Description != "" { return t.Description diff --git a/internal/domain/models/pr.go b/internal/domain/models/pr.go index 4b06289..9121170 100644 --- a/internal/domain/models/pr.go +++ b/internal/domain/models/pr.go @@ -1,7 +1,24 @@ package models +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 ProgressEvent struct { + Type ProgressEventType + Message string + Data map[string]interface{} +} + type ( - // PRData contiene la información extraída de una Pull Request. + // PRData contains information extracted from a Pull Request. PRData struct { ID int Title string @@ -13,12 +30,12 @@ type ( Description string } - // Commit representa un commit incluido en el PR. + // Commit represents a commit included in the PR. Commit struct { Message string } - // PRSummary es el resumen generado para el PR, con título, cuerpo y etiquetas. + // PRSummary is the generated summary for the PR, with title, body, and labels. PRSummary struct { Title string Body string diff --git a/internal/domain/models/release.go b/internal/domain/models/release.go index 94ee673..fd21546 100644 --- a/internal/domain/models/release.go +++ b/internal/domain/models/release.go @@ -3,7 +3,7 @@ package models import "time" type ( - // Release representa un release con toda la info + // Release represents a release with all information Release struct { Version string PreviousVersion string @@ -35,15 +35,15 @@ type ( URL string } - // DependencyChange representa un cambio en una dependencia del proyecto + // DependencyChange represents a change in a project dependency DependencyChange struct { - Name string // Nombre del paquete (ej: "github.com/user/repo", "react") - OldVersion string // Versión anterior (vacío si Type == DependencyAdded) - NewVersion string // Versión nueva (vacío si Type == DependencyRemoved) - Type DependencyChangeType // Tipo de cambio - Manager string // Gestor: "go.mod", "package.json", "Cargo.toml", etc. - Severity ChangeSeverity // Severidad: major/minor/patch para detectar breaking changes - IsDirect bool // true = dependencia directa, false = dev/indirect + Name string // Package name (e.g.: "github.com/user/repo", "react") + OldVersion string // Previous version (empty if Type == DependencyAdded) + NewVersion string // New version (empty if Type == DependencyRemoved) + Type DependencyChangeType // Change type + Manager string // Manager: "go.mod", "package.json", "Cargo.toml", etc. + Severity ChangeSeverity // Severity: major/minor/patch to detect breaking changes + IsDirect bool // true = direct dependency, false = dev/indirect } DependencyChangeType string @@ -63,20 +63,20 @@ type ( Deletions int } - // ReleaseItem representa un item en el changelog + // ReleaseItem represents an item in the changelog ReleaseItem struct { Type string // feat, fix, docs, etc Scope string Description string Breaking bool CommitHash string - PRNumber string // si tiene pr asociado + PRNumber string // if it has an associated PR } - // VersionBump indica el tipo de bump de version + // VersionBump indicates the version bump type VersionBump string - // ReleaseNotes es el resultado generado por la ia + // ReleaseNotes is the result generated by AI ReleaseNotes struct { Title string Summary string @@ -91,19 +91,19 @@ type ( Usage *TokenUsage } - // CodeExample representa un ejemplo de código con descripción + // CodeExample represents a code example with description CodeExample struct { - Title string // Título del ejemplo - Description string // Descripción breve - Code string // Código del ejemplo - Language string // Lenguaje (bash, go, etc.) + Title string // Example title + Description string // Short description + Code string // Example code + Language string // Language (bash, go, etc.) } - // Comparison representa una comparación antes/después + // Comparison represents a before/after comparison Comparison struct { - Feature string // Nombre de la feature - Before string // Estado anterior - After string // Estado nuevo + Feature string // Feature name + Before string // Previous state + After string // New state } VCSRelease struct { diff --git a/internal/domain/ports/ai_provider.go b/internal/domain/ports/ai_provider.go index e39cf19..69c89cf 100644 --- a/internal/domain/ports/ai_provider.go +++ b/internal/domain/ports/ai_provider.go @@ -4,20 +4,20 @@ import ( "context" ) -// CostAwareAIProvider define la interfaz para proveedores de IA que soportan tracking de costos. +// CostAwareAIProvider defines the interface for AI providers that support cost tracking. type CostAwareAIProvider interface { - // CountTokens cuenta los tokens de un prompt sin hacer la llamada real al modelo. - // Esto permite estimar el costo antes de ejecutar la generación. + // CountTokens counts the tokens of a prompt without making the actual model call. + // This allows estimating the cost before executing the generation. CountTokens(ctx context.Context, prompt string) (int, error) - // GetModelName retorna el nombre del modelo actual (ej: "gemini-2.5-flash") + // GetModelName returns the name of the current model (e.g.: "gemini-2.5-flash") GetModelName() string - // GetProviderName retorna el nombre del proveedor (ej: "gemini", "openai", "anthropic") + // GetProviderName returns the name of the provider (e.g.: "gemini", "openai", "anthropic") GetProviderName() string } -// TokenCounter es una interfaz más simple para proveedores que solo necesitan contar tokens. +// TokenCounter is a simpler interface for providers that only need to count tokens. type TokenCounter interface { CountTokens(ctx context.Context, content string) (int, error) } diff --git a/internal/domain/ports/ai_service.go b/internal/domain/ports/ai_service.go index 310db34..a534f90 100644 --- a/internal/domain/ports/ai_service.go +++ b/internal/domain/ports/ai_service.go @@ -6,26 +6,26 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/domain/models" ) -// CommitSummarizer es una interfaz que define el servicio para generar sugerencias de commits. +// CommitSummarizer is an interface that defines the service to generate commit suggestions. type CommitSummarizer interface { - //GenerateSuggestions genera una lista de sugerencias de mensajes de commit. + // GenerateSuggestions generates a list of commit message suggestions. GenerateSuggestions(ctx context.Context, info models.CommitInfo, count int) ([]models.CommitSuggestion, error) } -// PRSummarizer define la interfaz para los servicios que resumen Pull Requests. +// PRSummarizer defines the interface for services that summarize Pull Requests. type PRSummarizer interface { - // GeneratePRSummary genera un resumen de un Pull Request dado un prompt. + // GeneratePRSummary generates a summary of a Pull Request given a prompt. GeneratePRSummary(ctx context.Context, prompt string) (models.PRSummary, error) } -// ReleaseNotesGenerator define la interfaz para generar notas de release. +// ReleaseNotesGenerator defines the interface to generate release notes. type ReleaseNotesGenerator interface { GenerateNotes(ctx context.Context, release *models.Release) (*models.ReleaseNotes, error) } -// IssueContentGenerator define la interfaz para generar contenido de issues con IA. +// IssueContentGenerator defines the interface to generate issue content with AI. type IssueContentGenerator interface { - // GenerateIssueContent genera el título, descripción y labels de una issue - // basándose en el contexto proporcionado en el request. + // GenerateIssueContent generates the title, description, and labels of an issue + // based on the context provided in the request. GenerateIssueContent(ctx context.Context, request models.IssueGenerationRequest) (*models.IssueGenerationResult, error) } diff --git a/internal/domain/ports/binary_builder.go b/internal/domain/ports/binary_builder.go deleted file mode 100644 index 4d8bf6c..0000000 --- a/internal/domain/ports/binary_builder.go +++ /dev/null @@ -1,17 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Tomas-vilte/MateCommit/internal/i18n" -) - -// BinaryPackager define la interfaz para construir y empaquetar binarios -type BinaryPackager interface { - BuildAndPackageAll(ctx context.Context) ([]string, error) -} - -// BinaryBuilderFactory define la interfaz para crear nuevas instancias de BinaryPackager. -type BinaryBuilderFactory interface { - NewBuilder(mainPath, binaryName, version, commit, date, buildDir string, trans *i18n.Translations) BinaryPackager -} diff --git a/internal/domain/ports/commit_handler.go b/internal/domain/ports/commit_handler.go deleted file mode 100644 index eec0ca4..0000000 --- a/internal/domain/ports/commit_handler.go +++ /dev/null @@ -1,11 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Tomas-vilte/MateCommit/internal/domain/models" -) - -type CommitHandler interface { - HandleSuggestions(ctx context.Context, suggestions []models.CommitSuggestion) error -} diff --git a/internal/domain/ports/commit_service.go b/internal/domain/ports/commit_service.go deleted file mode 100644 index 0398027..0000000 --- a/internal/domain/ports/commit_service.go +++ /dev/null @@ -1,15 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Tomas-vilte/MateCommit/internal/domain/models" -) - -type CommitService interface { - // GenerateSuggestions genera sugerencias de commit basadas en los cambios detectados - // GenerateSuggestions genera sugerencias de commit basadas en los cambios detectados - GenerateSuggestions(ctx context.Context, count int, progress func(string)) ([]models.CommitSuggestion, error) - // GenerateSuggestionsWithIssue genera sugerencias considerando un issue específico - GenerateSuggestionsWithIssue(ctx context.Context, count int, issueNumber int, progress func(string)) ([]models.CommitSuggestion, error) -} diff --git a/internal/domain/ports/dependency_analyzer.go b/internal/domain/ports/dependency_analyzer.go index b2ce2ee..a2e01a3 100644 --- a/internal/domain/ports/dependency_analyzer.go +++ b/internal/domain/ports/dependency_analyzer.go @@ -6,14 +6,14 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/domain/models" ) -// DependencyAnalyzer define la interfaz para analizar dependencias de diferentes lenguajes +// DependencyAnalyzer defines the interface to analyze dependencies for different languages type DependencyAnalyzer interface { - // CanHandle detecta si este analyzer puede manejar el proyecto + // CanHandle detects if this analyzer can handle the project CanHandle(ctx context.Context, vcsClient VCSClient, previousTag, currentTag string) bool - // AnalyzeChanges analiza cambios de dependencias entre dos versiones + // AnalyzeChanges analyzes dependency changes between two versions AnalyzeChanges(ctx context.Context, vcsClient VCSClient, previousTag, currentTag string) ([]models.DependencyChange, error) - // Name retorna el nombre del gestor de dependencias + // Name returns the name of the dependency manager Name() string } diff --git a/internal/domain/ports/git_service.go b/internal/domain/ports/git_service.go deleted file mode 100644 index f41e878..0000000 --- a/internal/domain/ports/git_service.go +++ /dev/null @@ -1,42 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Tomas-vilte/MateCommit/internal/domain/models" -) - -// GitService define los métodos para interactuar con el sistema de control de versiones Git -type GitService interface { - - // GetChangedFiles Obtiene los archivos modificados - GetChangedFiles(ctx context.Context) ([]models.GitChange, error) - - // GetDiff Obtiene el diff completo - GetDiff(ctx context.Context) (string, error) - - // HasStagedChanges Verifica si hay cambios en staging - HasStagedChanges(ctx context.Context) bool - - // CreateCommit crea los commits - CreateCommit(ctx context.Context, message string) error - - // AddFileToStaging agrega un archivo al área de staging - AddFileToStaging(ctx context.Context, file string) error - - // GetCurrentBranch obtiene el nombre de la branch actual - GetCurrentBranch(ctx context.Context) (string, error) - - // GetRepoInfo obtiene la informacion del repo - GetRepoInfo(ctx context.Context) (string, string, string, error) - - GetLastTag(ctx context.Context) (string, error) - GetCommitCount(ctx context.Context) (int, error) - GetCommitsSinceTag(ctx context.Context, tag string) ([]models.Commit, error) - GetCommitsBetweenTags(ctx context.Context, fromTag, toTag string) ([]models.Commit, error) - GetTagDate(ctx context.Context, tag string) (string, error) - GetRecentCommitMessages(ctx context.Context, count int) (string, error) - CreateTag(ctx context.Context, version, message string) error - PushTag(ctx context.Context, version string) error - Push(ctx context.Context) error -} diff --git a/internal/domain/ports/issue_generator_service.go b/internal/domain/ports/issue_generator_service.go deleted file mode 100644 index 2ad81a5..0000000 --- a/internal/domain/ports/issue_generator_service.go +++ /dev/null @@ -1,34 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Tomas-vilte/MateCommit/internal/domain/models" -) - -// IssueGeneratorService define la interfaz para el servicio de generación de issues -type IssueGeneratorService interface { - // GenerateFromDiff genera contenido para una issue a partir del diff actual - GenerateFromDiff(ctx context.Context, hint string, skipLabels bool) (*models.IssueGenerationResult, error) - - // GenerateFromDescription genera contenido para una issue a partir de una descripción manual - GenerateFromDescription(ctx context.Context, description string, skipLabels bool) (*models.IssueGenerationResult, error) - - // GenerateFromPR genera contenido para una issue a partir de un Pull Request existente - GenerateFromPR(ctx context.Context, prNumber int, hint string, skipLabels bool) (*models.IssueGenerationResult, error) - - // GenerateWithTemplate genera contenido usando un template específico - GenerateWithTemplate(ctx context.Context, templateName string, hint string, fromDiff bool, description string, skipLabels bool) (*models.IssueGenerationResult, error) - - // CreateIssue crea la issue en el sistema VCS - CreateIssue(ctx context.Context, result *models.IssueGenerationResult, assignees []string) (*models.Issue, error) - - // GetAuthenticatedUser obtiene el usuario autenticado actual - GetAuthenticatedUser(ctx context.Context) (string, error) - - // InferBranchName infiere un nombre de rama basado en el número de issue y las etiquetas - InferBranchName(issueNumber int, labels []string) string - - // LinkIssueToPR vincula una issue a un Pull Request - LinkIssueToPR(ctx context.Context, prNumber int, issueNumber int) error -} diff --git a/internal/domain/ports/issue_template_service.go b/internal/domain/ports/issue_template_service.go deleted file mode 100644 index d407a3a..0000000 --- a/internal/domain/ports/issue_template_service.go +++ /dev/null @@ -1,26 +0,0 @@ -package ports - -import ( - "github.com/Tomas-vilte/MateCommit/internal/domain/models" -) - -// IssueTemplateService define la interfaz para el servicio de gestión de templates de issues -type IssueTemplateService interface { - // GetTemplatesDir obtiene el directorio donde se almacenan los templates - GetTemplatesDir() (string, error) - - // ListTemplates lista todos los templates disponibles - ListTemplates() ([]models.TemplateMetadata, error) - - // LoadTemplate carga un template desde un archivo - LoadTemplate(filePath string) (*models.IssueTemplate, error) - - // GetTemplateByName obtiene un template por su nombre - GetTemplateByName(name string) (*models.IssueTemplate, error) - - // InitializeTemplates inicializa los templates predefinidos - InitializeTemplates(force bool) error - - // MergeWithGeneratedContent combina un template con contenido generado por IA - MergeWithGeneratedContent(template *models.IssueTemplate, generated *models.IssueGenerationResult) *models.IssueGenerationResult -} diff --git a/internal/domain/ports/pr_service.go b/internal/domain/ports/pr_service.go deleted file mode 100644 index d20f610..0000000 --- a/internal/domain/ports/pr_service.go +++ /dev/null @@ -1,12 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Tomas-vilte/MateCommit/internal/domain/models" -) - -// PRService define la interfaz para el servicio de resumen de Pull Requests. -type PRService interface { - SummarizePR(ctx context.Context, prNumber int, progress func(string)) (models.PRSummary, error) -} diff --git a/internal/domain/ports/release_service.go b/internal/domain/ports/release_service.go deleted file mode 100644 index 8f385a4..0000000 --- a/internal/domain/ports/release_service.go +++ /dev/null @@ -1,38 +0,0 @@ -package ports - -import ( - "context" - - "github.com/Tomas-vilte/MateCommit/internal/domain/models" -) - -type ReleaseService interface { - // AnalyzeNextRelease analiza commits y determina la siguiente versión - AnalyzeNextRelease(ctx context.Context) (*models.Release, error) - - // GenerateReleaseNotes genera las notas del release con IA - GenerateReleaseNotes(ctx context.Context, release *models.Release) (*models.ReleaseNotes, error) - - // PublishRelease crea el release en el VCS (GitHub, GitLab, etc.) - // buildBinaries indica si se deben compilar y subir binarios al release - PublishRelease(ctx context.Context, release *models.Release, notes *models.ReleaseNotes, draft bool, buildBinaries bool) error - - // CreateTag crea un tag git con la versión y mensaje especificados - CreateTag(ctx context.Context, version, message string) error - - // PushTag sube el tag al repositorio remoto - PushTag(ctx context.Context, version string) error - - // GetRelease obtiene una release del repositorio - GetRelease(ctx context.Context, version string) (*models.VCSRelease, error) - - // UpdateRelease actualiza una release del repositorio - UpdateRelease(ctx context.Context, version, body string) error - - // EnrichReleaseContext enriquece el release con información adicional de GitHub (issues, PRs, contributors, etc.) - EnrichReleaseContext(ctx context.Context, release *models.Release) error - UpdateLocalChangelog(release *models.Release, notes *models.ReleaseNotes) error - CommitChangelog(ctx context.Context, version string) error - PushChanges(ctx context.Context) error - UpdateAppVersion(version string) error -} diff --git a/internal/domain/ports/vcs_client.go b/internal/domain/ports/vcs_client.go index 0a3d6bb..6a63a63 100644 --- a/internal/domain/ports/vcs_client.go +++ b/internal/domain/ports/vcs_client.go @@ -6,43 +6,43 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/domain/models" ) -// VCSClient define los métodos comunes para interactuar con las APIs de los sistemas de control de versiones. +// VCSClient defines common methods to interact with the APIs of version control systems. type VCSClient interface { - // UpdatePR actualiza una Pull Request (título, body y etiquetas) en el proveedor. + // UpdatePR updates a Pull Request (title, body, and labels) in the provider. UpdatePR(ctx context.Context, prNumber int, summary models.PRSummary) error - // GetPR obtiene los datos de PR (por ejemplo, para extraer commits, diff, etc.). + // GetPR gets the PR data (for example, to extract commits, diff, etc.). GetPR(ctx context.Context, prNumber int) (models.PRData, error) - // GetRepoLabels obtiene todas las labels disponibles en el repositorio + // GetRepoLabels gets all available labels in the repository GetRepoLabels(ctx context.Context) ([]string, error) - // CreateLabel crea una nueva label en el repositorio + // CreateLabel creates a new label in the repository CreateLabel(ctx context.Context, name string, color string, description string) error - // AddLabelsToPR agrega labels específicas a un PR + // AddLabelsToPR adds specific labels to a PR AddLabelsToPR(ctx context.Context, prNumber int, labels []string) error - // CreateRelease crea una nueva release en el repositorio - // buildBinaries indica si se deben compilar y subir binarios (opcional, solo algunos proveedores lo soportan) + // CreateRelease creates a new release in the repository + // buildBinaries indicates whether binaries should be compiled and uploaded (optional, only some providers support it) CreateRelease(ctx context.Context, release *models.Release, notes *models.ReleaseNotes, draft bool, buildBinaries bool) error - // GetRelease obtiene la release del repositorio + // GetRelease gets the release from the repository GetRelease(ctx context.Context, version string) (*models.VCSRelease, error) - // UpdateRelease actualiza una release del repositorio + // UpdateRelease updates a release from the repository UpdateRelease(ctx context.Context, version, body string) error - // GetClosedIssuesBetweenTags obtiene issues cerrados entre dos tags + // GetClosedIssuesBetweenTags gets closed issues between two tags GetClosedIssuesBetweenTags(ctx context.Context, previousTag, currentTag string) ([]models.Issue, error) - // GetMergedPRsBetweenTags obtiene PRs mergeados entre dos tags + // GetMergedPRsBetweenTags gets merged PRs between two tags GetMergedPRsBetweenTags(ctx context.Context, previousTag, currentTag string) ([]models.PullRequest, error) - // GetContributorsBetweenTags obtiene contributors entre dos tags + // GetContributorsBetweenTags gets contributors between two tags GetContributorsBetweenTags(ctx context.Context, previousTag, currentTag string) ([]string, error) - // GetFileStatsBetweenTags obtiene estadísticas de archivos entre dos tags + // GetFileStatsBetweenTags gets file statistics between two tags GetFileStatsBetweenTags(ctx context.Context, previousTag, currentTag string) (*models.FileStatistics, error) - // GetIssue obtiene información de un issue/ticket por su número + // GetIssue gets information for an issue/ticket by its number GetIssue(ctx context.Context, issueNumber int) (*models.Issue, error) - // GetFileAtTag obtiene el contenido de un archivo en un tag específico + // GetFileAtTag gets the content of a file at a specific tag GetFileAtTag(ctx context.Context, tag, filepath string) (string, error) - // GetPRIssues obtiene issues relacionadas con un PR basándose en branch name, commits y descripción + // GetPRIssues gets issues related to a PR based on branch name, commits, and description GetPRIssues(ctx context.Context, branchName string, commits []string, prDescription string) ([]models.Issue, error) - // UpdateIssueChecklist actualiza el checklist de un issue marcando elementos como completados + // UpdateIssueChecklist updates the checklist of an issue marking items as completed UpdateIssueChecklist(ctx context.Context, issueNumber int, indices []int) error - // CreateIssue crea una nueva issue en el repositorio + // CreateIssue creates a new issue in the repository CreateIssue(ctx context.Context, title string, body string, labels []string, assignees []string) (*models.Issue, error) - // GetAuthenticatedUser obtiene el usuario autenticado actual + // GetAuthenticatedUser gets the current authenticated user GetAuthenticatedUser(ctx context.Context) (string, error) } diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index ecda39a..5f98c7e 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -21,7 +21,7 @@ type Translations struct { func NewTranslations(defaultLang string, localesPath string) (*Translations, error) { if defaultLang == "" { - return nil, fmt.Errorf("el idioma predeterminado no puede estar vacío") + return nil, fmt.Errorf("default language cannot be empty") } var files []os.DirEntry @@ -49,7 +49,7 @@ func NewTranslations(defaultLang string, localesPath string) (*Translations, err } if err != nil { - return nil, fmt.Errorf("error leyendo archivo %s: %w", file.Name(), err) + return nil, fmt.Errorf("error reading file %s: %w", file.Name(), err) } bundle.MustParseMessageFileBytes(data, file.Name()) @@ -70,7 +70,7 @@ func (t *Translations) SetLanguage(lang string) error { return nil } } - return fmt.Errorf("lenguaje '%s' no soportado", lang) + return fmt.Errorf("unsupported language '%s'", lang) } func (t *Translations) GetMessage(messageID string, count int, templateData map[string]interface{}) string { @@ -82,7 +82,7 @@ func (t *Translations) GetMessage(messageID string, count int, templateData map[ TemplateData: templateData, }) if err != nil { - return "Falta la traducción: " + messageID + return "Missing translation: " + messageID } return localized } diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go index 98b1df3..513ca3d 100644 --- a/internal/i18n/i18n_test.go +++ b/internal/i18n/i18n_test.go @@ -12,7 +12,7 @@ func TestNewTranslations(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() @@ -26,11 +26,11 @@ func TestNewTranslations(t *testing.T) { // assert if err != nil { - t.Errorf("NewTranslations() no debería retornar error, obtuvo: %v", err) + t.Errorf("NewTranslations() should not return an error, got: %v", err) } if trans == nil { - t.Error("NewTranslations() no debería retornar nil") + t.Error("NewTranslations() should not return nil") } }) @@ -39,7 +39,7 @@ func TestNewTranslations(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() @@ -48,11 +48,11 @@ func TestNewTranslations(t *testing.T) { // assert if err == nil { - t.Error("NewTranslations() debería retornar error con idioma vacío") + t.Error("NewTranslations() should return error with empty language") } if trans != nil { - t.Error("NewTranslations() debería retornar nil cuando falla") + t.Error("NewTranslations() should return nil when it fails") } }) } @@ -63,7 +63,7 @@ func TestSetLanguage(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() @@ -74,7 +74,7 @@ func TestSetLanguage(t *testing.T) { trans, err := NewTranslations("en", tmpDir) if err != nil { - t.Fatal("Error en la configuración de la prueba:", err) + t.Fatal("Error in test setup:", err) } // act @@ -82,7 +82,7 @@ func TestSetLanguage(t *testing.T) { // assert if err != nil { - t.Errorf("SetLanguage() no debería retornar error, obtuvo: %v", err) + t.Errorf("SetLanguage() should not return error, got: %v", err) } }) @@ -91,7 +91,7 @@ func TestSetLanguage(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() @@ -100,7 +100,7 @@ func TestSetLanguage(t *testing.T) { trans, err := NewTranslations("es", tmpDir) if err != nil { - t.Fatal("Error en la configuración de la prueba:", err) + t.Fatal("Error in test setup:", err) } // act @@ -108,7 +108,7 @@ func TestSetLanguage(t *testing.T) { // asssert if err == nil { - t.Error("SetLanguage() debería retornar error con idioma no soportado") + t.Error("SetLanguage() should return error with unsupported language") } }) } @@ -119,7 +119,7 @@ func TestGetMessage(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() @@ -130,7 +130,7 @@ func TestGetMessage(t *testing.T) { trans, err := NewTranslations("es", tmpDir) if err != nil { - t.Fatal("Error en la configuración de la prueba:", err) + t.Fatal("Error in test setup:", err) } // act @@ -139,7 +139,7 @@ func TestGetMessage(t *testing.T) { // assert expected := "Bienvenido" if result != expected { - t.Errorf("GetMessage() = %v, quiere %v", result, expected) + t.Errorf("GetMessage() = %v, want %v", result, expected) } }) @@ -148,7 +148,7 @@ func TestGetMessage(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() @@ -159,7 +159,7 @@ func TestGetMessage(t *testing.T) { trans, err := NewTranslations("es", tmpDir) if err != nil { - t.Fatal("Error en la configuración de la prueba:", err) + t.Fatal("Error in test setup:", err) } // act @@ -168,7 +168,7 @@ func TestGetMessage(t *testing.T) { // assert expected := "Bienvenidos" if result != expected { - t.Errorf("GetMessage() = %v, quiere %v", result, expected) + t.Errorf("GetMessage() = %v, want %v", result, expected) } }) @@ -177,7 +177,7 @@ func TestGetMessage(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() @@ -187,7 +187,7 @@ func TestGetMessage(t *testing.T) { trans, err := NewTranslations("es", tmpDir) if err != nil { - t.Fatal("Error en la configuración de la prueba:", err) + t.Fatal("Error in test setup:", err) } templateData := map[string]interface{}{ @@ -200,7 +200,7 @@ func TestGetMessage(t *testing.T) { // assert expected := "¡Hola Juan!" if result != expected { - t.Errorf("GetMessage() = %v, quiere %v", result, expected) + t.Errorf("GetMessage() = %v, want %v", result, expected) } }) @@ -209,7 +209,7 @@ func TestGetMessage(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() @@ -218,16 +218,16 @@ func TestGetMessage(t *testing.T) { trans, err := NewTranslations("es", tmpDir) if err != nil { - t.Fatal("Error en la configuración de la prueba:", err) + t.Fatal("Error in test setup:", err) } // act result := trans.GetMessage("NonExistent", 1, nil) // assert - expected := "Falta la traducción: NonExistent" + expected := "Missing translation: NonExistent" if result != expected { - t.Errorf("GetMessage() = %v, quiere %v", result, expected) + t.Errorf("GetMessage() = %v, want %v", result, expected) } }) } @@ -238,11 +238,11 @@ func TestNewTranslations_Errors(t *testing.T) { tmpDir := createTempDir(t) defer func() { if err := os.RemoveAll(tmpDir); err != nil { - t.Errorf("error al eliminar directorio: %v", err) + t.Errorf("error deleting directory: %v", err) } }() - // Crear múltiples archivos válidos + // Create multiple valid files createTestFile(t, tmpDir, "active.es.toml", ` [Hello] other = "Hola"`) @@ -256,10 +256,10 @@ func TestNewTranslations_Errors(t *testing.T) { // Assert if err != nil { - t.Errorf("NewTranslations() no debería fallar con archivos válidos: %v", err) + t.Errorf("NewTranslations() should not fail with valid files: %v", err) } if trans == nil { - t.Error("NewTranslations() no debería retornar nil con archivos válidos") + t.Error("NewTranslations() should not return nil with valid files") } }) } @@ -267,7 +267,7 @@ func TestNewTranslations_Errors(t *testing.T) { func createTempDir(t *testing.T) string { tmpDir, err := os.MkdirTemp("", "i18n_test") if err != nil { - t.Fatal("No se pudo crear el directorio temporal:", err) + t.Fatal("Could not create temporary directory:", err) } return tmpDir } @@ -275,6 +275,6 @@ func createTempDir(t *testing.T) string { func createTestFile(t *testing.T, dir, filename, content string) { err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644) if err != nil { - t.Fatal("No se pudo crear el archivo de prueba:", err) + t.Fatal("Could not create test file:", err) } } diff --git a/internal/i18n/locales/active.en.toml b/internal/i18n/locales/active.en.toml index 126d01a..f05369a 100644 --- a/internal/i18n/locales/active.en.toml +++ b/internal/i18n/locales/active.en.toml @@ -34,24 +34,9 @@ other = "🔍 Detected issue #{{.Number}}: {{.Title}}" [issue_using_manual] other = "📋 Using issue #{{.Number}}: {{.Title}}" -[issue_including_context] -other = "📋 Including context from issue #{{.Number}}" - -[issue_vcs_init_error] -other = "⚠️ Could not initialize VCS client: {{.Error}}" - -[issue_fetch_error] -other = "⚠️ Could not fetch issue #{{.Number}} information: {{.Error}}" - -[no_staged_changes] -other = "❌ No staged changes to commit.\nUse 'git add' to stage your changes first" - [invalid_suggestions_count] other = "Number of suggestions must be between {{.Min}} and {{.Max}}" -[ai_init_error] -other = "❌ Error initializing {{.Provider}}: {{.Error}}" - [analyzing_changes] other = "🔍 Analyzing changes..." @@ -73,39 +58,6 @@ other = "Full setup (configure everything)" [config_edit_usage] other = "✏️ Open configuration file in editor" -[work_in_progress] -other = "🚧 This feature is under development." - -[config_set_lang_usage] -other = "🌍 Set default language" - -[config_set_lang_flag_usage] -other = "Language (en, es)" - -[unsupported_language] -other = "❌ Unsupported language. Use 'en' or 'es'" - -[language_configured] -other = "✅ Language set to: {{.Lang}}" - -[error_missing_api_key] -other = "The {{.Provider}} API key is not configured" - -[error_generating_content] -other = "Error generating content: {{.Error}}" - -[error_invalid_suggestion_count] -other = "Invalid number of suggestions requested" - -[error_no_files] -other = "No files provided for analysis" - -[error_no_suggestions] -other = "Could not generate any commit suggestions" - -[files_prefix] -other = "Files: " - [config_show_usage] other = "📋 Show current configuration" @@ -118,12 +70,6 @@ other = "🌍 Language: {{.Lang}}" [emojis_label] other = "😊 Emojis: {{.Emoji}}" -[max_length_label] -other = "📏 Maximum length: {{.MaxLength}}" - -[suggestions_header] -other = "Commit Suggestions" - [invalid_selection] other = "Invalid selection: must be between 1 and {{.Max}}" @@ -163,20 +109,6 @@ error_updating_issue = "Error updating GitHub issue." issue_updated_successfully = "GitHub issue updated successfully." -[jira_config_command_usage] -jira_config_usage = "Set up Jira credentials" -jira_config_base_url_usage = "Jira Base URL" -jira_config_api_key_usage = "Jira API Key" -jira_config_email_usage = "Email associated with Jira" -jira_config_missing_fields = "All fields are required: base-url, api-key, email" -jira_config_save_error = "Error saving Jira settings: {{.Error}}" -jira_config_success = "Jira configuration saved successfully" -ticket_command_usage = "Manage ticket integration settings" -disable_ticket_command_usage = "Disable ticket integration" -enable_ticket_command_usage = "Enable ticket integration" -ticket_disabled_success = "Ticket integration has been disabled successfully" -ticket_enabled_success = "Ticket integration has been enabled successfully" - [config_save] error_saving_config = "Error saving configuration: {{.Error}}" @@ -211,9 +143,6 @@ error_empty_prompt = "The prompt cannot be empty" error_generating_release_notes = "Error generating release notes: {{.Error}}" error_no_ai_response = "No response from AI" -[suggestion_header] -other = "=========[ Suggestion {{.Number}} ]=========" - [config_models] config_set_ai_active_usage = "Set the active AI for commit generation" config_set_ai_model_usage = "Set the model for a specific AI" @@ -313,15 +242,6 @@ 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}}'" -[commit_service] -undetected_changes = "No changes detected" -error_getting_diff = "Error getting changes: {{.Error}}" -no_differences_detected = "No differences were detected in the files" -error_get_id_ticket = "Error getting ticket ID: {{.Error}}" -error_get_ticket_info = "Error obtaining ticket information: {{.Error}}" -ticket_id_not_found_branch = "No ticket ID found in the branch name" -error_get_name_from_branch = "Error getting branch name: {{.Error}}" - [pr_service] error_get_pr = "Error getting the PR: {{.Error}}" error_create_summary_pr = "Error creating PR summary: {{.Error}}" @@ -362,13 +282,6 @@ summary_api = "🔑 API {{.AI}}: {{.Configured}}" summary_vcs_none = "No VCS provider configured" prompt_run_again = "Do you want to run the wizard again? (y/n): " -[ai_missing] -for_suggest = "❌ AI is not configured. Set your AI provider API Key to use 'suggest'.\n 👉 matecommit config init" -for_pr = "❌ AI is not configured. Set your AI provider API Key to use 'summarize-pr'.\n 👉 matecommit config init" - -ai_missing_for_suggest = "❌ AI is not configured. Set your AI provider API Key to use 'suggest'.\n 👉 matecommit config init" -ai_missing_for_pr = "❌ AI is not configured. Set your AI provider API Key to use 'summarize-pr'.\n 👉 matecommit config init" - [help_command_usage] other = "Shows a list of commands or help for one command" @@ -496,19 +409,6 @@ get_commits = "Error getting commits: {{.Error}}" extract_repo_info = "Could not extract owner and repo from URL: {{.Url}}" # PR Service - Issue linking -[pr_detected_issues] -other = "🔍 Issues detected in PR #{{.Number}}: {{.Issues}}" - -[pr_issues_will_close_on_merge] -other = "✅ {{.Count}} issue(s) will close automatically when PR is merged" - -[pr_breaking_changes_detected] -other = "⚠️ Breaking changes detected in {{.Count}} commits" - -[pr_test_plan_generated] -other = "📋 Test plan generated automatically" - -# UI - Spinners and Feedback [ui] generating_suggestions_banner = "Generating Commit Suggestions" generating_with_ai = "Generating suggestions with AI..." @@ -539,6 +439,7 @@ github_token_missing = "GitHub token not configured" no_changes_detected = "No changes detected in repository" ensure_modified_files = "Make sure you have modified files before generating suggestions" run_config_init = "Run: matecommit config init" +internal_error = "Internal system error" error_saving_config = "Error saving configuration" # UI - Preview and confirmations @@ -580,6 +481,7 @@ improvement_suggestions = "Improvement Suggestions:" technical_analysis = "🔧 Technical Analysis:" completed = "Completed" error = "Error:" +suggestion_number = "📝 Suggestion #{{.Number}}" # Doctor command @@ -672,15 +574,6 @@ full_option = "2. Full - Configure everything (AI, VCS, Tickets)" prompt_selection = "Select [1]: " # First run wizard -[first_run] -welcome_banner = "👋 Welcome to MateCommit!" -welcome_message = "Looks like this is your first time. MateCommit uses AI to generate smart commits." -what_to_do = "What would you like to do?" -quick_setup_option = "1. Quick setup (30 seconds)" -full_setup_option = "2. Full setup" -exit_option = "3. Exit" -prompt_choice = "Select [1]: " -setup_cancelled = "Setup cancelled" [completion] command_usage = "Generate completion script" command_description = "Outputs the completion script for your shell" @@ -711,61 +604,10 @@ updating = "🔄 Updating MateCommit..." success = "✅ MateCommit updated successfully!" error = "Error updating MateCommit" -[update.method_not_detected] -other = "Installation method not detected. Please update manually" - -[update.go_not_found] -other = "Go is not installed. Cannot update via 'go install'" - -[update.brew_not_found] -other = "Homebrew is not installed. Cannot update via 'brew'" - -[update.binary_not_supported] -other = "Binary update not supported yet. Please update manually:\n • Via Go: go install github.com/Tomas-vilte/MateCommit@latest\n • Via Homebrew: brew upgrade matecommit" - -[update.downloading] -other = "Downloading version {{.Version}}..." - -[update.extracting] -other = "Extracting files..." - -[update.installing] -other = "Installing new version..." - - - [build] compiling_binaries = "Compiling binaries for multiple platforms..." uploading_binaries = "Uploading {{.Count}} binaries to release..." -[build.errors] -upload_binaries = "Error uploading binaries: {{.Error}}" -create_zip = "Error creating zip: {{.Error}}" -create_targz = "Error creating tar.gz: {{.Error}}" -compile_target = "Error compiling for {{.GOOS}}/{{.GOARCH}}: {{.Output}}" -build_binaries = "Error building and packaging binaries: {{.Error}}" -open_file = "Error opening file {{.File}}: {{.Error}}" -upload_asset = "Error uploading asset {{.Asset}}: {{.Error}}" -create_temp_dir = "Error creating temporary directory: {{.Error}}" - -[update.errors] -get_latest = "Error getting latest version: {{.Error}}" -arch_not_supported = "Architecture not supported: {{.Arch}}" -os_not_supported = "Operating system not supported: {{.OS}}" -binary_not_found_manual = "Binary not found for {{.OS}}/{{.Arch}}. Download it manually from: {{.URL}}" -create_temp = "Error creating temporary directory: {{.Error}}" -download = "Error downloading binary: {{.Error}}" -download_http = "Error downloading: HTTP {{.StatusCode}}" -create_temp_file = "Error creating temporary file: {{.Error}}" -save_binary = "Error saving binary: {{.Error}}" -extract = "Error extracting file: {{.Error}}" -get_executable = "Error getting executable path: {{.Error}}" -resolve_symlinks = "Error resolving symlinks: {{.Error}}" -create_backup = "Error creating backup: {{.Error}}" -install_binary = "Error installing new binary: {{.Error}}" -chmod = "Error setting permissions: {{.Error}}" - -# Issue Generator Command [issue] command_usage = "Manage and generate issues" generate_usage = "Generate a new issue from code changes or description" @@ -781,7 +623,7 @@ preview_title = "Issue Preview" preview_title_label = "Title" preview_description_label = "Description" preview_labels_label = "Labels" -confirm_prompt = "Create this issue?" +confirm_prompt = "Create this issue? (Y/n)" created_successfully = "✅ Issue #{{.Number}} created successfully: {{.URL}}" cancelled = "Issue creation cancelled" dry_run_complete = "Dry run complete. No issue was created." @@ -831,105 +673,6 @@ template_list_usage_hint = "Use --template with the generate command" template_list_error = "Error listing templates" -[issue_generator] -ai_not_configured = "AI provider is not configured. Please run 'matecommit config init' first." -error_getting_diff = "Error getting git diff" -no_changes_detected = "No changes detected. Make some changes first or use --description instead." -error_getting_files = "Error getting changed files" -error_generating_content = "Error generating content with AI" -description_required = "Description is required when not using --from-diff" -vcs_not_configured = "VCS provider is not configured. Please run 'matecommit config init' first." -error_getting_pr = "Error getting Pull Request" -error_loading_template = "Error loading template" - -[issue_template] -bug_report_name = "Bug Report" -bug_report_about = "Create a report to help us improve" -bug_report_title = "[BUG] " -bug_report_body = """ -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Additional context** -Add any other context about the problem here. -""" - -feature_request_name = "Feature Request" -feature_request_about = "Suggest an idea for this project" -feature_request_title = "[FEATURE] " -feature_request_body = """ -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. -""" - -custom_issue_name = "Custom Issue" -custom_issue_about = "Create a custom issue" -custom_issue_title = "[ISSUE] " -custom_issue_body = """ -**Context** -Provide context about this issue. - -**Details** -Add detailed information here. -""" - -# GitHub Issue Forms - Bug Report -bug_report_intro = "Thanks for taking the time to fill out this bug report!" -bug_description_label = "What happened?" -bug_description_help = "A clear and concise description of what the bug is" -bug_description_placeholder = "The application crashes when..." -bug_steps_label = "Steps to reproduce" -bug_steps_help = "How can we reproduce this issue?" -bug_expected_label = "Expected behavior" -bug_expected_placeholder = "What did you expect to happen?" -bug_actual_label = "Actual behavior" -bug_actual_placeholder = "What actually happened?" -bug_version_label = "Version" -bug_additional_label = "Additional context" -bug_additional_help = "Add any other context about the problem (screenshots, logs, etc.)" - -# GitHub Issue Forms - Feature Request -feature_intro = "Thanks for suggesting a new feature!" -feature_problem_label = "Is your feature request related to a problem?" -feature_problem_help = "Describe the problem you're trying to solve" -feature_problem_placeholder = "I'm always frustrated when..." -feature_solution_label = "Describe the solution you'd like" -feature_solution_help = "What would you like to happen?" -feature_solution_placeholder = "I would like to be able to..." -feature_alternatives_label = "Describe alternatives you've considered" -feature_alternatives_help = "Have you considered any alternative solutions?" -feature_additional_label = "Additional context" -feature_additional_help = "Add any other context or screenshots about the feature" - -# GitHub Issue Forms - Custom Issue -custom_intro = "Provide details about your issue" -custom_description_label = "Description" -custom_description_help = "Describe your issue" -custom_description_placeholder = "Please provide details..." -custom_additional_label = "Additional information" -custom_additional_help = "Any other relevant details" - [stats] usage = "Show cost and usage statistics" monthly_flag = "Show monthly statistics instead of daily" diff --git a/internal/i18n/locales/active.es.toml b/internal/i18n/locales/active.es.toml index c2ddb30..46024f5 100644 --- a/internal/i18n/locales/active.es.toml +++ b/internal/i18n/locales/active.es.toml @@ -34,45 +34,12 @@ other = "🔍 Detectado issue #{{.Number}}: {{.Title}}" [issue_using_manual] other = "📋 Usando issue #{{.Number}}: {{.Title}}" -[issue_including_context] -other = "📋 Incluyendo contexto del issue #{{.Number}}" - -[issue_vcs_init_error] -other = "⚠️ No se pudo inicializar el cliente VCS: {{.Error}}" - -[issue_fetch_error] -other = "⚠️ No se pudo obtener información del issue #{{.Number}}: {{.Error}}" - -[no_staged_changes] -other = "❌ No hay cambios preparados para commitear.\nUsá 'git add' para preparar tus cambios primero" - [invalid_suggestions_count] other = "La cantidad de sugerencias tiene que estar entre {{.Min}} y {{.Max}}" -[ai_init_error] -other = "❌ Error al inicializar {{.Provider}}: {{.Error}}" - [analyzing_changes] other = "🔍 Analizando cambios..." -[error_missing_api_key] -other = "La API key de {{.Provider}} no está configurada" - -[error_generating_content] -other = "Error generando el contenido: {{.Error}}" - -[error_invalid_suggestion_count] -other = "Número de sugerencias solicitado no válido" - -[error_no_files] -other = "No se proporcionaron archivos para analizar" - -[error_no_suggestions] -other = "No se pudieron generar sugerencias de commit" - -[files_prefix] -other = "Archivos: " - [suggestion_generation_error] other = "❌ Error al generar sugerencias: {{.Error}}" @@ -91,21 +58,6 @@ other = "Configuración completa (configurar todo)" [config_edit_usage] other = "✏️ Abrir archivo de configuración en el editor" -[work_in_progress] -other = "🚧 Esta funcionalidad está en desarrollo." - -[config_set_lang_usage] -other = "🌍 Configurar idioma por defecto" - -[config_set_lang_flag_usage] -other = "Idioma (en, es)" - -[unsupported_language] -other = "❌ Idioma no soportado. Usá 'en' o 'es'" - -[language_configured] -other = "✅ Lenguaje configurado: {{.Lang}}" - [language_set_success] other = "✅ Idioma configurado a: {{.Lang}}" @@ -121,12 +73,6 @@ other = "🌍 Idioma: {{.Lang}}" [emojis_label] other = "😊 Emojis: {{.Emoji}}" -[max_length_label] -other = "📏 Longitud máxima: {{.MaxLength}}" - -[suggestions_header] -other = "Sugerencias de Commit" - [invalid_selection] other = "Selección inválida: tiene que estar entre 1 y {{.Max}}" @@ -172,20 +118,6 @@ criteria_status_prefix = "Estado de los Criterios:" missing_criteria_prefix = "Criterios Faltantes:" improvement_suggestions_prefix = "Sugerencias de Mejora:" -[jira_config_command_usage] -jira_config_usage = "Configura las credenciales de Jira" -jira_config_base_url_usage = "URL base de Jira" -jira_config_api_key_usage = "API Key de Jira" -jira_config_email_usage = "Email asociado a Jira" -jira_config_missing_fields = "Todos los campos son requeridos: base-url, api-key, email" -jira_config_save_error = "Error al guardar la configuración de Jira: {{.Error}}" -jira_config_success = "Configuración de Jira guardada exitosamente" -ticket_command_usage = "Gestionar la configuración de integración de tickets" -disable_ticket_command_usage = "Desactivar la integración de tickets" -enable_ticket_command_usage = "Activar la integración de tickets" -ticket_disabled_success = "La integración de tickets ha sido desactivada exitosamente" -ticket_enabled_success = "La integración de tickets ha sido activada exitosamente" - [config_save] error_saving_config = "Error al guardar la configuración: {{.Error}}" @@ -219,9 +151,6 @@ error_empty_prompt = "El prompt no puede estar vacio" error_generating_release_notes = "Error generando las notas de lanzamiento: {{.Error}}" error_no_ai_response = "No hubo respuesta de la IA" -[suggestion_header] -other = "=========[ Sugerencias {{.Number}} ]=========" - [config_models] config_set_ai_active_usage = "Establecer la IA activa para la generación de commits" config_set_ai_model_usage = "Establecer el modelo para una IA específica" @@ -320,15 +249,6 @@ 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}}'" -[commit_service] -undetected_changes = "No hay cambios detectados" -error_getting_diff = "Error al obtener los cambios: {{.Error}}" -no_differences_detected = "No se detectaron diferencias en los archivos" -error_get_id_ticket = "Error al obtener el ID del ticket: {{.Error}}" -error_get_ticket_info = "Error al obtener informacion del ticket: {{.Error}}" -ticket_id_not_found_branch = "No se encontro un ID de ticket en el nombre de la branch" -error_get_name_from_branch = "Error al obtener el nombre de la branch: {{.Error}}" - [pr_service] error_get_pr = "Error al obtener el PR: {{.Error}}" error_create_summary_pr = "Error al crear el resumen del PR: {{.Error}}" @@ -369,14 +289,6 @@ summary_api = "🔑 API {{.AI}}: {{.Configured}}" summary_vcs_none = "Proveedor VCS no configurado" prompt_run_again = "¿Querés volver a ejecutar el asistente? (s/n): " -[ai_missing] -for_suggest = "❌ La IA no está configurada. Configura tu API Key del proveedor de IA para usar 'suggest'.\n 👉 matecommit config init" -for_pr = "❌ La IA no está configurada. Configura tu API Key del proveedor de IA para usar 'summarize-pr'.\n 👉 matecommit config init" - -ai_missing_for_suggest = "❌ La IA no está configurada. Configura tu API Key del proveedor de IA para usar 'suggest'.\n 👉 matecommit config init" - -ai_missing_for_pr = "❌ La IA no está configurada. Configura tu API Key del proveedor de IA para usar 'summarize-pr'.\n 👉 matecommit config init" - [help_command_usage] other = "Muestra una lista de comandos o ayuda para un comando" @@ -528,19 +440,6 @@ get_commits = "Error al obtener commits: {{.Error}}" extract_repo_info = "No se pudo extraer el propietario y el repositorio de la URL: {{.Url}}" # PR Service - Issue linking -[pr_detected_issues] -other = "🔍 Issues detectadas en PR #{{.Number}}: {{.Issues}}" - -[pr_issues_will_close_on_merge] -other = "✅ {{.Count}} issue(s) se cerrarán automáticamente al mergear el PR" - -[pr_breaking_changes_detected] -other = "⚠️ Breaking changes detectados en {{.Count}} commits" - -[pr_test_plan_generated] -other = "📋 Test plan generado automáticamente" -# UI - Spinners y Feedback - [ui] generating_suggestions_banner = "Generando Sugerencias de Commit" generating_with_ai = "Generando sugerencias con IA..." @@ -571,7 +470,8 @@ github_token_missing = "Token de GitHub no configurado" no_changes_detected = "No se detectaron cambios en el repositorio" ensure_modified_files = "Asegúrate de tener archivos modificados antes de generar sugerencias" run_config_init = "Ejecuta: matecommit config init" -error_saving_config = "Error al guardar la configuración" +internal_error = "Error interno del sistema" +error_saving_config = "Error guardando la configuración" # UI - Preview y confirmaciones [ui_preview] @@ -612,6 +512,7 @@ improvement_suggestions = "Sugerencias de Mejora:" technical_analysis = "🔧 Análisis Técnico:" completed = "Completado" error = "Error:" +suggestion_number = "📝 Sugerencia #{{.Number}}" # Doctor command @@ -704,15 +605,6 @@ full_option = "2. Completa - Configurar todo (IA, VCS, Tickets)" prompt_selection = "Selecciona [1]: " # First run wizard -[first_run] -welcome_banner = "👋 ¡Bienvenido a MateCommit!" -welcome_message = "Parece que es tu primera vez. MateCommit usa IA para generar commits inteligentes." -what_to_do = "¿Qué querés hacer?" -quick_setup_option = "1. Configuración rápida (30 segundos)" -full_setup_option = "2. Configuración completa" -exit_option = "3. Salir" -prompt_choice = "Selecciona [1]: " -setup_cancelled = "Configuración cancelada" [completion] command_usage = "Generar script de autocompletado" command_description = "Muestra el script de autocompletado para tu shell" @@ -743,61 +635,10 @@ updating = "🔄 Actualizando MateCommit..." success = "✅ ¡MateCommit actualizado correctamente!" error = "Error al actualizar MateCommit" -[update.method_not_detected] -other = "Método de instalación no detectado. Por favor actualizá manualmente" - -[update.go_not_found] -other = "Go no está instalado. No se puede actualizar via 'go install'" - -[update.brew_not_found] -other = "Homebrew no está instalado. No se puede actualizar via 'brew'" - -[update.binary_not_supported] -other = "Actualización via binario no soportada todavía. Por favor actualizá manualmente:\n • Via Go: go install github.com/Tomas-vilte/MateCommit@latest\n • Via Homebrew: brew upgrade matecommit" - -[update.downloading] -other = "Descargando versión {{.Version}}..." - -[update.extracting] -other = "Extrayendo archivos..." - -[update.installing] -other = "Instalando nueva versión..." - - - [build] compiling_binaries = "Compilando binarios para múltiples plataformas..." uploading_binaries = "Subiendo {{.Count}} binarios al release..." -[build.errors] -upload_binaries = "Error subiendo binarios: {{.Error}}" -create_zip = "Error creando zip: {{.Error}}" -create_targz = "Error creando tar.gz: {{.Error}}" -compile_target = "Error compilando para {{.GOOS}}/{{.GOARCH}}: {{.Output}}" -build_binaries = "Error construyendo y empaquetando binarios: {{.Error}}" -open_file = "Error abriendo archivo {{.File}}: {{.Error}}" -upload_asset = "Error subiendo asset {{.Asset}}: {{.Error}}" -create_temp_dir = "Error creando directorio temporal: {{.Error}}" - -[update.errors] -get_latest = "Error obteniendo ultima version: {{.Error}}" -arch_not_supported = "Arquitectura no soportada: {{.Arch}}" -os_not_supported = "Sistema operativo no soportado: {{.OS}}" -binary_not_found_manual = "Binario no encontrado para {{.OS}}/{{.Arch}}. Descargalo manualmente desde: {{.URL}}" -create_temp = "Error creando directorio temporal: {{.Error}}" -download = "Error descargando binario: {{.Error}}" -download_http = "Error descargando: HTTP {{.StatusCode}}" -create_temp_file = "Error creando archivo temporal: {{.Error}}" -save_binary = "Error guardando binario: {{.Error}}" -extract = "Error extrayendo archivo: {{.Error}}" -get_executable = "Error obteniendo ruta del ejecutable: {{.Error}}" -resolve_symlinks = "Error resolviendo symlinks: {{.Error}}" -create_backup = "Error creando backup: {{.Error}}" -install_binary = "Error instalando nuevo binario: {{.Error}}" -chmod = "Error estableciendo permisos: {{.Error}}" - -# Comando Generador de Issues [issue] command_usage = "Gestionar y generar issues" generate_usage = "Generar una nueva issue desde cambios de código o descripción" @@ -813,7 +654,7 @@ preview_title = "Vista Previa de la Issue" preview_title_label = "Título" preview_description_label = "Descripción" preview_labels_label = "Labels" -confirm_prompt = "¿Crear esta issue?" +confirm_prompt = "¿Crear esta issue? (Y/n)" created_successfully = "✅ Issue #{{.Number}} creada exitosamente: {{.URL}}" cancelled = "Creación de issue cancelada" dry_run_complete = "Dry run completado. No se creó ninguna issue." @@ -862,105 +703,6 @@ template_list_hint = "Ejecutá 'matecommit issue template init' para crear templ template_list_usage_hint = "Usá --template con el comando generate" template_list_error = "Error listando templates" -[issue_generator] -ai_not_configured = "El proveedor de IA no está configurado. Por favor ejecutá 'matecommit config init' primero." -error_getting_diff = "Error obteniendo git diff" -no_changes_detected = "No se detectaron cambios. Hacé algunos cambios primero o usá --description." -error_getting_files = "Error obteniendo archivos modificados" -error_generating_content = "Error generando contenido con IA" -description_required = "La descripción es requerida cuando no se usa --from-diff" -vcs_not_configured = "El proveedor VCS no está configurado. Por favor ejecutá 'matecommit config init' primero." -error_getting_pr = "Error obteniendo Pull Request" -error_loading_template = "Error cargando template" - -[issue_template] -bug_report_name = "Reporte de Error" -bug_report_about = "Crea un reporte para ayudarnos a mejorar" -bug_report_title = "[BUG] " -bug_report_body = """ -**Descripción del error** -Una descripción clara y concisa de lo que es el error. - -**Para Reproducir** -Pasos para reproducir el comportamiento: -1. Ir a '...' -2. Hacer clic en '....' -3. Ver el error - -**Comportamiento esperado** -Una descripción clara y concisa de lo que esperabas que sucediera. - -**Capturas de pantalla** -Si corresponde, agrega capturas de pantalla para ayudar a explicar el problema. - -**Contexto adicional** -Agrega cualquier otro contexto sobre el problema aquí. -""" - -feature_request_name = "Solicitud de Funcionalidad" -feature_request_about = "Sugiere una idea para este proyecto" -feature_request_title = "[FEATURE] " -feature_request_body = """ -**¿Tu solicitud de funcionalidad está relacionada con un problema? Por favor descríbelo.** -Una descripción clara y concisa de cuál es el problema. Ej. Siempre me frustra cuando [...] - -**Describe la solución que te gustaría** -Una descripción clara y concisa de lo que quieres que suceda. - -**Describe las alternativas que has considerado** -Una descripción clara y concisa de cualquier solución o funcionalidad alternativa que hayas considerado. - -**Contexto adicional** -Agrega cualquier otro contexto o capturas de pantalla sobre la solicitud de funcionalidad aquí. -""" - -custom_issue_name = "Issue Personalizada" -custom_issue_about = "Crea una issue personalizada" -custom_issue_title = "[ISSUE] " -custom_issue_body = """ -**Contexto** -Proporciona contexto sobre esta issue. - -**Detalles** -Agrega información detallada aquí. -""" - -# GitHub Issue Forms - Reporte de Error -bug_report_intro = "Gracias por reportar este problema, tu feedback ayuda a mejorar el proyecto." -bug_description_label = "¿Qué pasó?" -bug_description_help = "Contame con tus palabras qué problema encontraste" -bug_description_placeholder = "Cuando intento guardar los cambios, la aplicación deja de responder..." -bug_steps_label = "¿Cómo lo reproduzco?" -bug_steps_help = "Detallá los pasos para que pueda reproducir el error" -bug_expected_label = "¿Qué esperabas que pasara?" -bug_expected_placeholder = "Esperaba que se guardaran los cambios sin problemas" -bug_actual_label = "¿Qué pasó en realidad?" -bug_actual_placeholder = "La aplicación se quedó trabada y tuve que cerrarla" -bug_version_label = "Versión" -bug_additional_label = "Información extra" -bug_additional_help = "Screenshots, logs, o cualquier detalle que pueda ayudar a entender el problema" - -# GitHub Issue Forms - Solicitud de Funcionalidad -feature_intro = "Gracias por contribuir con ideas para mejorar el proyecto." -feature_problem_label = "¿Qué problema querés resolver?" -feature_problem_help = "Contame qué necesidad o dificultad tenés actualmente" -feature_problem_placeholder = "Me cuesta mucho hacer X porque no hay forma de Y..." -feature_solution_label = "¿Qué solución proponés?" -feature_solution_help = "Describí cómo te imaginás que debería funcionar" -feature_solution_placeholder = "Estaría bueno poder hacer X desde Y para que sea más rápido..." -feature_alternatives_label = "¿Pensaste en otras opciones?" -feature_alternatives_help = "Si consideraste alternativas, contame cuáles" -feature_additional_label = "Contexto adicional" -feature_additional_help = "Screenshots, ejemplos, o links que puedan ayudar a entender la propuesta" - -# GitHub Issue Forms - Issue Personalizada -custom_intro = "Describí el tema que querés reportar o discutir." -custom_description_label = "Descripción" -custom_description_help = "Contame de qué se trata" -custom_description_placeholder = "Quisiera proponer/reportar/discutir..." -custom_additional_label = "Información adicional" -custom_additional_help = "Cualquier detalle extra que sea relevante" - [stats] usage = "Mostrar estadísticas de costo y uso" monthly_flag = "Mostrar estadísticas mensuales en lugar de diarias" diff --git a/internal/infrastructure/ai/cost_wrapper.go b/internal/infrastructure/ai/cost_wrapper.go index 456d3d0..82899d2 100644 --- a/internal/infrastructure/ai/cost_wrapper.go +++ b/internal/infrastructure/ai/cost_wrapper.go @@ -1,25 +1,31 @@ package ai import ( - "bufio" "context" "encoding/json" "fmt" - "os" - "strings" "time" + "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/cache" cost2 "github.com/Tomas-vilte/MateCommit/internal/services/cost" "github.com/Tomas-vilte/MateCommit/internal/services/routing" - "github.com/Tomas-vilte/MateCommit/internal/ui" - "github.com/fatih/color" ) -type GenerateFunc func(ctx context.Context, modelName string, prompt string) (interface{}, *models.TokenUsage, error) +type ConfirmationCallback func(result ConfirmationResult) (choice string, proceed bool) + +type GenerateFunc func(ctx context.Context, model string, prompt string) (interface{}, *models.TokenUsage, error) + +type ConfirmationResult struct { + EstimatedCost float64 + InputTokens int + OutputTokens int + SuggestedModel string + CurrentModel string + RationaleKey string +} type CostAwareWrapper struct { provider ports.CostAwareAIProvider @@ -27,29 +33,29 @@ type CostAwareWrapper struct { manager *cost2.Manager cache *cache.Cache modelSelector *routing.ModelSelector - trans *i18n.Translations estimatedOutputTokens int skipConfirmation bool + onConfirmation ConfirmationCallback } type WrapperConfig struct { Provider ports.CostAwareAIProvider BudgetDaily float64 - Trans *i18n.Translations EstimatedOutputTokens int SkipConfirmation bool + OnConfirmation ConfirmationCallback } -// NewCostAwareWrapper crea un wrapper agnóstico de proveedor +// NewCostAwareWrapper creates a provider-agnostic wrapper func NewCostAwareWrapper(cfg WrapperConfig) (*CostAwareWrapper, error) { - manager, err := cost2.NewManager(cfg.BudgetDaily, cfg.Trans) + manager, err := cost2.NewManager(cfg.BudgetDaily) if err != nil { - return nil, fmt.Errorf("error creando cost manager: %w", err) + return nil, fmt.Errorf("error creating cost manager: %w", err) } cacheService, err := cache.NewCache(24 * time.Hour) if err != nil { - return nil, fmt.Errorf("error creando cache: %w", err) + return nil, fmt.Errorf("error creating cache: %w", err) } return &CostAwareWrapper{ @@ -58,18 +64,18 @@ func NewCostAwareWrapper(cfg WrapperConfig) (*CostAwareWrapper, error) { manager: manager, cache: cacheService, modelSelector: routing.NewModelSelector(), - trans: cfg.Trans, estimatedOutputTokens: cfg.EstimatedOutputTokens, skipConfirmation: cfg.SkipConfirmation, + onConfirmation: cfg.OnConfirmation, }, nil } -// SetSkipConfirmation permite activar o desactivar la confirmación del usuario manual. +// SetSkipConfirmation allows enabling or disabling manual user confirmation. func (w *CostAwareWrapper) SetSkipConfirmation(skip bool) { w.skipConfirmation = skip } -// WrapGenerate envuelve cualquier función de generación con tracking +// WrapGenerate wraps any generation function with tracking func (w *CostAwareWrapper) WrapGenerate( ctx context.Context, command string, @@ -108,33 +114,32 @@ func (w *CostAwareWrapper) WrapGenerate( estimatedCost := w.calculator.EstimateCost(providerName, originalModel, inputTokens, w.estimatedOutputTokens) - if hasSuggestion && !w.skipConfirmation { - ui.SuspendActiveSpinner() - fmt.Println() - rationaleKey := w.modelSelector.GetRationale(suggestedModel) - rationale := w.trans.GetMessage(rationaleKey, 0, nil) - yellow := color.New(color.FgYellow) - _, _ = yellow.Println(w.trans.GetMessage("cost.routing_suggestion", 0, map[string]interface{}{ - "Rationale": rationale, - })) - _, _ = yellow.Println(w.trans.GetMessage("cost.routing_suggested_model", 0, map[string]interface{}{ - "Suggested": suggestedModel, - "Current": originalModel, - })) - - estimatedCost = w.calculator.EstimateCost(providerName, suggestedModel, inputTokens, w.estimatedOutputTokens) + budgetStatus, err := w.manager.CheckBudget(estimatedCost) + if err != nil { + return nil, nil, err } - if err := w.manager.CheckBudget(estimatedCost); err != nil { - return nil, nil, fmt.Errorf("%s: %w", w.trans.GetMessage("cost.budget_exceeded", 0, nil), err) + if budgetStatus.IsExceeded { + return nil, nil, errors.ErrQuotaExceeded } - if (estimatedCost > 0.0001 || hasSuggestion) && !w.skipConfirmation { - ui.SuspendActiveSpinner() - choice, proceed := w.askUserConfirmation(estimatedCost, inputTokens, w.estimatedOutputTokens, suggestedModel) - ui.ResumeSuspendedSpinner() + if (estimatedCost > 0.0001 || hasSuggestion) && !w.skipConfirmation && w.onConfirmation != nil { + rationaleKey := "" + if hasSuggestion { + rationaleKey = w.modelSelector.GetRationale(suggestedModel) + } + + choice, proceed := w.onConfirmation(ConfirmationResult{ + EstimatedCost: estimatedCost, + InputTokens: inputTokens, + OutputTokens: w.estimatedOutputTokens, + SuggestedModel: suggestedModel, + CurrentModel: originalModel, + RationaleKey: rationaleKey, + }) + if !proceed { - return nil, nil, fmt.Errorf("operación cancelada por el usuario") + return nil, nil, errors.NewAppError(errors.TypeInternal, "operation cancelled by user", nil) } if choice == "suggested" { modelToUse = suggestedModel @@ -170,68 +175,3 @@ func (w *CostAwareWrapper) WrapGenerate( return resp, usage, nil } - -// askUserConfirmation pregunta al usuario si desea continuar y permite cambiar al modelo sugerido -func (w *CostAwareWrapper) askUserConfirmation(estimatedCost float64, inputTokens, outputTokens int, suggestedModel string) (string, bool) { - cyan := color.New(color.FgCyan, color.Bold) - yellow := color.New(color.FgYellow) - originalModel := w.provider.GetModelName() - hasSuggestion := suggestedModel != "" && suggestedModel != originalModel - - fmt.Println() - _, _ = cyan.Println(w.trans.GetMessage("cost.confirmation_separator", 0, nil)) - _, _ = cyan.Println(w.trans.GetMessage("cost.confirmation_header", 0, nil)) - _, _ = cyan.Println(w.trans.GetMessage("cost.confirmation_separator", 0, nil)) - - fmt.Println(w.trans.GetMessage("cost.confirmation_input_tokens", 0, map[string]interface{}{ - "Tokens": yellow.Sprintf("%d", inputTokens), - })) - fmt.Println(w.trans.GetMessage("cost.confirmation_output_tokens", 0, map[string]interface{}{ - "Tokens": yellow.Sprintf("%d", outputTokens), - })) - - costLabel := w.trans.GetMessage("cost.confirmation_estimated_cost", 0, map[string]interface{}{ - "Cost": yellow.Sprintf("$%.4f", estimatedCost), - }) - if hasSuggestion { - fmt.Printf("%s (%s)\n", costLabel, suggestedModel) - } else { - fmt.Println(costLabel) - } - - _, _ = cyan.Println(w.trans.GetMessage("cost.confirmation_separator", 0, nil)) - - if hasSuggestion { - fmt.Printf("%s ", w.trans.GetMessage("cost.confirmation_use_suggested", 0, nil)) - fmt.Printf("%s\n", color.HiBlackString(w.trans.GetMessage("cost.confirmation_use_suggested_help", 0, nil))) - } else { - fmt.Printf("%s ", w.trans.GetMessage("cost.confirmation_prompt", 0, nil)) - } - - reader := bufio.NewReader(os.Stdin) - response, err := reader.ReadString('\n') - if err != nil { - return "", false - } - - response = strings.TrimSpace(strings.ToLower(response)) - - if !hasSuggestion { - proceed := response == "" || response == "y" || response == "yes" || response == "si" || response == "s" - if proceed { - return "original", true - } - return "", false - } - - switch response { - case "", "y", "yes", "si", "s": - return "suggested", true - case "m", "stay": - return "original", true - case "c", "cancel", "n", "no": - return "", false - default: - return "", false - } -} diff --git a/internal/infrastructure/ai/cost_wrapper_test.go b/internal/infrastructure/ai/cost_wrapper_test.go index 9adbbb2..245aa43 100644 --- a/internal/infrastructure/ai/cost_wrapper_test.go +++ b/internal/infrastructure/ai/cost_wrapper_test.go @@ -2,13 +2,11 @@ package ai import ( "context" - "io" "os" "testing" "time" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/services/cost" "github.com/stretchr/testify/mock" ) @@ -62,19 +60,14 @@ func setupTestWrapper(t *testing.T, budget float64) (*CostAwareWrapper, *mockPro } }) - trans, err := i18n.NewTranslations("en", "") - if err != nil { - t.Fatalf("failed to load translations: %v", err) - } - mockP := new(mockProvider) cfg := WrapperConfig{ Provider: mockP, BudgetDaily: budget, - Trans: trans, EstimatedOutputTokens: 200, SkipConfirmation: true, + OnConfirmation: nil, } wrapper, err := NewCostAwareWrapper(cfg) @@ -191,59 +184,40 @@ func TestCostAwareWrapper_WrapGenerate_BudgetExceeded(t *testing.T) { mockP.AssertExpectations(t) } -func TestCostAwareWrapper_AskUserConfirmation(t *testing.T) { - w, mockP, _ := setupTestWrapper(t, 1.0) - w.skipConfirmation = false - - tests := []struct { - name string - input string - wantModel string - wantProc bool - }{ - {"Accept suggested", "y\n", "suggested", true}, - {"Accept suggested caps", "YES\n", "suggested", true}, - {"Stay with original", "stay\n", "original", true}, - {"Cancel", "n\n", "", false}, - {"Invalid stays as cancel", "random\n", "", false}, +// TestCostAwareWrapper_AskUserConfirmation fue removido porque askUserConfirmation +// ya no es un método del wrapper - ahora se pasa como callback (onConfirmation) +// desde la capa CLI. El comportamiento de confirmación ahora se testea en los tests de CLI. + +func TestCostAwareWrapper_WrapGenerate_SuggestedModel(t *testing.T) { + // Arrange + budget := 1.0 + tempHome, err := os.MkdirTemp("", "matecommit-home-*") + if err != nil { + t.Fatalf("failed to create temp home: %v", err) } + oldHome := os.Getenv("HOME") + _ = os.Setenv("HOME", tempHome) + t.Cleanup(func() { + _ = os.Setenv("HOME", oldHome) + _ = os.RemoveAll(tempHome) + }) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockP.ExpectedCalls = nil - mockP.On("GetModelName").Return("gemini-1.5-flash") - - r, win, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - - oldStdin := os.Stdin - os.Stdin = r - defer func() { os.Stdin = oldStdin }() - - go func() { - _, _ = io.WriteString(win, tt.input) - _ = win.Close() - }() - - model, proceed := w.askUserConfirmation(0.01, 100, 100, "gemini-3-flash-preview") - - if model != tt.wantModel { - t.Errorf("got model %q, want %q", model, tt.wantModel) - } - if proceed != tt.wantProc { - t.Errorf("got proceed %v, want %v", proceed, tt.wantProc) - } - mockP.AssertExpectations(t) - }) + mockP := new(mockProvider) + cfg := WrapperConfig{ + Provider: mockP, + BudgetDaily: budget, + EstimatedOutputTokens: 200, + SkipConfirmation: false, + OnConfirmation: func(result ConfirmationResult) (string, bool) { + return "suggested", true + }, + } + + w, err := NewCostAwareWrapper(cfg) + if err != nil { + t.Fatalf("NewCostAwareWrapper() error = %v", err) } -} -func TestCostAwareWrapper_WrapGenerate_SuggestedModel(t *testing.T) { - // Arrange - w, mockP, _ := setupTestWrapper(t, 1.0) - w.skipConfirmation = false ctx := context.Background() prompt := "large prompt" @@ -251,15 +225,6 @@ func TestCostAwareWrapper_WrapGenerate_SuggestedModel(t *testing.T) { mockP.On("GetModelName").Return("gemini-1.5-flash") mockP.On("CountTokens", mock.Anything, mock.Anything).Return(20000, nil) - r, win, _ := os.Pipe() - oldStdin := os.Stdin - os.Stdin = r - defer func() { os.Stdin = oldStdin }() - go func() { - _, _ = io.WriteString(win, "y\n") - _ = win.Close() - }() - // Act var usedModel string _, usage, err := w.WrapGenerate(ctx, "summarize", prompt, func(ctx context.Context, model, p string) (interface{}, *models.TokenUsage, error) { diff --git a/internal/infrastructure/ai/gemini/base.go b/internal/infrastructure/ai/gemini/base.go index 30844f9..259d883 100644 --- a/internal/infrastructure/ai/gemini/base.go +++ b/internal/infrastructure/ai/gemini/base.go @@ -9,13 +9,13 @@ import ( var _ ports.CostAwareAIProvider = (*GeminiProvider)(nil) -// GeminiProvider es una base compartida para todos los servicios de gemini que implementa la interfaz ports.CostAwareAIProvider +// GeminiProvider is a shared base for all Gemini services that implements the ports.CostAwareAIProvider interface type GeminiProvider struct { Client *genai.Client model string } -// NewGeminiProvider crea una nueva instancia de GeminiProvider +// NewGeminiProvider creates a new instance of GeminiProvider func NewGeminiProvider(client *genai.Client, model string) *GeminiProvider { return &GeminiProvider{ Client: client, @@ -23,7 +23,7 @@ func NewGeminiProvider(client *genai.Client, model string) *GeminiProvider { } } -// CountTokens implementa ports.CostAwareAIProvider +// CountTokens implements ports.CostAwareAIProvider func (g *GeminiProvider) CountTokens(ctx context.Context, prompt string) (int, error) { resp, err := g.Client.Models.CountTokens(ctx, g.model, genai.Text(prompt), nil) if err != nil { @@ -32,12 +32,12 @@ func (g *GeminiProvider) CountTokens(ctx context.Context, prompt string) (int, e return int(resp.TotalTokens), nil } -// GetModelName implementa ports.CostAwareAIProvider +// GetModelName implements ports.CostAwareAIProvider func (g *GeminiProvider) GetModelName() string { return g.model } -// GetProviderName implementa ports.CostAwareAIProvider +// GetProviderName implements ports.CostAwareAIProvider func (g *GeminiProvider) GetProviderName() string { return "gemini" } diff --git a/internal/infrastructure/ai/gemini/commit_summarizer_service.go b/internal/infrastructure/ai/gemini/commit_summarizer_service.go index ae5b062..a50a0a1 100644 --- a/internal/infrastructure/ai/gemini/commit_summarizer_service.go +++ b/internal/infrastructure/ai/gemini/commit_summarizer_service.go @@ -8,9 +8,9 @@ import ( "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" "google.golang.org/genai" ) @@ -22,7 +22,6 @@ type GeminiCommitSummarizer struct { wrapper *ai.CostAwareWrapper generateFn ai.GenerateFunc config *config.Config - trans *i18n.Translations } type ( @@ -47,11 +46,10 @@ type ( } ) -func NewGeminiCommitSummarizer(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (*GeminiCommitSummarizer, error) { +func NewGeminiCommitSummarizer(ctx context.Context, cfg *config.Config, onConfirmation ai.ConfirmationCallback) (*GeminiCommitSummarizer, error) { providerCfg, exists := cfg.AIProviders["gemini"] if !exists || providerCfg.APIKey == "" { - msg := trans.GetMessage("error_missing_api_key", 0, map[string]interface{}{"Provider": "gemini"}) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.ErrAPIKeyMissing } client, err := genai.NewClient(ctx, &genai.ClientConfig{ @@ -59,10 +57,7 @@ func NewGeminiCommitSummarizer(ctx context.Context, cfg *config.Config, trans *i Backend: genai.BackendGeminiAPI, }) if err != nil { - msg := trans.GetMessage("ai_service.error_ai_client", 0, map[string]interface{}{ - "Error": err, - }) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error creating AI client", err) } modelName := string(cfg.AIConfig.Models[config.AIGemini]) @@ -75,17 +70,16 @@ func NewGeminiCommitSummarizer(ctx context.Context, cfg *config.Config, trans *i service := &GeminiCommitSummarizer{ GeminiProvider: NewGeminiProvider(client, modelName), config: cfg, - trans: trans, } wrapper, err := ai.NewCostAwareWrapper(ai.WrapperConfig{ Provider: service, BudgetDaily: budgetDaily, - Trans: trans, EstimatedOutputTokens: 800, + OnConfirmation: onConfirmation, }) if err != nil { - return nil, fmt.Errorf("error creando wrapper: %w", err) + return nil, fmt.Errorf("error creating wrapper: %w", err) } service.wrapper = wrapper @@ -108,23 +102,18 @@ func (s *GeminiCommitSummarizer) defaultGenerate(ctx context.Context, mName stri func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info models.CommitInfo, count int) ([]models.CommitSuggestion, error) { if count <= 0 { - msg := s.trans.GetMessage("error_invalid_suggestion_count", 0, nil) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "invalid suggestion count", nil) } if len(info.Files) == 0 { - msg := s.trans.GetMessage("error_no_files", 0, nil) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.NewAppError(domainErrors.TypeGit, "no files to summarize", nil) } prompt := s.generatePrompt(s.config.Language, info, count) resp, usage, err := s.wrapper.WrapGenerate(ctx, "suggest-commits", prompt, s.generateFn) if err != nil { - msg := s.trans.GetMessage("error_generating_content", 0, map[string]interface{}{ - "Error": err.Error(), - }) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error generating content", err) } var responseText string @@ -135,7 +124,7 @@ func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info m } if responseText == "" { - return nil, fmt.Errorf("respuesta vacía de la IA") + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "empty response from AI", nil) } suggestions, err := s.parseSuggestionsJSON(responseText) @@ -145,11 +134,10 @@ func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info m if respLen > 500 { preview = responseText[:500] + "..." } - return nil, fmt.Errorf("error al parsear respuesta JSON de la IA (longitud: %d caracteres): %w\nPrimeros caracteres: %s", - respLen, err, preview) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, fmt.Sprintf("error parsing AI JSON response (length: %d): %s", respLen, preview), err) } if len(suggestions) == 0 { - return nil, fmt.Errorf("la IA no generó ninguna sugerencia") + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "AI generated no suggestions", nil) } for i := range suggestions { suggestions[i].Usage = usage @@ -162,14 +150,14 @@ func (s *GeminiCommitSummarizer) GenerateSuggestions(ctx context.Context, info m func (s *GeminiCommitSummarizer) parseSuggestionsJSON(responseText string) ([]models.CommitSuggestion, error) { if responseText == "" { - return nil, fmt.Errorf("texto de respuesta vacío de la IA") + return nil, fmt.Errorf("empty response text from AI") } responseText = ExtractJSON(responseText) var jsonSuggestions []CommitSuggestionJSON if err := json.Unmarshal([]byte(responseText), &jsonSuggestions); err != nil { - return nil, fmt.Errorf("error al parsear JSON: %w", err) + return nil, fmt.Errorf("error parsing JSON: %w", err) } suggestions := make([]models.CommitSuggestion, 0, len(jsonSuggestions)) @@ -207,7 +195,6 @@ func (s *GeminiCommitSummarizer) generatePrompt(locale string, info models.Commi info.TicketInfo.TicketTitle != "") filesFormatted := formatChanges(info.Files) - diffFormatted := fmt.Sprintf("```diff\n%s\n```", info.Diff) ticketInfo := "" @@ -236,8 +223,8 @@ func (s *GeminiCommitSummarizer) generatePrompt(locale string, info models.Commi issueInstructions := "" if info.IssueInfo != nil && info.IssueInfo.Number > 0 { num := info.IssueInfo.Number - issueInstructions = fmt.Sprintf(ai.GetIssueReferenceInstructions(locale), num, num, num, num, num, - num, num, num) + data := ai.PromptData{IssueNumber: num} + issueInstructions, _ = ai.RenderPrompt("issueInstructions", ai.GetIssueReferenceInstructions(locale), data) } else { issueInstructions = ai.GetNoIssueReferenceInstruction(locale) } @@ -247,27 +234,22 @@ func (s *GeminiCommitSummarizer) generatePrompt(locale string, info models.Commi technicalAnalysis = ai.GetTechnicalAnalysisInstruction(locale) } - if info.TicketInfo != nil && info.TicketInfo.TicketTitle != "" { - return fmt.Sprintf(promptTemplate, - count, - filesFormatted, - diffFormatted, - ticketInfo, - issueInstructions, - info.RecentHistory, - count, - ) - } - - return fmt.Sprintf(promptTemplate, - count, - filesFormatted, - diffFormatted, - issueInstructions, - info.RecentHistory, - technicalAnalysis, - count, - ) + data := ai.PromptData{ + Count: count, + Files: filesFormatted, + Diff: diffFormatted, + Ticket: ticketInfo, + History: info.RecentHistory, + Instructions: issueInstructions, + TechnicalInfo: technicalAnalysis, + } + + rendered, err := ai.RenderPrompt("commitPrompt", promptTemplate, data) + if err != nil { + return "" // Handle better in production, for now we fail silently or return empty + } + + return rendered } func formatChanges(files []string) string { @@ -292,7 +274,7 @@ func formatCriteria(criteria []string) string { return strings.Join(formattedCriteria, "\n") } -// formatResponse formatea la respuesta de la API de Gemini en una cadena. +// formatResponse formats the Gemini API response into a string. func formatResponse(resp *genai.GenerateContentResponse) string { if resp == nil || len(resp.Candidates) == 0 { return "" @@ -311,7 +293,7 @@ func formatResponse(resp *genai.GenerateContentResponse) string { return formattedContent.String() } -// ensureIssueReference asegura que todas las sugerencias incluyan la referencia al issue correcta +// ensureIssueReference ensures all suggestions include the correct issue reference func (s *GeminiCommitSummarizer) ensureIssueReference(suggestions []models.CommitSuggestion, issueNumber int) []models.CommitSuggestion { issuePattern := regexp.MustCompile(`\(#\d+\)`) diff --git a/internal/infrastructure/ai/gemini/commit_summarizer_service_test.go b/internal/infrastructure/ai/gemini/commit_summarizer_service_test.go index 524e3e3..967e0b5 100644 --- a/internal/infrastructure/ai/gemini/commit_summarizer_service_test.go +++ b/internal/infrastructure/ai/gemini/commit_summarizer_service_test.go @@ -8,7 +8,6 @@ import ( "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" "google.golang.org/genai" ) @@ -50,20 +49,13 @@ func TestGeminiCommitSummarizer(t *testing.T) { AIProviders: map[string]config.AIProviderConfig{}, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - assert.NoError(t, err) - // act - service, err := NewGeminiCommitSummarizer(ctx, cfg, trans) + service, err := NewGeminiCommitSummarizer(ctx, cfg, nil) // assert - if service != nil { - t.Error("El servicio no deberia crearse con API key vacia") - } - - if err == nil { - t.Error("Deberia retornar un error con API key vacia") - } + assert.Nil(t, service) + assert.Error(t, err) + assert.Contains(t, err.Error(), "API key is missing") }) t.Run("GenerateSuggestions with invalid count", func(t *testing.T) { @@ -73,11 +65,8 @@ func TestGeminiCommitSummarizer(t *testing.T) { AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test-api-key", Model: "gemini-2.5-flash", Temperature: 0.3, MaxTokens: 10000}}, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - if err != nil { - t.Fatalf("Error al crear el traductor: %v", err) - } - service, err := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, err := NewGeminiCommitSummarizer(ctx, cfg, nil) if err != nil { t.Fatalf("Error creando servicio: %v", err) } @@ -91,13 +80,9 @@ func TestGeminiCommitSummarizer(t *testing.T) { suggestions, err := service.GenerateSuggestions(ctx, info, 0) // assert - if suggestions != nil { - t.Error("No deberian generarse sugerencias con count <= 0") - } - - if err == nil { - t.Error("Deberia retornar un error con count <= 0") - } + assert.Nil(t, suggestions) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid suggestion count") }) t.Run("GenerateSuggestions no files", func(t *testing.T) { @@ -107,11 +92,8 @@ func TestGeminiCommitSummarizer(t *testing.T) { AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test-api-key", Model: "gemini-2.5-flash", Temperature: 0.3, MaxTokens: 10000}}, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - if err != nil { - t.Fatalf("Error creando traductor: %v", err) - } - service, err := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, err := NewGeminiCommitSummarizer(ctx, cfg, nil) if err != nil { t.Fatalf("Error creando servicio: %v", err) } @@ -125,12 +107,9 @@ func TestGeminiCommitSummarizer(t *testing.T) { suggestions, err := service.GenerateSuggestions(ctx, info, 1) // assert - if suggestions != nil { - t.Error("No deberian generarse sugerencias sin archivos") - } - if err == nil { - t.Error("Deberia retornar un error sin archivos") - } + assert.Nil(t, suggestions) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no files to summarize") }) t.Run("ParseSuggestionsJSON correct format", func(t *testing.T) { @@ -140,9 +119,8 @@ func TestGeminiCommitSummarizer(t *testing.T) { AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test-api-key", Model: "gemini-2.5-flash", Temperature: 0.3, MaxTokens: 10000}}, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - assert.NoError(t, err) - service, err := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, err := NewGeminiCommitSummarizer(ctx, cfg, nil) assert.NoError(t, err) resp := &genai.GenerateContentResponse{ @@ -214,9 +192,8 @@ func TestGeminiCommitSummarizer(t *testing.T) { AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test-api-key", Model: "gemini-2.5-flash", Temperature: 0.3, MaxTokens: 10000}}, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - assert.NoError(t, err) - service, err := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, err := NewGeminiCommitSummarizer(ctx, cfg, nil) assert.NoError(t, err) info := models.CommitInfo{ @@ -245,9 +222,8 @@ func TestGeminiCommitSummarizer(t *testing.T) { AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test-api-key", Model: "gemini-2.5-flash", Temperature: 0.3, MaxTokens: 10000}}, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - assert.NoError(t, err) - service, err := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, err := NewGeminiCommitSummarizer(ctx, cfg, nil) assert.NoError(t, err) info := models.CommitInfo{ @@ -276,9 +252,8 @@ func TestGeminiCommitSummarizer(t *testing.T) { AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test-api-key", Model: "gemini-2.5-flash", Temperature: 0.3, MaxTokens: 10000}}, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - assert.NoError(t, err) - service, err := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, err := NewGeminiCommitSummarizer(ctx, cfg, nil) assert.NoError(t, err) info := models.CommitInfo{ @@ -292,10 +267,10 @@ func TestGeminiCommitSummarizer(t *testing.T) { t.Logf("Prompt generado:\n%s", prompt) // assert - assert.Contains(t, prompt, "Generate 3 suggestions now", "El prompt debe incluir la instrucción de generación") - assert.Contains(t, prompt, "Modified Files", "Debe incluir la sección de archivos modificados") - assert.Contains(t, prompt, "Code Changes", "Debe incluir la sección de diff") - assert.Contains(t, prompt, "technical analysis", "Debe incluir la sección de análisis técnico") + assert.Contains(t, prompt, "Generate 3 suggestions now", "The prompt should include the generation instruction") + assert.Contains(t, prompt, "Modified Files", "Should include the modified files section") + assert.Contains(t, prompt, "Code Changes", "Should include the diff section") + assert.Contains(t, prompt, "technical analysis", "Should include the technical analysis section") }) t.Run("parseSuggestionsJSON with nil response", func(t *testing.T) { @@ -309,7 +284,7 @@ func TestGeminiCommitSummarizer(t *testing.T) { // assert assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "texto de respuesta vacío") + assert.Contains(t, err.Error(), "empty response text from AI") }) t.Run("parseSuggestionsJSON with empty candidates", func(t *testing.T) { @@ -325,7 +300,7 @@ func TestGeminiCommitSummarizer(t *testing.T) { // assert assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "texto de respuesta vacío") + assert.Contains(t, err.Error(), "empty response text from AI") }) t.Run("parseSuggestionsJSON with invalid JSON", func(t *testing.T) { @@ -334,8 +309,8 @@ func TestGeminiCommitSummarizer(t *testing.T) { cfg := &config.Config{ AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test-api-key", Model: "gemini-2.5-flash", Temperature: 0.3, MaxTokens: 10000}}, } - trans, _ := i18n.NewTranslations("es", "../../../i18n/locales/") - service, _ := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, _ := NewGeminiCommitSummarizer(ctx, cfg, nil) resp := &genai.GenerateContentResponse{ Candidates: []*genai.Candidate{ @@ -349,15 +324,15 @@ func TestGeminiCommitSummarizer(t *testing.T) { // assert assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "parsear JSON") + assert.Contains(t, err.Error(), "error parsing JSON") }) t.Run("parseSuggestionsJSON status passthrough", func(t *testing.T) { // arrange ctx := context.Background() cfg := &config.Config{AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test-api-key", Model: "gemini-2.5-flash", Temperature: 0.3, MaxTokens: 10000}}} - trans, _ := i18n.NewTranslations("es", "../../../i18n/locales/") - service, _ := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, _ := NewGeminiCommitSummarizer(ctx, cfg, nil) testCases := []struct { inputStatus string @@ -425,6 +400,11 @@ func TestGenerateSuggestions_HappyPath(t *testing.T) { } }() oldHome := os.Getenv("HOME") + cfg := &config.Config{ + AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test"}}, + AIConfig: config.AIConfig{Models: map[config.AI]config.Model{config.AIGemini: "gemini-pro"}}, + Language: "en", + } _ = os.Setenv("HOME", tmpHome) defer func() { if err := os.Setenv("HOME", oldHome); err != nil { @@ -433,13 +413,8 @@ func TestGenerateSuggestions_HappyPath(t *testing.T) { }() ctx := context.Background() - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales/") - cfg := &config.Config{ - AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test"}}, - AIConfig: config.AIConfig{Models: map[config.AI]config.Model{config.AIGemini: "gemini-pro"}}, - Language: "en", - } - service, _ := NewGeminiCommitSummarizer(ctx, cfg, trans) + // act + service, _ := NewGeminiCommitSummarizer(ctx, cfg, nil) service.wrapper.SetSkipConfirmation(true) t.Run("successful suggestions generation", func(t *testing.T) { @@ -465,11 +440,10 @@ func TestGenerateSuggestions_HappyPath(t *testing.T) { } func TestGeneratePrompt_WithCriteria(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales/") cfg := &config.Config{ AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test"}}, } - service := &GeminiCommitSummarizer{trans: trans, config: cfg} + service := &GeminiCommitSummarizer{config: cfg} t.Run("formats criteria correctly in English", func(t *testing.T) { info := models.CommitInfo{ diff --git a/internal/infrastructure/ai/gemini/gemini_factory.go b/internal/infrastructure/ai/gemini/gemini_factory.go deleted file mode 100644 index 14572a3..0000000 --- a/internal/infrastructure/ai/gemini/gemini_factory.go +++ /dev/null @@ -1,64 +0,0 @@ -package gemini - -import ( - "context" - "fmt" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" -) - -// GeminiProviderFactory implementa AIProviderFactory para Gemini -type GeminiProviderFactory struct{} - -// NewGeminiProviderFactory crea una nueva factory para Gemini -func NewGeminiProviderFactory() *GeminiProviderFactory { - return &GeminiProviderFactory{} -} - -// CreateCommitSummarizer crea un servicio Gemini para sugerencias de commits -func (f *GeminiProviderFactory) CreateCommitSummarizer( - ctx context.Context, - cfg *config.Config, - trans *i18n.Translations, -) (ports.CommitSummarizer, error) { - return NewGeminiCommitSummarizer(ctx, cfg, trans) -} - -// CreatePRSummarizer crea un servicio Gemini para resumir PRs -func (f *GeminiProviderFactory) CreatePRSummarizer( - ctx context.Context, - cfg *config.Config, - trans *i18n.Translations, -) (ports.PRSummarizer, error) { - return NewGeminiPRSummarizer(ctx, cfg, trans) -} - -// CreateIssueContentGenerator crea un servicio Gemini para generar contenido de issues -func (f *GeminiProviderFactory) CreateIssueContentGenerator( - ctx context.Context, - cfg *config.Config, - trans *i18n.Translations, -) (ports.IssueContentGenerator, error) { - return NewGeminiIssueContentGenerator(ctx, cfg, trans) -} - -// ValidateConfig valida la configuración de Gemini -func (f *GeminiProviderFactory) ValidateConfig(cfg *config.Config) error { - providerCfg, exists := cfg.AIProviders["gemini"] - if !exists { - return fmt.Errorf("configuracion de gemini no encontrada") - } - - if providerCfg.APIKey == "" { - return fmt.Errorf("gemini API key es requerida") - } - - return nil -} - -// Name retorna el nombre del proveedor -func (f *GeminiProviderFactory) Name() string { - return "gemini" -} diff --git a/internal/infrastructure/ai/gemini/gemini_factory_test.go b/internal/infrastructure/ai/gemini/gemini_factory_test.go deleted file mode 100644 index 1081a36..0000000 --- a/internal/infrastructure/ai/gemini/gemini_factory_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package gemini - -import ( - "context" - "testing" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGeminiProviderFactory(t *testing.T) { - factory := NewGeminiProviderFactory() - trans, err := i18n.NewTranslations("en", "../../../i18n/locales/") - require.NoError(t, err) - - t.Run("Name", func(t *testing.T) { - assert.Equal(t, "gemini", factory.Name()) - }) - - t.Run("ValidateConfig - Valid", func(t *testing.T) { - cfg := &config.Config{ - AIProviders: map[string]config.AIProviderConfig{ - "gemini": {APIKey: "test-key"}, - }, - } - err := factory.ValidateConfig(cfg) - assert.NoError(t, err) - }) - - t.Run("ValidateConfig - Missing Provider", func(t *testing.T) { - cfg := &config.Config{ - AIProviders: map[string]config.AIProviderConfig{}, - } - err := factory.ValidateConfig(cfg) - assert.Error(t, err) - assert.Contains(t, err.Error(), "configuracion de gemini no encontrada") - }) - - t.Run("ValidateConfig - Missing API Key", func(t *testing.T) { - cfg := &config.Config{ - AIProviders: map[string]config.AIProviderConfig{ - "gemini": {APIKey: ""}, - }, - } - err := factory.ValidateConfig(cfg) - assert.Error(t, err) - assert.Contains(t, err.Error(), "gemini API key es requerida") - }) - - t.Run("CreateServices - Missing API Key Errors", func(t *testing.T) { - cfg := &config.Config{ - AIProviders: map[string]config.AIProviderConfig{}, - } - ctx := context.Background() - - _, err := factory.CreateCommitSummarizer(ctx, cfg, trans) - assert.Error(t, err) - - _, err = factory.CreatePRSummarizer(ctx, cfg, trans) - assert.Error(t, err) - - _, err = factory.CreateIssueContentGenerator(ctx, cfg, trans) - assert.Error(t, err) - }) -} diff --git a/internal/infrastructure/ai/gemini/helper.go b/internal/infrastructure/ai/gemini/helper.go index 1f1f1c4..3486468 100644 --- a/internal/infrastructure/ai/gemini/helper.go +++ b/internal/infrastructure/ai/gemini/helper.go @@ -9,7 +9,7 @@ import ( "google.golang.org/genai" ) -// extractUsage extrae los metadatos de uso de la respuesta de Gemini +// extractUsage extracts usage metadata from the Gemini response func extractUsage(resp *genai.GenerateContentResponse) *models.TokenUsage { if resp == nil || resp.UsageMetadata == nil { return nil @@ -21,7 +21,7 @@ func extractUsage(resp *genai.GenerateContentResponse) *models.TokenUsage { } } -// GetGenerateConfig retorna la configuración óptima para el modelo, activando Thinking Mode si es compatible. +// GetGenerateConfig returns the optimal configuration for the model, enabling Thinking Mode if compatible. func GetGenerateConfig(modelName string, responseType string) *genai.GenerateContentConfig { config := &genai.GenerateContentConfig{ Temperature: float32Ptr(0.3), @@ -43,8 +43,8 @@ func GetGenerateConfig(modelName string, responseType string) *genai.GenerateCon return config } -// ExtractJSON intenta extraer un bloque JSON válido de un texto, manejando bloques de código markdown -// y posible texto extra que los modelos con "Thinking" pueden generar. +// ExtractJSON attempts to extract a valid JSON block from text, handling markdown code blocks +// and possible extra text that models with "Thinking" mode might generate. func ExtractJSON(text string) string { text = strings.TrimSpace(text) @@ -140,8 +140,8 @@ func ExtractJSON(text string) string { var jsonStringRegex = regexp.MustCompile(`"(?:\\.|[^"\\])*"`) -// SanitizeJSON limpia el JSON malformado que a veces generan los LLMs, -// como saltos de línea sin escapar dentro de Literales de Cadena. +// 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 strings.ReplaceAll(m, "\n", "\\n") diff --git a/internal/infrastructure/ai/gemini/issue_content_generator.go b/internal/infrastructure/ai/gemini/issue_content_generator.go index d10af4a..b7522f3 100644 --- a/internal/infrastructure/ai/gemini/issue_content_generator.go +++ b/internal/infrastructure/ai/gemini/issue_content_generator.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" "google.golang.org/genai" ) @@ -19,16 +19,14 @@ type GeminiIssueContentGenerator struct { wrapper *ai.CostAwareWrapper generateFn ai.GenerateFunc config *config.Config - trans *i18n.Translations } var _ ports.IssueContentGenerator = (*GeminiIssueContentGenerator)(nil) -func NewGeminiIssueContentGenerator(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (*GeminiIssueContentGenerator, error) { +func NewGeminiIssueContentGenerator(ctx context.Context, cfg *config.Config, onConfirmation ai.ConfirmationCallback) (*GeminiIssueContentGenerator, error) { providerCfg, exists := cfg.AIProviders["gemini"] if !exists || providerCfg.APIKey == "" { - msg := trans.GetMessage("error_missing_api_key", 0, map[string]interface{}{"Provider": "gemini"}) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.ErrAPIKeyMissing } client, err := genai.NewClient(ctx, &genai.ClientConfig{ @@ -36,10 +34,7 @@ func NewGeminiIssueContentGenerator(ctx context.Context, cfg *config.Config, tra Backend: genai.BackendGeminiAPI, }) if err != nil { - msg := trans.GetMessage("ai_service.error_ai_client", 0, map[string]interface{}{ - "Error": err, - }) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error creating AI client", err) } modelName := string(cfg.AIConfig.Models[config.AIGemini]) @@ -52,17 +47,16 @@ func NewGeminiIssueContentGenerator(ctx context.Context, cfg *config.Config, tra service := &GeminiIssueContentGenerator{ GeminiProvider: NewGeminiProvider(client, modelName), config: cfg, - trans: trans, } wrapper, err := ai.NewCostAwareWrapper(ai.WrapperConfig{ Provider: service, BudgetDaily: budgetDaily, - Trans: trans, EstimatedOutputTokens: 600, + OnConfirmation: onConfirmation, }) if err != nil { - return nil, fmt.Errorf("error creando wrapper: %w", err) + return nil, fmt.Errorf("error creating wrapper: %w", err) } service.wrapper = wrapper @@ -83,13 +77,13 @@ func (s *GeminiIssueContentGenerator) defaultGenerate(ctx context.Context, mName return resp, usage, nil } -// GenerateIssueContent genera contenido de issue usando Gemini AI. +// GenerateIssueContent generates issue content using Gemini AI. func (s *GeminiIssueContentGenerator) GenerateIssueContent(ctx context.Context, request models.IssueGenerationRequest) (*models.IssueGenerationResult, error) { prompt := s.buildIssuePrompt(request) resp, usage, err := s.wrapper.WrapGenerate(ctx, "generate-issue", prompt, s.generateFn) if err != nil { - return nil, fmt.Errorf("error generando contenido de issue: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error generating issue content", err) } var responseText string @@ -100,12 +94,12 @@ func (s *GeminiIssueContentGenerator) GenerateIssueContent(ctx context.Context, } if responseText == "" { - return nil, fmt.Errorf("ningún contenido generado por IA") + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "empty response from AI", nil) } result, err := s.parseIssueResponse(responseText) if err != nil { - return nil, fmt.Errorf("error al parsear la respuesta de la IA: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error parsing AI response", err) } result.Usage = usage @@ -113,7 +107,7 @@ func (s *GeminiIssueContentGenerator) GenerateIssueContent(ctx context.Context, return result, nil } -// buildIssuePrompt construye el prompt para generar contenido de issue. +// buildIssuePrompt builds the prompt to generate issue content. func (s *GeminiIssueContentGenerator) buildIssuePrompt(request models.IssueGenerationRequest) string { var sb strings.Builder @@ -141,14 +135,23 @@ func (s *GeminiIssueContentGenerator) buildIssuePrompt(request models.IssueGener sb.WriteString(fmt.Sprintf("User Hint: %s\n\n", request.Hint)) } - template := ai.GetIssuePromptTemplate(request.Language) - return fmt.Sprintf(template, sb.String()) + templateStr := ai.GetIssuePromptTemplate(request.Language) + data := ai.PromptData{ + IssueInfo: sb.String(), + } + + rendered, err := ai.RenderPrompt("issuePrompt", templateStr, data) + if err != nil { + return "" + } + + return rendered } -// parseIssueResponse parsea la respuesta JSON de Gemini. +// parseIssueResponse parses the Gemini JSON response. func (s *GeminiIssueContentGenerator) parseIssueResponse(content string) (*models.IssueGenerationResult, error) { if content == "" { - return nil, fmt.Errorf("empty response from AI") + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "empty response from AI", nil) } content = ExtractJSON(content) @@ -183,7 +186,7 @@ func (s *GeminiIssueContentGenerator) parseIssueResponse(content string) (*model return result, nil } -// cleanLabels limpia y valida las labels, mantiene solo las permitidas. +// cleanLabels cleans and validates labels, keeping only the allowed ones. func (s *GeminiIssueContentGenerator) cleanLabels(labels []string) []string { allowedLabels := map[string]bool{ "feature": true, diff --git a/internal/infrastructure/ai/gemini/issue_content_generator_test.go b/internal/infrastructure/ai/gemini/issue_content_generator_test.go index 274ec18..fe6571f 100644 --- a/internal/infrastructure/ai/gemini/issue_content_generator_test.go +++ b/internal/infrastructure/ai/gemini/issue_content_generator_test.go @@ -7,24 +7,19 @@ import ( "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/require" "google.golang.org/genai" ) func TestNewGeminiIssueContentGenerator(t *testing.T) { - trans, err := i18n.NewTranslations("en", "../../../i18n/locales/") - require.NoError(t, err) - t.Run("should return error if API key is missing", func(t *testing.T) { cfg := &config.Config{ AIProviders: map[string]config.AIProviderConfig{}, } - gen, err := NewGeminiIssueContentGenerator(context.Background(), cfg, trans) + gen, err := NewGeminiIssueContentGenerator(context.Background(), cfg, nil) assert.Error(t, err) assert.Nil(t, gen) - assert.Contains(t, err.Error(), "API key is not configured") + assert.Contains(t, err.Error(), "API key is missing") }) t.Run("should create generator if API key is present", func(t *testing.T) { @@ -33,18 +28,16 @@ func TestNewGeminiIssueContentGenerator(t *testing.T) { "gemini": {APIKey: "fake-key"}, }, } - gen, err := NewGeminiIssueContentGenerator(context.Background(), cfg, trans) + gen, err := NewGeminiIssueContentGenerator(context.Background(), cfg, nil) assert.NoError(t, err) assert.NotNil(t, gen) }) } func TestBuildIssuePrompt(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales/") cfg := &config.Config{} gen := &GeminiIssueContentGenerator{ config: cfg, - trans: trans, } tests := []struct { @@ -140,6 +133,7 @@ func TestParseIssueResponse(t *testing.T) { result, err := gen.parseIssueResponse(formatResponse(resp)) assert.Error(t, err) assert.Nil(t, result) + assert.Contains(t, err.Error(), "empty response from AI") }) } @@ -194,12 +188,11 @@ func TestGenerateIssueContent_HappyPath(t *testing.T) { }() ctx := context.Background() - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales/") cfg := &config.Config{ AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test"}}, AIConfig: config.AIConfig{Models: map[config.AI]config.Model{config.AIGemini: "gemini-pro"}}, } - gen, _ := NewGeminiIssueContentGenerator(ctx, cfg, trans) + gen, _ := NewGeminiIssueContentGenerator(ctx, cfg, nil) gen.wrapper.SetSkipConfirmation(true) t.Run("successful issue content generation", func(t *testing.T) { diff --git a/internal/infrastructure/ai/gemini/pull_requests_summarizer_service.go b/internal/infrastructure/ai/gemini/pull_requests_summarizer_service.go index 2295066..a0d9850 100644 --- a/internal/infrastructure/ai/gemini/pull_requests_summarizer_service.go +++ b/internal/infrastructure/ai/gemini/pull_requests_summarizer_service.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" "google.golang.org/genai" ) @@ -21,7 +21,6 @@ type GeminiPRSummarizer struct { wrapper *ai.CostAwareWrapper generateFn ai.GenerateFunc config *config.Config - trans *i18n.Translations } type PRSummaryJSON struct { @@ -30,21 +29,17 @@ type PRSummaryJSON struct { Labels []string `json:"labels"` } -func NewGeminiPRSummarizer(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (*GeminiPRSummarizer, error) { +func NewGeminiPRSummarizer(ctx context.Context, cfg *config.Config, onConfirmation ai.ConfirmationCallback) (*GeminiPRSummarizer, error) { providerCfg, exists := cfg.AIProviders["gemini"] if !exists || providerCfg.APIKey == "" { - msg := trans.GetMessage("error_missing_api_key", 0, map[string]interface{}{"Provider": "gemini"}) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.ErrAPIKeyMissing } client, err := genai.NewClient(ctx, &genai.ClientConfig{ APIKey: providerCfg.APIKey, Backend: genai.BackendGeminiAPI, }) if err != nil { - msg := trans.GetMessage("ai_service.error_ai_client", 0, map[string]interface{}{ - "Error": err, - }) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error creating AI client", err) } modelName := string(cfg.AIConfig.Models[config.AIGemini]) @@ -56,14 +51,13 @@ func NewGeminiPRSummarizer(ctx context.Context, cfg *config.Config, trans *i18n. service := &GeminiPRSummarizer{ GeminiProvider: NewGeminiProvider(client, modelName), config: cfg, - trans: trans, } wrapper, err := ai.NewCostAwareWrapper(ai.WrapperConfig{ Provider: service, BudgetDaily: budgetDaily, - Trans: trans, EstimatedOutputTokens: 500, + OnConfirmation: onConfirmation, }) if err != nil { return nil, fmt.Errorf("error creando wrapper: %w", err) @@ -92,7 +86,7 @@ func (gps *GeminiPRSummarizer) GeneratePRSummary(ctx context.Context, prContent resp, usage, err := gps.wrapper.WrapGenerate(ctx, "summarize-pr", prompt, gps.generateFn) if err != nil { - return models.PRSummary{}, fmt.Errorf("error al generar resumen de PR: %w", err) + return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeAI, "error generating PR summary", err) } var responseText string @@ -102,12 +96,12 @@ func (gps *GeminiPRSummarizer) GeneratePRSummary(ctx context.Context, prContent responseText = s } if responseText == "" { - return models.PRSummary{}, fmt.Errorf("respuesta vacía de la IA") + return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeAI, "empty response from AI", nil) } responseText = ExtractJSON(responseText) var jsonSummary PRSummaryJSON if err := json.Unmarshal([]byte(responseText), &jsonSummary); err != nil { - return models.PRSummary{}, fmt.Errorf("error al parsear JSON de PR: %w", err) + return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeAI, "error parsing AI JSON response", err) } if strings.TrimSpace(jsonSummary.Title) == "" { respLen := len(responseText) @@ -115,7 +109,7 @@ func (gps *GeminiPRSummarizer) GeneratePRSummary(ctx context.Context, prContent if respLen > 500 { preview = responseText[:500] + "..." } - return models.PRSummary{}, fmt.Errorf("la IA no generó un título para el PR. Respuesta (longitud: %d): %s", respLen, preview) + return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeAI, fmt.Sprintf("AI generated no PR title (length: %d): %s", respLen, preview), nil) } return models.PRSummary{ Title: jsonSummary.Title, @@ -126,6 +120,15 @@ func (gps *GeminiPRSummarizer) GeneratePRSummary(ctx context.Context, prContent } func (gps *GeminiPRSummarizer) generatePRPrompt(prContent string) string { - template := ai.GetPRPromptTemplate(gps.config.Language) - return fmt.Sprintf(template, prContent) + templateStr := ai.GetPRPromptTemplate(gps.config.Language) + data := ai.PromptData{ + PRContent: prContent, + } + + rendered, err := ai.RenderPrompt("prPrompt", templateStr, data) + if err != nil { + return "" + } + + return rendered } diff --git a/internal/infrastructure/ai/gemini/pull_requests_summarizer_service_test.go b/internal/infrastructure/ai/gemini/pull_requests_summarizer_service_test.go index 2695606..0ba6ca3 100644 --- a/internal/infrastructure/ai/gemini/pull_requests_summarizer_service_test.go +++ b/internal/infrastructure/ai/gemini/pull_requests_summarizer_service_test.go @@ -7,7 +7,6 @@ import ( "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" "google.golang.org/genai" ) @@ -20,15 +19,13 @@ func TestGeminiPRSummarizer(t *testing.T) { AIProviders: map[string]config.AIProviderConfig{}, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - assert.NoError(t, err) - // Act - summarizer, err := NewGeminiPRSummarizer(ctx, cfg, trans) + summarizer, err := NewGeminiPRSummarizer(ctx, cfg, nil) // Assert - assert.Nil(t, summarizer, "El summarizer no debería crearse con API key vacía") - assert.Error(t, err, "Debería retornar un error con API key vacía") + assert.Nil(t, summarizer) + assert.Error(t, err) + assert.Contains(t, err.Error(), "API key is missing") }) t.Run("GeneratePRSummary with empty prompt", func(t *testing.T) { @@ -43,10 +40,7 @@ func TestGeminiPRSummarizer(t *testing.T) { }, } - trans, err := i18n.NewTranslations("es", "../../../i18n/locales/") - assert.NoError(t, err) - - summarizer, err := NewGeminiPRSummarizer(ctx, cfg, trans) + summarizer, err := NewGeminiPRSummarizer(ctx, cfg, nil) assert.NoError(t, err, "Error creando summarizer") // Act @@ -65,10 +59,7 @@ func TestGeminiPRSummarizer(t *testing.T) { Language: "en", } - trans, err := i18n.NewTranslations("en", "../../../i18n/locales/") - assert.NoError(t, err) - - summarizer, err := NewGeminiPRSummarizer(ctx, cfg, trans) + summarizer, err := NewGeminiPRSummarizer(ctx, cfg, nil) assert.NoError(t, err) prContent := "Some PR content to summarize" @@ -144,12 +135,11 @@ func TestGeneratePRSummary_HappyPath(t *testing.T) { }() ctx := context.Background() - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales/") cfg := &config.Config{ AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test"}}, AIConfig: config.AIConfig{Models: map[config.AI]config.Model{config.AIGemini: "gemini-pro"}}, } - summarizer, _ := NewGeminiPRSummarizer(ctx, cfg, trans) + summarizer, _ := NewGeminiPRSummarizer(ctx, cfg, nil) summarizer.wrapper.SetSkipConfirmation(true) t.Run("successful PR summary", func(t *testing.T) { @@ -162,7 +152,7 @@ func TestGeneratePRSummary_HappyPath(t *testing.T) { }, &models.TokenUsage{TotalTokens: 50}, nil } - summary, err := summarizer.GeneratePRSummary(ctx, "content") + summary, err := summarizer.GeneratePRSummary(ctx, "successful content") assert.NoError(t, err) assert.Equal(t, "Awesome Feature", summary.Title) @@ -180,10 +170,10 @@ func TestGeneratePRSummary_HappyPath(t *testing.T) { }, &models.TokenUsage{}, nil } - summary, err := summarizer.GeneratePRSummary(ctx, "content") + summary, err := summarizer.GeneratePRSummary(ctx, "content with empty title") assert.Error(t, err) - assert.Contains(t, err.Error(), "respuesta vacía") + assert.Contains(t, err.Error(), "no PR title") assert.Empty(t, summary.Title) }) } diff --git a/internal/infrastructure/ai/gemini/release_generator.go b/internal/infrastructure/ai/gemini/release_generator.go index fc2c976..beb0b9c 100644 --- a/internal/infrastructure/ai/gemini/release_generator.go +++ b/internal/infrastructure/ai/gemini/release_generator.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" "google.golang.org/genai" ) @@ -20,7 +20,6 @@ type ReleaseNotesGenerator struct { *GeminiProvider wrapper *ai.CostAwareWrapper generateFn ai.GenerateFunc - trans *i18n.Translations lang string owner string repo string @@ -34,13 +33,10 @@ type ReleaseNotesJSON struct { Contributors string `json:"contributors"` } -func NewReleaseNotesGenerator(ctx context.Context, cfg *config.Config, trans *i18n.Translations, owner, repo string) (*ReleaseNotesGenerator, error) { +func NewReleaseNotesGenerator(ctx context.Context, cfg *config.Config, onConfirmation ai.ConfirmationCallback, owner, repo string) (*ReleaseNotesGenerator, error) { providerCfg, exists := cfg.AIProviders["gemini"] if !exists || providerCfg.APIKey == "" { - msg := trans.GetMessage("error_missing_api_key", 0, map[string]interface{}{ - "Provider": "gemini", - }) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.ErrAPIKeyMissing } client, err := genai.NewClient(ctx, &genai.ClientConfig{ @@ -48,10 +44,7 @@ func NewReleaseNotesGenerator(ctx context.Context, cfg *config.Config, trans *i1 Backend: genai.BackendGeminiAPI, }) if err != nil { - msg := trans.GetMessage("ai_service.error_ai_client", 0, map[string]interface{}{ - "Error": err, - }) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error creating AI client", err) } modelName := string(cfg.AIConfig.Models[config.AIGemini]) @@ -64,7 +57,6 @@ func NewReleaseNotesGenerator(ctx context.Context, cfg *config.Config, trans *i1 service := &ReleaseNotesGenerator{ GeminiProvider: NewGeminiProvider(client, modelName), lang: cfg.Language, - trans: trans, owner: owner, repo: repo, } @@ -72,11 +64,11 @@ func NewReleaseNotesGenerator(ctx context.Context, cfg *config.Config, trans *i1 wrapper, err := ai.NewCostAwareWrapper(ai.WrapperConfig{ Provider: service, BudgetDaily: budgetDaily, - Trans: trans, EstimatedOutputTokens: 700, + OnConfirmation: onConfirmation, }) if err != nil { - return nil, fmt.Errorf("error creando wrapper: %w", err) + return nil, fmt.Errorf("error creating wrapper: %w", err) } service.wrapper = wrapper @@ -102,7 +94,7 @@ func (g *ReleaseNotesGenerator) GenerateNotes(ctx context.Context, release *mode resp, usage, err := g.wrapper.WrapGenerate(ctx, "generate-release", prompt, g.generateFn) if err != nil { - return nil, fmt.Errorf("error generando release notes: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error generating release notes", err) } var responseText string @@ -113,13 +105,12 @@ func (g *ReleaseNotesGenerator) GenerateNotes(ctx context.Context, release *mode } if responseText == "" { - msg := g.trans.GetMessage("ai_service.error_no_ai_response", 0, nil) - return nil, fmt.Errorf("%s", msg) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "empty response from AI", nil) } notes, err := g.parseJSONResponse(responseText, release) if err != nil { - return nil, fmt.Errorf("error al parsear respuesta JSON de release notes: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error parsing AI JSON response", err) } notes.Usage = usage @@ -128,18 +119,26 @@ func (g *ReleaseNotesGenerator) GenerateNotes(ctx context.Context, release *mode } func (g *ReleaseNotesGenerator) buildPrompt(release *models.Release) string { - template := ai.GetReleasePromptTemplate(g.lang) + templateStr := ai.GetReleasePromptTemplate(g.lang) changes := g.formatChangesForPrompt(release) - return fmt.Sprintf(template, - g.owner, - g.repo, - release.PreviousVersion, - release.Version, - release.VersionBump, - changes, - ) + data := ai.PromptData{ + RepoOwner: g.owner, + RepoName: g.repo, + PreviousVersion: release.PreviousVersion, + CurrentVersion: release.PreviousVersion, + LatestVersion: release.Version, + ReleaseDate: string(release.VersionBump), + Changelog: changes, + } + + rendered, err := ai.RenderPrompt("releasePrompt", templateStr, data) + if err != nil { + return "" + } + + return rendered } func (g *ReleaseNotesGenerator) formatChangesForPrompt(release *models.Release) string { @@ -249,7 +248,7 @@ func (g *ReleaseNotesGenerator) parseJSONResponse(content string, release *model var jsonNotes ReleaseNotesJSON if err := json.Unmarshal([]byte(content), &jsonNotes); err != nil { - return nil, fmt.Errorf("error al parsear JSON de release notes: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "error parsing AI JSON response", err) } notes := &models.ReleaseNotes{ diff --git a/internal/infrastructure/ai/gemini/release_generator_test.go b/internal/infrastructure/ai/gemini/release_generator_test.go index 4196e3f..53f1290 100644 --- a/internal/infrastructure/ai/gemini/release_generator_test.go +++ b/internal/infrastructure/ai/gemini/release_generator_test.go @@ -8,7 +8,6 @@ import ( "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" "google.golang.org/genai" ) @@ -27,10 +26,8 @@ func TestNewReleaseNotesGenerator(t *testing.T) { } // Act - trans, err := i18n.NewTranslations("en", "") - assert.NoError(t, err) - - generator, err := NewReleaseNotesGenerator(ctx, cfg, trans, "test-owner", "test-repo") + // Act + generator, err := NewReleaseNotesGenerator(ctx, cfg, nil, "test-owner", "test-repo") // Assert assert.NoError(t, err) @@ -45,16 +42,13 @@ func TestNewReleaseNotesGenerator_MissingKey(t *testing.T) { cfg := &config.Config{ AIProviders: map[string]config.AIProviderConfig{}, } - trans, err := i18n.NewTranslations("en", "") - assert.NoError(t, err) - // Act - generator, err := NewReleaseNotesGenerator(ctx, cfg, trans, "test-owner", "test-repo") + generator, err := NewReleaseNotesGenerator(ctx, cfg, nil, "test-owner", "test-repo") // Assert assert.Error(t, err) assert.Nil(t, generator) - assert.Contains(t, err.Error(), "The gemini API key is not configured") + assert.Contains(t, err.Error(), "API key is missing") } func TestBuildPrompt(t *testing.T) { @@ -226,7 +220,7 @@ func TestParseJSONResponse(t *testing.T) { // Assert assert.Error(t, err) assert.Nil(t, notes) - assert.Contains(t, err.Error(), "parsear JSON") + assert.Contains(t, err.Error(), "error parsing AI JSON response") }) t.Run("handles JSON with code fences", func(t *testing.T) { @@ -273,12 +267,12 @@ func TestGenerateNotes(t *testing.T) { }() ctx := context.Background() - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales/") cfg := &config.Config{ AIProviders: map[string]config.AIProviderConfig{"gemini": {APIKey: "test"}}, AIConfig: config.AIConfig{Models: map[config.AI]config.Model{config.AIGemini: "gemini-pro"}}, } - generator, _ := NewReleaseNotesGenerator(ctx, cfg, trans, "owner", "repo") + // act + generator, _ := NewReleaseNotesGenerator(ctx, cfg, nil, "owner", "repo") generator.wrapper.SetSkipConfirmation(true) t.Run("successful generation", func(t *testing.T) { @@ -330,6 +324,6 @@ func TestGenerateNotes(t *testing.T) { // Assert assert.Error(t, err) - assert.Contains(t, err.Error(), "No response from AI") + assert.Contains(t, err.Error(), "empty response from AI") }) } diff --git a/internal/infrastructure/ai/prompts.go b/internal/infrastructure/ai/prompts.go index 20c345f..8e633f3 100644 --- a/internal/infrastructure/ai/prompts.go +++ b/internal/infrastructure/ai/prompts.go @@ -1,38 +1,77 @@ package ai import ( + "bytes" "fmt" "strings" + "text/template" "github.com/Tomas-vilte/MateCommit/internal/domain/models" ) const ( - issueReferenceInstructionsES = `Si hay un issue asociado (#%d), DEBES incluir la referencia en el título del commit: - - Para features/mejoras: "tipo: mensaje (#%d)" - - Para bugs: "fix: mensaje (#%d)" o "fix(scope): mensaje (fixes #%d)" + issueReferenceInstructionsES = `Si hay un issue asociado (#{{.IssueNumber}}), DEBES incluir la referencia en el título del commit: + - Para features/mejoras: "tipo: mensaje (#{{.IssueNumber}})" + - Para bugs: "fix: mensaje (#{{.IssueNumber}})" o "fix(scope): mensaje (fixes #{{.IssueNumber}})" - Ejemplos válidos: - ✅ feat: add dark mode support (#%d) - ✅ fix: resolve authentication error (fixes #%d) - ✅ feat(api): implement caching layer (#%d) - - No omitas la referencia del issue #%d.` - - issueReferenceInstructionsEN = `There is an associated issue (#%d), you MUST include the reference in the commit title: - - For features/improvements: "type: message (#%d)" - - For bugs: "fix: message (#%d)" or "fix(scope): message (fixes #%d)" + ✅ feat: add dark mode support (#{{.IssueNumber}}) + ✅ fix: resolve authentication error (fixes #{{.IssueNumber}}) + ✅ feat(api): implement caching layer (#{{.IssueNumber}}) + - No omitas la referencia del issue #{{.IssueNumber}}.` + + issueReferenceInstructionsEN = `There is an associated issue (#{{.IssueNumber}}), you MUST include the reference in the commit title: + - For features/improvements: "type: message (#{{.IssueNumber}})" + - For bugs: "fix: message (#{{.IssueNumber}})" or "fix(scope): message (fixes #{{.IssueNumber}})" - Valid examples: - ✅ feat: add dark mode support (#%d) - ✅ fix: resolve authentication error (fixes #%d) - ✅ feat(api): implement caching layer (#%d) - - NEVER omit the reference to issue #%d.` + ✅ feat: add dark mode support (#{{.IssueNumber}}) + ✅ fix: resolve authentication error (fixes #{{.IssueNumber}}) + ✅ feat(api): implement caching layer (#{{.IssueNumber}}) + - NEVER omit the reference to issue #{{.IssueNumber}}.` ) +// PromptData holds the parameters for template rendering +type PromptData struct { + Count int + Files string + Diff string + Ticket string + History string + Instructions string + IssueNumber int + RelatedIssues string + IssueInfo string + RepoOwner string + RepoName string + PreviousVersion string + CurrentVersion string + LatestVersion string + ReleaseDate string + Changelog string + PRContent string + TechnicalInfo string +} + +// RenderPrompt renders a prompt template with the provided data +func RenderPrompt(name, tmplStr string, data interface{}) (string, error) { + tmpl, err := template.New(name).Parse(tmplStr) + if err != nil { + return "", fmt.Errorf("error parsing template %s: %w", name, err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return "", fmt.Errorf("error executing template %s: %w", name, err) + } + + return buf.String(), nil +} + const ( prPromptTemplateEN = `# Task Act as a Senior Tech Lead and generate a Pull Request summary. # PR Content - %s + {{.PRContent}} # Golden Rules (Constraints) 1. **No Hallucinations:** If it's not in the diff, DO NOT invent it. @@ -58,7 +97,7 @@ const ( Actuá como un Desarrollador Senior y genera un resumen del Pull Request. # Contenido del PR - %s + {{.PRContent}} # Reglas de Oro (Constraints) 1. **Cero alucinaciones:** Si algo no está explícito en el diff, no lo inventes. @@ -85,14 +124,14 @@ const ( const ( promptTemplateWithTicketEN = `# Task - Act as a Git Specialist and generate %d commit message suggestions. + Act as a Git Specialist and generate {{.Count}} commit message suggestions. # Context - - Modified Files: %s - - Diff: %s - - Ticket/Issue: %s - - Recent History: %s - - Issue Instructions: %s + - Modified Files: {{.Files}} + - Diff: {{.Diff}} + - Ticket/Issue: {{.Ticket}} + - Recent History: {{.History}} + - Issue Instructions: {{.Instructions}} # Quality Guidelines 1. **Conventional Commits:** Strictly follow ` + "`type(scope): description`" + `. @@ -133,17 +172,17 @@ const ( } ] - Generate %d suggestions now.` + Generate {{.Count}} suggestions now.` promptTemplateWithTicketES = `# Tarea - Actuá como un especialista en Git y genera %d sugerencias de commits. + Actuá como un especialista en Git y genera {{.Count}} sugerencias de commits. # Contexto - - Archivos: %s - - Diff: %s - - Ticket/Issue: %s - - Historial reciente: %s - - Instrucciones Issue: %s + - Archivos: {{.Files}} + - Diff: {{.Diff}} + - Ticket/Issue: {{.Ticket}} + - Historial reciente: {{.History}} + - Instrucciones Issue: {{.Instructions}} # Criterios de Calidad (Guidelines) 1. **Conventional Commits:** Respeta estrictamente ` + "`tipo(scope): descripción`" + `. @@ -184,18 +223,18 @@ const ( } ] - Genera %d sugerencias ahora.` + Generate {{.Count}} suggestions now.` ) const ( promptTemplateWithoutTicketES = `# Tarea - Actuá como un especialista en Git y genera %d sugerencias de commits basadas en el código. + Actuá como un especialista en Git y genera {{.Count}} sugerencias de commits basadas en el código. # Inputs - - Archivos Modificados: %s - - Cambios (Diff): %s - - Instrucciones Issues: %s - - Historial: %s + - Archivos Modificados: {{.Files}} + - Cambios (Diff): {{.Diff}} + - Instrucciones Issues: {{.Instructions}} + - Historial: {{.History}} # Estrategia de Generación 1. **Analiza el Diff:** Identifica qué lógica cambió realmente. Ignora cambios de formato/espacios. @@ -230,18 +269,18 @@ const ( } ] - %s + {{.TechnicalInfo}} - Genera %d sugerencias ahora.` + Generate {{.Count}} suggestions now.` promptTemplateWithoutTicketEN = `# Task - Act as a Git Specialist and generate %d commit message suggestions based on code changes. + Act as a Git Specialist and generate {{.Count}} commit message suggestions based on code changes. # Inputs - - Modified Files: %s - - Code Changes (Diff): %s - - Issue Instructions: %s - - Recent History: %s + - Modified Files: {{.Files}} + - Code Changes (Diff): {{.Diff}} + - Issue Instructions: {{.Instructions}} + - Recent History: {{.History}} # Generation Strategy 1. **Analyze Diff:** Identify logic changes vs formatting. @@ -275,9 +314,9 @@ const ( } ] - %s + {{.TechnicalInfo}} - Generate %d suggestions now.` + Generate {{.Count}} suggestions now.` ) const ( @@ -285,11 +324,11 @@ const ( Generá release notes profesionales para un CHANGELOG.md siguiendo el estándar "Keep a Changelog". # Datos del Release -- Repo: %s/%s -- Versiones: %s -> %s (%s) +- Repo: {{.RepoOwner}}/{{.RepoName}} +- Versiones: {{.CurrentVersion}} -> {{.LatestVersion}} ({{.ReleaseDate}}) # Changelog (Diff) -%s +{{.Changelog}} # Instrucciones Críticas @@ -386,11 +425,11 @@ Generá las release notes ahora siguiendo estas instrucciones al pie de la letra Generate professional release notes for a CHANGELOG.md following the "Keep a Changelog" standard. # Release Information -- Repository: %s/%s -- Versions: %s -> %s (%s) +- Repository: {{.RepoOwner}}/{{.RepoName}} +- Versions: {{.CurrentVersion}} -> {{.LatestVersion}} ({{.ReleaseDate}}) # Changelog (Diff) -%s +{{.Changelog}} # Critical Instructions @@ -483,7 +522,7 @@ IMPORTANT: Everything in English. Valid JSON without markdown. Generate the release notes now following these instructions to the letter.` ) -// GetPRPromptTemplate devuelve el template adecuado según el idioma +// GetPRPromptTemplate returns the appropriate template based on the language func GetPRPromptTemplate(lang string) string { switch lang { case "es": @@ -493,7 +532,7 @@ func GetPRPromptTemplate(lang string) string { } } -// GetCommitPromptTemplate devuelve el template para commits según el idioma y si hay ticket +// GetCommitPromptTemplate returns the commit template based on language and whether there is a ticket func GetCommitPromptTemplate(lang string, hasTicket bool) string { switch { case lang == "es" && hasTicket: @@ -507,7 +546,7 @@ func GetCommitPromptTemplate(lang string, hasTicket bool) string { } } -// GetReleasePromptTemplate devuelve el template para releases según el idioma +// GetReleasePromptTemplate returns the release template based on the language func GetReleasePromptTemplate(lang string) string { switch lang { case "es": @@ -517,7 +556,7 @@ func GetReleasePromptTemplate(lang string) string { } } -// GetIssueReferenceInstructions devuelve las instrucciones de referencias de issues según el idioma +// GetIssueReferenceInstructions returns issue reference instructions based on the language func GetIssueReferenceInstructions(lang string) string { switch lang { case "es": @@ -531,7 +570,7 @@ const ( prIssueContextInstructionsES = ` **IMPORTANTE - Contexto de Issues/Tickets:** Este PR está relacionado con los siguientes issues: - %s + {{.RelatedIssues}} **INSTRUCCIONES CLAVES:** 1. DEBES incluir AL INICIO del resumen (primeras líneas) las referencias de cierre: @@ -555,7 +594,7 @@ const ( prIssueContextInstructionsEN = ` **IMPORTANT - Issue/Ticket Context:** This PR is related to the following issues: - %s + {{.RelatedIssues}} **MANDATORY INSTRUCTIONS:** 1. You MUST include at the BEGINNING of the summary (first lines) the closing references: @@ -577,7 +616,7 @@ const ( ` ) -// GetPRIssueContextInstructions devuelve las instrucciones de contexto de issues para PRs +// GetPRIssueContextInstructions returns issue context instructions for PRs func GetPRIssueContextInstructions(locale string) string { if locale == "es" { return prIssueContextInstructionsES @@ -585,7 +624,7 @@ func GetPRIssueContextInstructions(locale string) string { return prIssueContextInstructionsEN } -// FormatIssuesForPrompt formatea la lista de issues para incluir en el prompt +// FormatIssuesForPrompt formats the issue list to be included in the prompt func FormatIssuesForPrompt(issues []models.Issue, locale string) string { if len(issues) == 0 { return "" @@ -680,7 +719,7 @@ const ( Act as a Senior Tech Lead and generate a high-quality GitHub issue based on the provided inputs. # Inputs - %s + {{.IssueInfo}} # Golden Rules (Constraints) 1. **Active Voice:** Write in FIRST PERSON ("I implemented", "I added", "We refactored"). Avoid passive voice like "It was implemented". @@ -710,7 +749,7 @@ const ( Actuá como un Tech Lead y generá un issue de GitHub profesional basado en los inputs. # Entradas (Inputs) - %s + {{.IssueInfo}} # Reglas de Oro (Constraints) 1. **Voz Activa:** Escribí en PRIMERA PERSONA ("Implementé", "Agregué", "Corregí"). Prohibido usar voz pasiva robótica. @@ -739,7 +778,7 @@ const ( Generá el issue ahora.` ) -// GetIssuePromptTemplate devuelve el template adecuado para generación de issues según el idioma +// GetIssuePromptTemplate returns the appropriate issue generation template based on language func GetIssuePromptTemplate(lang string) string { switch lang { case "es": diff --git a/internal/infrastructure/ai/registry/ai_provider_registry.go b/internal/infrastructure/ai/registry/ai_provider_registry.go deleted file mode 100644 index e3632e1..0000000 --- a/internal/infrastructure/ai/registry/ai_provider_registry.go +++ /dev/null @@ -1,89 +0,0 @@ -package registry - -import ( - "context" - "fmt" - "sync" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" -) - -// AIProviderFactory define la interfaz para crear servicios de IA -type AIProviderFactory interface { - // CreateCommitSummarizer crea un servicio para generar sugerencias de commits - CreateCommitSummarizer(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (ports.CommitSummarizer, error) - - // CreatePRSummarizer crea un servicio para resumir Pull Requests - CreatePRSummarizer(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (ports.PRSummarizer, error) - - // CreateIssueContentGenerator crea un servicio para generar contenido de issues - CreateIssueContentGenerator(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (ports.IssueContentGenerator, error) - - // ValidateConfig valida la configuración para este proveedor - ValidateConfig(cfg *config.Config) error - - // Name retorna el nombre del proveedor - Name() string -} - -// AIProviderRegistry gestiona el registro de proveedores de IA -type AIProviderRegistry struct { - mu sync.RWMutex - factories map[string]AIProviderFactory -} - -// NewAIProviderRegistry crea un nuevo registro de proveedores de IA -func NewAIProviderRegistry() *AIProviderRegistry { - return &AIProviderRegistry{ - factories: make(map[string]AIProviderFactory), - } -} - -// Register registra un nuevo proveedor de IA -func (r *AIProviderRegistry) Register(name string, factory AIProviderFactory) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.factories[name]; exists { - return fmt.Errorf("proveedor IA '%s' ya esta registrado", name) - } - - r.factories[name] = factory - return nil -} - -// Get obtiene un factory por nombre -func (r *AIProviderRegistry) Get(name string) (AIProviderFactory, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - factory, exists := r.factories[name] - if !exists { - return nil, fmt.Errorf("proveedor IA '%s' no encontrado en el registro", name) - } - - return factory, nil -} - -// List retorna la lista de proveedores registrados -func (r *AIProviderRegistry) List() []string { - r.mu.RLock() - defer r.mu.RUnlock() - - providers := make([]string, 0, len(r.factories)) - for name := range r.factories { - providers = append(providers, name) - } - return providers -} - -// IsRegistered verifica si un proveedor está registrado -func (r *AIProviderRegistry) IsRegistered(name string) bool { - r.mu.RLock() - defer r.mu.RUnlock() - - _, exists := r.factories[name] - return exists -} diff --git a/internal/infrastructure/ai/registry/ai_provider_registry_test.go b/internal/infrastructure/ai/registry/ai_provider_registry_test.go deleted file mode 100644 index 8d883a0..0000000 --- a/internal/infrastructure/ai/registry/ai_provider_registry_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package registry - -import ( - "context" - "testing" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// MockAIProviderFactory es un mock para testing -type MockAIProviderFactory struct { - name string -} - -func (m *MockAIProviderFactory) CreateCommitSummarizer(_ context.Context, _ *config.Config, _ *i18n.Translations) (ports.CommitSummarizer, error) { - return nil, nil -} - -func (m *MockAIProviderFactory) CreatePRSummarizer(_ context.Context, _ *config.Config, _ *i18n.Translations) (ports.PRSummarizer, error) { - return nil, nil -} - -func (m *MockAIProviderFactory) CreateIssueContentGenerator(_ context.Context, _ *config.Config, _ *i18n.Translations) (ports.IssueContentGenerator, error) { - return nil, nil -} - -func (m *MockAIProviderFactory) ValidateConfig(_ *config.Config) error { - return nil -} - -func (m *MockAIProviderFactory) Name() string { - return m.name -} - -func TestNewAIProviderRegistry(t *testing.T) { - registry := NewAIProviderRegistry() - assert.NotNil(t, registry) - assert.Empty(t, registry.List()) -} - -func TestRegister(t *testing.T) { - registry := NewAIProviderRegistry() - mockFactory := &MockAIProviderFactory{name: "test-provider"} - - err := registry.Register("test", mockFactory) - assert.NoError(t, err) - assert.True(t, registry.IsRegistered("test")) - - err = registry.Register("test", mockFactory) - assert.Error(t, err) - require.Error(t, err) - assert.Contains(t, err.Error(), "ya esta registrado") -} - -func TestGet(t *testing.T) { - registry := NewAIProviderRegistry() - mockFactory := &MockAIProviderFactory{name: "test-provider"} - - _, err := registry.Get("nonexistent") - assert.Error(t, err) - assert.Contains(t, err.Error(), "no encontrado en el registro") - - err = registry.Register("test", mockFactory) - require.NoError(t, err) - - factory, err := registry.Get("test") - assert.NoError(t, err) - assert.NotNil(t, factory) - assert.Equal(t, "test-provider", factory.Name()) -} - -func TestList(t *testing.T) { - registry := NewAIProviderRegistry() - - assert.Empty(t, registry.List()) - - _ = registry.Register("gemini", &MockAIProviderFactory{name: "gemini"}) - _ = registry.Register("openai", &MockAIProviderFactory{name: "openai"}) - _ = registry.Register("claude", &MockAIProviderFactory{name: "claude"}) - - providers := registry.List() - assert.Len(t, providers, 3) - assert.Contains(t, providers, "gemini") - assert.Contains(t, providers, "openai") - assert.Contains(t, providers, "claude") -} - -func TestIsRegistered(t *testing.T) { - registry := NewAIProviderRegistry() - mockFactory := &MockAIProviderFactory{name: "test"} - - assert.False(t, registry.IsRegistered("test")) - - _ = registry.Register("test", mockFactory) - assert.True(t, registry.IsRegistered("test")) - assert.False(t, registry.IsRegistered("other")) -} - -func TestConcurrentAccess(t *testing.T) { - registry := NewAIProviderRegistry() - - done := make(chan bool) - for i := 0; i < 10; i++ { - go func(id int) { - mockFactory := &MockAIProviderFactory{name: "provider"} - _ = registry.Register(string(rune('a'+id)), mockFactory) - done <- true - }(i) - } - - for i := 0; i < 10; i++ { - <-done - } - - providers := registry.List() - assert.Len(t, providers, 10) -} diff --git a/internal/infrastructure/builder/binary_builder.go b/internal/infrastructure/builder/binary_builder.go index 3b44524..6e5a996 100644 --- a/internal/infrastructure/builder/binary_builder.go +++ b/internal/infrastructure/builder/binary_builder.go @@ -11,9 +11,9 @@ import ( "os/exec" "path/filepath" "strings" + "time" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/Tomas-vilte/MateCommit/internal/domain/errors" ) type BuildTarget struct { @@ -28,19 +28,65 @@ type BinaryBuilder struct { commit string date string buildDir string - trans *i18n.Translations } -func NewBinaryBuilder(mainPath, binaryName, version, commit, date, buildDir string, trans *i18n.Translations) *BinaryBuilder { - return &BinaryBuilder{ +type Option func(*BinaryBuilder) + +func WithVersion(version string) Option { + return func(b *BinaryBuilder) { + b.version = version + } +} + +func WithCommit(commit string) Option { + return func(b *BinaryBuilder) { + b.commit = commit + } +} + +func WithBuildDir(dir string) Option { + return func(b *BinaryBuilder) { + b.buildDir = dir + } +} + +func WithDate(date string) Option { + return func(b *BinaryBuilder) { + b.date = date + } +} + +func NewBinaryBuilder(mainPath, binaryName string, opts ...Option) *BinaryBuilder { + b := &BinaryBuilder{ mainPath: mainPath, binaryName: binaryName, - version: version, - commit: commit, - date: date, - buildDir: buildDir, - trans: trans, + buildDir: "./dist", + date: time.Now().Format(time.RFC3339), + } + + for _, opt := range opts { + opt(b) + } + return b +} + +func (b *BinaryBuilder) Build() error { + if b.version == "" { + return errors.ErrBuildNoVersion + } + + if b.commit == "" { + return errors.ErrBuildNoCommit + } + + if b.buildDir == "" { + return errors.ErrBuildNoBuildDir + } + + if b.date == "" { + return errors.ErrBuildNoDate } + return nil } func (b *BinaryBuilder) GetBuildTargets() []BuildTarget { @@ -87,7 +133,7 @@ func (b *BinaryBuilder) BuildBinary(ctx context.Context, target BuildTarget) (st output, err := cmd.CombinedOutput() if err != nil { - return "", fmt.Errorf("%s", b.trans.GetMessage("build.errors.compile_target", 0, map[string]interface{}{"GOOS": target.GOOS, "GOARCH": target.GOARCH, "Output": string(output)})) + return "", errors.NewAppError(errors.TypeInternal, fmt.Sprintf("failed to compile for %s/%s: %s", target.GOOS, target.GOARCH, string(output)), err) } return outputPath, nil @@ -106,12 +152,12 @@ func (b *BinaryBuilder) PackageBinary(binaryPath string, target BuildTarget) (st if target.GOOS == "windows" { archivePath = filepath.Join(b.buildDir, fmt.Sprintf("%s_%s_%s_%s.zip", binaryNameLower, version, target.GOOS, b.mapArch(target.GOARCH))) if err := b.createZip(binaryPath, archivePath, binaryName); err != nil { - return "", fmt.Errorf("%s", b.trans.GetMessage("build.errors.create_zip", 0, map[string]interface{}{"Error": err})) + return "", errors.NewAppError(errors.TypeInternal, "failed to create zip archive", err) } } else { archivePath = filepath.Join(b.buildDir, fmt.Sprintf("%s_%s_%s_%s.tar.gz", binaryNameLower, version, target.GOOS, b.mapArch(target.GOARCH))) if err := b.createTarGz(binaryPath, archivePath, binaryName); err != nil { - return "", fmt.Errorf("%s", b.trans.GetMessage("build.errors.create_targz", 0, map[string]interface{}{"Error": err})) + return "", errors.NewAppError(errors.TypeInternal, "failed to create tar.gz archive", err) } } @@ -232,6 +278,6 @@ func (b *BinaryBuilder) BuildAndPackageAll(ctx context.Context) ([]string, error type DefaultBinaryBuilderFactory struct{} -func (f *DefaultBinaryBuilderFactory) NewBuilder(mainPath, binaryName, version, commit, date, buildDir string, trans *i18n.Translations) ports.BinaryPackager { - return NewBinaryBuilder(mainPath, binaryName, version, commit, date, buildDir, trans) +func (f *DefaultBinaryBuilderFactory) NewBuilder(mainPath, binaryName string, opts ...Option) *BinaryBuilder { + return NewBinaryBuilder(mainPath, binaryName, opts...) } diff --git a/internal/infrastructure/builder/binary_builder_test.go b/internal/infrastructure/builder/binary_builder_test.go index 09f3ccf..929d801 100644 --- a/internal/infrastructure/builder/binary_builder_test.go +++ b/internal/infrastructure/builder/binary_builder_test.go @@ -9,14 +9,17 @@ import ( "path/filepath" "testing" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBinaryBuilder_GetBuildTargets(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales") - builder := NewBinaryBuilder("main.go", "app", "v1.0.0", "sha", "date", "build", trans) + builder := NewBinaryBuilder("main.go", "app", + WithVersion("v1.0.0"), + WithCommit("sha"), + WithDate("date"), + WithBuildDir("build"), + ) targets := builder.GetBuildTargets() assert.Len(t, targets, 6) @@ -34,8 +37,12 @@ func TestBinaryBuilder_PackageBinary_Zip(t *testing.T) { } }() - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales") - builder := NewBinaryBuilder("main.go", "app", "v1.0.0", "sha", "date", tempDir, trans) + builder := NewBinaryBuilder("main.go", "app", + WithVersion("v1.0.0"), + WithCommit("sha"), + WithDate("date"), + WithBuildDir(tempDir), + ) binaryName := "app.exe" binaryPath := filepath.Join(tempDir, binaryName) @@ -82,8 +89,12 @@ func TestBinaryBuilder_PackageBinary_TarGz(t *testing.T) { } }() - trans, _ := i18n.NewTranslations("en", "../../../i18n/locales") - builder := NewBinaryBuilder("main.go", "app", "v1.0.0", "sha", "date", tempDir, trans) + builder := NewBinaryBuilder("main.go", "app", + WithVersion("v1.0.0"), + WithCommit("sha"), + WithDate("date"), + WithBuildDir(tempDir), + ) binaryName := "app" binaryPath := filepath.Join(tempDir, binaryName) diff --git a/internal/infrastructure/cache/cache.go b/internal/infrastructure/cache/cache.go index 4ab2cf3..71e1f93 100644 --- a/internal/infrastructure/cache/cache.go +++ b/internal/infrastructure/cache/cache.go @@ -24,12 +24,12 @@ type Cache struct { func NewCache(ttl time.Duration) (*Cache, error) { homeDir, err := os.UserHomeDir() if err != nil { - return nil, fmt.Errorf("error obteniendo home directory: %w", err) + return nil, fmt.Errorf("error obtaining home directory: %w", err) } cacheDir := filepath.Join(homeDir, ".matecommit", "cache") if err := os.MkdirAll(cacheDir, 0755); err != nil { - return nil, fmt.Errorf("error creando directorio de caché: %w", err) + return nil, fmt.Errorf("error creating cache directory: %w", err) } cache := &Cache{ @@ -42,13 +42,13 @@ func NewCache(ttl time.Duration) (*Cache, error) { return cache, nil } -// GenerateHash genera un hash SHA256 del contenido +// GenerateHash generates a SHA256 hash of the content func (c *Cache) GenerateHash(content string) string { hash := sha256.Sum256([]byte(content)) return hex.EncodeToString(hash[:]) } -// Get obtiene una respuesta del caché +// Get gets a response from the cache func (c *Cache) Get(hash string) (json.RawMessage, bool, error) { filePath := filepath.Join(c.cacheDir, hash+".json") @@ -57,12 +57,12 @@ func (c *Cache) Get(hash string) (json.RawMessage, bool, error) { if os.IsNotExist(err) { return nil, false, nil } - return nil, false, fmt.Errorf("error leyendo caché: %w", err) + return nil, false, fmt.Errorf("error reading cache: %w", err) } var cached CachedResponse if err := json.Unmarshal(data, &cached); err != nil { - return nil, false, fmt.Errorf("error deserializando caché: %w", err) + return nil, false, fmt.Errorf("error unmarshaling cache: %w", err) } if time.Since(cached.CreatedAt) > c.ttl { @@ -73,11 +73,11 @@ func (c *Cache) Get(hash string) (json.RawMessage, bool, error) { return cached.Response, true, nil } -// Set guarda una respuesta en el caché +// Set saves a response to the cache func (c *Cache) Set(hash string, response interface{}) error { responseData, err := json.Marshal(response) if err != nil { - return fmt.Errorf("error serializando respuesta: %w", err) + return fmt.Errorf("error marshaling response: %w", err) } cached := CachedResponse{ @@ -88,22 +88,22 @@ func (c *Cache) Set(hash string, response interface{}) error { data, err := json.MarshalIndent(cached, "", " ") if err != nil { - return fmt.Errorf("error serializando caché: %w", err) + return fmt.Errorf("error marshaling cache: %w", err) } filePath := filepath.Join(c.cacheDir, hash+".json") if err := os.WriteFile(filePath, data, 0644); err != nil { - return fmt.Errorf("error guardando caché: %w", err) + return fmt.Errorf("error saving cache: %w", err) } return nil } -// CleanExpired elimina archivos de caché expirados +// CleanExpired removes expired cache files func (c *Cache) CleanExpired() error { entries, err := os.ReadDir(c.cacheDir) if err != nil { - return fmt.Errorf("error leyendo directorio de caché: %w", err) + return fmt.Errorf("error reading cache directory: %w", err) } for _, entry := range entries { @@ -125,7 +125,7 @@ func (c *Cache) CleanExpired() error { return nil } -// Clean elimina todo el cache +// Clean removes the entire cache func (c *Cache) Clean() error { return os.RemoveAll(c.cacheDir) } diff --git a/internal/infrastructure/dependency/gomod_analyzer.go b/internal/infrastructure/dependency/gomod_analyzer.go index 26c7871..12d486e 100644 --- a/internal/infrastructure/dependency/gomod_analyzer.go +++ b/internal/infrastructure/dependency/gomod_analyzer.go @@ -2,11 +2,11 @@ package dependency import ( "context" - "fmt" "regexp" "strconv" "strings" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" ) @@ -31,12 +31,12 @@ func (g *GoModAnalyzer) CanHandle(ctx context.Context, vcsClient ports.VCSClient func (g *GoModAnalyzer) AnalyzeChanges(ctx context.Context, vcsClient ports.VCSClient, previousTag, currentTag string) ([]models.DependencyChange, error) { oldContent, err := g.getFileContent(ctx, vcsClient, previousTag, "go.mod") if err != nil { - return nil, fmt.Errorf("error leyendo go.mod viejo: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to read old go.mod", err) } newContent, err := g.getFileContent(ctx, vcsClient, currentTag, "go.mod") if err != nil { - return nil, fmt.Errorf("error leyendo go.mod nuevo: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to read new go.mod", err) } oldDeps := g.parseGoMod(oldContent) @@ -47,7 +47,7 @@ func (g *GoModAnalyzer) AnalyzeChanges(ctx context.Context, vcsClient ports.VCSC func (g *GoModAnalyzer) getFileContent(ctx context.Context, vcsClient ports.VCSClient, tag, filepath string) (string, error) { if vcsClient == nil { - return "", fmt.Errorf("vcsClient es nil") + return "", domainErrors.NewAppError(domainErrors.TypeInternal, "vcsClient is nil", nil) } return vcsClient.GetFileAtTag(ctx, tag, filepath) } @@ -60,9 +60,9 @@ type goDep struct { func (g *GoModAnalyzer) parseGoMod(content string) map[string]goDep { deps := make(map[string]goDep) - // Regex para líneas dentro de require() + // Regex for lines inside require() requireRegex := regexp.MustCompile(`^\s+(\S+)\s+v?(\S+)(\s+//\s*indirect)?`) - // Regex para líneas require individuales + // Regex for individual require lines singleRequireRegex := regexp.MustCompile(`^require\s+(\S+)\s+v?(\S+)(\s+//\s*indirect)?`) inRequire := false @@ -153,7 +153,7 @@ func (g *GoModAnalyzer) computeChanges(oldDeps, newDeps map[string]goDep) []mode return changes } -// calculateSeverity determina la severidad del cambio basado en semver +// calculateSeverity determines the change severity based on semver func (g *GoModAnalyzer) calculateSeverity(oldVersion, newVersion string) models.ChangeSeverity { oldParts := g.parseVersion(oldVersion) newParts := g.parseVersion(newVersion) @@ -177,16 +177,16 @@ func (g *GoModAnalyzer) calculateSeverity(oldVersion, newVersion string) models. return models.UnknownChange } -// parseVersion extrae [major, minor, patch] de una version string +// parseVersion extracts [major, minor, patch] from a version string func (g *GoModAnalyzer) parseVersion(version string) []int { version = strings.TrimPrefix(version, "v") - // Remover pre-release tag (después de -) + // Remove pre-release tag (after -) if idx := strings.Index(version, "-"); idx != -1 { version = version[:idx] } - // Remover build metadata (después de +) + // Remove build metadata (after +) if idx := strings.Index(version, "+"); idx != -1 { version = version[:idx] } diff --git a/internal/infrastructure/dependency/gomod_analyzer_test.go b/internal/infrastructure/dependency/gomod_analyzer_test.go index d81bf9f..4844440 100644 --- a/internal/infrastructure/dependency/gomod_analyzer_test.go +++ b/internal/infrastructure/dependency/gomod_analyzer_test.go @@ -204,7 +204,7 @@ require ( Return("", errors.New("tag not found")) }, expectError: true, - errorMsg: "error leyendo go.mod viejo", + errorMsg: "failed to read old go.mod", }, { name: "error reading new go.mod", @@ -215,7 +215,7 @@ require ( Return("", errors.New("tag not found")) }, expectError: true, - errorMsg: "error leyendo go.mod nuevo", + errorMsg: "failed to read new go.mod", }, } diff --git a/internal/infrastructure/dependency/package_json_analyzer.go b/internal/infrastructure/dependency/package_json_analyzer.go index 48bd908..3d7b60a 100644 --- a/internal/infrastructure/dependency/package_json_analyzer.go +++ b/internal/infrastructure/dependency/package_json_analyzer.go @@ -3,10 +3,10 @@ package dependency import ( "context" "encoding/json" - "fmt" "strconv" "strings" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" ) @@ -27,26 +27,25 @@ func (p *PackageJsonAnalyzer) CanHandle(ctx context.Context, vcsClient ports.VCS func (p *PackageJsonAnalyzer) AnalyzeChanges(ctx context.Context, vcsClient ports.VCSClient, previousTag, currentTag string) ([]models.DependencyChange, error) { oldContent, err := p.getFileContent(ctx, vcsClient, previousTag, "package.json") if err != nil { - return nil, fmt.Errorf("error al leer el package.json antiguo: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to read old package.json", err) } newContent, err := p.getFileContent(ctx, vcsClient, currentTag, "package.json") if err != nil { - return nil, fmt.Errorf("error al leer el nuevo package.json: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to read new package.json", err) } oldDeps, err := p.parsePackageJson(oldContent) if err != nil { - return nil, fmt.Errorf("error al parsear viejo package.json: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to parse old package.json", err) } newDeps, err := p.parsePackageJson(newContent) if err != nil { - return nil, fmt.Errorf("error al parsear nuevo package.json: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to parse new package.json", err) } return p.computeChanges(oldDeps, newDeps), nil - } func (p *PackageJsonAnalyzer) Name() string { @@ -55,7 +54,7 @@ func (p *PackageJsonAnalyzer) Name() string { func (p *PackageJsonAnalyzer) getFileContent(ctx context.Context, vcsClient ports.VCSClient, tag, filepath string) (string, error) { if vcsClient == nil { - return "", fmt.Errorf("vcsClient is nil") + return "", domainErrors.NewAppError(domainErrors.TypeInternal, "vcsClient is nil", nil) } return vcsClient.GetFileAtTag(ctx, tag, filepath) } @@ -139,7 +138,7 @@ func (p *PackageJsonAnalyzer) computeChanges(oldDeps, newDeps map[string]npmDep) return changes } -// cleanVersion remueve prefijos como ^, ~, >=, etc de versiones npm +// cleanVersion removes prefixes like ^, ~, >=, etc from npm versions func (p *PackageJsonAnalyzer) cleanVersion(version string) string { version = strings.TrimPrefix(version, "^") version = strings.TrimPrefix(version, "~") @@ -151,7 +150,7 @@ func (p *PackageJsonAnalyzer) cleanVersion(version string) string { return strings.TrimSpace(version) } -// calculateSeverity determina la severidad del cambio basado en semver +// calculateSeverity determines the severity of the change based on semver func (p *PackageJsonAnalyzer) calculateSeverity(oldVersion, newVersion string) models.ChangeSeverity { oldClean := p.cleanVersion(oldVersion) newClean := p.cleanVersion(newVersion) @@ -178,7 +177,7 @@ func (p *PackageJsonAnalyzer) calculateSeverity(oldVersion, newVersion string) m return models.UnknownChange } -// parseVersion extrae [major, minor, patch] de una version string +// parseVersion extracts [major, minor, patch] from a version string func (p *PackageJsonAnalyzer) parseVersion(version string) []int { parts := strings.Split(version, ".") result := make([]int, 0, 3) diff --git a/internal/infrastructure/dependency/package_json_analyzer_test.go b/internal/infrastructure/dependency/package_json_analyzer_test.go index ae48de7..8fcd99f 100644 --- a/internal/infrastructure/dependency/package_json_analyzer_test.go +++ b/internal/infrastructure/dependency/package_json_analyzer_test.go @@ -102,7 +102,7 @@ func TestPackageJsonAnalyzer_AnalyzeChanges(t *testing.T) { Return("", errors.New("tag not found")) }, expectError: true, - errorMsg: "error al leer el package.json antiguo", + errorMsg: "failed to read old package.json", }, { name: "error reading new package.json", @@ -113,7 +113,7 @@ func TestPackageJsonAnalyzer_AnalyzeChanges(t *testing.T) { Return("", errors.New("tag not found")) }, expectError: true, - errorMsg: "error al leer el nuevo package.json", + errorMsg: "failed to read new package.json", }, { name: "error parsing old package.json", @@ -124,7 +124,7 @@ func TestPackageJsonAnalyzer_AnalyzeChanges(t *testing.T) { Return(`{"dependencies": {}}`, nil) }, expectError: true, - errorMsg: "error al parsear viejo package.json", + errorMsg: "failed to parse old package.json", }, { name: "error parsing new package.json", @@ -135,7 +135,7 @@ func TestPackageJsonAnalyzer_AnalyzeChanges(t *testing.T) { Return("invalid json", nil) }, expectError: true, - errorMsg: "error al parsear nuevo package.json", + errorMsg: "failed to parse new package.json", }, } diff --git a/internal/infrastructure/dependency/registry.go b/internal/infrastructure/dependency/registry.go index 82bd82e..75e35f7 100644 --- a/internal/infrastructure/dependency/registry.go +++ b/internal/infrastructure/dependency/registry.go @@ -20,12 +20,12 @@ func NewAnalyzerRegistry() *AnalyzerRegistry { } } -// RegisterAnalyzer agrega un analyzer personalizado +// RegisterAnalyzer adds a custom analyzer func (r *AnalyzerRegistry) RegisterAnalyzer(analyzer ports.DependencyAnalyzer) { r.analyzers = append(r.analyzers, analyzer) } -// AnalyzeAll ejecuta todos los analyzers aplicables y combina los resultados +// AnalyzeAll executes all applicable analyzers and combines the results func (r *AnalyzerRegistry) AnalyzeAll(ctx context.Context, vcsClient ports.VCSClient, previousTag, currentTag string) ([]models.DependencyChange, error) { var allChanges []models.DependencyChange @@ -41,7 +41,7 @@ func (r *AnalyzerRegistry) AnalyzeAll(ctx context.Context, vcsClient ports.VCSCl return allChanges, nil } -// GetSupportedAnalyzers retorna lista de analyzers detectados +// GetSupportedAnalyzers returns a list of detected analyzers func (r *AnalyzerRegistry) GetSupportedAnalyzers(ctx context.Context, vcsClient ports.VCSClient, previousTag, currentTag string) []string { var supported []string diff --git a/internal/infrastructure/di/container.go b/internal/infrastructure/di/container.go deleted file mode 100644 index c4bc6c7..0000000 --- a/internal/infrastructure/di/container.go +++ /dev/null @@ -1,224 +0,0 @@ -package di - -import ( - "context" - "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/ai/registry" - ticketregistry "github.com/Tomas-vilte/MateCommit/internal/infrastructure/tickets/registry" - vcsregistry "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/registry" - "github.com/Tomas-vilte/MateCommit/internal/services" -) - -type Container struct { - config *config.Config - translations *i18n.Translations - - aiRegistry *registry.AIProviderRegistry - vcsRegistry *vcsregistry.VCSProviderRegistry - ticketRegistry *ticketregistry.TicketProviderRegistry - - gitService ports.GitService - commitService ports.CommitService - prService ports.PRService - issueTemplateService ports.IssueTemplateService - issueGeneratorService ports.IssueGeneratorService -} - -// NewContainer crea un nuevo contenedor de dependencias -func NewContainer(cfg *config.Config, trans *i18n.Translations) *Container { - return &Container{ - config: cfg, - translations: trans, - aiRegistry: registry.NewAIProviderRegistry(), - vcsRegistry: vcsregistry.NewVCSProviderRegistry(), - ticketRegistry: ticketregistry.NewTicketProviderRegistry(), - } -} - -// RegisterAIProvider registra un proveedor de IA -func (c *Container) RegisterAIProvider(name string, factory registry.AIProviderFactory) error { - return c.aiRegistry.Register(name, factory) -} - -// RegisterVCSProvider registra un proveedor VCS -func (c *Container) RegisterVCSProvider(name string, factory vcsregistry.VCSProviderFactory) error { - return c.vcsRegistry.Register(name, factory) -} - -// RegisterTicketProvider registra un proveedor de tickets -func (c *Container) RegisterTicketProvider(name string, factory ticketregistry.TicketProviderFactory) error { - return c.ticketRegistry.Register(name, factory) -} - -// SetGitService establece el servicio Git -func (c *Container) SetGitService(gitService ports.GitService) { - c.gitService = gitService -} - -// GetGitService retorna el servicio Git -func (c *Container) GetGitService() ports.GitService { - return c.gitService -} - -// GetAIRegistry retorna el registro de proveedores AI -func (c *Container) GetAIRegistry() *registry.AIProviderRegistry { - return c.aiRegistry -} - -// GetVCSRegistry retorna el registro de proveedores VCS -func (c *Container) GetVCSRegistry() *vcsregistry.VCSProviderRegistry { - return c.vcsRegistry -} - -// GetTicketRegistry retorna el registro de proveedores de tickets -func (c *Container) GetTicketRegistry() *ticketregistry.TicketProviderRegistry { - return c.ticketRegistry -} - -// GetCommitService retorna el servicio de commits (lazy initialization) -func (c *Container) GetCommitService(ctx context.Context) (ports.CommitService, error) { - if c.commitService != nil { - return c.commitService, nil - } - - if c.gitService == nil { - return nil, fmt.Errorf("servicio git no creado") - } - - var aiProvider ports.CommitSummarizer - if c.config.AIConfig.ActiveAI != "" { - aiFactory, err := c.aiRegistry.Get(string(c.config.AIConfig.ActiveAI)) - if err == nil { - aiProvider, err = aiFactory.CreateCommitSummarizer(ctx, c.config, c.translations) - if err != nil { - // Log warning pero continuar sin AI - aiProvider = nil - } - } - } - - ticketManager, err := c.ticketRegistry.CreateClientFromConfig(ctx, c.config, c.translations) - if err != nil { - ticketManager = nil - } - - c.commitService = services.NewCommitService( - c.gitService, - aiProvider, - ticketManager, - nil, - c.config, - c.translations, - ) - - return c.commitService, nil -} - -// GetPRSummarizer retorna el servicio de IA para resumir PRs (lazy initialization) -func (c *Container) GetPRSummarizer(ctx context.Context) (ports.PRSummarizer, error) { - if c.config.AIConfig.ActiveAI == "" { - return nil, fmt.Errorf("no hay IA activa configurada") - } - - aiFactory, err := c.aiRegistry.Get(string(c.config.AIConfig.ActiveAI)) - if err != nil { - return nil, fmt.Errorf("proveedor de IA '%s' no encontrado: %w", c.config.AIConfig.ActiveAI, err) - } - - aiSummarizer, err := aiFactory.CreatePRSummarizer(ctx, c.config, c.translations) - if err != nil { - return nil, fmt.Errorf("error al crear el servicio de IA para PRs: %w", err) - } - - return aiSummarizer, nil -} - -// GetPRService retorna el servicio de PRs (lazy initialization) -func (c *Container) GetPRService(ctx context.Context) (ports.PRService, error) { - if c.prService != nil { - return c.prService, nil - } - - if c.gitService == nil { - return nil, fmt.Errorf("servicio git no creado") - } - - var aiSummarizer ports.PRSummarizer - aiSummarizer, err := c.GetPRSummarizer(ctx) - if err != nil { - aiSummarizer = nil - } - - vcsClient, err := c.vcsRegistry.CreateClientFromConfig(ctx, c.gitService, c.config, c.translations) - if err != nil { - return nil, fmt.Errorf("error al crear cliente VCS: %w", err) - } - - c.prService = services.NewPRService(vcsClient, aiSummarizer, c.translations, c.config) - - return c.prService, nil -} - -// GetConfig retorna la configuración -func (c *Container) GetConfig() *config.Config { - return c.config -} - -// GetTranslations retorna las traducciones -func (c *Container) GetTranslations() *i18n.Translations { - return c.translations -} - -// GetIssueTemplateService retorna el servicio de templates de issues (lazy initialization) -func (c *Container) GetIssueTemplateService() ports.IssueTemplateService { - if c.issueTemplateService != nil { - return c.issueTemplateService - } - - c.issueTemplateService = services.NewIssueTemplateService(c.config, c.translations) - return c.issueTemplateService -} - -// GetIssueGeneratorService retorna el servicio de generación de issues (lazy initialization) -func (c *Container) GetIssueGeneratorService(ctx context.Context) (ports.IssueGeneratorService, error) { - if c.issueGeneratorService != nil { - return c.issueGeneratorService, nil - } - - if c.gitService == nil { - return nil, fmt.Errorf("servicio git no creado") - } - - templateService := c.GetIssueTemplateService() - - var aiGenerator ports.IssueContentGenerator - if c.config.AIConfig.ActiveAI != "" { - aiFactory, err := c.aiRegistry.Get(string(c.config.AIConfig.ActiveAI)) - if err == nil { - aiGenerator, err = aiFactory.CreateIssueContentGenerator(ctx, c.config, c.translations) - if err != nil { - aiGenerator = nil - } - } - } - - vcsClient, err := c.vcsRegistry.CreateClientFromConfig(ctx, c.gitService, c.config, c.translations) - if err != nil { - return nil, fmt.Errorf("error al crear cliente VCS: %w", err) - } - - c.issueGeneratorService = services.NewIssueGeneratorService( - c.gitService, - aiGenerator, - vcsClient, - templateService, - c.config, - c.translations, - ) - - return c.issueGeneratorService, nil -} diff --git a/internal/infrastructure/di/container_test.go b/internal/infrastructure/di/container_test.go deleted file mode 100644 index d5947b0..0000000 --- a/internal/infrastructure/di/container_test.go +++ /dev/null @@ -1,332 +0,0 @@ -package di - -import ( - "context" - "testing" - - "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/ai/registry" - ticketregistry "github.com/Tomas-vilte/MateCommit/internal/infrastructure/tickets/registry" - vcsregistry "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/registry" - "github.com/stretchr/testify/mock" -) - -type mockAIFactory struct { - mock.Mock -} - -func (m *mockAIFactory) CreateCommitSummarizer(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (ports.CommitSummarizer, error) { - args := m.Called(ctx, cfg, trans) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(ports.CommitSummarizer), args.Error(1) -} - -func (m *mockAIFactory) CreatePRSummarizer(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (ports.PRSummarizer, error) { - args := m.Called(ctx, cfg, trans) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(ports.PRSummarizer), args.Error(1) -} - -func (m *mockAIFactory) CreateIssueContentGenerator(ctx context.Context, cfg *config.Config, trans *i18n.Translations) (ports.IssueContentGenerator, error) { - args := m.Called(ctx, cfg, trans) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(ports.IssueContentGenerator), args.Error(1) -} - -func (m *mockAIFactory) ValidateConfig(cfg *config.Config) error { - args := m.Called(cfg) - return args.Error(0) -} - -func (m *mockAIFactory) Name() string { - return "mock" -} - -type mockVCSFactory struct { - mock.Mock -} - -func (m *mockVCSFactory) CreateClient(ctx context.Context, owner, repo, token string, trans *i18n.Translations) (ports.VCSClient, error) { - args := m.Called(ctx, owner, repo, token, trans) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(ports.VCSClient), args.Error(1) -} - -func (m *mockVCSFactory) ValidateConfig(cfg *config.VCSConfig) error { - args := m.Called(cfg) - return args.Error(0) -} - -func (m *mockVCSFactory) Name() string { - return "mock" -} - -type mockTicketFactory struct { - mock.Mock -} - -func (m *mockTicketFactory) CreateClient(ctx context.Context, cfg config.TicketProviderConfig, trans *i18n.Translations) (ports.TickerManager, error) { - args := m.Called(ctx, cfg, trans) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(ports.TickerManager), args.Error(1) -} - -func (m *mockTicketFactory) ValidateConfig(cfg config.TicketProviderConfig) error { - args := m.Called(cfg) - return args.Error(0) -} - -func (m *mockTicketFactory) Name() string { - return "mock" -} - -func TestNewContainer(t *testing.T) { - cfg := &config.Config{ - Language: "en", - } - trans := &i18n.Translations{} - - container := NewContainer(cfg, trans) - - if container == nil { - t.Fatal("Container should not be nil") - } - - if container.config != cfg { - t.Error("Container config does not match input config") - } - - if container.translations != trans { - t.Error("Container translations do not match input translations") - } - - if container.aiRegistry == nil { - t.Error("AI registry should be initialized") - } - - if container.vcsRegistry == nil { - t.Error("VCS registry should be initialized") - } - - if container.ticketRegistry == nil { - t.Error("Ticket registry should be initialized") - } -} - -func TestRegisterAIProvider(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - mockFactory := &mockAIFactory{} - err := container.RegisterAIProvider("mock", mockFactory) - - if err != nil { - t.Fatalf("Failed to register AI provider: %v", err) - } - - err = container.RegisterAIProvider("mock", mockFactory) - if err == nil { - t.Error("Should not allow registering the same provider twice") - } -} - -func TestRegisterVCSProvider(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - mockFactory := &mockVCSFactory{} - err := container.RegisterVCSProvider("mock", mockFactory) - - if err != nil { - t.Fatalf("Failed to register VCS provider: %v", err) - } - - err = container.RegisterVCSProvider("mock", mockFactory) - if err == nil { - t.Error("Should not allow registering the same provider twice") - } -} - -func TestRegisterTicketProvider(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - mockFactory := &mockTicketFactory{} - err := container.RegisterTicketProvider("mock", mockFactory) - - if err != nil { - t.Fatalf("Failed to register Ticket provider: %v", err) - } - - err = container.RegisterTicketProvider("mock", mockFactory) - if err == nil { - t.Error("Should not allow registering the same provider twice") - } -} - -func TestGetAIRegistry(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - aiRegistry := container.GetAIRegistry() - if aiRegistry == nil { - t.Error("AI aiRegistry should not be nil") - } - - if aiRegistry != container.aiRegistry { - t.Error("Returned aiRegistry should be the same as internal aiRegistry") - } -} - -func TestGetVCSRegistry(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - vcsRegistry := container.GetVCSRegistry() - if vcsRegistry == nil { - t.Error("VCS vcsRegistry should not be nil") - } - - if vcsRegistry != container.vcsRegistry { - t.Error("Returned vcsRegistry should be the same as internal vcsRegistry") - } -} - -func TestGetTicketRegistry(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - ticketRegistry := container.GetTicketRegistry() - if ticketRegistry == nil { - t.Error("Ticket ticketRegistry should not be nil") - } - - if ticketRegistry != container.ticketRegistry { - t.Error("Returned ticketRegistry should be the same as internal ticketRegistry") - } -} - -func TestGetConfig(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - returnedCfg := container.GetConfig() - if returnedCfg != cfg { - t.Error("Returned config should be the same as input config") - } -} - -func TestGetTranslations(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - returnedTrans := container.GetTranslations() - if returnedTrans != trans { - t.Error("Returned translations should be the same as input translations") - } -} - -func TestGetIssueTemplateService(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - service := container.GetIssueTemplateService() - if service == nil { - t.Fatal("IssueTemplateService should not be nil") - } - - if service != container.GetIssueTemplateService() { - t.Error("Returned service should be a singleton") - } -} - -func TestGetIssueGeneratorService(t *testing.T) { - t.Run("should fail if GitService is not set", func(t *testing.T) { - cfg := &config.Config{Language: "en"} - trans := &i18n.Translations{} - container := NewContainer(cfg, trans) - - _, err := container.GetIssueGeneratorService(context.Background()) - if err == nil { - t.Error("Expected error because GitService is nil") - } - }) - - t.Run("should create IssueGeneratorService successfully", func(t *testing.T) { - cfg := &config.Config{ - Language: "en", - ActiveVCSProvider: "mock", - AIConfig: config.AIConfig{ - ActiveAI: "mock", - }, - VCSConfigs: make(map[string]config.VCSConfig), - } - cfg.VCSConfigs["mock"] = config.VCSConfig{Provider: "mock"} - - trans, _ := i18n.NewTranslations("en", "") - container := NewContainer(cfg, trans) - - aiFactory := &mockAIFactory{} - vcsFactory := &mockVCSFactory{} - - aiFactory.On("CreateIssueContentGenerator", mock.Anything, cfg, trans).Return(nil, nil) - vcsFactory.On("CreateClient", mock.Anything, "owner", "repo", "", trans).Return(nil, nil) - vcsFactory.On("ValidateConfig", mock.Anything).Return(nil) - - _ = container.RegisterAIProvider("mock", aiFactory) - _ = container.RegisterVCSProvider("mock", vcsFactory) - - mockGit := &mockGitService{} - container.SetGitService(mockGit) - - service, err := container.GetIssueGeneratorService(context.Background()) - if err != nil { - t.Fatalf("Failed to get IssueGeneratorService: %v", err) - } - - if service == nil { - t.Fatal("IssueGeneratorService should not be nil") - } - - secondService, _ := container.GetIssueGeneratorService(context.Background()) - if service != secondService { - t.Error("Returned service should be a singleton") - } - - aiFactory.AssertExpectations(t) - vcsFactory.AssertExpectations(t) - }) -} - -type mockGitService struct { - ports.GitService -} - -func (m *mockGitService) GetRepoInfo(_ context.Context) (string, string, string, error) { - return "owner", "repo", "mock", nil -} - -var _ registry.AIProviderFactory = (*mockAIFactory)(nil) -var _ vcsregistry.VCSProviderFactory = (*mockVCSFactory)(nil) -var _ ticketregistry.TicketProviderFactory = (*mockTicketFactory)(nil) diff --git a/internal/infrastructure/factory/pr_service_factory.go b/internal/infrastructure/factory/pr_service_factory.go deleted file mode 100644 index 718307b..0000000 --- a/internal/infrastructure/factory/pr_service_factory.go +++ /dev/null @@ -1,63 +0,0 @@ -package factory - -import ( - "context" - "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 PRServiceFactoryInterface interface { - CreatePRService(ctx context.Context) (ports.PRService, error) -} - -type prServiceFactory struct { - config *config.Config - aiService ports.PRSummarizer - trans *i18n.Translations - gitService ports.GitService -} - -func NewPrServiceFactory(cfg *config.Config, trans *i18n.Translations, aiService ports.PRSummarizer, gitService ports.GitService) PRServiceFactoryInterface { - return &prServiceFactory{ - config: cfg, - trans: trans, - aiService: aiService, - gitService: gitService, - } -} - -func (f *prServiceFactory) CreatePRService(ctx context.Context) (ports.PRService, error) { - owner, repo, provider, err := f.gitService.GetRepoInfo(ctx) - if err != nil { - return nil, fmt.Errorf("error al obtener la informacion del repositorio: %w", err) - } - - vcsConfig, exists := f.config.VCSConfigs[provider] - if !exists { - if f.config.ActiveVCSProvider != "" { - 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) - } - provider = f.config.ActiveVCSProvider - } else { - return nil, fmt.Errorf("proveedor de VCS '%s' detectado automáticamente pero no configurado", provider) - } - } - - var vcsClient ports.VCSClient - - switch provider { - case "github": - vcsClient = github.NewGitHubClient(owner, repo, vcsConfig.Token, f.trans) - default: - return nil, fmt.Errorf("proveedor de VCS no compatible: %s", provider) - } - - return services.NewPRService(vcsClient, f.aiService, f.trans, f.config), nil -} diff --git a/internal/infrastructure/git/git_service.go b/internal/infrastructure/git/git_service.go index 1bcd5bd..106f672 100644 --- a/internal/infrastructure/git/git_service.go +++ b/internal/infrastructure/git/git_service.go @@ -7,52 +7,41 @@ import ( "regexp" "strings" + "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" ) -var _ ports.GitService = (*GitService)(nil) +type GitService struct{} -type GitService struct { - trans *i18n.Translations +func NewGitService() *GitService { + return &GitService{} } -func NewGitService(trans *i18n.Translations) *GitService { - return &GitService{ - trans: trans, - } -} - -// HasStagedChanges verifica si hay cambios en el área de staging +// HasStagedChanges checks if there are changes in the staging area func (s *GitService) HasStagedChanges(ctx context.Context) bool { cmd := exec.CommandContext(ctx, "git", "diff", "--cached", "--quiet") err := cmd.Run() - // Si el comando retorna error (exit status 1), significa que hay cambios staged + // If the command returns an error (exit status 1), it means there are staged changes return err != nil && cmd.ProcessState.ExitCode() == 1 } -func (s *GitService) GetChangedFiles(ctx context.Context) ([]models.GitChange, error) { +func (s *GitService) GetChangedFiles(ctx context.Context) ([]string, error) { cmd := exec.CommandContext(ctx, "git", "status", "--porcelain") output, err := cmd.Output() if err != nil { return nil, err } - changes := make([]models.GitChange, 0) + changes := make([]string, 0) lines := strings.Split(string(output), "\n") for _, line := range lines { if len(line) > 3 { - status := strings.TrimSpace(line[:2]) path := strings.TrimSpace(line[3:]) if path != "" { - changes = append(changes, models.GitChange{ - Path: path, - Status: status, - }) + changes = append(changes, path) } } } @@ -84,7 +73,7 @@ func (s *GitService) GetDiff(ctx context.Context) (string, error) { fileContentCmd := exec.CommandContext(ctx, "git", "show", ":"+file) content, err := fileContentCmd.Output() if err != nil { - combinedDiff += "\n=== Nuevo archivo" + " " + file + "===\n" + combinedDiff += "\n=== New file" + " " + file + "===\n" combinedDiff += string(content) } } @@ -96,21 +85,20 @@ func (s *GitService) GetDiff(ctx context.Context) (string, error) { func (s *GitService) CreateCommit(ctx context.Context, message string) error { if !s.HasStagedChanges(ctx) { - msg := s.trans.GetMessage("git.no_staged_changes", 0, nil) - return fmt.Errorf("%s", msg) + return errors.ErrNoChanges } cmd := exec.CommandContext(ctx, "git", "commit", "-m", message) - return cmd.Run() + if err := cmd.Run(); err != nil { + return fmt.Errorf("%w: %v", errors.ErrCreateCommit, err) + } + return nil } func (s *GitService) AddFileToStaging(ctx context.Context, file string) error { repoRoot, err := s.getRepoRoot(ctx) if err != nil { - msg := s.trans.GetMessage("git.get_repo_root", 0, map[string]interface{}{ - "Error": err, - }) - return fmt.Errorf("%s", msg) + return fmt.Errorf("%w: %v", errors.ErrGetRepoRoot, err) } cmd := exec.CommandContext(ctx, "git", "add", "--", file) @@ -119,12 +107,8 @@ func (s *GitService) AddFileToStaging(ctx context.Context, file string) error { cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - fullErr := fmt.Sprintf("%v → %s", err, strings.TrimSpace(stderr.String())) - msg := s.trans.GetMessage("git.add_file", 0, map[string]interface{}{ - "File": file, - "Error": fullErr, - }) - return fmt.Errorf("%s", msg) + fullErr := fmt.Sprintf("%v: %s", err, strings.TrimSpace(stderr.String())) + return fmt.Errorf("%w [%s]: %s", errors.ErrAddFile, file, fullErr) } return nil } @@ -133,16 +117,12 @@ func (s *GitService) GetCurrentBranch(ctx context.Context) (string, error) { cmd := exec.CommandContext(ctx, "git", "branch", "--show-current") output, err := cmd.Output() if err != nil { - msg := s.trans.GetMessage("git.get_branch_name", 0, map[string]interface{}{ - "Error": err, - }) - return "", fmt.Errorf("%s", msg) + return "", fmt.Errorf("%w: %v", errors.ErrGetBranch, err) } branchName := strings.TrimSpace(string(output)) if branchName == "" { - msg := s.trans.GetMessage("git.branch_not_detected", 0, nil) - return "", fmt.Errorf("%s", msg) + return "", errors.ErrNoBranch } return branchName, nil @@ -152,21 +132,18 @@ func (s *GitService) GetRepoInfo(ctx context.Context) (string, string, string, e cmd := exec.CommandContext(ctx, "git", "remote", "get-url", "origin") output, err := cmd.Output() if err != nil { - msg := s.trans.GetMessage("git.get_repo_url", 0, map[string]interface{}{ - "Error": err, - }) - return "", "", "", fmt.Errorf("%s", msg) + return "", "", "", fmt.Errorf("%w: %v", errors.ErrGetRepoURL, err) } url := strings.TrimSpace(string(output)) - return parseRepoURL(url, s.trans) + return parseRepoURL(url) } func (s *GitService) GetLastTag(ctx context.Context) (string, error) { cmd := exec.CommandContext(ctx, "git", "describe", "--tags", "--abbrev=0") output, err := cmd.Output() if err != nil { - // no hay tags + // no tags found return "", nil } return strings.TrimSpace(string(output)), nil @@ -175,7 +152,7 @@ func (s *GitService) GetLastTag(ctx context.Context) (string, error) { func (s *GitService) GetCommitsSinceTag(ctx context.Context, tag string) ([]models.Commit, error) { var args []string if tag == "" { - // si no hay tag anterior, obtener todos los commits + // if no previous tag exists, get all commits args = []string{"log", "--pretty=format:%H|%s|%b", "--no-merges"} } else { args = []string{"log", tag + "..HEAD", "--pretty=format:%H|%s|%b", "--no-merges"} @@ -184,10 +161,7 @@ func (s *GitService) GetCommitsSinceTag(ctx context.Context, tag string) ([]mode cmd := exec.CommandContext(ctx, "git", args...) output, err := cmd.Output() if err != nil { - msg := s.trans.GetMessage("git.get_commits", 0, map[string]interface{}{ - "Error": err, - }) - return nil, fmt.Errorf("%s", msg) + return nil, fmt.Errorf("%w: %v", errors.ErrGetCommits, err) } if len(output) == 0 { @@ -225,10 +199,7 @@ func (s *GitService) GetCommitsBetweenTags(ctx context.Context, fromTag, toTag s cmd := exec.CommandContext(ctx, "git", args...) output, err := cmd.Output() if err != nil { - msg := s.trans.GetMessage("git.get_commits", 0, map[string]interface{}{ - "Error": err, - }) - return nil, fmt.Errorf("%s", msg) + return nil, fmt.Errorf("%w: %v", errors.ErrGetCommits, err) } if len(output) == 0 { @@ -256,13 +227,14 @@ func (s *GitService) GetCommitsBetweenTags(ctx context.Context, fromTag, toTag s return commits, nil } -func (s *GitService) GetRecentCommitMessages(ctx context.Context, count int) (string, error) { +func (s *GitService) GetRecentCommitMessages(ctx context.Context, count int) ([]string, error) { cmd := exec.CommandContext(ctx, "git", "log", fmt.Sprintf("-%d", count), "--pretty=format:%s %b") output, err := cmd.Output() if err != nil { - return "", err + return nil, err } - return string(output), nil + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + return lines, nil } func (s *GitService) CreateTag(ctx context.Context, version, message string) error { @@ -294,14 +266,12 @@ func (s *GitService) GetCommitCount(ctx context.Context) (int, error) { // GetTagDate returns the creation date of a tag in YYYY-MM-DD format func (s *GitService) GetTagDate(ctx context.Context, tag string) (string, error) { - // Get the date of the tag using git log cmd := exec.CommandContext(ctx, "git", "log", "-1", "--format=%ai", tag) output, err := cmd.Output() if err != nil { return "", fmt.Errorf("error getting tag date: %w", err) } - // Parse the date (format: "2024-03-20 15:30:45 -0300") dateStr := strings.TrimSpace(string(output)) if len(dateStr) >= 10 { return dateStr[:10], nil // Return YYYY-MM-DD @@ -310,7 +280,7 @@ func (s *GitService) GetTagDate(ctx context.Context, tag string) (string, error) return dateStr, nil } -func parseRepoURL(url string, trans *i18n.Translations) (string, string, string, error) { +func parseRepoURL(url string) (string, string, string, error) { sshRegex := regexp.MustCompile(`git@([^:]+):([^/]+)/(.+)\.git$`) httpsRegex := regexp.MustCompile(`https://([^/]+)/([^/]+)/(.+?)(?:\.git)?$`) @@ -327,10 +297,7 @@ func parseRepoURL(url string, trans *i18n.Translations) (string, string, string, return matches[2], repoName, provider, nil } - msg := trans.GetMessage("git.extract_repo_info", 0, map[string]interface{}{ - "Url": url, - }) - return "", "", "", fmt.Errorf("%s", msg) + return "", "", "", fmt.Errorf("%w [%s]", errors.ErrExtractRepoInfo, url) } func detectProvider(host string) string { @@ -343,15 +310,12 @@ func detectProvider(host string) string { return "unknown" } -// getRepoRoot obtiene la ruta absoluta de la raíz del repositorio git +// getRepoRoot gets the absolute path to the root of the git repository func (s *GitService) getRepoRoot(ctx context.Context) (string, error) { cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel") output, err := cmd.Output() if err != nil { - msg := s.trans.GetMessage("git.get_repo_root", 0, map[string]interface{}{ - "Error": err, - }) - return "", fmt.Errorf("%s", msg) + return "", fmt.Errorf("%w: %v", errors.ErrGetRepoRoot, err) } return strings.TrimSpace(string(output)), nil } diff --git a/internal/infrastructure/git/git_service_test.go b/internal/infrastructure/git/git_service_test.go index b9444b6..19384a2 100644 --- a/internal/infrastructure/git/git_service_test.go +++ b/internal/infrastructure/git/git_service_test.go @@ -2,14 +2,14 @@ package git import ( "context" + "errors" "os" "os/exec" "path/filepath" "strings" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/stretchr/testify/assert" ) @@ -68,8 +68,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() // Act - Verificar sin cambios staged hasStagedBefore := service.HasStagedChanges(context.Background()) @@ -103,8 +102,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() // Act - Obtener la branch actual (debería ser 'main' por defecto) branchName, err := service.GetCurrentBranch(context.Background()) @@ -140,8 +138,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() if err := os.WriteFile("test.txt", []byte("test content"), 0644); err != nil { t.Fatalf("Error creando archivo de prueba: %v", err) @@ -156,17 +153,10 @@ func TestGitService(t *testing.T) { } if len(changes) > 0 { - expectedChange := models.GitChange{ - Path: "test.txt", - Status: "??", - } - - if changes[0].Path != expectedChange.Path { - t.Errorf("Path esperado %s, se obtuvo %s", expectedChange.Path, changes[0].Path) - } + expectedPath := "test.txt" - if changes[0].Status != expectedChange.Status { - t.Errorf("Status esperado %s, se obtuvo %s", expectedChange.Status, changes[0].Status) + if changes[0] != expectedPath { + t.Errorf("Path esperado %s, se obtuvo %s", expectedPath, changes[0]) } } }) @@ -176,8 +166,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() if err := os.WriteFile("test.txt", []byte("test content"), 0644); err != nil { t.Fatalf("Error creando archivo de prueba: %v", err) @@ -211,8 +200,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() // Act err := service.CreateCommit(context.Background(), "Test commit") @@ -221,8 +209,8 @@ func TestGitService(t *testing.T) { if err == nil { t.Error("Se esperaba un error al crear commit sin cambios staged") } - if err.Error() != "No staged changes found" { - t.Errorf("Mensaje de error inesperado: %v", err) + if !errors.Is(err, domainErrors.ErrNoChanges) { + t.Errorf("Se esperaba ErrNoChanges, obtuve: %v", err) } }) @@ -231,8 +219,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() if err := os.WriteFile("test.txt", []byte("test content"), 0644); err != nil { t.Fatalf("Error creando archivo: %v", err) @@ -264,8 +251,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() if err := os.WriteFile("test.txt", []byte("test content"), 0644); err != nil { t.Fatalf("Error creando archivo: %v", err) @@ -306,8 +292,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() if err := os.WriteFile("nuevo.txt", []byte("archivo nuevo"), 0644); err != nil { t.Fatalf("Error creando archivo nuevo: %v", err) @@ -331,8 +316,7 @@ func TestGitService(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() // Act diff, err := service.GetDiff(context.Background()) @@ -353,8 +337,7 @@ func TestAddFileToStaging(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() testFile := "newfile.txt" if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { return @@ -377,8 +360,7 @@ func TestAddFileToStaging(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() testFile := "deleted.txt" // Crear y committear archivo @@ -413,8 +395,7 @@ func TestAddFileToStaging(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() err := service.AddFileToStaging(context.Background(), "fantasma.txt") if err == nil { @@ -443,8 +424,7 @@ func TestAddFileToStaging(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() testFile := "dir1/dir2/archivos.txt" if err := os.MkdirAll(filepath.Dir(testFile), 0755); err != nil { @@ -519,10 +499,9 @@ func TestGetRepoInfo(t *testing.T) { }, } - trans, _ := i18n.NewTranslations("en", "") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - owner, repo, provider, err := parseRepoURL(tt.url, trans) + owner, repo, provider, err := parseRepoURL(tt.url) if tt.expectedError { assert.Error(t, err) @@ -553,8 +532,7 @@ func TestGitService_NewMethods(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() // Caso 1: Sin tags tag, err := service.GetLastTag(context.Background()) @@ -580,8 +558,7 @@ func TestGitService_NewMethods(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() // Crear historial createCommitHelper(t, "init.txt", "chore: initial commit") @@ -608,8 +585,7 @@ func TestGitService_NewMethods(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() createCommitHelper(t, "file.txt", "work done") err := service.CreateTag(context.Background(), "v2.0.0", "Release v2.0.0") @@ -624,8 +600,7 @@ func TestGitService_NewMethods(t *testing.T) { tempDir := setupTestRepo(t) defer cleanupTestRepo(t, tempDir) - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() // Repositorio vacío (recién inicializado, sin commits) puede dar error o 0 dependiendo de la versión de git/setup // Vamos a crear al menos uno para asegurar @@ -666,8 +641,7 @@ func TestGitService_NewMethods(t *testing.T) { t.Fatalf("Error agregando remote: %v", err) } - trans, _ := i18n.NewTranslations("en", "") - service := NewGitService(trans) + service := NewGitService() createCommitHelper(t, "code.txt", "ready to release") // Push antes de tener el tag debería fallar o no hacer nada relevante, primero creamos el tag diff --git a/internal/infrastructure/httpclient/httpclient.go b/internal/infrastructure/httpclient/httpclient.go deleted file mode 100644 index 664c583..0000000 --- a/internal/infrastructure/httpclient/httpclient.go +++ /dev/null @@ -1,7 +0,0 @@ -package httpclient - -import "net/http" - -type HTTPClient interface { - Do(req *http.Request) (*http.Response, error) -} diff --git a/internal/infrastructure/tickets/jira/jira_factory.go b/internal/infrastructure/tickets/jira/jira_factory.go deleted file mode 100644 index c9be739..0000000 --- a/internal/infrastructure/tickets/jira/jira_factory.go +++ /dev/null @@ -1,47 +0,0 @@ -package jira - -import ( - "context" - "fmt" - "net/http" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" -) - -// JiraProviderFactory implementa TicketProviderFactory para Jira -type JiraProviderFactory struct{} - -// NewJiraProviderFactory crea una nueva factory para Jira -func NewJiraProviderFactory() *JiraProviderFactory { - return &JiraProviderFactory{} -} - -// CreateClient crea un cliente Jira -func (f *JiraProviderFactory) CreateClient( - _ context.Context, - cfg config.TicketProviderConfig, - _ *i18n.Translations, -) (ports.TickerManager, error) { - return NewJiraService(cfg.BaseURL, cfg.APIKey, cfg.Email, &http.Client{}), nil -} - -// ValidateConfig valida la configuración de Jira -func (f *JiraProviderFactory) ValidateConfig(cfg config.TicketProviderConfig) error { - if cfg.BaseURL == "" { - return fmt.Errorf("jira base URL es requerida") - } - if cfg.APIKey == "" { - return fmt.Errorf("jira API key es requerida") - } - if cfg.Email == "" { - return fmt.Errorf("jira email es requerido") - } - return nil -} - -// Name retorna el nombre del proveedor -func (f *JiraProviderFactory) Name() string { - return "jira" -} diff --git a/internal/infrastructure/tickets/jira/jira_service.go b/internal/infrastructure/tickets/jira/jira_service.go index e2b18d1..67c3f8b 100644 --- a/internal/infrastructure/tickets/jira/jira_service.go +++ b/internal/infrastructure/tickets/jira/jira_service.go @@ -8,26 +8,31 @@ import ( "regexp" "strings" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/httpclient" ) -// Constantes para patrones de criterios de aceptación +// Constants for acceptance criteria patterns const ( AcceptanceCriteriaEN = "Acceptance Criteria" AcceptanceCriteriaES = "Criterio de Aceptación" ) -// JiraService representa el servicio para interactuar con la API de Jira. +// httpClient is a minimal interface for testing purposes +type httpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// JiraService represents the service to interact with the Jira API. type JiraService struct { baseURL string apiKey string jiraEmail string - client httpclient.HTTPClient + client httpClient } -// NewJiraService crea una nueva instancia de JiraService. -func NewJiraService(baseURL, apiKey, email string, client httpclient.HTTPClient) *JiraService { +// NewJiraService creates a new instance of JiraService. +func NewJiraService(baseURL, apiKey, email string, client httpClient) *JiraService { return &JiraService{ baseURL: baseURL, apiKey: apiKey, @@ -36,7 +41,7 @@ func NewJiraService(baseURL, apiKey, email string, client httpclient.HTTPClient) } } -// JiraFields representa los campos de un ticket de Jira. +// JiraFields represents the fields of a Jira ticket. type ( JiraFields struct { Summary string `json:"summary"` @@ -63,17 +68,17 @@ type ( } ) -// GetTicketInfo obtiene la información de un ticket de Jira. +// GetTicketInfo gets the information of a Jira ticket. func (s *JiraService) GetTicketInfo(ticketID string) (*models.TicketInfo, error) { customFields, err := s.GetCustomFields() if err != nil { - return nil, fmt.Errorf("no se pudieron obtener los campos personalizados: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to get custom fields from Jira", err) } criteriaFieldID := findCriteriaFieldID(customFields) ticketFields, err := s.fetchTicketFields(ticketID) if err != nil { - return nil, fmt.Errorf("no se pudieron obtener los campos de tickets: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to fetch ticket fields from Jira", err) } description := parseAtlassianDoc(ticketFields.Description.Content) @@ -89,37 +94,35 @@ func (s *JiraService) GetTicketInfo(ticketID string) (*models.TicketInfo, error) return ticketInfo, nil } -// fetchTicketFields obtiene los campos de un ticket de Jira. +// fetchTicketFields gets the fields of a Jira ticket. func (s *JiraService) fetchTicketFields(ticketID string) (*JiraFields, error) { url := fmt.Sprintf("%s/rest/api/3/issue/%s", s.baseURL, ticketID) resp, err := s.makeRequest("GET", url) if err != nil { - return nil, fmt.Errorf("error al realizar una solicitud a la API de Jira: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to make request to Jira API", err) } defer func() { - if err := resp.Body.Close(); err != nil { - fmt.Println("error al cerrar el cuerpo de la respuesta:", err) - } + _ = resp.Body.Close() }() switch resp.StatusCode { case http.StatusOK: - // Todo está bien, continuamos con el procesamiento + // OK case http.StatusNotFound: - return nil, fmt.Errorf("ticket con ID %s no existe en Jira", ticketID) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, fmt.Sprintf("ticket %s not found in Jira", ticketID), nil) case http.StatusUnauthorized: - return nil, fmt.Errorf("no autorizado: verifica tus credenciales de Jira") + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "unauthorized: check Jira credentials", nil) case http.StatusInternalServerError: - return nil, fmt.Errorf("error interno del servidor de Jira") + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "jira internal server error", nil) default: - return nil, fmt.Errorf("error inesperado al obtener el ticket: %s", resp.Status) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, fmt.Sprintf("unexpected error fetching ticket: %s", resp.Status), nil) } var result struct { Fields map[string]json.RawMessage `json:"fields"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("error decodificando respuesta: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to decode jira response", err) } jiraFields := &JiraFields{ @@ -129,7 +132,7 @@ func (s *JiraService) fetchTicketFields(ticketID string) (*JiraFields, error) { if rawSummary, ok := result.Fields["summary"]; ok { var summary string if err := json.Unmarshal(rawSummary, &summary); err != nil { - return nil, fmt.Errorf("error decodificando resumen: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to unmarshal ticket summary", err) } jiraFields.Summary = summary } @@ -137,7 +140,7 @@ func (s *JiraService) fetchTicketFields(ticketID string) (*JiraFields, error) { if rawDescription, ok := result.Fields["description"]; ok { var description AtlassianDoc if err := json.Unmarshal(rawDescription, &description); err != nil { - return nil, fmt.Errorf("error decodificando descripcion: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to unmarshal ticket description", err) } jiraFields.Description = description } @@ -155,7 +158,7 @@ func (s *JiraService) fetchTicketFields(ticketID string) (*JiraFields, error) { return jiraFields, nil } -// extractCriteria extrae los criterios de aceptación de los campos del ticket. +// extractCriteria extracts acceptance criteria from the ticket fields. func (s *JiraService) extractCriteria(fields *JiraFields, criteriaFieldID, description string) ([]string, string) { var criteria []string @@ -172,11 +175,11 @@ func (s *JiraService) extractCriteria(fields *JiraFields, criteriaFieldID, descr return criteria, description } -// makeRequest realiza una solicitud HTTP a la API de Jira. +// makeRequest performs an HTTP request to the Jira API. func (s *JiraService) makeRequest(method, url string) (*http.Response, error) { req, err := http.NewRequest(method, url, nil) if err != nil { - return nil, fmt.Errorf("error creando requests: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to create jira request", err) } req.Header.Set("Authorization", getBasicAuth(s.jiraEmail, s.apiKey)) @@ -184,19 +187,19 @@ func (s *JiraService) makeRequest(method, url string) (*http.Response, error) { resp, err := s.client.Do(req) if err != nil { - return nil, fmt.Errorf("error al realizar la solicitud: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to perform jira request", err) } return resp, nil } -// getBasicAuth genera el encabezado de autenticación básica. +// getBasicAuth generates the basic authentication header. func getBasicAuth(username, token string) string { credentials := fmt.Sprintf("%s:%s", username, token) return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(credentials))) } -// findCriteriaFieldID busca el ID del campo de criterios de aceptación. +// findCriteriaFieldID finds the ID of the acceptance criteria field. func findCriteriaFieldID(customFields map[string]string) string { patterns := []string{AcceptanceCriteriaEN, AcceptanceCriteriaES} @@ -211,7 +214,7 @@ func findCriteriaFieldID(customFields map[string]string) string { return "" } -// extractCriteriaFromCustomField extrae los criterios de un campo personalizado. +// extractCriteriaFromCustomField extracts criteria from a custom field. func extractCriteriaFromCustomField(fields map[string]CustomField, fieldID string) ([]string, string) { if fieldID == "" { return nil, "" @@ -219,7 +222,7 @@ func extractCriteriaFromCustomField(fields map[string]CustomField, fieldID strin fieldValue, ok := fields[fieldID] if !ok { - return nil, "" // Campo no existe en el mapa + return nil, "" // Field does not exist in the map } var criteriaText string @@ -253,27 +256,25 @@ func extractCriteriaFromCustomField(fields map[string]CustomField, fieldID strin return filteredCriteria, criteriaText } -// GetCustomFields obtiene los campos personalizados de Jira. +// GetCustomFields gets the custom fields from Jira. func (s *JiraService) GetCustomFields() (map[string]string, error) { url := fmt.Sprintf("%s/rest/api/3/field", s.baseURL) resp, err := s.makeRequest("GET", url) if err != nil { - return nil, fmt.Errorf("error al obtener campos personalizados: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to fetch custom fields", err) } defer func() { - if err := resp.Body.Close(); err != nil { - fmt.Println("error al cerrar el cuerpo de la respuesta:", err) - } + _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("error al obtener campos personalizados: %s", resp.Status) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, fmt.Sprintf("failed to get custom fields: %s", resp.Status), nil) } var fields []map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&fields); err != nil { - return nil, fmt.Errorf("error al decodificar campos personalizados: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to decode custom fields", err) } customFields := make(map[string]string) @@ -288,7 +289,7 @@ func (s *JiraService) GetCustomFields() (map[string]string, error) { return customFields, nil } -// parseAtlassianDoc convierte el contenido de un documento de Atlassian en una cadena de texto. +// parseAtlassianDoc converts an Atlassian document content into a string. func parseAtlassianDoc(content []DocContent) string { var result strings.Builder parseAtlassianDocRecursive(content, &result) @@ -317,12 +318,12 @@ func parseAtlassianDocRecursive(content []DocContent, result *strings.Builder) { parseAtlassianDocRecursive(item.Content, result) } default: - // Ignora otros tipos o agrega manejo según sea necesario + // Ignore other types or add handling as necessary } } } -// extractAndRemoveCriteria extrae y elimina los criterios de aceptación de un texto. +// extractAndRemoveCriteria extracts and removes acceptance criteria from text. func extractAndRemoveCriteria(text string) ([]string, string) { lines := strings.Split(text, "\n") var criteria []string @@ -352,11 +353,11 @@ func extractAndRemoveCriteria(text string) ([]string, string) { return criteria, description } -// removeCriteriaFromDescription elimina los criterios de aceptación de la descripción. +// removeCriteriaFromDescription removes acceptance criteria from the description. func removeCriteriaFromDescription(description string) string { patterns := []string{ - "Acceptance criteria:.*(\n.*)*", // Elimina todo lo que sigue después de "Acceptance criteria:" - "Criterio de aceptacion:.*(\n.*)*", // Elimina todo lo que sigue después de "Criterio de aceptacion:" + "Acceptance criteria:.*(\n.*)*", // Removes everything following "Acceptance criteria:" + "Criterio de aceptacion:.*(\n.*)*", // Removes everything following "Criterio de aceptacion:" } for _, pattern := range patterns { diff --git a/internal/infrastructure/tickets/jira/jira_service_test.go b/internal/infrastructure/tickets/jira/jira_service_test.go index 9c375dd..a8ad6b6 100644 --- a/internal/infrastructure/tickets/jira/jira_service_test.go +++ b/internal/infrastructure/tickets/jira/jira_service_test.go @@ -122,7 +122,7 @@ func TestGetTicketInfo_CustomFieldsError(t *testing.T) { // Assert assert.Error(t, err) assert.Nil(t, ticketInfo) - assert.Contains(t, err.Error(), "no se pudieron obtener los campos personalizados") + assert.Contains(t, err.Error(), "failed to get custom fields from Jira") mockClient.AssertExpectations(t) } @@ -149,7 +149,7 @@ func TestGetTicketInfo_TicketFieldsError(t *testing.T) { // Assert assert.Error(t, err) assert.Nil(t, ticketInfo) - assert.Contains(t, err.Error(), "no se pudieron obtener los campos de tickets") + assert.Contains(t, err.Error(), "failed to fetch ticket fields from Jira") mockClient.AssertExpectations(t) } @@ -307,7 +307,7 @@ func TestGetCustomFields_ErrorStatusCode(t *testing.T) { // Assert assert.Error(t, err, "Se esperaba un error debido al código de estado no OK") assert.Nil(t, customFields, "No debería devolverse ningún campo personalizado") - assert.Contains(t, err.Error(), "error al obtener campos personalizados: 500 Internal Server Error", "El mensaje de error no coincide") + assert.Contains(t, err.Error(), "failed to get custom fields: 500 Internal Server Error", "El mensaje de error no coincide") mockClient.AssertExpectations(t) } @@ -345,7 +345,7 @@ func TestMakeRequest(t *testing.T) { // Assert assert.Error(t, err, "Se esperaba un error al crear la solicitud HTTP") assert.Nil(t, resp, "No debería devolverse ninguna respuesta") - assert.Contains(t, err.Error(), "error creando requests", "El mensaje de error no coincide") + assert.Contains(t, err.Error(), "failed to create jira request", "El mensaje de error no coincide") }) t.Run("ErrorMakingRequest", func(t *testing.T) { @@ -363,7 +363,7 @@ func TestMakeRequest(t *testing.T) { // Assert assert.Error(t, err, "Se esperaba un error al realizar la solicitud HTTP") - assert.Contains(t, err.Error(), "error al realizar la solicitud", "El mensaje de error no coincide") + assert.Contains(t, err.Error(), "failed to perform jira request", "El mensaje de error no coincide") mockClient.AssertExpectations(t) }) } diff --git a/internal/infrastructure/tickets/registry/ticket_provider_registry.go b/internal/infrastructure/tickets/registry/ticket_provider_registry.go deleted file mode 100644 index 0438bc0..0000000 --- a/internal/infrastructure/tickets/registry/ticket_provider_registry.go +++ /dev/null @@ -1,111 +0,0 @@ -package registry - -import ( - "context" - "fmt" - "sync" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" -) - -// TicketProviderFactory define la interfaz para crear clientes de tickets -type TicketProviderFactory interface { - // CreateClient crea un cliente de tickets con la configuración proporcionada - CreateClient(ctx context.Context, cfg config.TicketProviderConfig, trans *i18n.Translations) (ports.TickerManager, error) - - // ValidateConfig valida la configuración para este proveedor - ValidateConfig(cfg config.TicketProviderConfig) error - - // Name retorna el nombre del proveedor - Name() string -} - -// TicketProviderRegistry gestiona el registro de proveedores de tickets -type TicketProviderRegistry struct { - mu sync.RWMutex - factories map[string]TicketProviderFactory -} - -// NewTicketProviderRegistry crea un nuevo registro de proveedores de tickets -func NewTicketProviderRegistry() *TicketProviderRegistry { - return &TicketProviderRegistry{ - factories: make(map[string]TicketProviderFactory), - } -} - -// Register registra un nuevo proveedor de tickets -func (r *TicketProviderRegistry) Register(name string, factory TicketProviderFactory) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.factories[name]; exists { - return fmt.Errorf("proveedor de tickets '%s' ya esta registrado", name) - } - - r.factories[name] = factory - return nil -} - -// Get obtiene un factory por nombre -func (r *TicketProviderRegistry) Get(name string) (TicketProviderFactory, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - factory, exists := r.factories[name] - if !exists { - return nil, fmt.Errorf("proveedor de tickets '%s' no encontrado en el registro", name) - } - - return factory, nil -} - -// List retorna la lista de proveedores registrados -func (r *TicketProviderRegistry) List() []string { - r.mu.RLock() - defer r.mu.RUnlock() - - providers := make([]string, 0, len(r.factories)) - for name := range r.factories { - providers = append(providers, name) - } - return providers -} - -// IsRegistered verifica si un proveedor está registrado -func (r *TicketProviderRegistry) IsRegistered(name string) bool { - r.mu.RLock() - defer r.mu.RUnlock() - - _, exists := r.factories[name] - return exists -} - -// CreateClientFromConfig crea un cliente de tickets usando la configuración -func (r *TicketProviderRegistry) CreateClientFromConfig( - ctx context.Context, - cfg *config.Config, - trans *i18n.Translations, -) (ports.TickerManager, error) { - if cfg.ActiveTicketService == "" || !cfg.UseTicket { - // Return nil if no ticket service is active - return nil, nil - } - - ticketCfg, exists := cfg.TicketProviders[cfg.ActiveTicketService] - if !exists { - return nil, fmt.Errorf("configuración no encontrada para proveedor de tickets: %s", cfg.ActiveTicketService) - } - - factory, err := r.Get(cfg.ActiveTicketService) - if err != nil { - return nil, err - } - - if err := factory.ValidateConfig(ticketCfg); err != nil { - return nil, fmt.Errorf("configuracion de tickets invalida para %s: %w", cfg.ActiveTicketService, err) - } - - return factory.CreateClient(ctx, ticketCfg, trans) -} diff --git a/internal/infrastructure/tickets/registry/ticket_provider_registry_test.go b/internal/infrastructure/tickets/registry/ticket_provider_registry_test.go deleted file mode 100644 index 20d029c..0000000 --- a/internal/infrastructure/tickets/registry/ticket_provider_registry_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package registry - -import ( - "context" - "testing" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" -) - -// MockTicketProviderFactory for testing -type MockTicketProviderFactory struct { - name string -} - -func (m *MockTicketProviderFactory) CreateClient(_ context.Context, _ config.TicketProviderConfig, _ *i18n.Translations) (ports.TickerManager, error) { - return nil, nil -} - -func (m *MockTicketProviderFactory) ValidateConfig(_ config.TicketProviderConfig) error { - return nil -} - -func (m *MockTicketProviderFactory) Name() string { - return m.name -} - -func TestNewTicketProviderRegistry(t *testing.T) { - registry := NewTicketProviderRegistry() - - if registry == nil { - t.Fatal("Registry should not be nil") - } - - if registry.factories == nil { - t.Error("Factories map should be initialized") - } -} - -func TestRegisterTicketProvider(t *testing.T) { - registry := NewTicketProviderRegistry() - factory := &MockTicketProviderFactory{name: "mock"} - - err := registry.Register("mock", factory) - if err != nil { - t.Fatalf("Failed to register provider: %v", err) - } - - // Try to register the same provider again - err = registry.Register("mock", factory) - if err == nil { - t.Error("Should not allow registering the same provider twice") - } -} - -func TestGetTicketProvider(t *testing.T) { - registry := NewTicketProviderRegistry() - factory := &MockTicketProviderFactory{name: "mock"} - - _ = registry.Register("mock", factory) - - retrievedFactory, err := registry.Get("mock") - if err != nil { - t.Fatalf("Failed to get registered provider: %v", err) - } - - if retrievedFactory == nil { - t.Error("Retrieved factory should not be nil") - } - - if retrievedFactory.Name() != "mock" { - t.Errorf("Expected factory name 'mock', got '%s'", retrievedFactory.Name()) - } -} - -func TestGetTicketProviderNotFound(t *testing.T) { - registry := NewTicketProviderRegistry() - - _, err := registry.Get("nonexistent") - if err == nil { - t.Error("Should return error for non-existent provider") - } -} - -func TestListTicketProviders(t *testing.T) { - registry := NewTicketProviderRegistry() - - // Empty registry - list := registry.List() - if len(list) != 0 { - t.Error("List should be empty initially") - } - - // Register multiple providers - _ = registry.Register("jira", &MockTicketProviderFactory{name: "jira"}) - _ = registry.Register("linear", &MockTicketProviderFactory{name: "linear"}) - - list = registry.List() - if len(list) != 2 { - t.Errorf("Expected 2 providers, got %d", len(list)) - } - - // Verify providers are in the list - found := make(map[string]bool) - for _, name := range list { - found[name] = true - } - - if !found["jira"] || !found["linear"] { - t.Error("Expected providers not found in list") - } -} - -func TestIsRegisteredTicketProvider(t *testing.T) { - registry := NewTicketProviderRegistry() - - if registry.IsRegistered("mock") { - t.Error("Provider should not be registered initially") - } - - _ = registry.Register("mock", &MockTicketProviderFactory{name: "mock"}) - - if !registry.IsRegistered("mock") { - t.Error("Provider should be registered after Register call") - } - - if registry.IsRegistered("nonexistent") { - t.Error("Nonexistent provider should not be registered") - } -} - -func TestCreateClientFromConfig(t *testing.T) { - registry := NewTicketProviderRegistry() - factory := &MockTicketProviderFactory{name: "jira"} - _ = registry.Register("jira", factory) - - cfg := &config.Config{ - ActiveTicketService: "jira", - UseTicket: true, - TicketProviders: map[string]config.TicketProviderConfig{ - "jira": { - APIKey: "test-key", - BaseURL: "https://test.atlassian.net", - Email: "test@example.com", - }, - }, - } - - trans := &i18n.Translations{} - ctx := context.Background() - - client, err := registry.CreateClientFromConfig(ctx, cfg, trans) - if err != nil { - t.Fatalf("Failed to create client from config: %v", err) - } - - // Mock returns nil, which is acceptable - if client != nil { - t.Error("Mock should return nil client") - } -} - -func TestCreateClientFromConfigNoActiveService(t *testing.T) { - registry := NewTicketProviderRegistry() - - cfg := &config.Config{ - ActiveTicketService: "", - UseTicket: false, - } - - trans := &i18n.Translations{} - ctx := context.Background() - - client, err := registry.CreateClientFromConfig(ctx, cfg, trans) - if err != nil { - t.Fatalf("Should not error when no active service: %v", err) - } - - if client != nil { - t.Error("Should return nil client when no active service") - } -} - -func TestCreateClientFromConfigProviderNotFound(t *testing.T) { - registry := NewTicketProviderRegistry() - - cfg := &config.Config{ - ActiveTicketService: "nonexistent", - UseTicket: true, - TicketProviders: map[string]config.TicketProviderConfig{ - "nonexistent": { - APIKey: "test-key", - }, - }, - } - - trans := &i18n.Translations{} - ctx := context.Background() - - _, err := registry.CreateClientFromConfig(ctx, cfg, trans) - if err == nil { - t.Error("Should error when provider is not registered") - } -} - -func TestCreateClientFromConfigNoConfig(t *testing.T) { - registry := NewTicketProviderRegistry() - factory := &MockTicketProviderFactory{name: "jira"} - _ = registry.Register("jira", factory) - - cfg := &config.Config{ - ActiveTicketService: "jira", - UseTicket: true, - TicketProviders: map[string]config.TicketProviderConfig{}, // Empty config - } - - trans := &i18n.Translations{} - ctx := context.Background() - - _, err := registry.CreateClientFromConfig(ctx, cfg, trans) - if err == nil { - t.Error("Should error when provider config is missing") - } -} - -// Verify interface implementation -var _ TicketProviderFactory = (*MockTicketProviderFactory)(nil) diff --git a/internal/infrastructure/vcs/github/github_factory.go b/internal/infrastructure/vcs/github/github_factory.go deleted file mode 100644 index 850e171..0000000 --- a/internal/infrastructure/vcs/github/github_factory.go +++ /dev/null @@ -1,40 +0,0 @@ -package github - -import ( - "context" - "fmt" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" -) - -// GitHubProviderFactory implementa VCSProviderFactory para GitHub -type GitHubProviderFactory struct{} - -// NewGitHubProviderFactory crea una nueva factory para GitHub -func NewGitHubProviderFactory() *GitHubProviderFactory { - return &GitHubProviderFactory{} -} - -// CreateClient crea un cliente GitHub -func (f *GitHubProviderFactory) CreateClient( - _ context.Context, - owner, repo, token string, - trans *i18n.Translations, -) (ports.VCSClient, error) { - return NewGitHubClient(owner, repo, token, trans), nil -} - -// ValidateConfig valida la configuración de GitHub -func (f *GitHubProviderFactory) ValidateConfig(cfg *config.VCSConfig) error { - if cfg.Token == "" { - return fmt.Errorf("token de github necesario") - } - return nil -} - -// Name retorna el nombre del proveedor -func (f *GitHubProviderFactory) Name() string { - return "github" -} diff --git a/internal/infrastructure/vcs/github/github_service.go b/internal/infrastructure/vcs/github/github_service.go index 8082040..7b9b300 100644 --- a/internal/infrastructure/vcs/github/github_service.go +++ b/internal/infrastructure/vcs/github/github_service.go @@ -14,9 +14,7 @@ import ( "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/builder" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/httpclient" "github.com/google/go-github/v80/github" "golang.org/x/oauth2" ) @@ -38,7 +36,7 @@ type IssuesService interface { ListByRepo(ctx context.Context, owner, repo string, opts *github.IssueListByRepoOptions) ([]*github.Issue, *github.Response, error) Get(ctx context.Context, owner, repo string, number int) (*github.Issue, *github.Response, error) Edit(ctx context.Context, owner, repo string, number int, issue *github.IssueRequest) (*github.Issue, *github.Response, error) - Create(ctx context.Context, owner, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error) // ← NUEVO + Create(ctx context.Context, owner, repo string, issue *github.IssueRequest) (*github.Issue, *github.Response, error) // ← NEW } type RepositoriesService interface { @@ -58,6 +56,25 @@ type UsersService interface { Get(ctx context.Context, user string) (*github.User, *github.Response, error) } +// binaryBuilder is a minimal interface for testing purposes +type binaryBuilder interface { + BuildAndPackageAll(ctx context.Context) ([]string, error) +} + +// binaryBuilderFactory is a minimal interface for testing purposes +type binaryBuilderFactory interface { + NewBuilder(mainPath, binaryName string, opts ...builder.Option) binaryBuilder +} + +// defaultBinaryBuilderFactoryAdapter adapts builder.DefaultBinaryBuilderFactory to binaryBuilderFactory +type defaultBinaryBuilderFactoryAdapter struct { + *builder.DefaultBinaryBuilderFactory +} + +func (a *defaultBinaryBuilderFactoryAdapter) NewBuilder(mainPath, binaryName string, opts ...builder.Option) binaryBuilder { + return a.DefaultBinaryBuilderFactory.NewBuilder(mainPath, binaryName, opts...) +} + type GitHubClient struct { prService PullRequestsService issuesService IssuesService @@ -66,11 +83,10 @@ type GitHubClient struct { usersService UsersService owner string repo string - trans *i18n.Translations token string - httpClient httpclient.HTTPClient + httpClient *http.Client mainPath string - binaryBuilderFactory ports.BinaryBuilderFactory + binaryBuilderFactory binaryBuilderFactory } var allowedLabels = map[string]struct { @@ -85,7 +101,16 @@ var allowedLabels = map[string]struct { "test": {"8A2BE2", "label.test"}, } -func NewGitHubClient(owner, repo, token string, trans *i18n.Translations) *GitHubClient { +var labelDescriptions = map[string]string{ + "feature": "New feature", + "fix": "Bug fix", + "refactor": "Code refactor", + "docs": "Documentation", + "infra": "Infrastructure", + "test": "Test", +} + +func NewGitHubClient(owner, repo, token string) *GitHubClient { var httpClient *http.Client if token != "" { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) @@ -101,11 +126,10 @@ func NewGitHubClient(owner, repo, token string, trans *i18n.Translations) *GitHu usersService: client.Users, owner: owner, repo: repo, - trans: trans, token: token, httpClient: httpClient, mainPath: "./cmd/main.go", - binaryBuilderFactory: &builder.DefaultBinaryBuilderFactory{}, + binaryBuilderFactory: &defaultBinaryBuilderFactoryAdapter{&builder.DefaultBinaryBuilderFactory{}}, } } @@ -117,7 +141,6 @@ func NewGitHubClientWithServices( usersService UsersService, owner string, repo string, - trans *i18n.Translations, ) *GitHubClient { return &GitHubClient{ prService: prService, @@ -127,11 +150,10 @@ func NewGitHubClientWithServices( releaseService: releaseService, owner: owner, repo: repo, - trans: trans, token: "", httpClient: &http.Client{}, mainPath: "./cmd/main.go", - binaryBuilderFactory: &builder.DefaultBinaryBuilderFactory{}, + binaryBuilderFactory: &defaultBinaryBuilderFactoryAdapter{&builder.DefaultBinaryBuilderFactory{}}, } } @@ -143,26 +165,17 @@ func (ghc *GitHubClient) UpdatePR(ctx context.Context, prNumber int, summary mod _, resp, err := ghc.prService.Edit(ctx, ghc.owner, ghc.repo, prNumber, pr) if err != nil { - // Detectar error 403 de permisos insuficientes + // Detect 403 forbidden error due to insufficient permissions if resp != nil && resp.StatusCode == http.StatusForbidden { - return fmt.Errorf("%s\n\n%s", - ghc.trans.GetMessage("error.insufficient_permissions", 0, map[string]interface{}{ - "pr_number": prNumber, - "owner": ghc.owner, - "repo": ghc.repo, - }), - ghc.trans.GetMessage("error.token_scopes_help", 0, nil)) + return fmt.Errorf("insufficient permissions to update PR #%d in %s/%s\n\nPlease ensure your token has the required scopes (repo, write:org)", + prNumber, ghc.owner, ghc.repo) } - return fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.update_pr", 0, map[string]interface{}{ - "pr_number": prNumber, - }), err) + return fmt.Errorf("failed to update PR #%d: %w", prNumber, err) } if len(summary.Labels) > 0 { if err := ghc.AddLabelsToPR(ctx, prNumber, summary.Labels); err != nil { - return fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.add_labels", 0, map[string]interface{}{ - "pr_number": prNumber, - }), err) + return fmt.Errorf("failed to add labels to PR #%d: %w", prNumber, err) } } @@ -172,12 +185,12 @@ func (ghc *GitHubClient) UpdatePR(ctx context.Context, prNumber int, summary mod func (ghc *GitHubClient) GetPR(ctx context.Context, prNumber int) (models.PRData, error) { pr, _, err := ghc.prService.Get(ctx, ghc.owner, ghc.repo, prNumber) if err != nil { - return models.PRData{}, fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.get_pr", 0, map[string]interface{}{"pr_number": prNumber}), err) + return models.PRData{}, fmt.Errorf("failed to get PR #%d: %w", prNumber, err) } commits, _, err := ghc.prService.ListCommits(ctx, ghc.owner, ghc.repo, prNumber, &github.ListOptions{}) if err != nil { - return models.PRData{}, fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.get_commits", 0, map[string]interface{}{"pr_number": prNumber}), err) + return models.PRData{}, fmt.Errorf("failed to get commits for PR #%d: %w", prNumber, err) } prCommits := make([]models.Commit, len(commits)) @@ -189,19 +202,15 @@ func (ghc *GitHubClient) GetPR(ctx context.Context, prNumber int) (models.PRData diff, resp, err := ghc.prService.GetRaw(ctx, ghc.owner, ghc.repo, prNumber, github.RawOptions{Type: github.Diff}) if err != nil { - // Si es error 406 (diff demasiado grande), usar fallback commit por commit + // If 406 error (diff too large), use fallback commit by commit if resp != nil && resp.StatusCode == http.StatusNotAcceptable { - fmt.Printf("%s\n", ghc.trans.GetMessage("warning.pr_too_large", 0, map[string]interface{}{ - "pr_number": prNumber, - })) + fmt.Printf("Warning: PR #%d is too large, fetching diffs commit by commit\n", prNumber) diff, err = ghc.getDiffFromCommits(ctx, commits) if err != nil { - return models.PRData{}, fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.get_diff_from_commits", 0, map[string]interface{}{ - "pr_number": prNumber, - }), err) + return models.PRData{}, fmt.Errorf("failed to get diff from commits for PR #%d: %w", prNumber, err) } } else { - return models.PRData{}, fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.get_diff", 0, map[string]interface{}{"pr_number": prNumber}), err) + return models.PRData{}, fmt.Errorf("failed to get diff for PR #%d: %w", prNumber, err) } } @@ -225,7 +234,7 @@ func (ghc *GitHubClient) AddLabelsToPR(ctx context.Context, prNumber int, labels existingLabels, err := ghc.GetRepoLabels(ctx) if err != nil { - return fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.get_labels", 0, nil), err) + return fmt.Errorf("failed to get repository labels: %w", err) } if err := ghc.ensureLabelsExist(ctx, existingLabels, validLabels); err != nil { @@ -238,7 +247,7 @@ func (ghc *GitHubClient) AddLabelsToPR(ctx context.Context, prNumber int, labels func (ghc *GitHubClient) GetRepoLabels(ctx context.Context) ([]string, error) { labels, _, err := ghc.issuesService.ListLabels(ctx, ghc.owner, ghc.repo, &github.ListOptions{PerPage: 100}) if err != nil { - return nil, fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.get_repo_labels", 0, nil), err) + return nil, fmt.Errorf("failed to list repository labels: %w", err) } labelNames := make([]string, len(labels)) @@ -282,18 +291,18 @@ func (ghc *GitHubClient) CreateRelease(ctx context.Context, release *models.Rele if err != nil { if resp != nil { if resp.StatusCode == http.StatusUnprocessableEntity { - return fmt.Errorf("%s", ghc.trans.GetMessage("error.release_already_exists", 0, map[string]interface{}{"Version": release.Version})) + return fmt.Errorf("release already exists for version %s", release.Version) } if resp.StatusCode == http.StatusNotFound { - return fmt.Errorf("%s", ghc.trans.GetMessage("error.repo_or_tag_not_found", 0, map[string]interface{}{"Version": release.Version})) + return fmt.Errorf("repository or tag not found for version %s", release.Version) } } - return fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.create_release", 0, nil), err) + return fmt.Errorf("failed to create release: %w", err) } if buildBinaries { if err := ghc.uploadBinaries(ctx, createdRelease.GetID(), release.Version); err != nil { - return fmt.Errorf("%s", ghc.trans.GetMessage("build.errors.upload_binaries", 0, map[string]interface{}{"Error": err})) + return fmt.Errorf("failed to upload binaries: %w", err) } } @@ -304,9 +313,7 @@ func (ghc *GitHubClient) GetRelease(ctx context.Context, version string) (*model release, resp, err := ghc.releaseService.GetReleaseByTag(ctx, ghc.owner, ghc.repo, version) if err != nil { if resp != nil && resp.StatusCode == 404 { - return nil, fmt.Errorf("%s", ghc.trans.GetMessage("error.repo_or_tag_not_found", 0, map[string]interface{}{ - "Version": version, - })) + return nil, fmt.Errorf("repository or tag not found for version %s", version) } return nil, err } @@ -324,9 +331,7 @@ func (ghc *GitHubClient) UpdateRelease(ctx context.Context, version, body string release, resp, err := ghc.releaseService.GetReleaseByTag(ctx, ghc.owner, ghc.repo, version) if err != nil { if resp != nil && resp.StatusCode == 404 { - return fmt.Errorf("%s", ghc.trans.GetMessage("error.repo_or_tag_not_found", 0, map[string]interface{}{ - "Version": version, - })) + return fmt.Errorf("repository or tag not found for version %s", version) } return err } @@ -504,7 +509,7 @@ func (ghc *GitHubClient) GetFileStatsBetweenTags(ctx context.Context, previousTa func (ghc *GitHubClient) GetIssue(ctx context.Context, issueNumber int) (*models.Issue, error) { issue, _, err := ghc.issuesService.Get(ctx, ghc.owner, ghc.repo, issueNumber) if err != nil { - return nil, fmt.Errorf("error obteniendo issue #%d: %w", issueNumber, err) + return nil, fmt.Errorf("error getting issue #%d: %w", issueNumber, err) } labels := make([]string, 0, len(issue.Labels)) @@ -559,7 +564,7 @@ func (ghc *GitHubClient) CreateIssue(ctx context.Context, title string, body str ghIssue, _, err := ghc.issuesService.Create(ctx, ghc.owner, ghc.repo, issueRequest) if err != nil { - return nil, fmt.Errorf("error creando issue: %w", err) + return nil, fmt.Errorf("error creating issue: %w", err) } issue := &models.Issue{ @@ -584,11 +589,11 @@ func (ghc *GitHubClient) CreateIssue(ctx context.Context, title string, body str func (ghc *GitHubClient) GetAuthenticatedUser(ctx context.Context) (string, error) { user, _, err := ghc.usersService.Get(ctx, "") if err != nil { - return "", fmt.Errorf("error obteniendo usuario autenticado: %w", err) + return "", fmt.Errorf("error obtaining authenticated user: %w", err) } if user.Login == nil { - return "", fmt.Errorf("usuario autenticado no tiene login") + return "", fmt.Errorf("authenticated user has no login") } return *user.Login, nil @@ -598,13 +603,13 @@ func extractAcceptanceCriteria(body string) []string { var criteria []string lines := strings.Split(body, "\n") - // Regex para capturar checkboxes markdown: - // ^ - Inicio de linea (permitiendo espacios antes) - // [\-\*\+] - Bullet: - o * o + - // \s+ - Espacio obligatorio + // Regex to capture markdown checkboxes: + // ^ - Start of line (allowing leading spaces) + // [\-\*\+] - Bullet: - or * or + + // \s+ - Required space // \[([ xX])\] - Checkbox: [ ], [x], [X] - // \s+ - Espacio obligatorio - // (.+) - El texto del criterio + // \s+ - Required space + // (.+) - The criterion text re := regexp.MustCompile(`^\s*[\-*+]\s+\[([ xX])]\s+(.+)`) for _, line := range lines { @@ -631,12 +636,12 @@ func (ghc *GitHubClient) GetFileAtTag(ctx context.Context, tag, filepath string) } if fileContent == nil { - return "", fmt.Errorf("archivo no encontrado: %s en %s", filepath, tag) + return "", fmt.Errorf("file not found: %s in %s", filepath, tag) } content, err := fileContent.GetContent() if err != nil { - return "", fmt.Errorf("error decodificando contenido del archivo: %w", err) + return "", fmt.Errorf("error decoding file content: %w", err) } return content, nil @@ -725,7 +730,7 @@ func (ghc *GitHubClient) labelExists(existingLabels []string, target string) boo func (ghc *GitHubClient) addLabelsToIssue(ctx context.Context, prNumber int, labels []string) error { _, _, err := ghc.issuesService.AddLabelsToIssue(ctx, ghc.owner, ghc.repo, prNumber, labels) if err != nil { - return fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.add_labels", 0, map[string]interface{}{"pr_number": prNumber}), err) + return fmt.Errorf("failed to add labels to PR #%d: %w", prNumber, err) } return nil } @@ -733,7 +738,7 @@ func (ghc *GitHubClient) addLabelsToIssue(ctx context.Context, prNumber int, lab func (ghc *GitHubClient) UpdateIssueChecklist(ctx context.Context, issueNumber int, checkedIndices []int) error { issue, _, err := ghc.issuesService.Get(ctx, ghc.owner, ghc.repo, issueNumber) if err != nil { - return fmt.Errorf("error obteniendo issue #%d: %w", issueNumber, err) + return fmt.Errorf("error getting issue #%d: %w", issueNumber, err) } body := issue.GetBody() @@ -773,7 +778,7 @@ func (ghc *GitHubClient) UpdateIssueChecklist(ctx context.Context, issueNumber i _, _, err = ghc.issuesService.Edit(ctx, ghc.owner, ghc.repo, issueNumber, issueRequest) if err != nil { - return fmt.Errorf("error actualizando body del issue #%d: %w", issueNumber, err) + return fmt.Errorf("error updating issue body #%d: %w", issueNumber, err) } return nil @@ -784,10 +789,10 @@ func (ghc *GitHubClient) ensureLabelsExist(ctx context.Context, existingLabels [ if !ghc.labelExists(existingLabels, label) { meta := allowedLabels[label] - description := ghc.trans.GetMessage(meta.Key, 0, map[string]interface{}{"label": label}) + description := labelDescriptions[label] if err := ghc.CreateLabel(ctx, label, meta.Color, description); err != nil { if !strings.Contains(err.Error(), "already_exists") && !strings.Contains(err.Error(), "422") { - return fmt.Errorf("%s: %w", ghc.trans.GetMessage("error.create_label", 0, map[string]interface{}{"label": label}), err) + return fmt.Errorf("failed to create label '%s': %w", label, err) } fmt.Printf("Label '%s' already exists, continuing...\n", label) } @@ -796,24 +801,18 @@ func (ghc *GitHubClient) ensureLabelsExist(ctx context.Context, existingLabels [ return nil } -// getDiffFromCommits obtiene el diff combinado de todos los commits cuando el diff completo del PR es demasiado grande +// getDiffFromCommits gets the combined diff of all commits when the total PR diff is too large func (ghc *GitHubClient) getDiffFromCommits(ctx context.Context, commits []*github.RepositoryCommit) (string, error) { var combinedDiff strings.Builder - fmt.Printf("%s\n", ghc.trans.GetMessage("info.fetching_commit_diffs", 0, map[string]interface{}{ - "total": len(commits), - })) + fmt.Printf("Fetching diffs for %d commits...\n", len(commits)) for i, commit := range commits { sha := commit.GetSHA() - fmt.Printf("%s (%d/%d)\n", ghc.trans.GetMessage("info.processing_commit", 0, map[string]interface{}{ - "current": i + 1, - "total": len(commits), - "sha": sha[:8], - }), i+1, len(commits)) + fmt.Printf("Processing commit %d/%d (%s)\n", i+1, len(commits), sha[:8]) fullCommit, _, err := ghc.repoService.GetCommit(ctx, ghc.owner, ghc.repo, sha, nil) if err != nil { - return "", fmt.Errorf("%s %s: %w", ghc.trans.GetMessage("error.get_commit_diff", 0, nil), sha[:8], err) + return "", fmt.Errorf("failed to get diff for commit %s: %w", sha[:8], err) } if fullCommit.GetStats().GetTotal() > 0 { @@ -852,7 +851,7 @@ func (ghc *GitHubClient) isAllowedLabel(label string) bool { func (ghc *GitHubClient) uploadBinaries(ctx context.Context, releaseID int64, version string) error { tempDir, err := os.MkdirTemp("", "matecommit-build-*") if err != nil { - return fmt.Errorf("%s", ghc.trans.GetMessage("build.errors.create_temp_dir", 0, map[string]interface{}{"Error": err})) + return fmt.Errorf("failed to create temporary directory for build: %w", err) } defer func() { if err := os.RemoveAll(tempDir); err != nil { @@ -869,24 +868,23 @@ func (ghc *GitHubClient) uploadBinaries(ctx context.Context, releaseID int64, ve builderBinary := ghc.binaryBuilderFactory.NewBuilder( ghc.mainPath, "matecommit", - version, - commit, - date, - tempDir, - ghc.trans, + builder.WithVersion(version), + builder.WithCommit(commit), + builder.WithDate(date), + builder.WithBuildDir(tempDir), ) - fmt.Println(ghc.trans.GetMessage("build.compiling_binaries", 0, nil)) + fmt.Println("Compiling binaries...") archives, err := builderBinary.BuildAndPackageAll(ctx) if err != nil { - return fmt.Errorf("%s", ghc.trans.GetMessage("build.errors.build_binaries", 0, map[string]interface{}{"Error": err})) + return fmt.Errorf("failed to build binaries: %w", err) } - fmt.Printf("%s\n", ghc.trans.GetMessage("build.uploading_binaries", 0, map[string]interface{}{"Count": len(archives)})) + fmt.Printf("Uploading %d binaries to release...\n", len(archives)) for _, archivePath := range archives { file, err := os.Open(archivePath) if err != nil { - return fmt.Errorf("%s", ghc.trans.GetMessage("build.errors.open_file", 0, map[string]interface{}{"File": archivePath, "Error": err})) + return fmt.Errorf("failed to open archive %s: %w", archivePath, err) } uploadOpts := &github.UploadOptions{ @@ -897,7 +895,7 @@ func (ghc *GitHubClient) uploadBinaries(ctx context.Context, releaseID int64, ve _, _, err = ghc.releaseService.UploadReleaseAsset(ctx, ghc.owner, ghc.repo, releaseID, uploadOpts, file) _ = file.Close() if err != nil { - return fmt.Errorf("%s", ghc.trans.GetMessage("build.errors.upload_asset", 0, map[string]interface{}{"Asset": archivePath, "Error": err})) + return fmt.Errorf("failed to upload asset %s: %w", archivePath, err) } } return nil diff --git a/internal/infrastructure/vcs/github/github_service_test.go b/internal/infrastructure/vcs/github/github_service_test.go index 7888bd3..3863938 100644 --- a/internal/infrastructure/vcs/github/github_service_test.go +++ b/internal/infrastructure/vcs/github/github_service_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/google/go-github/v80/github" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -17,7 +16,6 @@ import ( ) func newTestClient(pr *MockPRService, issues *MockIssuesService, release *MockReleaseService, userService *MockUserService) *GitHubClient { - trans, _ := i18n.NewTranslations("es", "../../../i18n/locales/") repo := &MockRepoService{} client := NewGitHubClientWithServices( pr, @@ -27,7 +25,6 @@ func newTestClient(pr *MockPRService, issues *MockIssuesService, release *MockRe userService, "test-owner", "test-repo", - trans, ) client.mainPath = "../../../../../cmd/main.go" return client @@ -226,7 +223,8 @@ func TestGitHubClient_UpdatePR_ErrorCases(t *testing.T) { err := client.UpdatePR(context.Background(), prNumber, summary) - assert.ErrorContains(t, err, client.trans.GetMessage("error.update_pr", 0, map[string]interface{}{"pr_number": prNumber})) + assert.Error(t, err) + assert.Contains(t, err.Error(), fmt.Sprintf("failed to update PR #%d", prNumber)) mockPR.AssertExpectations(t) }) @@ -249,7 +247,7 @@ func TestGitHubClient_UpdatePR_ErrorCases(t *testing.T) { err := client.UpdatePR(context.Background(), prNumber, summary) assert.Error(t, err) - assert.ErrorContains(t, err, client.trans.GetMessage("error.add_labels", 0, map[string]interface{}{"pr_number": prNumber})) + assert.Contains(t, err.Error(), fmt.Sprintf("failed to add labels to PR #%d", prNumber)) mockIssues.AssertExpectations(t) }) @@ -276,12 +274,8 @@ func TestGitHubClient_UpdatePR_ErrorCases(t *testing.T) { err := client.UpdatePR(context.Background(), prNumber, summary) assert.Error(t, err) - assert.ErrorContains(t, err, client.trans.GetMessage("error.insufficient_permissions", 0, map[string]interface{}{ - "pr_number": prNumber, - "owner": "test-owner", - "repo": "test-repo", - })) - assert.ErrorContains(t, err, client.trans.GetMessage("error.token_scopes_help", 0, nil)) + assert.Contains(t, err.Error(), "insufficient permissions") + assert.Contains(t, err.Error(), "required scopes") mockPR.AssertExpectations(t) }) } @@ -299,7 +293,7 @@ func TestGitHubClient_AddLabelsToPR_ErrorCases(t *testing.T) { err := client.AddLabelsToPR(context.Background(), 123, []string{"fix"}) assert.Error(t, err) - assert.ErrorContains(t, err, client.trans.GetMessage("error.get_labels", 0, nil)) + assert.Contains(t, err.Error(), "failed to get repository labels") }) t.Run("should return error if CreateLabel fails", func(t *testing.T) { @@ -317,7 +311,7 @@ func TestGitHubClient_AddLabelsToPR_ErrorCases(t *testing.T) { err := client.AddLabelsToPR(context.Background(), 123, []string{"feature"}) assert.Error(t, err) - assert.ErrorContains(t, err, client.trans.GetMessage("error.create_label", 0, map[string]interface{}{"label": "feature"})) + assert.Contains(t, err.Error(), "failed to create label 'feature'") }) t.Run("should return error if AddLabelsToIssue fails", func(t *testing.T) { @@ -335,7 +329,7 @@ func TestGitHubClient_AddLabelsToPR_ErrorCases(t *testing.T) { err := client.AddLabelsToPR(context.Background(), 123, []string{"fix"}) assert.Error(t, err) - assert.ErrorContains(t, err, client.trans.GetMessage("error.add_labels", 0, map[string]interface{}{"pr_number": 123})) + assert.Contains(t, err.Error(), "failed to add labels to PR #123") }) } @@ -351,7 +345,7 @@ func TestGitHubClient_GetPR_ErrorCases(t *testing.T) { Return(&github.PullRequest{}, &github.Response{}, assert.AnError) _, err := client.GetPR(context.Background(), 123) - assert.ErrorContains(t, err, client.trans.GetMessage("error.get_pr", 0, map[string]interface{}{"pr_number": 123})) + assert.Contains(t, err.Error(), "failed to get PR #123") }) t.Run("should return error if ListCommits fails", func(t *testing.T) { @@ -367,7 +361,7 @@ func TestGitHubClient_GetPR_ErrorCases(t *testing.T) { Return([]*github.RepositoryCommit{}, &github.Response{}, assert.AnError) _, err := client.GetPR(context.Background(), 123) - assert.ErrorContains(t, err, client.trans.GetMessage("error.get_commits", 0, map[string]interface{}{"pr_number": 123})) + assert.Contains(t, err.Error(), "failed to get commits for PR #123") }) t.Run("should return error if GetRaw fails", func(t *testing.T) { @@ -385,7 +379,7 @@ func TestGitHubClient_GetPR_ErrorCases(t *testing.T) { Return("", nil, assert.AnError) _, err := client.GetPR(context.Background(), 123) - assert.ErrorContains(t, err, client.trans.GetMessage("error.get_diff", 0, map[string]interface{}{"pr_number": 123})) + assert.Contains(t, err.Error(), "failed to get diff for PR #123") }) } @@ -395,7 +389,7 @@ func TestGitHubClient_CreateRelease(t *testing.T) { mockIssues := &MockIssuesService{} mockRelease := &MockReleaseService{} mockUserService := &MockUserService{} - client := newTestClient(mockPR, mockIssues, mockRelease, mockUserService) + var client *GitHubClient release := &models.Release{Version: "v1.0.0"} notes := &models.ReleaseNotes{Title: "Release v1.0.0", Changelog: "Changes"} @@ -412,11 +406,10 @@ func TestGitHubClient_CreateRelease(t *testing.T) { mockUserService, "test-owner", "test-repo", - client.trans, ) client.binaryBuilderFactory = mockFactory - mockFactory.On("NewBuilder", mock.Anything, "matecommit", "v1.0.0", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + mockFactory.On("NewBuilder", mock.Anything, "matecommit", mock.Anything). Return(mockPackager) tmpFile, err := os.CreateTemp("", "dummy-archive-*.tar.gz") @@ -461,7 +454,7 @@ func TestGitHubClient_CreateRelease(t *testing.T) { err := client.CreateRelease(context.Background(), release, notes, false, true) assert.Error(t, err) - assert.Contains(t, err.Error(), client.trans.GetMessage("error.release_already_exists", 0, map[string]interface{}{"Version": "v1.0.0"})) + assert.Contains(t, err.Error(), "release already exists for version v1.0.0") }) t.Run("should return error if repo or tag not found", func(t *testing.T) { @@ -480,7 +473,7 @@ func TestGitHubClient_CreateRelease(t *testing.T) { err := client.CreateRelease(context.Background(), release, notes, false, true) assert.Error(t, err) - assert.Contains(t, err.Error(), client.trans.GetMessage("error.repo_or_tag_not_found", 0, map[string]interface{}{"Version": "v1.0.0"})) + assert.Contains(t, err.Error(), "repository or tag not found for version v1.0.0") }) t.Run("should return generic error for other failures", func(t *testing.T) { @@ -498,7 +491,7 @@ func TestGitHubClient_CreateRelease(t *testing.T) { err := client.CreateRelease(context.Background(), release, notes, false, true) assert.Error(t, err) - assert.Contains(t, err.Error(), client.trans.GetMessage("error.create_release", 0, nil)) + assert.Contains(t, err.Error(), "failed to create release") }) } @@ -548,7 +541,7 @@ func TestGitHubClient_GetRelease(t *testing.T) { assert.Error(t, err) assert.Nil(t, release) - assert.Contains(t, err.Error(), client.trans.GetMessage("error.repo_or_tag_not_found", 0, map[string]interface{}{"Version": "v1.0.0"})) + assert.Contains(t, err.Error(), "repository or tag not found for version v1.0.0") mockRelease.AssertExpectations(t) }) @@ -610,7 +603,7 @@ func TestGitHubClient_UpdateRelease(t *testing.T) { err := client.UpdateRelease(context.Background(), "v1.0.0", "Updated body") assert.Error(t, err) - assert.Contains(t, err.Error(), client.trans.GetMessage("error.repo_or_tag_not_found", 0, map[string]interface{}{"Version": "v1.0.0"})) + assert.Contains(t, err.Error(), "repository or tag not found for version v1.0.0") mockRelease.AssertExpectations(t) }) @@ -722,7 +715,7 @@ func TestGitHubClient_GetIssue(t *testing.T) { _, err := client.GetIssue(context.Background(), issueNumber) assert.Error(t, err) - assert.Contains(t, err.Error(), fmt.Sprintf("error obteniendo issue #%d", issueNumber)) + assert.Contains(t, err.Error(), fmt.Sprintf("error getting issue #%d", issueNumber)) mockIssues.AssertExpectations(t) }) } @@ -1139,7 +1132,7 @@ func TestGitHubClient_GetFileAtTag(t *testing.T) { _, err := client.GetFileAtTag(context.Background(), tag, filepath) assert.Error(t, err) - assert.Contains(t, err.Error(), "archivo no encontrado") + assert.Contains(t, err.Error(), "file not found") }) t.Run("should return error if GetContents fails", func(t *testing.T) { @@ -1412,7 +1405,7 @@ func TestGitHubClient_CreateIssue(t *testing.T) { _, err := client.CreateIssue(context.Background(), "Title", "Body", nil, nil) assert.Error(t, err) - assert.Contains(t, err.Error(), "error creando issue") + assert.Contains(t, err.Error(), "error creating issue") mockIssues.AssertExpectations(t) }) } diff --git a/internal/infrastructure/vcs/github/mocks.go b/internal/infrastructure/vcs/github/mocks.go index 7268a86..1b4a508 100644 --- a/internal/infrastructure/vcs/github/mocks.go +++ b/internal/infrastructure/vcs/github/mocks.go @@ -4,8 +4,7 @@ import ( "context" "os" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/builder" "github.com/google/go-github/v80/github" "github.com/stretchr/testify/mock" ) @@ -140,12 +139,14 @@ type MockBinaryBuilderFactory struct { mock.Mock } -func (m *MockBinaryBuilderFactory) NewBuilder(mainPath, binaryName, version, commit, date, buildDir string, trans *i18n.Translations) ports.BinaryPackager { - args := m.Called(mainPath, binaryName, version, commit, date, buildDir, trans) +func (m *MockBinaryBuilderFactory) NewBuilder(mainPath, binaryName string, opts ...builder.Option) binaryBuilder { + // Variadic arguments in mock.Called need careful handling. + // We'll pass them as a single argument for simplicity in this mock. + args := m.Called(mainPath, binaryName, opts) if args.Get(0) == nil { return nil } - return args.Get(0).(ports.BinaryPackager) + return args.Get(0).(binaryBuilder) } type MockUserService struct { diff --git a/internal/infrastructure/vcs/registry/vcs_provider_registry.go b/internal/infrastructure/vcs/registry/vcs_provider_registry.go deleted file mode 100644 index e898938..0000000 --- a/internal/infrastructure/vcs/registry/vcs_provider_registry.go +++ /dev/null @@ -1,122 +0,0 @@ -package registry - -import ( - "context" - "fmt" - "sync" - - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" -) - -// VCSProviderFactory define la interfaz para crear clientes VCS -type VCSProviderFactory interface { - // CreateClient crea un cliente VCS con las credenciales proporcionadas - CreateClient(ctx context.Context, owner, repo, token string, trans *i18n.Translations) (ports.VCSClient, error) - - // ValidateConfig valida la configuración para este proveedor - ValidateConfig(cfg *config.VCSConfig) error - - // Name retorna el nombre del proveedor - Name() string -} - -// VCSProviderRegistry gestiona el registro de proveedores VCS -type VCSProviderRegistry struct { - mu sync.RWMutex - factories map[string]VCSProviderFactory -} - -// NewVCSProviderRegistry crea un nuevo registro de proveedores VCS -func NewVCSProviderRegistry() *VCSProviderRegistry { - return &VCSProviderRegistry{ - factories: make(map[string]VCSProviderFactory), - } -} - -// Register registra un nuevo proveedor VCS -func (r *VCSProviderRegistry) Register(name string, factory VCSProviderFactory) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.factories[name]; exists { - return fmt.Errorf("proveedor VCS '%s' ya esta registrado", name) - } - - r.factories[name] = factory - return nil -} - -// Get obtiene un factory por nombre -func (r *VCSProviderRegistry) Get(name string) (VCSProviderFactory, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - factory, exists := r.factories[name] - if !exists { - return nil, errors.NewVCSProviderNotSupportedError(name) - } - - return factory, nil -} - -// List retorna la lista de proveedores registrados -func (r *VCSProviderRegistry) List() []string { - r.mu.RLock() - defer r.mu.RUnlock() - - providers := make([]string, 0, len(r.factories)) - for name := range r.factories { - providers = append(providers, name) - } - return providers -} - -// IsRegistered verifica si un proveedor está registrado -func (r *VCSProviderRegistry) IsRegistered(name string) bool { - r.mu.RLock() - defer r.mu.RUnlock() - - _, exists := r.factories[name] - return exists -} - -// CreateClientFromConfig crea un cliente VCS usando la configuración -// Este método centraliza la lógica duplicada de creación de VCS clients -func (r *VCSProviderRegistry) CreateClientFromConfig( - ctx context.Context, - gitService ports.GitService, - cfg *config.Config, - trans *i18n.Translations, -) (ports.VCSClient, error) { - owner, repo, provider, err := gitService.GetRepoInfo(ctx) - if err != nil { - return nil, fmt.Errorf("error al obtener la info del repo: %w", err) - } - - vcsConfig, exists := cfg.VCSConfigs[provider] - if !exists { - if cfg.ActiveVCSProvider != "" { - vcsConfig, exists = cfg.VCSConfigs[cfg.ActiveVCSProvider] - if !exists { - return nil, errors.NewVCSConfigNotFoundError(cfg.ActiveVCSProvider) - } - provider = cfg.ActiveVCSProvider - } else { - return nil, errors.NewVCSProviderNotConfiguredError(provider) - } - } - - factory, err := r.Get(provider) - if err != nil { - return nil, err - } - - if err := factory.ValidateConfig(&vcsConfig); err != nil { - return nil, fmt.Errorf("configuracion VCS invalida para %s: %w", provider, err) - } - - return factory.CreateClient(ctx, owner, repo, vcsConfig.Token, trans) -} diff --git a/internal/providers/ai.go b/internal/providers/ai.go new file mode 100644 index 0000000..b1f2812 --- /dev/null +++ b/internal/providers/ai.go @@ -0,0 +1,68 @@ +package providers + +import ( + "context" + "fmt" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai/gemini" +) + +// NewCommitSummarizer creates a CommitSummarizer based on the configured provider +func NewCommitSummarizer(ctx context.Context, cfg *config.Config, onConfirmation ai.ConfirmationCallback) (ports.CommitSummarizer, error) { + if cfg.AIConfig.ActiveAI == "" { + return nil, fmt.Errorf("no AI provider configured") + } + + switch cfg.AIConfig.ActiveAI { + case "gemini": + return gemini.NewGeminiCommitSummarizer(ctx, cfg, onConfirmation) + default: + return nil, fmt.Errorf("AI provider '%s' not supported", cfg.AIConfig.ActiveAI) + } +} + +// NewPRSummarizer creates a PRSummarizer based on the configured provider +func NewPRSummarizer(ctx context.Context, cfg *config.Config, onConfirmation ai.ConfirmationCallback) (ports.PRSummarizer, error) { + if cfg.AIConfig.ActiveAI == "" { + return nil, fmt.Errorf("no AI provider configured") + } + + switch cfg.AIConfig.ActiveAI { + case "gemini": + return gemini.NewGeminiPRSummarizer(ctx, cfg, onConfirmation) + default: + return nil, fmt.Errorf("AI provider '%s' not supported", cfg.AIConfig.ActiveAI) + } +} + +// NewIssueContentGenerator creates an IssueContentGenerator based on the configured provider +func NewIssueContentGenerator(ctx context.Context, cfg *config.Config, onConfirmation ai.ConfirmationCallback) (ports.IssueContentGenerator, error) { + if cfg.AIConfig.ActiveAI == "" { + return nil, fmt.Errorf("no AI provider configured") + } + + switch cfg.AIConfig.ActiveAI { + case "gemini": + return gemini.NewGeminiIssueContentGenerator(ctx, cfg, onConfirmation) + default: + return nil, fmt.Errorf("AI provider '%s' not supported", cfg.AIConfig.ActiveAI) + } +} + +// NewReleaseNotesGenerator creates a ReleaseNotesGenerator based on the configured provider +// Note: requires owner and repo which must be obtained from the git service first +func NewReleaseNotesGenerator(ctx context.Context, cfg *config.Config, onConfirmation ai.ConfirmationCallback, owner, repo string) (ports.ReleaseNotesGenerator, error) { + if cfg.AIConfig.ActiveAI == "" { + return nil, fmt.Errorf("no AI provider configured") + } + + switch cfg.AIConfig.ActiveAI { + case "gemini": + return gemini.NewReleaseNotesGenerator(ctx, cfg, onConfirmation, owner, repo) + default: + return nil, fmt.Errorf("AI provider '%s' not supported", cfg.AIConfig.ActiveAI) + } +} diff --git a/internal/providers/tickets.go b/internal/providers/tickets.go new file mode 100644 index 0000000..a2e7d48 --- /dev/null +++ b/internal/providers/tickets.go @@ -0,0 +1,30 @@ +package providers + +import ( + "context" + "fmt" + "net/http" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/tickets/jira" +) + +// NewTicketManager creates a TicketManager based on the configured provider +func NewTicketManager(ctx context.Context, cfg *config.Config) (ports.TickerManager, error) { + if cfg.ActiveTicketService == "" || !cfg.UseTicket { + return nil, nil // Tickets disabled + } + + ticketCfg, exists := cfg.TicketProviders[cfg.ActiveTicketService] + if !exists { + return nil, fmt.Errorf("ticket provider '%s' not configured", cfg.ActiveTicketService) + } + + switch cfg.ActiveTicketService { + case "jira": + return jira.NewJiraService(ticketCfg.BaseURL, ticketCfg.APIKey, ticketCfg.Email, &http.Client{}), nil + default: + return nil, fmt.Errorf("ticket provider '%s' not supported", cfg.ActiveTicketService) + } +} diff --git a/internal/providers/vcs.go b/internal/providers/vcs.go new file mode 100644 index 0000000..4e35e5c --- /dev/null +++ b/internal/providers/vcs.go @@ -0,0 +1,35 @@ +package providers + +import ( + "context" + "fmt" + + "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/git" + "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" +) + +// NewVCSClient creates a VCSClient based on the configuration and automatic detection of the remote +func NewVCSClient(ctx context.Context, gitService *git.GitService, cfg *config.Config) (ports.VCSClient, error) { + owner, repo, provider, err := gitService.GetRepoInfo(ctx) + if err != nil { + return nil, fmt.Errorf("error getting repo info: %w", err) + } + + vcsConfig, exists := cfg.VCSConfigs[provider] + if !exists { + vcsConfig, exists = cfg.VCSConfigs[cfg.ActiveVCSProvider] + if !exists { + return nil, fmt.Errorf("VCS provider '%s' not configured", provider) + } + provider = cfg.ActiveVCSProvider + } + + switch provider { + case "github": + return github.NewGitHubClient(owner, repo, vcsConfig.Token), nil + default: + return nil, fmt.Errorf("VCS provider '%s' not supported", provider) + } +} diff --git a/internal/services/commit_service.go b/internal/services/commit_service.go index abba0c5..7ac9142 100644 --- a/internal/services/commit_service.go +++ b/internal/services/commit_service.go @@ -5,51 +5,64 @@ import ( "fmt" "regexp" "strconv" + "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" ) -var _ ports.CommitService = (*CommitService)(nil) +// commitGitService defines only the methods needed by CommitService. +type commitGitService interface { + GetChangedFiles(ctx context.Context) ([]string, error) + GetDiff(ctx context.Context) (string, error) + GetRecentCommitMessages(ctx context.Context, limit int) ([]string, error) + GetRepoInfo(ctx context.Context) (string, string, string, error) + GetCurrentBranch(ctx context.Context) (string, error) +} type CommitService struct { - git ports.GitService + git commitGitService ai ports.CommitSummarizer ticketManager ports.TickerManager vcsClient ports.VCSClient config *config.Config - trans *i18n.Translations } -func NewCommitService( - git ports.GitService, - ai ports.CommitSummarizer, - ticketManager ports.TickerManager, - vcsClient ports.VCSClient, - cfg *config.Config, - trans *i18n.Translations) *CommitService { - return &CommitService{ - git: git, - ai: ai, - ticketManager: ticketManager, - vcsClient: vcsClient, - config: cfg, - trans: trans, +type Option func(*CommitService) + +func WithTicketManager(tm ports.TickerManager) Option { + return func(s *CommitService) { + s.ticketManager = tm } } -func (s *CommitService) GenerateSuggestions(ctx context.Context, count int, progress func(string)) ([]models.CommitSuggestion, error) { - commitInfo, err := s.buildCommitInfo(ctx, 0, progress) - if err != nil { - return nil, err +func WithVCSClient(vcs ports.VCSClient) Option { + return func(s *CommitService) { + s.vcsClient = vcs } - return s.ai.GenerateSuggestions(ctx, commitInfo, count) } -func (s *CommitService) GenerateSuggestionsWithIssue(ctx context.Context, count int, issueNumber int, progress func(string)) ([]models.CommitSuggestion, error) { +func WithConfig(cfg *config.Config) Option { + return func(s *CommitService) { + s.config = cfg + } +} + +func NewCommitService(gitSvc commitGitService, ai ports.CommitSummarizer, opts ...Option) *CommitService { + s := &CommitService{ + git: gitSvc, + ai: ai, + } + for _, opt := range opts { + opt(s) + } + return s +} + +func (s *CommitService) GenerateSuggestions(ctx context.Context, count int, issueNumber int, progress func(models.ProgressEvent)) ([]models.CommitSuggestion, error) { commitInfo, err := s.buildCommitInfo(ctx, issueNumber, progress) if err != nil { return nil, err @@ -57,12 +70,11 @@ func (s *CommitService) GenerateSuggestionsWithIssue(ctx context.Context, count return s.ai.GenerateSuggestions(ctx, commitInfo, count) } -func (s *CommitService) buildCommitInfo(ctx context.Context, issueNumber int, progress func(string)) (models.CommitInfo, error) { +func (s *CommitService) buildCommitInfo(ctx context.Context, issueNumber int, progress func(models.ProgressEvent)) (models.CommitInfo, error) { var commitInfo models.CommitInfo if s.ai == nil { - msg := s.trans.GetMessage("ai_missing.for_suggest", 0, nil) - return commitInfo, fmt.Errorf("%s", msg) + return commitInfo, domainErrors.ErrAPIKeyMissing } changes, err := s.git.GetChangedFiles(ctx) @@ -71,51 +83,35 @@ func (s *CommitService) buildCommitInfo(ctx context.Context, issueNumber int, pr } if len(changes) == 0 { - msg := s.trans.GetMessage("commit_service.undetected_changes", 0, nil) - return commitInfo, fmt.Errorf("%s", msg) + return commitInfo, domainErrors.ErrNoChanges } diff, err := s.git.GetDiff(ctx) if err != nil { - msg := s.trans.GetMessage("commit_service.error_getting_diff", 0, map[string]interface{}{ - "Error": err, - }) - return commitInfo, fmt.Errorf("%s", msg) + return commitInfo, domainErrors.NewAppError(domainErrors.TypeGit, "error getting git diff", err) } if diff == "" { - msg := s.trans.GetMessage("commit_service.no_differences_detected", 0, nil) - return commitInfo, fmt.Errorf("%s", msg) - } - - files := make([]string, 0) - for _, change := range changes { - files = append(files, change.Path) + return commitInfo, domainErrors.ErrNoChanges } recentHistory, _ := s.git.GetRecentCommitMessages(ctx, 10) commitInfo = models.CommitInfo{ - Files: files, + Files: changes, Diff: diff, - RecentHistory: recentHistory, + RecentHistory: strings.Join(recentHistory, "\n"), } - if s.config.UseTicket { + if s.config != nil && s.config.UseTicket { ticketID, err := s.getTicketIDFromBranch(ctx) if err != nil { - msg := s.trans.GetMessage("commit_service.error_get_id_ticket", 0, map[string]interface{}{ - "Error": err, - }) - return commitInfo, fmt.Errorf("%s", msg) + return commitInfo, domainErrors.NewAppError(domainErrors.TypeGit, "error getting ticket ID from branch", err) } ticketInfo, err := s.ticketManager.GetTicketInfo(ticketID) if err != nil { - msg := s.trans.GetMessage("commit_service.error_get_ticket_info", 0, map[string]interface{}{ - "Error": err, - }) - return commitInfo, fmt.Errorf("%s", msg) + return commitInfo, domainErrors.NewAppError(domainErrors.TypeInternal, "error getting ticket info", err) } commitInfo.TicketInfo = ticketInfo @@ -129,37 +125,34 @@ func (s *CommitService) buildCommitInfo(ctx context.Context, issueNumber int, pr if detectedIssue > 0 { vcsClient, err := s.getOrCreateVCSClient(ctx) if err != nil { - msg := s.trans.GetMessage("issue_vcs_init_error", 0, map[string]interface{}{ - "Error": err.Error(), - }) if progress != nil { - progress(msg) + progress(models.ProgressEvent{ + Type: models.ProgressGeneric, + Message: fmt.Sprintf("issue_vcs_init_error: %v", err), + }) } } else { issueInfo, err := vcsClient.GetIssue(ctx, detectedIssue) if err != nil { - msg := s.trans.GetMessage("issue_fetch_error", 0, map[string]interface{}{ - "Number": detectedIssue, - "Error": err.Error(), - }) if progress != nil { - progress(msg) - } - } else { - var msg string - if issueNumber == 0 { - msg = s.trans.GetMessage("issue_detected_auto", 0, map[string]interface{}{ - "Number": detectedIssue, - "Title": issueInfo.Title, - }) - } else { - msg = s.trans.GetMessage("issue_using_manual", 0, map[string]interface{}{ - "Number": detectedIssue, - "Title": issueInfo.Title, + progress(models.ProgressEvent{ + Type: models.ProgressGeneric, + Data: map[string]interface{}{ + "Error": err.Error(), + "IssueID": detectedIssue, + }, }) } + } else { if progress != nil { - progress(msg) + progress(models.ProgressEvent{ + Type: models.ProgressIssuesDetected, + Data: map[string]interface{}{ + "IssueID": detectedIssue, + "Title": issueInfo.Title, + "IsAuto": issueNumber == 0, + }, + }) } commitInfo.IssueInfo = issueInfo @@ -183,9 +176,13 @@ func (s *CommitService) getOrCreateVCSClient(ctx context.Context) (ports.VCSClie return s.vcsClient, nil } + if s.config == nil { + return nil, domainErrors.ErrConfigMissing + } + owner, repo, provider, err := s.git.GetRepoInfo(ctx) if err != nil { - return nil, fmt.Errorf("error al obtener información del repositorio: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeGit, "error getting repo info", err) } vcsConfig, exists := s.config.VCSConfigs[provider] @@ -193,24 +190,25 @@ func (s *CommitService) getOrCreateVCSClient(ctx context.Context) (ports.VCSClie if s.config.ActiveVCSProvider != "" { vcsConfig, exists = s.config.VCSConfigs[s.config.ActiveVCSProvider] if !exists { - return nil, fmt.Errorf("configuración para el proveedor de VCS '%s' no encontrada", s.config.ActiveVCSProvider) + return nil, domainErrors.ErrConfigMissing } provider = s.config.ActiveVCSProvider } else { - return nil, fmt.Errorf("proveedor de VCS '%s' detectado automáticamente pero no configurado", provider) + return nil, domainErrors.NewAppError(domainErrors.TypeConfiguration, fmt.Sprintf("VCS provider '%s' not configured", provider), nil) } } switch provider { case "github": - return github.NewGitHubClient(owner, repo, vcsConfig.Token, s.trans), nil + // Removing 'nil' for translations as per decoupling plan (assuming GitHubClient is updated) + return github.NewGitHubClient(owner, repo, vcsConfig.Token), nil default: - return nil, fmt.Errorf("proveedor de VCS no compatible: %s", provider) + return nil, domainErrors.ErrVCSNotSupported } } -// detectIssueNumber intenta detectar automáticamente el número de issue -// Prioridad: 1) Branch name, 2) Commits recientes +// detectIssueNumber attempts to automatically detect the issue number +// Priority: 1) Branch name, 2) Recent commits func (s *CommitService) detectIssueNumber(ctx context.Context) int { if issueNum := s.detectIssueFromBranch(ctx); issueNum > 0 { return issueNum @@ -223,8 +221,8 @@ func (s *CommitService) detectIssueNumber(ctx context.Context) int { return 0 } -// detectIssueFromBranch detecta issue number desde el nombre de la rama -// Patrones soportados: 123-desc, feature/123-desc, #123, issue-123, issue/123 +// detectIssueFromBranch detects issue number from the branch name +// Supported patterns: 123-desc, feature/123-desc, #123, issue-123, issue/123 func (s *CommitService) detectIssueFromBranch(ctx context.Context) int { branchName, err := s.git.GetCurrentBranch(ctx) if err != nil { @@ -251,8 +249,8 @@ func (s *CommitService) detectIssueFromBranch(ctx context.Context) int { return 0 } -// detectIssueFromCommits detecta issue number desde commits recientes -// Busca keywords de GitHub: fixes, closes, resolves seguido de #123 +// detectIssueFromCommits detects issue number from recent commits +// Search for GitHub keywords: fixes, closes, resolves followed by #123 func (s *CommitService) detectIssueFromCommits(ctx context.Context) int { commitMessages, err := s.git.GetRecentCommitMessages(ctx, 5) if err != nil { @@ -269,9 +267,11 @@ func (s *CommitService) detectIssueFromCommits(ctx context.Context) int { for _, keyword := range keywords { pattern := fmt.Sprintf(`(?i)\b%s\s+#(\d+)\b`, keyword) re := regexp.MustCompile(pattern) - if match := re.FindStringSubmatch(commitMessages); len(match) > 1 { - if num, err := strconv.Atoi(match[1]); err == nil { - return num + for _, msg := range commitMessages { + if match := re.FindStringSubmatch(msg); len(match) > 1 { + if num, err := strconv.Atoi(match[1]); err == nil { + return num + } } } } @@ -282,17 +282,13 @@ func (s *CommitService) detectIssueFromCommits(ctx context.Context) int { func (s *CommitService) getTicketIDFromBranch(ctx context.Context) (string, error) { branchName, err := s.git.GetCurrentBranch(ctx) if err != nil { - msg := s.trans.GetMessage("commit_service.error_get_name_from_branch", 0, map[string]interface{}{ - "Error": err, - }) - return "", fmt.Errorf("%s", msg) + return "", domainErrors.NewAppError(domainErrors.TypeGit, "error getting branch name", err) } re := regexp.MustCompile(`([A-Za-z]+-\d+)`) match := re.FindString(branchName) if match == "" { - msg := s.trans.GetMessage("commit_service.ticket_id_not_found_branch", 0, nil) - return "", fmt.Errorf("%s", msg) + return "", domainErrors.NewAppError(domainErrors.TypeGit, "ticket ID not found in branch", nil) } return match, nil diff --git a/internal/services/commit_service_test.go b/internal/services/commit_service_test.go index 37d8aa3..04ba063 100644 --- a/internal/services/commit_service_test.go +++ b/internal/services/commit_service_test.go @@ -6,27 +6,24 @@ import ( "testing" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "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" ) -func setupTest(t *testing.T) (*MockGitService, *MockAIProvider, *MockJiraService, *MockVCSClient, *config.Config, *i18n.Translations) { +func setupTest(t *testing.T) (*MockGitService, *MockAIProvider, *MockJiraService, *MockVCSClient, *config.Config) { mockGit := new(MockGitService) mockAI := new(MockAIProvider) mockJiraService := new(MockJiraService) mockVCS := new(MockVCSClient) cfgApp := &config.Config{UseTicket: true} - trans, err := i18n.NewTranslations("es", "../i18n/locales") - require.NoError(t, err) - return mockGit, mockAI, mockJiraService, mockVCS, cfgApp, trans + return mockGit, mockAI, mockJiraService, mockVCS, cfgApp } func TestCommitService_GenerateSuggestions(t *testing.T) { t.Run("successful generation with ticket info", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, mockJira, mockVCS, cfg := setupTest(t) mockGit.On("GetCurrentBranch", mock.Anything).Return("feature/PROJ-1234-user-authentication", nil) @@ -38,10 +35,10 @@ func TestCommitService_GenerateSuggestions(t *testing.T) { } mockJira.On("GetTicketInfo", "PROJ-1234").Return(ticketInfo, nil) - changes := []models.GitChange{{Path: "file1.go", Status: "M"}} + changes := []string{"file1.go"} mockGit.On("GetChangedFiles", mock.Anything).Return(changes, nil) mockGit.On("GetDiff", mock.Anything).Return("some diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockVCS.On("GetIssue", mock.Anything, 1234).Return(&models.Issue{Number: 1234, Title: "Issue Title"}, nil) cfg.VCSConfigs = map[string]config.VCSConfig{ @@ -58,8 +55,12 @@ func TestCommitService_GenerateSuggestions(t *testing.T) { return info.TicketInfo.TicketID == "PROJ-1234" && info.Diff == "some diff" }), 3).Return(expectedResponse, nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, + WithTicketManager(mockJira), + WithVCSClient(mockVCS), + WithConfig(cfg), + ) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.NoError(t, err) assert.Equal(t, expectedResponse, suggestions) @@ -69,142 +70,151 @@ func TestCommitService_GenerateSuggestions(t *testing.T) { }) t.Run("no changes detected", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, + WithConfig(cfg), + ) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "No hay cambios detectados") + assert.ErrorIs(t, err, domainErrors.ErrNoChanges) }) t.Run("error getting diff", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) - changes := []models.GitChange{{Path: "file1.go", Status: "M"}} + changes := []string{"file1.go"} mockGit.On("GetChangedFiles", mock.Anything).Return(changes, nil) mockGit.On("GetDiff", mock.Anything).Return("", errors.New("git error")) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, + WithConfig(cfg), + ) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "Error al obtener los cambios") + assert.Contains(t, err.Error(), "GIT: error getting git diff") }) t.Run("no differences string (empty diff)", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) - changes := []models.GitChange{{Path: "file1.go", Status: "M"}} + changes := []string{"file1.go"} mockGit.On("GetChangedFiles", mock.Anything).Return(changes, nil) mockGit.On("GetDiff", mock.Anything).Return("", nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, + WithConfig(cfg), + ) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.Error(t, err) assert.Nil(t, suggestions) assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "No se detectaron diferencias en los archivos") + assert.ErrorIs(t, err, domainErrors.ErrNoChanges) }) t.Run("error getting branch name", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockGit.On("GetCurrentBranch", mock.Anything).Return("", errors.New("branch error")) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, + WithConfig(cfg), + ) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.Error(t, err) assert.Nil(t, suggestions) assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "Error al obtener el nombre de la branch") + assert.Contains(t, err.Error(), "GIT: error getting branch name") }) t.Run("branch without ticket ID", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockGit.On("GetCurrentBranch", mock.Anything).Return("main", nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, + WithConfig(cfg), + ) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "No se encontro un ID de ticket en el nombre de la branch") + assert.Contains(t, err.Error(), "GIT: ticket ID not found in branch") }) t.Run("error getting ticket info", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, mockJira, _, cfg := setupTest(t) - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockGit.On("GetCurrentBranch", mock.Anything).Return("feat/PROJ-123", nil) mockJira.On("GetTicketInfo", "PROJ-123").Return(&models.TicketInfo{}, errors.New("jira error")) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, + WithTicketManager(mockJira), + WithConfig(cfg), + ) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.Error(t, err) assert.Nil(t, suggestions) - assert.Error(t, err) - assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "Error al obtener informacion del ticket") + assert.Contains(t, err.Error(), "INTERNAL: error getting ticket info") }) t.Run("AI service nil", func(t *testing.T) { - mockGit, _, mockJira, mockVCS, cfg, trans := setupTest(t) - service := NewCommitService(mockGit, nil, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) - assert.Error(t, err) - assert.Nil(t, suggestions) + mockGit, _, _, _, _ := setupTest(t) + service := NewCommitService(mockGit, nil) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.Error(t, err) assert.Nil(t, suggestions) - assert.Contains(t, err.Error(), "La IA no está configurada") + assert.ErrorIs(t, err, domainErrors.ErrAPIKeyMissing) }) t.Run("Detect Issue from Commits - Error", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) cfg.UseTicket = false - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockGit.On("GetCurrentBranch", mock.Anything).Return("main", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 5).Return("", errors.New("git log error")) + mockGit.On("GetRecentCommitMessages", mock.Anything, 5).Return([]string{}, errors.New("git log error")) mockAI.On("GenerateSuggestions", mock.Anything, mock.MatchedBy(func(info models.CommitInfo) bool { return info.IssueInfo == nil }), 3).Return([]models.CommitSuggestion{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - _, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, WithConfig(cfg)) + _, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.NoError(t, err) }) t.Run("Detect Issue from Commits - Simple Pattern", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, mockVCS, cfg := setupTest(t) cfg.UseTicket = false - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockGit.On("GetCurrentBranch", mock.Anything).Return("main", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 5).Return("Just a commit fixes #999", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 5).Return([]string{"Just a commit fixes #999"}, nil) mockGit.On("GetRepoInfo", mock.Anything).Return("owner", "repo", "github", nil) cfg.VCSConfigs = map[string]config.VCSConfig{"github": {Token: "token"}} @@ -215,19 +225,19 @@ func TestCommitService_GenerateSuggestions(t *testing.T) { return info.IssueInfo != nil && info.IssueInfo.Number == 999 }), 3).Return([]models.CommitSuggestion{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - _, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, WithVCSClient(mockVCS), WithConfig(cfg)) + _, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.NoError(t, err) mockVCS.AssertCalled(t, "GetIssue", mock.Anything, 999) }) t.Run("GetOrCreateVCSClient - Error getting repo info", func(t *testing.T) { - mockGit, mockAI, mockJira, _, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) cfg.UseTicket = false - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockGit.On("GetCurrentBranch", mock.Anything).Return("issue/123", nil) mockGit.On("GetRepoInfo", mock.Anything).Return("", "", "", errors.New("repo error")) @@ -236,18 +246,18 @@ func TestCommitService_GenerateSuggestions(t *testing.T) { return info.IssueInfo == nil }), 3).Return([]models.CommitSuggestion{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, nil, cfg, trans) - _, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, WithConfig(cfg)) + _, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.NoError(t, err) }) t.Run("GetOrCreateVCSClient - Provider config not found", func(t *testing.T) { - mockGit, mockAI, mockJira, _, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) cfg.UseTicket = false - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockGit.On("GetCurrentBranch", mock.Anything).Return("issue/123", nil) mockGit.On("GetRepoInfo", mock.Anything).Return("owner", "repo", "gitlab", nil) @@ -257,18 +267,18 @@ func TestCommitService_GenerateSuggestions(t *testing.T) { return info.IssueInfo == nil }), 3).Return([]models.CommitSuggestion{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, nil, cfg, trans) - _, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, WithConfig(cfg)) + _, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.NoError(t, err) }) t.Run("GetOrCreateVCSClient - Unsupported provider", func(t *testing.T) { - mockGit, mockAI, mockJira, _, cfg, trans := setupTest(t) + mockGit, mockAI, _, _, cfg := setupTest(t) cfg.UseTicket = false - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockGit.On("GetCurrentBranch", mock.Anything).Return("issue/123", nil) mockGit.On("GetRepoInfo", mock.Anything).Return("owner", "repo", "bitbucket", nil) @@ -278,8 +288,8 @@ func TestCommitService_GenerateSuggestions(t *testing.T) { return info.IssueInfo == nil }), 3).Return([]models.CommitSuggestion{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, nil, cfg, trans) - _, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, WithConfig(cfg)) + _, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.NoError(t, err) }) @@ -287,12 +297,12 @@ func TestCommitService_GenerateSuggestions(t *testing.T) { func TestCommitService_GenerateSuggestionsWithIssue(t *testing.T) { t.Run("explicit issue number", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, mockVCS, cfg := setupTest(t) cfg.UseTicket = false - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockVCS.On("GetIssue", mock.Anything, 100).Return(&models.Issue{Number: 100, Title: "Explicit Issue"}, nil) @@ -300,20 +310,20 @@ func TestCommitService_GenerateSuggestionsWithIssue(t *testing.T) { return info.IssueInfo != nil && info.IssueInfo.Number == 100 }), 3).Return([]models.CommitSuggestion{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - suggestions, err := service.GenerateSuggestionsWithIssue(context.Background(), 3, 100, func(s string) {}) + service := NewCommitService(mockGit, mockAI, WithVCSClient(mockVCS), WithConfig(cfg)) + suggestions, err := service.GenerateSuggestions(context.Background(), 3, 100, func(e models.ProgressEvent) {}) assert.NoError(t, err) assert.NotNil(t, suggestions) }) t.Run("issue fetch error", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, mockVCS, cfg := setupTest(t) cfg.UseTicket = false - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f.go"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f.go"}, nil) mockGit.On("GetDiff", mock.Anything).Return("diff", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockVCS.On("GetIssue", mock.Anything, 100).Return(&models.Issue{}, errors.New("fetch error")) @@ -321,8 +331,8 @@ func TestCommitService_GenerateSuggestionsWithIssue(t *testing.T) { return info.IssueInfo == nil }), 3).Return([]models.CommitSuggestion{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - _, err := service.GenerateSuggestionsWithIssue(context.Background(), 3, 100, func(s string) {}) + service := NewCommitService(mockGit, mockAI, WithVCSClient(mockVCS), WithConfig(cfg)) + _, err := service.GenerateSuggestions(context.Background(), 3, 100, func(e models.ProgressEvent) {}) assert.NoError(t, err) }) @@ -330,21 +340,21 @@ func TestCommitService_GenerateSuggestionsWithIssue(t *testing.T) { func TestCommitService_IssueDetection(t *testing.T) { t.Run("detect from branch name", func(t *testing.T) { - mockGit, mockAI, mockJira, mockVCS, cfg, trans := setupTest(t) + mockGit, mockAI, _, mockVCS, cfg := setupTest(t) cfg.UseTicket = false mockGit.On("GetCurrentBranch", mock.Anything).Return("issue/123-fix", nil) - mockGit.On("GetChangedFiles", mock.Anything).Return([]models.GitChange{{Path: "f"}}, nil) + mockGit.On("GetChangedFiles", mock.Anything).Return([]string{"f"}, nil) mockGit.On("GetDiff", mock.Anything).Return("d", nil) - mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return("history", nil) + mockGit.On("GetRecentCommitMessages", mock.Anything, 10).Return([]string{"history"}, nil) mockVCS.On("GetIssue", mock.Anything, 123).Return(&models.Issue{Number: 123}, nil) mockAI.On("GenerateSuggestions", mock.Anything, mock.MatchedBy(func(info models.CommitInfo) bool { return info.IssueInfo != nil && info.IssueInfo.Number == 123 }), 3).Return([]models.CommitSuggestion{}, nil) - service := NewCommitService(mockGit, mockAI, mockJira, mockVCS, cfg, trans) - _, err := service.GenerateSuggestions(context.Background(), 3, func(s string) {}) + service := NewCommitService(mockGit, mockAI, WithVCSClient(mockVCS), WithConfig(cfg)) + _, err := service.GenerateSuggestions(context.Background(), 3, 0, func(e models.ProgressEvent) {}) assert.NoError(t, err) mockVCS.AssertCalled(t, "GetIssue", mock.Anything, 123) }) diff --git a/internal/services/cost/calculator.go b/internal/services/cost/calculator.go index 7fa08ff..0cb30fd 100644 --- a/internal/services/cost/calculator.go +++ b/internal/services/cost/calculator.go @@ -38,7 +38,7 @@ func NewCalculator() *Calculator { return &Calculator{} } -// EstimateCost calcula el costo estimado basado en proveedor, modelo y tokens +// EstimateCost calculates the estimated cost based on provider, model, and tokens func (c *Calculator) EstimateCost(provider, model string, inputTokens, outputTokens int) float64 { provider = strings.ToLower(provider) model = strings.ToLower(model) @@ -67,25 +67,25 @@ func (c *Calculator) EstimateCost(provider, model string, inputTokens, outputTok return inputCost + outputCost } -// GetPricing retorna la tabla de precios para un proveedor y modelo +// GetPricing returns the pricing table for a provider and model func (c *Calculator) GetPricing(provider, model string) (PricingTable, error) { provider = strings.ToLower(provider) model = strings.ToLower(model) providerPricing, exists := pricing[provider] if !exists { - return PricingTable{}, fmt.Errorf("proveedor %s no encontrado", provider) + return PricingTable{}, fmt.Errorf("provider %s not found", provider) } modelPricing, exists := providerPricing[model] if !exists { - return PricingTable{}, fmt.Errorf("modelo %s no encontrado para proveedor %s", model, provider) + return PricingTable{}, fmt.Errorf("model %s not found for provider %s", model, provider) } return modelPricing, nil } -// AddPricing permite agregar precios dinámicamente (útil para testing o nuevos modelos) +// AddPricing allows adding pricing dynamically (useful for testing or new models) func (c *Calculator) AddPricing(provider, model string, table PricingTable) { provider = strings.ToLower(provider) model = strings.ToLower(model) diff --git a/internal/services/cost/calculator_test.go b/internal/services/cost/calculator_test.go index 4361f8f..b526c09 100644 --- a/internal/services/cost/calculator_test.go +++ b/internal/services/cost/calculator_test.go @@ -131,7 +131,7 @@ func TestCalculator_GetPricing(t *testing.T) { if err == nil { t.Error("GetPricing() error nil, want error for invalid provider") } - if !strings.Contains(err.Error(), "proveedor") { + if !strings.Contains(err.Error(), "provider") { t.Errorf("Error message %q doesn't mention provider", err.Error()) } }) @@ -144,7 +144,7 @@ func TestCalculator_GetPricing(t *testing.T) { if err == nil { t.Error("GetPricing() error nil, want error for invalid model") } - if !strings.Contains(err.Error(), "modelo") { + if !strings.Contains(err.Error(), "model") { t.Errorf("Error message %q doesn't mention model", err.Error()) } }) diff --git a/internal/services/cost/manager.go b/internal/services/cost/manager.go index aa019eb..83d1cf1 100644 --- a/internal/services/cost/manager.go +++ b/internal/services/cost/manager.go @@ -6,9 +6,6 @@ import ( "os" "path/filepath" "time" - - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/fatih/color" ) type ActivityRecord struct { @@ -24,31 +21,39 @@ type ActivityRecord struct { Hash string `json:"hash"` } +type BudgetStatus struct { + IsExceeded bool + PercentUsed float64 + TodayTotal float64 + Estimated float64 + Limit float64 + IsWarning bool + WarningLevel int // 50, 75, 90 +} + type Manager struct { historyPath string budgetDaily float64 - trans *i18n.Translations } -func NewManager(budgetDaily float64, trans *i18n.Translations) (*Manager, error) { +func NewManager(budgetDaily float64) (*Manager, error) { homeDir, err := os.UserHomeDir() if err != nil { - return nil, fmt.Errorf("error obteniendo home directory: %w", err) + return nil, fmt.Errorf("error getting home directory: %w", err) } matecommitDir := filepath.Join(homeDir, ".matecommit") if err := os.MkdirAll(matecommitDir, 0755); err != nil { - return nil, fmt.Errorf("error creando directorio .matecommit: %w", err) + return nil, fmt.Errorf("error creating .matecommit directory: %w", err) } return &Manager{ historyPath: filepath.Join(matecommitDir, "history.json"), budgetDaily: budgetDaily, - trans: trans, }, nil } -// SaveActivity guarda un registro de actividad +// SaveActivity saves an activity record func (m *Manager) SaveActivity(record ActivityRecord) error { records, err := m.loadHistory() if err != nil { @@ -59,90 +64,53 @@ func (m *Manager) SaveActivity(record ActivityRecord) error { data, err := json.MarshalIndent(records, "", " ") if err != nil { - return fmt.Errorf("error serializando historial: %w", err) + return fmt.Errorf("error serializing history: %w", err) } if err := os.WriteFile(m.historyPath, data, 0644); err != nil { - return fmt.Errorf("error guardando historial: %w", err) + return fmt.Errorf("error saving history: %w", err) } return nil } -// CheckBudget verifica si el costo estimado excede el presupuesto diario -// y muestra alertas visuales cuando se acerca al límite -func (m *Manager) CheckBudget(estimatedCost float64) error { +// CheckBudget checks if the estimated cost exceeds the daily budget +func (m *Manager) CheckBudget(estimatedCost float64) (*BudgetStatus, error) { if m.budgetDaily <= 0 { - return nil + return &BudgetStatus{}, nil } todayTotal, err := m.GetDailyTotal() if err != nil { - return err + return nil, err } percentUsed := (todayTotal / m.budgetDaily) * 100 newPercent := ((todayTotal + estimatedCost) / m.budgetDaily) * 100 - if percentUsed >= 50 && percentUsed < 75 { - yellow := color.New(color.FgYellow) - _, _ = yellow.Println(m.trans.GetMessage("cost.budget_alert_50", 0, map[string]interface{}{ - "Percent": fmt.Sprintf("%.0f", percentUsed), - "Spent": fmt.Sprintf("%.4f", todayTotal), - "Limit": fmt.Sprintf("%.2f", m.budgetDaily), - })) - } else if percentUsed >= 75 && percentUsed < 90 { - yellow := color.New(color.FgYellow, color.Bold) - _, _ = yellow.Println(m.trans.GetMessage("cost.budget_alert_75_title", 0, map[string]interface{}{ - "Percent": fmt.Sprintf("%.0f", percentUsed), - })) - _, _ = yellow.Println(m.trans.GetMessage("cost.budget_alert_75_spent", 0, map[string]interface{}{ - "Spent": fmt.Sprintf("%.4f", todayTotal), - "Limit": fmt.Sprintf("%.2f", m.budgetDaily), - })) - } else if percentUsed >= 90 { - red := color.New(color.FgRed, color.Bold) - _, _ = red.Println(m.trans.GetMessage("cost.budget_alert_90_title", 0, map[string]interface{}{ - "Percent": fmt.Sprintf("%.0f", percentUsed), - })) - _, _ = red.Println(m.trans.GetMessage("cost.budget_alert_90_spent", 0, map[string]interface{}{ - "Spent": fmt.Sprintf("%.4f", todayTotal), - "Limit": fmt.Sprintf("%.2f", m.budgetDaily), - })) - _, _ = red.Println(m.trans.GetMessage("cost.budget_alert_90_remaining", 0, map[string]interface{}{ - "Remaining": fmt.Sprintf("%.4f", m.budgetDaily-todayTotal), - })) + status := &BudgetStatus{ + IsExceeded: newPercent > 100, + PercentUsed: percentUsed, + TodayTotal: todayTotal, + Estimated: estimatedCost, + Limit: m.budgetDaily, } - if newPercent > 100 { - red := color.New(color.FgRed, color.Bold) - fmt.Println() - _, _ = red.Println(m.trans.GetMessage("cost.budget_exceeded_title", 0, nil)) - fmt.Println(m.trans.GetMessage("cost.budget_exceeded_spent_today", 0, map[string]interface{}{ - "Spent": fmt.Sprintf("%.4f", todayTotal), - })) - fmt.Println(m.trans.GetMessage("cost.budget_exceeded_estimated", 0, map[string]interface{}{ - "Cost": fmt.Sprintf("%.4f", estimatedCost), - })) - fmt.Println(m.trans.GetMessage("cost.budget_exceeded_total", 0, map[string]interface{}{ - "Total": fmt.Sprintf("%.4f", todayTotal+estimatedCost), - })) - fmt.Println(m.trans.GetMessage("cost.budget_exceeded_limit", 0, map[string]interface{}{ - "Limit": fmt.Sprintf("%.2f", m.budgetDaily), - })) - fmt.Println(m.trans.GetMessage("cost.budget_exceeded_excess", 0, map[string]interface{}{ - "Excess": fmt.Sprintf("%.4f", (todayTotal+estimatedCost)-m.budgetDaily), - })) - fmt.Println() - - return fmt.Errorf("presupuesto diario excedido: actual $%.4f + estimado $%.4f > límite $%.2f", - todayTotal, estimatedCost, m.budgetDaily) + if percentUsed >= 90 { + status.IsWarning = true + status.WarningLevel = 90 + } else if percentUsed >= 75 { + status.IsWarning = true + status.WarningLevel = 75 + } else if percentUsed >= 50 { + status.IsWarning = true + status.WarningLevel = 50 } - return nil + return status, nil } -// GetDailyTotal obtiene el total gastado hoy +// GetDailyTotal gets the total spent today func (m *Manager) GetDailyTotal() (float64, error) { records, err := m.loadHistory() if err != nil { @@ -161,7 +129,7 @@ func (m *Manager) GetDailyTotal() (float64, error) { return total, nil } -// GetMonthlyTotal obtiene el total gastado este mes +// GetMonthlyTotal gets the total spent this month func (m *Manager) GetMonthlyTotal() (float64, error) { records, err := m.loadHistory() if err != nil { @@ -180,7 +148,7 @@ func (m *Manager) GetMonthlyTotal() (float64, error) { return total, nil } -// GetHistory obtiene todos los registros +// GetHistory gets all records func (m *Manager) GetHistory() ([]ActivityRecord, error) { return m.loadHistory() } @@ -191,12 +159,12 @@ func (m *Manager) loadHistory() ([]ActivityRecord, error) { if os.IsNotExist(err) { return []ActivityRecord{}, nil } - return nil, fmt.Errorf("error leyendo historial: %w", err) + return nil, fmt.Errorf("error reading history: %w", err) } var records []ActivityRecord if err := json.Unmarshal(data, &records); err != nil { - return nil, fmt.Errorf("error deserializando historial: %w", err) + return nil, fmt.Errorf("error deserializing history: %w", err) } return records, nil diff --git a/internal/services/cost/manager_test.go b/internal/services/cost/manager_test.go index 32ca8c7..3fd08f2 100644 --- a/internal/services/cost/manager_test.go +++ b/internal/services/cost/manager_test.go @@ -6,8 +6,6 @@ import ( "path/filepath" "testing" "time" - - "github.com/Tomas-vilte/MateCommit/internal/i18n" ) func setupTestManager(t *testing.T, budgetDaily float64) (*Manager, string) { @@ -16,15 +14,9 @@ func setupTestManager(t *testing.T, budgetDaily float64) (*Manager, string) { t.Fatalf("failed to create temp dir: %v", err) } - trans, err := i18n.NewTranslations("en", "") - if err != nil { - t.Fatalf("failed to load translations: %v", err) - } - m := &Manager{ historyPath: filepath.Join(tempDir, "history.json"), budgetDaily: budgetDaily, - trans: trans, } return m, tempDir @@ -32,7 +24,7 @@ func setupTestManager(t *testing.T, budgetDaily float64) (*Manager, string) { func TestNewManager(t *testing.T) { // Act - m, err := NewManager(1.0, nil) + m, err := NewManager(1.0) // Assert if err != nil { @@ -179,11 +171,14 @@ func TestManager_CheckBudget(t *testing.T) { } // Act - err := m.CheckBudget(tt.estimated) + status, err := m.CheckBudget(tt.estimated) // Assert - if (err != nil) != tt.wantErr { - t.Errorf("CheckBudget() error = %v, wantErr %v", err, tt.wantErr) + if err != nil && !tt.wantErr { + t.Errorf("CheckBudget() unexpected error = %v", err) + } + if status != nil && status.IsExceeded != tt.wantErr { + t.Errorf("CheckBudget() status.IsExceeded = %v, want %v", status.IsExceeded, tt.wantErr) } }) } @@ -226,12 +221,18 @@ func TestManager_CheckBudgetAlerts(t *testing.T) { _ = m.SaveActivity(record) // Act - err := m.CheckBudget(0.01) + status, err := m.CheckBudget(0.01) // Assert if err != nil { t.Errorf("CheckBudget() unexpected error = %v", err) } + if status == nil { + t.Fatal("CheckBudget() returned nil status") + } + if !status.IsWarning { + t.Errorf("CheckBudget() expected status.IsWarning = true for spend %v", tt.existingSpend) + } }) } } diff --git a/internal/services/issue_generator_service.go b/internal/services/issue_generator_service.go index 95409dc..47da662 100644 --- a/internal/services/issue_generator_service.go +++ b/internal/services/issue_generator_service.go @@ -7,64 +7,85 @@ import ( "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" ) -var _ ports.IssueGeneratorService = (*IssueGeneratorService)(nil) +// issueGitService defines only the methods needed by IssueGeneratorService. +type issueGitService interface { + GetDiff(ctx context.Context) (string, error) + GetChangedFiles(ctx context.Context) ([]string, error) +} + +// issueTemplateService is a minimal interface for testing purposes +type issueTemplateService interface { + GetTemplateByName(name string) (*models.IssueTemplate, error) + MergeWithGeneratedContent(template *models.IssueTemplate, generated *models.IssueGenerationResult) *models.IssueGenerationResult +} type IssueGeneratorService struct { - git ports.GitService + git issueGitService ai ports.IssueContentGenerator vcsClient ports.VCSClient - templateService ports.IssueTemplateService + templateService issueTemplateService config *config.Config - trans *i18n.Translations +} + +type IssueGeneratorOption func(*IssueGeneratorService) + +func WithIssueVCSClient(vcs ports.VCSClient) IssueGeneratorOption { + return func(s *IssueGeneratorService) { + s.vcsClient = vcs + } +} + +func WithIssueTemplateService(ts issueTemplateService) IssueGeneratorOption { + return func(s *IssueGeneratorService) { + s.templateService = ts + } +} + +func WithIssueConfig(cfg *config.Config) IssueGeneratorOption { + return func(s *IssueGeneratorService) { + s.config = cfg + } } func NewIssueGeneratorService( - git ports.GitService, + gitSvc issueGitService, ai ports.IssueContentGenerator, - vcsClient ports.VCSClient, - templateService ports.IssueTemplateService, - cfg *config.Config, - trans *i18n.Translations, + opts ...IssueGeneratorOption, ) *IssueGeneratorService { - return &IssueGeneratorService{ - git: git, - ai: ai, - vcsClient: vcsClient, - templateService: templateService, - config: cfg, - trans: trans, + s := &IssueGeneratorService{ + git: gitSvc, + ai: ai, } + for _, opt := range opts { + opt(s) + } + return s } -// GenerateFromDiff genera contenido de issue basándose en el diff actual de git. -// Analiza los cambios locales (staged y unstaged) para crear un título, descripción y labels apropiados. +// GenerateFromDiff generates issue content based on the current git diff. +// It analyzes local changes (staged and unstaged) to create an appropriate title, description, and labels. func (s *IssueGeneratorService) GenerateFromDiff(ctx context.Context, hint string, skipLabels bool) (*models.IssueGenerationResult, error) { if s.ai == nil { - return nil, fmt.Errorf("%s", s.trans.GetMessage("issue_generator.ai_not_configured", 0, nil)) + return nil, domainErrors.ErrAPIKeyMissing } diff, err := s.git.GetDiff(ctx) if err != nil { - return nil, fmt.Errorf("%s: %w", s.trans.GetMessage("issue_generator.error_getting_diff", 0, nil), err) + return nil, domainErrors.NewAppError(domainErrors.TypeGit, "failed to get diff", err) } if diff == "" { - return nil, fmt.Errorf("%s", s.trans.GetMessage("issue_generator.no_changes_detected", 0, nil)) + return nil, domainErrors.ErrNoChanges } - changedFilesRaw, err := s.git.GetChangedFiles(ctx) + changedFiles, err := s.git.GetChangedFiles(ctx) if err != nil { - return nil, fmt.Errorf("%s: %w", s.trans.GetMessage("issue_generator.error_getting_files", 0, nil), err) - } - - changedFiles := make([]string, 0, len(changedFilesRaw)) - for _, change := range changedFilesRaw { - changedFiles = append(changedFiles, change.Path) + return nil, domainErrors.NewAppError(domainErrors.TypeGit, "failed to get changed files", err) } request := models.IssueGenerationRequest{ @@ -76,7 +97,7 @@ func (s *IssueGeneratorService) GenerateFromDiff(ctx context.Context, hint strin result, err := s.ai.GenerateIssueContent(ctx, request) if err != nil { - return nil, fmt.Errorf("%s: %w", s.trans.GetMessage("issue_generator.error_generating_content", 0, nil), err) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "failed to generate issue content", err) } if !skipLabels { @@ -87,25 +108,27 @@ func (s *IssueGeneratorService) GenerateFromDiff(ctx context.Context, hint strin return result, nil } -// GenerateFromDescription genera contenido de issue basándose en una descripción manual. -// Útil cuando el usuario quiere crear una issue sin tener cambios locales. +// GenerateFromDescription generates issue content based on a manual description. +// Useful when the user wants to create an issue without having local changes. func (s *IssueGeneratorService) GenerateFromDescription(ctx context.Context, description string, skipLabels bool) (*models.IssueGenerationResult, error) { if s.ai == nil { - return nil, fmt.Errorf("%s", s.trans.GetMessage("issue_generator.ai_not_configured", 0, nil)) + return nil, domainErrors.ErrAPIKeyMissing } if description == "" { - return nil, fmt.Errorf("%s", s.trans.GetMessage("issue_generator.description_required", 0, nil)) + return nil, domainErrors.NewAppError(domainErrors.TypeConfiguration, "description is required", nil) } request := models.IssueGenerationRequest{ Description: description, - Language: s.config.Language, + } + if s.config != nil { + request.Language = s.config.Language } result, err := s.ai.GenerateIssueContent(ctx, request) if err != nil { - return nil, fmt.Errorf("%s: %w", s.trans.GetMessage("issue_generator.error_generating_content", 0, nil), err) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "failed to generate issue content", err) } if skipLabels { @@ -117,16 +140,16 @@ func (s *IssueGeneratorService) GenerateFromDescription(ctx context.Context, des func (s *IssueGeneratorService) GenerateFromPR(ctx context.Context, prNumber int, hint string, skipLabels bool) (*models.IssueGenerationResult, error) { if s.ai == nil { - return nil, fmt.Errorf("%s", s.trans.GetMessage("issue_generator.ai_not_configured", 0, nil)) + return nil, domainErrors.ErrAPIKeyMissing } if s.vcsClient == nil { - return nil, fmt.Errorf("%s", s.trans.GetMessage("issue_generator.vcs_not_configured", 0, nil)) + return nil, domainErrors.ErrConfigMissing } prData, err := s.vcsClient.GetPR(ctx, prNumber) if err != nil { - return nil, fmt.Errorf("%s: %w", s.trans.GetMessage("issue_generator.error_getting_pr", 0, nil), err) + return nil, domainErrors.NewAppError(domainErrors.TypeVCS, "failed to get PR", err) } var contextBuilder strings.Builder @@ -158,7 +181,7 @@ func (s *IssueGeneratorService) GenerateFromPR(ctx context.Context, prNumber int result, err := s.ai.GenerateIssueContent(ctx, request) if err != nil { - return nil, fmt.Errorf("%s: %w", s.trans.GetMessage("issue_generator.error_generating_content", 0, nil), err) + return nil, domainErrors.NewAppError(domainErrors.TypeAI, "failed to generate issue content", err) } result.Description = fmt.Sprintf("%s\n\n---\n*Related PR: #%d*", result.Description, prNumber) @@ -173,7 +196,7 @@ func (s *IssueGeneratorService) GenerateFromPR(ctx context.Context, prNumber int func (s *IssueGeneratorService) GenerateWithTemplate(ctx context.Context, templateName string, hint string, fromDiff bool, description string, skipLabels bool) (*models.IssueGenerationResult, error) { template, err := s.templateService.GetTemplateByName(templateName) if err != nil { - return nil, fmt.Errorf("%s: %w", s.trans.GetMessage("issue_generator.error_loading_template", 0, nil), err) + return nil, domainErrors.NewAppError(domainErrors.TypeConfiguration, fmt.Sprintf("failed to load template: %s", templateName), err) } var baseResult *models.IssueGenerationResult @@ -182,7 +205,7 @@ func (s *IssueGeneratorService) GenerateWithTemplate(ctx context.Context, templa } else if description != "" { baseResult, err = s.GenerateFromDescription(ctx, description, skipLabels) } else { - return nil, fmt.Errorf("%s", s.trans.GetMessage("issue.error_no_input", 0, nil)) + return nil, domainErrors.NewAppError(domainErrors.TypeConfiguration, "no input provided", nil) } if err != nil { return nil, err @@ -245,13 +268,13 @@ func (s *IssueGeneratorService) extractFilesFromDiff(diff string) []string { return files } -// CreateIssue crea una nueva issue en el repositorio usando el VCS client configurado. -// Retorna la issue creada con su número y URL asignados. +// CreateIssue creates a new issue in the repository using the configured VCS client. +// It returns the created issue with its assigned number and URL. func (s *IssueGeneratorService) CreateIssue(ctx context.Context, result *models.IssueGenerationResult, assignees []string) (*models.Issue, error) { return s.vcsClient.CreateIssue(ctx, result.Title, result.Description, result.Labels, assignees) } -// GetAuthenticatedUser obtiene el username del usuario autenticado actual. +// GetAuthenticatedUser gets the username of the current authenticated user. func (s *IssueGeneratorService) GetAuthenticatedUser(ctx context.Context) (string, error) { return s.vcsClient.GetAuthenticatedUser(ctx) } @@ -280,15 +303,15 @@ func (s *IssueGeneratorService) InferBranchName(issueNumber int, labels []string return fmt.Sprintf("%s/issue-%d", prefix, issueNumber) } -// LinkIssueToPR vincula una issue a un Pull Request agregando "Closes #issueNumber" a la descripción del PR. +// LinkIssueToPR links an issue to a Pull Request by adding "Closes #issueNumber" to the PR description. func (s *IssueGeneratorService) LinkIssueToPR(ctx context.Context, prNumber int, issueNumber int) error { if s.vcsClient == nil { - return fmt.Errorf("%s", s.trans.GetMessage("issue_generator.vcs_not_configured", 0, nil)) + return domainErrors.ErrConfigMissing } prData, err := s.vcsClient.GetPR(ctx, prNumber) if err != nil { - return fmt.Errorf("%s: %w", s.trans.GetMessage("issue_generator.error_getting_pr", 0, nil), err) + return domainErrors.NewAppError(domainErrors.TypeVCS, "failed to get PR", err) } linkText := fmt.Sprintf("\n\nCloses #%d", issueNumber) @@ -300,9 +323,7 @@ func (s *IssueGeneratorService) LinkIssueToPR(ctx context.Context, prNumber int, } if err := s.vcsClient.UpdatePR(ctx, prNumber, summary); err != nil { - return fmt.Errorf("%s: %w", s.trans.GetMessage("error.update_pr", 0, map[string]interface{}{ - "pr_number": prNumber, - }), err) + return domainErrors.NewAppError(domainErrors.TypeVCS, "failed to update PR", err) } return nil diff --git a/internal/services/issue_generator_service_test.go b/internal/services/issue_generator_service_test.go index a0b632b..195aed6 100644 --- a/internal/services/issue_generator_service_test.go +++ b/internal/services/issue_generator_service_test.go @@ -6,23 +6,21 @@ import ( "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" ) func TestIssueGeneratorService(t *testing.T) { ctx := context.Background() - trans := &i18n.Translations{} cfg := &config.Config{Language: "en"} t.Run("GenerateFromDiff - Success", func(t *testing.T) { mockGit := new(MockGitService) mockAI := new(MockIssueContentGenerator) - service := NewIssueGeneratorService(mockGit, mockAI, nil, nil, cfg, trans) + service := NewIssueGeneratorService(mockGit, mockAI, WithIssueConfig(cfg)) mockGit.On("GetDiff", ctx).Return("diff content", nil) - mockGit.On("GetChangedFiles", ctx).Return([]models.GitChange{{Path: "main.go"}}, nil) + mockGit.On("GetChangedFiles", ctx).Return([]string{"main.go"}, nil) expectedRequest := models.IssueGenerationRequest{ Diff: "diff content", @@ -49,7 +47,7 @@ func TestIssueGeneratorService(t *testing.T) { t.Run("GenerateFromDescription - Success", func(t *testing.T) { mockAI := new(MockIssueContentGenerator) - service := NewIssueGeneratorService(nil, mockAI, nil, nil, cfg, trans) + service := NewIssueGeneratorService(nil, mockAI, WithIssueConfig(cfg)) expectedRequest := models.IssueGenerationRequest{ Description: "manual description", @@ -73,7 +71,7 @@ func TestIssueGeneratorService(t *testing.T) { t.Run("GenerateFromPR - Success", func(t *testing.T) { mockAI := new(MockIssueContentGenerator) mockVCS := new(MockVCSClient) - service := NewIssueGeneratorService(nil, mockAI, mockVCS, nil, cfg, trans) + service := NewIssueGeneratorService(nil, mockAI, WithIssueVCSClient(mockVCS), WithIssueConfig(cfg)) prData := models.PRData{ Title: "PR Title", @@ -104,7 +102,7 @@ func TestIssueGeneratorService(t *testing.T) { mockGit := new(MockGitService) mockAI := new(MockIssueContentGenerator) mockTemplate := new(MockIssueTemplateService) - service := NewIssueGeneratorService(mockGit, mockAI, nil, mockTemplate, cfg, trans) + service := NewIssueGeneratorService(mockGit, mockAI, WithIssueTemplateService(mockTemplate), WithIssueConfig(cfg)) template := &models.IssueTemplate{ Name: "bug_report", @@ -126,7 +124,7 @@ func TestIssueGeneratorService(t *testing.T) { mockTemplate.On("GetTemplateByName", "bug_report").Return(template, nil) mockGit.On("GetDiff", ctx).Return("some changes", nil) - mockGit.On("GetChangedFiles", ctx).Return([]models.GitChange{{Path: "file.go"}}, nil) + mockGit.On("GetChangedFiles", ctx).Return([]string{"file.go"}, nil) mockAI.On("GenerateIssueContent", ctx, mock.Anything).Return(generated, nil) mockTemplate.On("MergeWithGeneratedContent", template, mock.Anything).Return(merged) diff --git a/internal/services/issue_template_service.go b/internal/services/issue_template_service.go index efa9770..7162ed5 100644 --- a/internal/services/issue_template_service.go +++ b/internal/services/issue_template_service.go @@ -7,30 +7,35 @@ import ( "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "gopkg.in/yaml.v3" ) -var _ ports.IssueTemplateService = (*IssueTemplateService)(nil) - type IssueTemplateService struct { config *config.Config - trans *i18n.Translations } -func NewIssueTemplateService(cfg *config.Config, trans *i18n.Translations) *IssueTemplateService { - return &IssueTemplateService{ - config: cfg, - trans: trans, +type IssueOption func(*IssueTemplateService) + +func WithTemplateConfig(cfg *config.Config) IssueOption { + return func(s *IssueTemplateService) { + s.config = cfg + } +} + +func NewIssueTemplateService(opts ...IssueOption) *IssueTemplateService { + s := &IssueTemplateService{} + for _, opt := range opts { + opt(s) } + return s } func (s *IssueTemplateService) GetTemplatesDir() (string, error) { cwd, err := os.Getwd() if err != nil { - return "", fmt.Errorf("error obteniendo directorio actual: %w", err) + return "", domainErrors.NewAppError(domainErrors.TypeInternal, "failed to get current working directory", err) } provider := strings.ToLower(s.config.ActiveVCSProvider) @@ -60,7 +65,7 @@ func (s *IssueTemplateService) ListTemplates() ([]models.TemplateMetadata, error entries, err := os.ReadDir(templatesDir) if err != nil { - return nil, fmt.Errorf("error leyendo directorio de templates: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeInternal, "failed to read templates directory", err) } templates := make([]models.TemplateMetadata, 0) @@ -88,7 +93,7 @@ func (s *IssueTemplateService) ListTemplates() ([]models.TemplateMetadata, error func (s *IssueTemplateService) LoadTemplate(filePath string) (*models.IssueTemplate, error) { content, err := os.ReadFile(filePath) if err != nil { - return nil, fmt.Errorf("error leyendo archivo de template: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeConfiguration, fmt.Sprintf("failed to read template file: %s", filePath), err) } return s.parseTemplate(string(content), filePath) } @@ -98,9 +103,9 @@ func (s *IssueTemplateService) parseTemplate(content string, filePath string) (* FilePath: filePath, } - // Para archivos .yml, parseamos directamente todo el contenido como YAML + // For .yml files, we parse all content directly as YAML if err := yaml.Unmarshal([]byte(content), template); err != nil { - return nil, fmt.Errorf("error parseando template YAML: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeConfiguration, fmt.Sprintf("failed to parse YAML template: %s", filePath), err) } return template, nil @@ -124,7 +129,7 @@ func (s *IssueTemplateService) GetTemplateByName(name string) (*models.IssueTemp } } - return nil, fmt.Errorf("template '%s' no encontrado", name) + return nil, domainErrors.NewAppError(domainErrors.TypeConfiguration, fmt.Sprintf("template '%s' not found", name), nil) } func (s *IssueTemplateService) InitializeTemplates(force bool) error { @@ -134,7 +139,7 @@ func (s *IssueTemplateService) InitializeTemplates(force bool) error { } if err := os.MkdirAll(templatesDir, 0755); err != nil { - return fmt.Errorf("error creando directorio de templates: %w", err) + return domainErrors.NewAppError(domainErrors.TypeInternal, "failed to create templates directory", err) } templates := map[string]string{ @@ -155,13 +160,13 @@ func (s *IssueTemplateService) InitializeTemplates(force bool) error { } if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { - return fmt.Errorf("error escribiendo template %s: %w", filePath, err) + return domainErrors.NewAppError(domainErrors.TypeInternal, fmt.Sprintf("failed to write template: %s", filePath), err) } created++ } if created == 0 && skipped > 0 { - return fmt.Errorf("los templates ya existen, usa --force para sobrescribirlas") + return domainErrors.NewAppError(domainErrors.TypeConfiguration, "templates_already_exist", nil) } return nil } @@ -181,24 +186,24 @@ func (s *IssueTemplateService) buildTemplateContent(templateType string) string func (s *IssueTemplateService) buildBugReportTemplate() string { template := map[string]interface{}{ - "name": s.trans.GetMessage("issue_template.bug_report_name", 0, nil), - "description": s.trans.GetMessage("issue_template.bug_report_about", 0, nil), - "title": s.trans.GetMessage("issue_template.bug_report_title", 0, nil), + "name": "Bug report", + "description": "Create a report to help us improve", + "title": "[BUG] ", "labels": []string{"bug"}, "body": []map[string]interface{}{ { "type": "markdown", "attributes": map[string]string{ - "value": s.trans.GetMessage("issue_template.bug_report_intro", 0, nil), + "value": "Thank you for reporting a bug!", }, }, { "type": "textarea", "id": "description", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.bug_description_label", 0, nil), - "description": s.trans.GetMessage("issue_template.bug_description_help", 0, nil), - "placeholder": s.trans.GetMessage("issue_template.bug_description_placeholder", 0, nil), + "label": "Description", + "description": "A clear and concise description of what the bug is.", + "placeholder": "Enter bug description", }, "validations": map[string]bool{ "required": true, @@ -208,8 +213,8 @@ func (s *IssueTemplateService) buildBugReportTemplate() string { "type": "textarea", "id": "steps", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.bug_steps_label", 0, nil), - "description": s.trans.GetMessage("issue_template.bug_steps_help", 0, nil), + "label": "Steps to reproduce", + "description": "Explain how you encountered the bug.", "placeholder": "1. \n2. \n3. ", }, "validations": map[string]bool{ @@ -220,8 +225,8 @@ func (s *IssueTemplateService) buildBugReportTemplate() string { "type": "textarea", "id": "expected", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.bug_expected_label", 0, nil), - "placeholder": s.trans.GetMessage("issue_template.bug_expected_placeholder", 0, nil), + "label": "Expected behavior", + "placeholder": "What did you expect to happen?", }, "validations": map[string]bool{ "required": true, @@ -231,8 +236,8 @@ func (s *IssueTemplateService) buildBugReportTemplate() string { "type": "textarea", "id": "actual", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.bug_actual_label", 0, nil), - "placeholder": s.trans.GetMessage("issue_template.bug_actual_placeholder", 0, nil), + "label": "Actual behavior", + "placeholder": "What actually happened?", }, "validations": map[string]bool{ "required": true, @@ -242,7 +247,7 @@ func (s *IssueTemplateService) buildBugReportTemplate() string { "type": "input", "id": "version", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.bug_version_label", 0, nil), + "label": "Version", "placeholder": "v1.0.0", }, }, @@ -250,8 +255,8 @@ func (s *IssueTemplateService) buildBugReportTemplate() string { "type": "textarea", "id": "additional", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.bug_additional_label", 0, nil), - "description": s.trans.GetMessage("issue_template.bug_additional_help", 0, nil), + "label": "Additional information", + "description": "Add any other context about the problem here.", }, }, }, @@ -263,24 +268,24 @@ func (s *IssueTemplateService) buildBugReportTemplate() string { func (s *IssueTemplateService) buildFeatureRequestTemplate() string { template := map[string]interface{}{ - "name": s.trans.GetMessage("issue_template.feature_request_name", 0, nil), - "description": s.trans.GetMessage("issue_template.feature_request_about", 0, nil), - "title": s.trans.GetMessage("issue_template.feature_request_title", 0, nil), + "name": "Feature request", + "description": "Suggest an idea for this project", + "title": "[FEATURE] ", "labels": []string{"enhancement"}, "body": []map[string]interface{}{ { "type": "markdown", "attributes": map[string]string{ - "value": s.trans.GetMessage("issue_template.feature_intro", 0, nil), + "value": "Thank you for suggesting a feature!", }, }, { "type": "textarea", "id": "problem", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.feature_problem_label", 0, nil), - "description": s.trans.GetMessage("issue_template.feature_problem_help", 0, nil), - "placeholder": s.trans.GetMessage("issue_template.feature_problem_placeholder", 0, nil), + "label": "Problem description", + "description": "A clear and concise description of what the problem is.", + "placeholder": "I'm always frustrated when...", }, "validations": map[string]bool{ "required": true, @@ -290,9 +295,9 @@ func (s *IssueTemplateService) buildFeatureRequestTemplate() string { "type": "textarea", "id": "solution", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.feature_solution_label", 0, nil), - "description": s.trans.GetMessage("issue_template.feature_solution_help", 0, nil), - "placeholder": s.trans.GetMessage("issue_template.feature_solution_placeholder", 0, nil), + "label": "Proposed solution", + "description": "A clear and concise description of what you want to happen.", + "placeholder": "I would like to see...", }, "validations": map[string]bool{ "required": true, @@ -302,16 +307,16 @@ func (s *IssueTemplateService) buildFeatureRequestTemplate() string { "type": "textarea", "id": "alternatives", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.feature_alternatives_label", 0, nil), - "description": s.trans.GetMessage("issue_template.feature_alternatives_help", 0, nil), + "label": "Alternatives considered", + "description": "A clear and concise description of any alternative solutions.", }, }, { "type": "textarea", "id": "additional", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.feature_additional_label", 0, nil), - "description": s.trans.GetMessage("issue_template.feature_additional_help", 0, nil), + "label": "Additional information", + "description": "Add any other context or screenshots about the feature request here.", }, }, }, @@ -323,24 +328,24 @@ func (s *IssueTemplateService) buildFeatureRequestTemplate() string { func (s *IssueTemplateService) buildCustomTemplate() string { template := map[string]interface{}{ - "name": s.trans.GetMessage("issue_template.custom_issue_name", 0, nil), - "description": s.trans.GetMessage("issue_template.custom_issue_about", 0, nil), - "title": s.trans.GetMessage("issue_template.custom_issue_title", 0, nil), + "name": "Custom issue", + "description": "File a custom issue", + "title": "[ISSUE] ", "labels": []string{}, "body": []map[string]interface{}{ { "type": "markdown", "attributes": map[string]string{ - "value": s.trans.GetMessage("issue_template.custom_intro", 0, nil), + "value": "Open a custom issue.", }, }, { "type": "textarea", "id": "description", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.custom_description_label", 0, nil), - "description": s.trans.GetMessage("issue_template.custom_description_help", 0, nil), - "placeholder": s.trans.GetMessage("issue_template.custom_description_placeholder", 0, nil), + "label": "Description", + "description": "Enter the issue description.", + "placeholder": "Describe your issue here", }, "validations": map[string]bool{ "required": true, @@ -350,8 +355,8 @@ func (s *IssueTemplateService) buildCustomTemplate() string { "type": "textarea", "id": "additional", "attributes": map[string]interface{}{ - "label": s.trans.GetMessage("issue_template.custom_additional_label", 0, nil), - "description": s.trans.GetMessage("issue_template.custom_additional_help", 0, nil), + "label": "Additional information", + "description": "Any additional context.", }, }, }, @@ -378,7 +383,7 @@ func (s *IssueTemplateService) MergeWithGeneratedContent(template *models.IssueT descBuilder.WriteString(generated.Description) descBuilder.WriteString("\n\n") - // Solo para templates .md que tienen Body como string + // Only for .md templates that have Body as a string if template.BodyContent != "" { descBuilder.WriteString("---\n\n") descBuilder.WriteString(template.BodyContent) diff --git a/internal/services/issue_template_service_test.go b/internal/services/issue_template_service_test.go index 2f693a0..0a5c643 100644 --- a/internal/services/issue_template_service_test.go +++ b/internal/services/issue_template_service_test.go @@ -7,7 +7,6 @@ import ( "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/require" ) @@ -40,7 +39,7 @@ func TestIssueTemplateService_GetTemplatesDir(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &config.Config{ActiveVCSProvider: tt.provider} - service := NewIssueTemplateService(cfg, nil) + service := NewIssueTemplateService(WithTemplateConfig(cfg)) dir, err := service.GetTemplatesDir() assert.NoError(t, err) @@ -103,7 +102,7 @@ labels: title: [unclosed` _, err := service.parseTemplate(content, "test.yml") assert.Error(t, err) - assert.Contains(t, err.Error(), "error parseando template YAML") + assert.Contains(t, err.Error(), "CONFIGURATION: failed to parse YAML template") }) } @@ -120,9 +119,7 @@ func TestIssueTemplateService_FilesystemOps(t *testing.T) { }() cfg := &config.Config{ActiveVCSProvider: "github"} - trans, err := i18n.NewTranslations("en", "") - require.NoError(t, err) - service := NewIssueTemplateService(cfg, trans) + service := NewIssueTemplateService(WithTemplateConfig(cfg)) t.Run("InitializeTemplates", func(t *testing.T) { err := service.InitializeTemplates(false) @@ -138,7 +135,7 @@ func TestIssueTemplateService_FilesystemOps(t *testing.T) { t.Run("InitializeTemplates - Already exists", func(t *testing.T) { err := service.InitializeTemplates(false) assert.Error(t, err) - assert.Contains(t, err.Error(), "los templates ya existen") + assert.Contains(t, err.Error(), "CONFIGURATION: templates_already_exist") err = service.InitializeTemplates(true) assert.NoError(t, err) @@ -168,7 +165,7 @@ func TestIssueTemplateService_FilesystemOps(t *testing.T) { _, err = service.GetTemplateByName("non_existent") assert.Error(t, err) - assert.Contains(t, err.Error(), "no encontrado") + assert.Contains(t, err.Error(), "not found") }) } @@ -256,14 +253,14 @@ func TestIssueTemplateService_MergeWithGeneratedContent(t *testing.T) { } func TestIssueTemplateService_GetTemplateByName_NotFound(t *testing.T) { cfg := &config.Config{ActiveVCSProvider: "github"} - service := NewIssueTemplateService(cfg, nil) + service := NewIssueTemplateService(WithTemplateConfig(cfg)) _, err := service.GetTemplateByName("ghost") assert.Error(t, err) } func TestIssueTemplateService_MergeWithGeneratedContent_Realistic(t *testing.T) { cfg := &config.Config{} - service := NewIssueTemplateService(cfg, nil) + service := NewIssueTemplateService(WithTemplateConfig(cfg)) template := &models.IssueTemplate{ Title: "[BUG] ", Body: []interface{}{ diff --git a/internal/services/mocks.go b/internal/services/mocks.go index 8698bb5..46ad7c3 100644 --- a/internal/services/mocks.go +++ b/internal/services/mocks.go @@ -55,9 +55,9 @@ func (m *MockGitService) AddFileToStaging(ctx context.Context, file string) erro return args.Error(0) } -func (m *MockGitService) GetChangedFiles(ctx context.Context) ([]models.GitChange, error) { +func (m *MockGitService) GetChangedFiles(ctx context.Context) ([]string, error) { args := m.Called(ctx) - return args.Get(0).([]models.GitChange), args.Error(1) + return args.Get(0).([]string), args.Error(1) } func (m *MockGitService) GetDiff(ctx context.Context) (string, error) { @@ -116,9 +116,9 @@ func (m *MockGitService) GetTagDate(ctx context.Context, tag string) (string, er return args.String(0), args.Error(1) } -func (m *MockGitService) GetRecentCommitMessages(ctx context.Context, count int) (string, error) { +func (m *MockGitService) GetRecentCommitMessages(ctx context.Context, count int) ([]string, error) { args := m.Called(ctx, count) - return args.String(0), args.Error(1) + return args.Get(0).([]string), args.Error(1) } func (m *MockGitService) CreateTag(ctx context.Context, version, message string) error { diff --git a/internal/services/pull_request_service.go b/internal/services/pull_request_service.go index 99fbb71..771c95c 100644 --- a/internal/services/pull_request_service.go +++ b/internal/services/pull_request_service.go @@ -6,43 +6,65 @@ import ( "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" ) -var _ ports.PRService = (*PRService)(nil) +// prVCSClient defines the methods needed by PRService from a VCS provider. +type prVCSClient interface { + GetPR(ctx context.Context, prNumber int) (models.PRData, error) + GetPRIssues(ctx context.Context, branchName string, commitMessages []string, description string) ([]models.Issue, error) + UpdatePR(ctx context.Context, prNumber int, summary models.PRSummary) error +} + +// prAIProvider defines the methods needed by PRService from an AI provider. +type prAIProvider interface { + GeneratePRSummary(ctx context.Context, prompt string) (models.PRSummary, error) +} type PRService struct { - vcsClient ports.VCSClient - aiService ports.PRSummarizer - trans *i18n.Translations + vcsClient prVCSClient + aiService prAIProvider config *config.Config } -func NewPRService(vcsClient ports.VCSClient, aiService ports.PRSummarizer, trans *i18n.Translations, - cfg *config.Config) *PRService { - return &PRService{ - vcsClient: vcsClient, - aiService: aiService, - trans: trans, - config: cfg, +type PROption func(*PRService) + +func WithPRVCSClient(vcs prVCSClient) PROption { + return func(s *PRService) { + s.vcsClient = vcs + } +} + +func WithPRAIProvider(ai prAIProvider) PROption { + return func(s *PRService) { + s.aiService = ai + } +} + +func WithPRConfig(cfg *config.Config) PROption { + return func(s *PRService) { + s.config = cfg } } -func (s *PRService) SummarizePR(ctx context.Context, prNumber int, progress func(string)) (models.PRSummary, error) { +func NewPRService(opts ...PROption) *PRService { + s := &PRService{} + for _, opt := range opts { + opt(s) + } + return s +} + +func (s *PRService) SummarizePR(ctx context.Context, prNumber int, progress func(models.ProgressEvent)) (models.PRSummary, error) { if s.aiService == nil { - msg := s.trans.GetMessage("ai_missing.ai_missing_for_pr", 0, nil) - return models.PRSummary{}, fmt.Errorf("%s", msg) + return models.PRSummary{}, domainErrors.ErrAPIKeyMissing } prData, err := s.vcsClient.GetPR(ctx, prNumber) if err != nil { - msg := s.trans.GetMessage("pr_service.error_get_pr", 0, map[string]interface{}{ - "Error": err, - }) - return models.PRSummary{}, fmt.Errorf("%s", msg) + return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeVCS, "error getting PR", err) } var commitMessages []string @@ -58,53 +80,63 @@ func (s *PRService) SummarizePR(ctx context.Context, prNumber int, progress func for i, issue := range issues { issueNums[i] = fmt.Sprintf("#%d", issue.Number) } - msg := s.trans.GetMessage("pr_detected_issues", 0, map[string]interface{}{ - "Number": prNumber, - "Issues": strings.Join(issueNums, ", "), - }) - progress(msg) + + if progress != nil { + progress(models.ProgressEvent{ + Type: models.ProgressIssuesDetected, + Data: map[string]interface{}{ + "PRNumber": prNumber, + "Issues": issueNums, + }, + }) + } } prompt := s.buildPRPrompt(prData) summary, err := s.aiService.GeneratePRSummary(ctx, prompt) if err != nil { - msg := s.trans.GetMessage("pr_service.error_create_summary_pr", 0, map[string]interface{}{ - "Error": err, - }) - return models.PRSummary{}, fmt.Errorf("%s", msg) + return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeAI, "error generating PR summary", err) } if len(prData.RelatedIssues) > 0 { summary = s.ensurePRIssueReferences(summary, prData.RelatedIssues) - - msg := s.trans.GetMessage("pr_issues_will_close_on_merge", 0, map[string]interface{}{ - "Count": len(prData.RelatedIssues), - }) - progress(msg) + if progress != nil { + progress(models.ProgressEvent{ + Type: models.ProgressIssuesClosing, + Data: map[string]interface{}{ + "Count": len(prData.RelatedIssues), + }, + }) + } } breakingChanges := s.detectBreakingChanges(prData.Commits) if len(breakingChanges) > 0 { - msg := s.trans.GetMessage("pr_breaking_changes_detected", 0, map[string]interface{}{ - "Count": len(breakingChanges), - }) - progress(msg) + if progress != nil { + progress(models.ProgressEvent{ + Type: models.ProgressBreakingChanges, + Data: map[string]interface{}{ + "Count": len(breakingChanges), + }, + }) + } summary = s.addBreakingChangesToSummary(summary, breakingChanges) } testPlan := s.generateTestPlan(prData) if testPlan != "" { - progress(s.trans.GetMessage("pr_test_plan_generated", 0, nil)) + if progress != nil { + progress(models.ProgressEvent{ + Type: models.ProgressTestPlan, + }) + } summary.Body += testPlan } err = s.vcsClient.UpdatePR(ctx, prNumber, summary) if err != nil { - msg := s.trans.GetMessage("pr_service.error_update_pr", 0, map[string]interface{}{ - "Error": err, - }) - return models.PRSummary{}, fmt.Errorf("%s", msg) + return models.PRSummary{}, domainErrors.NewAppError(domainErrors.TypeVCS, "error updating PR", err) } return summary, nil @@ -120,14 +152,10 @@ func (s *PRService) buildPRPrompt(prData models.PRData) string { diffLines := strings.Count(prData.Diff, "\n") filesChanged := strings.Count(prData.Diff, "diff --git") - prompt += "📊 **Métricas:**\n" - prompt += fmt.Sprintf("- %d commits\n", commitCount) - prompt += fmt.Sprintf("- %d archivos cambiados\n", filesChanged) - prompt += fmt.Sprintf("- ~%d líneas en diff\n\n", diffLines) - + prompt += fmt.Sprintf("Stats: %d commits, %d files, ~%d lines\n\n", commitCount, filesChanged, diffLines) breakingChanges := s.detectBreakingChanges(prData.Commits) if len(breakingChanges) > 0 { - prompt += "⚠️ **Breaking Changes detectados:**\n" + prompt += "⚠️ Breaking Changes:\n" for _, bc := range breakingChanges { prompt += fmt.Sprintf("- %s\n", bc) } @@ -137,7 +165,8 @@ func (s *PRService) buildPRPrompt(prData models.PRData) string { if len(prData.RelatedIssues) > 0 { locale := s.config.Language issuesFormatted := ai.FormatIssuesForPrompt(prData.RelatedIssues, locale) - issueContext := fmt.Sprintf(ai.GetPRIssueContextInstructions(locale), issuesFormatted) + data := ai.PromptData{RelatedIssues: issuesFormatted} + issueContext, _ := ai.RenderPrompt("prIssueContext", ai.GetPRIssueContextInstructions(locale), data) prompt += issueContext + "\n\n" } @@ -147,7 +176,7 @@ func (s *PRService) buildPRPrompt(prData models.PRData) string { } prompt += "\n" - prompt += "Archivos principales modificados:\n" + prompt += "Main files modified:\n" lines := strings.Split(prData.Diff, "\n") fileCount := 0 for _, line := range lines { @@ -230,16 +259,15 @@ func (s *PRService) generateTestPlan(prData models.PRData) string { testPlan.WriteString("\n\n## Test Plan\n\n") for _, issue := range prData.RelatedIssues { - testPlan.WriteString(fmt.Sprintf("- [ ] Verificar que #%d esté resuelto\n", issue.Number)) + testPlan.WriteString(fmt.Sprintf("- [ ] Verify #%d is resolved\n", issue.Number)) } - testPlan.WriteString("- [ ] Ejecutar tests existentes\n") - testPlan.WriteString("- [ ] Verificar que no hay regresiones\n") + testPlan.WriteString("- [ ] Run existing tests\n") + testPlan.WriteString("- [ ] Verify no regressions\n") return testPlan.String() } -// addBreakingChangesToSummary agrega sección de breaking changes al resumen func (s *PRService) addBreakingChangesToSummary(summary models.PRSummary, breakingChanges []string) models.PRSummary { if len(breakingChanges) == 0 { return summary diff --git a/internal/services/pull_request_service_test.go b/internal/services/pull_request_service_test.go index b03ab38..c0bb741 100644 --- a/internal/services/pull_request_service_test.go +++ b/internal/services/pull_request_service_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai/gemini" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" "github.com/stretchr/testify/assert" @@ -24,9 +24,7 @@ func TestPRService_SummarizePR_Success(t *testing.T) { mockVCS := new(MockVCSClient) mockAI := new(MockPRSummarizer) - trans, err := i18n.NewTranslations("es", "../i18n/locales") cfg := &config.Config{} - require.NoError(t, err) prData := models.PRData{ ID: 123, @@ -49,10 +47,14 @@ func TestPRService_SummarizePR_Success(t *testing.T) { mockAI.On("GeneratePRSummary", ctx, mock.AnythingOfType("string")).Return(expectedSummary, nil) mockVCS.On("UpdatePR", ctx, prNumber, expectedSummary).Return(nil) - service := NewPRService(mockVCS, mockAI, trans, cfg) + service := NewPRService( + WithPRVCSClient(mockVCS), + WithPRAIProvider(mockAI), + WithPRConfig(cfg), + ) // Act - result, err := service.SummarizePR(ctx, prNumber, func(s string) {}) + result, err := service.SummarizePR(ctx, prNumber, func(e models.ProgressEvent) {}) // Assert assert.NoError(t, err) @@ -67,22 +69,24 @@ func TestPRService_SummarizePR_GetPRError(t *testing.T) { mockVCS := new(MockVCSClient) mockAI := new(MockPRSummarizer) - trans, err := i18n.NewTranslations("es", "../i18n/locales") cfg := &config.Config{} - require.NoError(t, err) expectedError := errors.New("API error") mockVCS.On("GetPR", ctx, prNumber).Return(models.PRData{}, expectedError) - service := NewPRService(mockVCS, mockAI, trans, cfg) + service := NewPRService( + WithPRVCSClient(mockVCS), + WithPRAIProvider(mockAI), + WithPRConfig(cfg), + ) // act - _, err = service.SummarizePR(ctx, prNumber, func(s string) {}) + _, err := service.SummarizePR(ctx, prNumber, func(e models.ProgressEvent) {}) // assert assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al obtener el PR") + assert.Contains(t, err.Error(), "VCS: error getting PR") mockVCS.AssertExpectations(t) mockAI.AssertNotCalled(t, "GeneratePRSummary") } @@ -94,8 +98,6 @@ func TestPRService_SummarizePR_GenerateError(t *testing.T) { mockVCS := new(MockVCSClient) mockAI := new(MockPRSummarizer) cfg := &config.Config{} - trans, err := i18n.NewTranslations("es", "../i18n/locales") - require.NoError(t, err) prData := models.PRData{ ID: 123, @@ -110,14 +112,18 @@ func TestPRService_SummarizePR_GenerateError(t *testing.T) { mockVCS.On("GetPRIssues", ctx, mock.Anything, mock.Anything, mock.Anything).Return([]models.Issue(nil), nil) mockAI.On("GeneratePRSummary", ctx, mock.Anything).Return(models.PRSummary{}, expectedError) - service := NewPRService(mockVCS, mockAI, trans, cfg) + service := NewPRService( + WithPRVCSClient(mockVCS), + WithPRAIProvider(mockAI), + WithPRConfig(cfg), + ) // Act - _, err = service.SummarizePR(ctx, prNumber, func(s string) {}) + _, err := service.SummarizePR(ctx, prNumber, func(e models.ProgressEvent) {}) // Assert assert.Error(t, err) - assert.Contains(t, err.Error(), "Error al crear el resumen del PR") + assert.Contains(t, err.Error(), "AI: error generating PR summary") mockVCS.AssertExpectations(t) mockAI.AssertExpectations(t) mockVCS.AssertNotCalled(t, "UpdatePR") @@ -129,9 +135,7 @@ func TestPRService_SummarizePR_UpdateError(t *testing.T) { mockVCS := new(MockVCSClient) mockAI := new(MockPRSummarizer) - trans, err := i18n.NewTranslations("es", "../i18n/locales") cfg := &config.Config{} - require.NoError(t, err) prData := models.PRData{ ID: 123, @@ -152,26 +156,30 @@ func TestPRService_SummarizePR_UpdateError(t *testing.T) { mockAI.On("GeneratePRSummary", ctx, mock.Anything).Return(summary, nil) mockVCS.On("UpdatePR", ctx, prNumber, summary).Return(expectedError) - service := NewPRService(mockVCS, mockAI, trans, cfg) + service := NewPRService( + WithPRVCSClient(mockVCS), + WithPRAIProvider(mockAI), + WithPRConfig(cfg), + ) - _, err = service.SummarizePR(ctx, prNumber, func(s string) {}) + _, err := service.SummarizePR(ctx, prNumber, func(e models.ProgressEvent) {}) - assert.ErrorContains(t, err, "Error al actualizar el PR: update failed") + assert.ErrorContains(t, err, "VCS: error updating PR") mockVCS.AssertExpectations(t) mockAI.AssertExpectations(t) } func TestPRService_SummarizePR_NilAIService(t *testing.T) { - trans, err := i18n.NewTranslations("es", "../i18n/locales") - require.NoError(t, err) cfg := &config.Config{} - service := NewPRService(nil, nil, trans, cfg) + service := NewPRService( + WithPRConfig(cfg), + ) - summary, err := service.SummarizePR(context.Background(), 1, func(msg string) {}) + summary, err := service.SummarizePR(context.Background(), 1, func(e models.ProgressEvent) {}) assert.Error(t, err) - assert.Contains(t, err.Error(), "La IA no está configurada") + assert.ErrorIs(t, err, domainErrors.ErrAPIKeyMissing) assert.Empty(t, summary.Title) } @@ -181,8 +189,6 @@ func TestPRService_SummarizePR_WithRelatedIssues(t *testing.T) { mockVCS := new(MockVCSClient) mockAI := new(MockPRSummarizer) - trans, err := i18n.NewTranslations("es", "../i18n/locales") - require.NoError(t, err) cfg := &config.Config{Language: "es"} prData := models.PRData{ @@ -216,9 +222,13 @@ func TestPRService_SummarizePR_WithRelatedIssues(t *testing.T) { return contextContains(s.Body, "Summary content", "Fixes #123", "Fixes #456", "Fixes #789", "## Test Plan") })).Return(nil) - service := NewPRService(mockVCS, mockAI, trans, cfg) + service := NewPRService( + WithPRVCSClient(mockVCS), + WithPRAIProvider(mockAI), + WithPRConfig(cfg), + ) - _, err = service.SummarizePR(ctx, prNumber, func(s string) {}) + _, err := service.SummarizePR(ctx, prNumber, func(e models.ProgressEvent) {}) assert.NoError(t, err) mockVCS.AssertExpectations(t) @@ -231,8 +241,6 @@ func TestPRService_SummarizePR_BreakingChanges(t *testing.T) { mockVCS := new(MockVCSClient) mockAI := new(MockPRSummarizer) - trans, err := i18n.NewTranslations("es", "../i18n/locales") - require.NoError(t, err) cfg := &config.Config{} prData := models.PRData{ @@ -246,16 +254,20 @@ func TestPRService_SummarizePR_BreakingChanges(t *testing.T) { mockVCS.On("GetPRIssues", ctx, mock.Anything, mock.Anything, mock.Anything).Return([]models.Issue(nil), nil) mockAI.On("GeneratePRSummary", ctx, mock.MatchedBy(func(prompt string) bool { - return contextContains(prompt, "Breaking Changes detectados", "feat!: breaking change here") + return contextContains(prompt, "⚠️ Breaking Changes:", "feat!: breaking change here") })).Return(expectedSummary, nil) mockVCS.On("UpdatePR", ctx, prNumber, mock.MatchedBy(func(s models.PRSummary) bool { - return contextContains(s.Body, "## ⚠️ Breaking Changes", "- feat!: breaking change here") + return contextContains(s.Body, "Breaking Changes", "feat!: breaking change here") })).Return(nil) - service := NewPRService(mockVCS, mockAI, trans, cfg) + service := NewPRService( + WithPRVCSClient(mockVCS), + WithPRAIProvider(mockAI), + WithPRConfig(cfg), + ) - _, err = service.SummarizePR(ctx, prNumber, func(s string) {}) + _, err := service.SummarizePR(ctx, prNumber, func(e models.ProgressEvent) {}) assert.NoError(t, err) mockVCS.AssertExpectations(t) @@ -292,16 +304,13 @@ func TestBuildPRPrompt(t *testing.T) { expected := `PR #456 by dev123 Branch: -📊 **Métricas:** -- 2 commits -- 1 archivos cambiados -- ~0 líneas en diff +Stats: 2 commits, 1 files, ~0 lines Commits: - feat: add new API - docs: update readme -Archivos principales modificados: +Main files modified: - api.go Changes (diff completo): @@ -338,14 +347,10 @@ func setupTestConfig(t *testing.T) TestConfig { func setupServices(t *testing.T, testConfig TestConfig) (*PRService, error) { t.Helper() - trans, err := i18n.NewTranslations("es", "../i18n/locales/") - require.NoError(t, err) - githubClient := github.NewGitHubClient( testConfig.GithubOwner, testConfig.GithubRepo, testConfig.GithubToken, - trans, ) cfg := &config.Config{ @@ -366,12 +371,16 @@ func setupServices(t *testing.T, testConfig TestConfig) (*PRService, error) { } ctx := context.Background() - geminiSummarizer, err := gemini.NewGeminiPRSummarizer(ctx, cfg, trans) + geminiSummarizer, err := gemini.NewGeminiPRSummarizer(ctx, cfg, nil) if err != nil { return nil, err } - prService := NewPRService(githubClient, geminiSummarizer, trans, cfg) + prService := NewPRService( + WithPRVCSClient(githubClient), + WithPRAIProvider(geminiSummarizer), + WithPRConfig(cfg), + ) return prService, nil } @@ -388,8 +397,8 @@ func TestPRService_SummarizePR_Integration(t *testing.T) { t.Run("should successfully summarize a real PR", func(t *testing.T) { ctx := context.Background() - summary, err := prService.SummarizePR(ctx, testConfig.PRNumber, func(s string) { - t.Log(s) + summary, err := prService.SummarizePR(ctx, testConfig.PRNumber, func(e models.ProgressEvent) { + t.Logf("Progress: %v", e) }) require.NoError(t, err) diff --git a/internal/services/release_service.go b/internal/services/release_service.go index 53a2340..33f06e7 100644 --- a/internal/services/release_service.go +++ b/internal/services/release_service.go @@ -9,9 +9,9 @@ import ( "strings" "github.com/Tomas-vilte/MateCommit/internal/config" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" "github.com/Tomas-vilte/MateCommit/internal/domain/models" "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/Tomas-vilte/MateCommit/internal/infrastructure/dependency" ) @@ -21,55 +21,84 @@ var ( versionRegex = regexp.MustCompile(`v?(\d+)\.(\d+)\.(\d+)`) ) -var _ ports.ReleaseService = (*ReleaseService)(nil) +// releaseGitService defines only the methods needed by ReleaseService. +type releaseGitService interface { + GetLastTag(ctx context.Context) (string, error) + GetCommitCount(ctx context.Context) (int, error) + GetCommitsSinceTag(ctx context.Context, tag string) ([]models.Commit, error) + CreateTag(ctx context.Context, version, message string) error + PushTag(ctx context.Context, version string) error + GetTagDate(ctx context.Context, version string) (string, error) + AddFileToStaging(ctx context.Context, file string) error + HasStagedChanges(ctx context.Context) bool + CreateCommit(ctx context.Context, message string) error + Push(ctx context.Context) error + GetRepoInfo(ctx context.Context) (string, string, string, error) +} type ReleaseService struct { - git ports.GitService + git releaseGitService vcsClient ports.VCSClient notesGen ports.ReleaseNotesGenerator - trans *i18n.Translations depAnalyzer *dependency.AnalyzerRegistry config *config.Config } +type ReleaseOption func(*ReleaseService) + +func WithReleaseVCSClient(vcs ports.VCSClient) ReleaseOption { + return func(s *ReleaseService) { + s.vcsClient = vcs + } +} + +func WithReleaseNotesGenerator(rng ports.ReleaseNotesGenerator) ReleaseOption { + return func(s *ReleaseService) { + s.notesGen = rng + } +} + +func WithReleaseConfig(cfg *config.Config) ReleaseOption { + return func(s *ReleaseService) { + s.config = cfg + } +} + func NewReleaseService( - git ports.GitService, - vcsClient ports.VCSClient, - notesGen ports.ReleaseNotesGenerator, - trans *i18n.Translations, - config *config.Config, + gitSvc releaseGitService, + opts ...ReleaseOption, ) *ReleaseService { - return &ReleaseService{ - git: git, - vcsClient: vcsClient, - notesGen: notesGen, - trans: trans, + s := &ReleaseService{ + git: gitSvc, depAnalyzer: dependency.NewAnalyzerRegistry(), - config: config, } + for _, opt := range opts { + opt(s) + } + return s } func (s *ReleaseService) AnalyzeNextRelease(ctx context.Context) (*models.Release, error) { lastTag, err := s.git.GetLastTag(ctx) if err != nil { - return nil, fmt.Errorf("error al obtener ultimo tag: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeGit, "error getting last tag", err) } if lastTag == "" { count, _ := s.git.GetCommitCount(ctx) if count == 0 { - return nil, fmt.Errorf("no hay commits en el repositorio") + return nil, domainErrors.NewAppError(domainErrors.TypeGit, "no commits found in repository", nil) } lastTag = "v0.0.0" } commits, err := s.git.GetCommitsSinceTag(ctx, lastTag) if err != nil { - return nil, fmt.Errorf("error al obtener commits: %w", err) + return nil, domainErrors.NewAppError(domainErrors.TypeGit, "error getting commits", err) } if len(commits) == 0 { - return nil, fmt.Errorf("no hay commits nuevos desde %s", lastTag) + return nil, domainErrors.ErrNoChanges } release := &models.Release{ @@ -96,7 +125,7 @@ func (s *ReleaseService) GenerateReleaseNotes(ctx context.Context, release *mode func (s *ReleaseService) PublishRelease(ctx context.Context, release *models.Release, notes *models.ReleaseNotes, draft bool, buildBinaries bool) error { if s.vcsClient == nil { - return fmt.Errorf("cliente VCS no configurado. Configura un proveedor VCS con 'matecommit config set-vcsClient'") + return domainErrors.ErrConfigMissing } return s.vcsClient.CreateRelease(ctx, release, notes, draft, buildBinaries) @@ -112,27 +141,21 @@ func (s *ReleaseService) PushTag(ctx context.Context, version string) error { func (s *ReleaseService) GetRelease(ctx context.Context, version string) (*models.VCSRelease, error) { if s.vcsClient == nil { - return nil, fmt.Errorf("%s", s.trans.GetMessage("error.vcs_provider_not_configured", 0, map[string]interface{}{ - "Provider": "VCS", - })) + return nil, domainErrors.ErrVCSNotSupported } return s.vcsClient.GetRelease(ctx, version) } func (s *ReleaseService) UpdateRelease(ctx context.Context, version, body string) error { if s.vcsClient == nil { - return fmt.Errorf("%s", s.trans.GetMessage("error.vcs_provider_not_configured", 0, map[string]interface{}{ - "Provider": "VCS", - })) + return domainErrors.ErrConfigMissing } return s.vcsClient.UpdateRelease(ctx, version, body) } func (s *ReleaseService) EnrichReleaseContext(ctx context.Context, release *models.Release) error { if s.vcsClient == nil { - return fmt.Errorf("%s", s.trans.GetMessage("error.vcs_provider_not_configured", 0, map[string]interface{}{ - "Provider": "VCS", - })) + return domainErrors.ErrConfigMissing } if issues, err := s.vcsClient.GetClosedIssuesBetweenTags(ctx, release.PreviousVersion, release.Version); err == nil { @@ -164,8 +187,6 @@ func (s *ReleaseService) UpdateLocalChangelog(release *models.Release, notes *mo newContent := s.buildChangelogFromNotes(context.Background(), release, notes) - fmt.Println(s.trans.GetMessage("release.changelog_update_started", 0, nil)) - return s.prependToChangelog(changelogFile, newContent) } @@ -216,7 +237,7 @@ func (s *ReleaseService) analyzeDependencyChanges(ctx context.Context, release * return s.depAnalyzer.AnalyzeAll(ctx, s.vcsClient, release.PreviousVersion, release.Version) } -// categorizeCommits categoriza los commits según conventional commits +// categorizeCommits categorizes commits according to conventional commits func (s *ReleaseService) categorizeCommits(release *models.Release) { for _, commit := range release.AllCommits { msg := commit.Message @@ -280,7 +301,7 @@ func (s *ReleaseService) categorizeCommits(release *models.Release) { } } -// calculateVersion calcula la nueva versión basándose en semantic versioning +// 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) @@ -299,12 +320,12 @@ func (s *ReleaseService) calculateVersion(currentTag string, release *models.Rel patch = 0 bump = models.MajorBump } else if len(release.Features) > 0 { - // MINOR: nuevas features + // MINOR: new features minor++ patch = 0 bump = models.MinorBump } else if len(release.BugFixes) > 0 || len(release.Improvements) > 0 { - // PATCH: bug fixes o mejoras + // PATCH: bug fixes or improvements patch++ bump = models.PatchBump } @@ -333,7 +354,7 @@ func (s *ReleaseService) generateBasicNotes(release *models.Release) *models.Rel } } -// buildChangelogFromNotes formatea el registro de cambios utilizando destacados generados por IA +// buildChangelogFromNotes formats the changelog using AI-generated highlights func (s *ReleaseService) buildChangelogFromNotes(ctx context.Context, release *models.Release, notes *models.ReleaseNotes) string { var sb strings.Builder @@ -384,7 +405,7 @@ func (s *ReleaseService) buildChangelogFromNotes(ctx context.Context, release *m return sb.String() } -// buildChangelog formatea el registro de cambios a partir de confirmaciones sin procesar (respaldo cuando la IA no está disponible) +// buildChangelog formats the changelog from raw commits (fallback when AI is not available) func (s *ReleaseService) buildChangelog(release *models.Release) string { var sb strings.Builder @@ -458,18 +479,17 @@ func (s *ReleaseService) CommitChangelog(ctx context.Context, version string) er if _, err := os.Stat(mainGoFile); err == nil { if err := s.git.AddFileToStaging(ctx, mainGoFile); err != nil { - return fmt.Errorf("error haciendo git add de archivo de versión (%s): %w", mainGoFile, err) + return domainErrors.NewAppError(domainErrors.TypeGit, fmt.Sprintf("failed to add version file to staging: %s", mainGoFile), err) } } if !s.git.HasStagedChanges(ctx) { - fmt.Println(s.trans.GetMessage("release.commit_no_staged", 0, nil)) - return nil + return domainErrors.ErrNoChanges } message := fmt.Sprintf("chore: update changelog and bump version to %s", version) if err := s.git.CreateCommit(ctx, message); err != nil { - return fmt.Errorf("%s", s.trans.GetMessage("release.error_committing_changelog", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeGit, "failed to commit changelog and version bump", err) } return nil } @@ -494,17 +514,17 @@ func (s *ReleaseService) UpdateAppVersion(version string) error { content, err := os.ReadFile(mainGoFile) if err != nil { - return fmt.Errorf("error leyendo archivo de versión (%s): %w", mainGoFile, err) + return domainErrors.NewAppError(domainErrors.TypeInternal, fmt.Sprintf("failed to read version file: %s", mainGoFile), err) } re, err := regexp.Compile(versionPattern) if err != nil { - return fmt.Errorf("patrón de versión inválido (%s): %w", versionPattern, err) + return domainErrors.NewAppError(domainErrors.TypeInternal, fmt.Sprintf("invalid version pattern: %s", versionPattern), err) } currentContent := string(content) if !re.MatchString(currentContent) { - return fmt.Errorf("no se encontró el patrón de versión en %s", mainGoFile) + return domainErrors.NewAppError(domainErrors.TypeInternal, fmt.Sprintf("version pattern not found in %s", mainGoFile), nil) } match := re.FindString(currentContent) @@ -513,7 +533,7 @@ func (s *ReleaseService) UpdateAppVersion(version string) error { valMatch := valueRe.FindStringIndex(match) if valMatch == nil { - return fmt.Errorf("no se encontró una cadena entre comillas en el patrón coincidente") + return domainErrors.NewAppError(domainErrors.TypeInternal, "could not find quoted string in matching pattern", nil) } cleanVersion := strings.TrimPrefix(version, "v") @@ -522,7 +542,7 @@ func (s *ReleaseService) UpdateAppVersion(version string) error { newContent := strings.Replace(currentContent, match, newMatch, 1) if err := os.WriteFile(mainGoFile, []byte(newContent), 0644); err != nil { - return fmt.Errorf("error escribiendo archivo de versión: %w", err) + return domainErrors.NewAppError(domainErrors.TypeInternal, fmt.Sprintf("failed to write version file: %s", mainGoFile), err) } return nil diff --git a/internal/services/release_service_test.go b/internal/services/release_service_test.go index 5d4242c..e249819 100644 --- a/internal/services/release_service_test.go +++ b/internal/services/release_service_test.go @@ -10,7 +10,6 @@ import ( "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" @@ -21,7 +20,7 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { mockGit := new(MockGitService) mockVCS := new(MockVCSClient) mockNotesGen := new(MockReleaseNotesGenerator) - service := NewReleaseService(mockGit, mockVCS, mockNotesGen, nil, nil) + service := NewReleaseService(mockGit, WithReleaseVCSClient(mockVCS), WithReleaseNotesGenerator(mockNotesGen)) mockGit.On("GetLastTag", mock.Anything).Return("v1.0.0", nil) mockGit.On("GetCommitsSinceTag", mock.Anything, "v1.0.0").Return([]models.Commit{ @@ -44,7 +43,7 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { t.Run("Success with breaking change", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("GetLastTag", mock.Anything).Return("v1.0.0", nil) mockGit.On("GetCommitsSinceTag", mock.Anything, "v1.0.0").Return([]models.Commit{ @@ -62,7 +61,7 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { t.Run("Success with no previous tags (initial release)", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("GetLastTag", mock.Anything).Return("", nil) mockGit.On("GetCommitCount", mock.Anything).Return(1, nil) @@ -81,7 +80,7 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { t.Run("Error getting last tag", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("GetLastTag", mock.Anything).Return("", errors.New("git error")) @@ -89,14 +88,14 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { assert.Error(t, err) assert.Nil(t, release) - assert.Contains(t, err.Error(), "error al obtener ultimo tag") + assert.Contains(t, err.Error(), "GIT: error getting last tag") mockGit.AssertExpectations(t) }) t.Run("No changes since last tag", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("GetLastTag", mock.Anything).Return("v1.0.0", nil) mockGit.On("GetCommitsSinceTag", mock.Anything, "v1.0.0").Return([]models.Commit{}, nil) @@ -105,14 +104,14 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { assert.Error(t, err) assert.Nil(t, release) - assert.Contains(t, err.Error(), "no hay commits nuevos") + assert.Contains(t, err.Error(), "GIT: no staged changes detected") mockGit.AssertExpectations(t) }) t.Run("Error getting commits", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("GetLastTag", mock.Anything).Return("v1.0.0", nil) mockGit.On("GetCommitsSinceTag", mock.Anything, "v1.0.0").Return(nil, errors.New("git error")) @@ -121,14 +120,14 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { assert.Error(t, err) assert.Nil(t, release) - assert.Contains(t, err.Error(), "error al obtener commits") + assert.Contains(t, err.Error(), "GIT: error getting commits") mockGit.AssertExpectations(t) }) t.Run("Empty repository", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("GetLastTag", mock.Anything).Return("", nil) mockGit.On("GetCommitCount", mock.Anything).Return(0, nil) @@ -137,7 +136,7 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { assert.Error(t, err) assert.Nil(t, release) - assert.Contains(t, err.Error(), "no hay commits en el repositorio") + assert.Contains(t, err.Error(), "GIT: no commits found in repository") mockGit.AssertExpectations(t) }) @@ -146,7 +145,7 @@ func TestReleaseService_AnalyzeNextRelease(t *testing.T) { func TestReleaseService_GenerateReleaseNotes(t *testing.T) { t.Run("Use AI generator when available", func(t *testing.T) { mockNotesGen := new(MockReleaseNotesGenerator) - service := NewReleaseService(nil, nil, mockNotesGen, nil, nil) + service := NewReleaseService(nil, WithReleaseNotesGenerator(mockNotesGen)) release := &models.Release{Version: "v1.1.0"} expectedNotes := &models.ReleaseNotes{ @@ -164,7 +163,7 @@ func TestReleaseService_GenerateReleaseNotes(t *testing.T) { }) t.Run("Fallback to basic notes when generator is nil", func(t *testing.T) { - service := NewReleaseService(nil, nil, nil, nil, nil) // No generator + service := NewReleaseService(nil) // No generator release := &models.Release{ Version: "v1.1.0", @@ -189,7 +188,7 @@ func TestReleaseService_GenerateReleaseNotes(t *testing.T) { t.Run("Error from generator", func(t *testing.T) { mockNotesGen := new(MockReleaseNotesGenerator) - service := NewReleaseService(nil, nil, mockNotesGen, nil, nil) + service := NewReleaseService(nil, WithReleaseNotesGenerator(mockNotesGen)) release := &models.Release{Version: "v1.1.0"} @@ -207,7 +206,7 @@ func TestReleaseService_GenerateReleaseNotes(t *testing.T) { func TestReleaseService_GetRelease(t *testing.T) { t.Run("should get release successfully", func(t *testing.T) { mockVCS := new(MockVCSClient) - service := NewReleaseService(nil, mockVCS, nil, nil, nil) + service := NewReleaseService(nil, WithReleaseVCSClient(mockVCS)) expectedRelease := &models.VCSRelease{ TagName: "v1.2.0", @@ -228,11 +227,7 @@ func TestReleaseService_GetRelease(t *testing.T) { }) t.Run("should return error if VCS client not configured", func(t *testing.T) { - trans, err := i18n.NewTranslations("en", "../i18n/locales") - if err != nil { - trans, _ = i18n.NewTranslations("en", "../../i18n/locales") - } - service := NewReleaseService(nil, nil, nil, trans, nil) + service := NewReleaseService(nil) release, err := service.GetRelease(context.Background(), "v1.2.0") @@ -242,7 +237,7 @@ func TestReleaseService_GetRelease(t *testing.T) { t.Run("should propagate VCS client error", func(t *testing.T) { mockVCS := new(MockVCSClient) - service := NewReleaseService(nil, mockVCS, nil, nil, nil) + service := NewReleaseService(nil, WithReleaseVCSClient(mockVCS)) mockVCS.On("GetRelease", mock.Anything, "v1.2.0").Return((*models.VCSRelease)(nil), errors.New("release not found")) @@ -258,7 +253,7 @@ func TestReleaseService_GetRelease(t *testing.T) { func TestReleaseService_UpdateRelease(t *testing.T) { t.Run("should update release successfully", func(t *testing.T) { mockVCS := new(MockVCSClient) - service := NewReleaseService(nil, mockVCS, nil, nil, nil) + service := NewReleaseService(nil, WithReleaseVCSClient(mockVCS)) mockVCS.On("UpdateRelease", mock.Anything, "v1.2.0", "Updated release notes").Return(nil) @@ -269,20 +264,16 @@ func TestReleaseService_UpdateRelease(t *testing.T) { }) t.Run("should return error if VCS client not configured", func(t *testing.T) { - trans, err := i18n.NewTranslations("en", "../i18n/locales") - if err != nil { - trans, _ = i18n.NewTranslations("en", "../../i18n/locales") - } - service := NewReleaseService(nil, nil, nil, trans, nil) + service := NewReleaseService(nil) - err = service.UpdateRelease(context.Background(), "v1.2.0", "Updated notes") + err := service.UpdateRelease(context.Background(), "v1.2.0", "Updated notes") assert.Error(t, err) }) t.Run("should propagate VCS client error", func(t *testing.T) { mockVCS := new(MockVCSClient) - service := NewReleaseService(nil, mockVCS, nil, nil, nil) + service := NewReleaseService(nil, WithReleaseVCSClient(mockVCS)) mockVCS.On("UpdateRelease", mock.Anything, "v1.2.0", "Updated notes").Return(errors.New("update failed")) @@ -297,7 +288,7 @@ func TestReleaseService_UpdateRelease(t *testing.T) { func TestReleaseService_PublishRelease(t *testing.T) { t.Run("should publish release successfully", func(t *testing.T) { mockVCS := new(MockVCSClient) - service := NewReleaseService(nil, mockVCS, nil, nil, nil) + service := NewReleaseService(nil, WithReleaseVCSClient(mockVCS)) release := &models.Release{Version: "v1.0.0"} notes := &models.ReleaseNotes{Title: "Release v1.0.0"} @@ -311,7 +302,7 @@ func TestReleaseService_PublishRelease(t *testing.T) { }) t.Run("should return error if VCS client not configured", func(t *testing.T) { - service := NewReleaseService(nil, nil, nil, nil, nil) + service := NewReleaseService(nil) release := &models.Release{Version: "v1.0.0"} notes := &models.ReleaseNotes{} @@ -319,12 +310,12 @@ func TestReleaseService_PublishRelease(t *testing.T) { err := service.PublishRelease(context.Background(), release, notes, false, true) assert.Error(t, err) - assert.Contains(t, err.Error(), "cliente VCS no configurado") + assert.Contains(t, err.Error(), "CONFIGURATION: configuration is missing") }) t.Run("should propagate VCS client error", func(t *testing.T) { mockVCS := new(MockVCSClient) - service := NewReleaseService(nil, mockVCS, nil, nil, nil) + service := NewReleaseService(nil, WithReleaseVCSClient(mockVCS)) release := &models.Release{Version: "v1.0.0"} notes := &models.ReleaseNotes{} @@ -342,7 +333,7 @@ func TestReleaseService_PublishRelease(t *testing.T) { func TestReleaseService_CreateTag(t *testing.T) { t.Run("should create tag successfully", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("CreateTag", mock.Anything, "v1.0.0", "Release v1.0.0").Return(nil) @@ -354,7 +345,7 @@ func TestReleaseService_CreateTag(t *testing.T) { t.Run("should propagate git error", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("CreateTag", mock.Anything, "v1.0.0", "Release v1.0.0").Return(errors.New("tag already exists")) @@ -369,7 +360,7 @@ func TestReleaseService_CreateTag(t *testing.T) { func TestReleaseService_PushTag(t *testing.T) { t.Run("should push tag successfully", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("PushTag", mock.Anything, "v1.0.0").Return(nil) @@ -381,7 +372,7 @@ func TestReleaseService_PushTag(t *testing.T) { t.Run("should propagate git error", func(t *testing.T) { mockGit := new(MockGitService) - service := NewReleaseService(mockGit, nil, nil, nil, nil) + service := NewReleaseService(mockGit) mockGit.On("PushTag", mock.Anything, "v1.0.0").Return(errors.New("push failed")) @@ -396,7 +387,7 @@ func TestReleaseService_PushTag(t *testing.T) { func TestReleaseService_EnrichReleaseContext(t *testing.T) { t.Run("should enrich release context successfully", func(t *testing.T) { mockVCS := new(MockVCSClient) - service := NewReleaseService(nil, mockVCS, nil, nil, nil) + service := NewReleaseService(nil, WithReleaseVCSClient(mockVCS)) release := &models.Release{ PreviousVersion: "v1.0.0", @@ -425,22 +416,18 @@ func TestReleaseService_EnrichReleaseContext(t *testing.T) { }) t.Run("should return error if VCS client not configured", func(t *testing.T) { - trans, err := i18n.NewTranslations("en", "../i18n/locales") - if err != nil { - trans, _ = i18n.NewTranslations("en", "../../i18n/locales") - } - service := NewReleaseService(nil, nil, nil, trans, nil) + service := NewReleaseService(nil) release := &models.Release{} - err = service.EnrichReleaseContext(context.Background(), release) + err := service.EnrichReleaseContext(context.Background(), release) assert.Error(t, err) }) t.Run("should continue even if some enrichments fail", func(t *testing.T) { mockVCS := new(MockVCSClient) - service := NewReleaseService(nil, mockVCS, nil, nil, nil) + service := NewReleaseService(nil, WithReleaseVCSClient(mockVCS)) release := &models.Release{ PreviousVersion: "v1.0.0", @@ -489,7 +476,7 @@ var ( VersionFile: mainGoPath, } - service := NewReleaseService(nil, nil, nil, nil, cfg) + service := NewReleaseService(nil, WithReleaseConfig(cfg)) err = service.UpdateAppVersion("v1.1.0") assert.NoError(t, err) @@ -515,7 +502,7 @@ const CurrentVersion = "0.0.1" VersionPattern: `CurrentVersion\s*=\s*".*"`, } - service := NewReleaseService(nil, nil, nil, nil, cfg) + service := NewReleaseService(nil, WithReleaseConfig(cfg)) err = service.UpdateAppVersion("v0.0.2") assert.NoError(t, err) @@ -533,7 +520,7 @@ const CurrentVersion = "0.0.1" require.NoError(t, err) cfg := &config.Config{VersionFile: versionFile} - service := NewReleaseService(nil, nil, nil, nil, cfg) + service := NewReleaseService(nil, WithReleaseConfig(cfg)) err = service.UpdateAppVersion("v1.0.0") assert.Error(t, err) diff --git a/internal/services/routing/model_selector.go b/internal/services/routing/model_selector.go index 271aeea..a081216 100644 --- a/internal/services/routing/model_selector.go +++ b/internal/services/routing/model_selector.go @@ -6,15 +6,15 @@ func NewModelSelector() *ModelSelector { return &ModelSelector{} } -// SelectBestModel selecciona el modelo óptimo basado en la operación y cantidad de tokens +// SelectBestModel selects the optimal model based on the operation and token count // -// Estrategia de Smart Routing: -// - Operaciones pequeñas (< 1k tokens): Flash-Lite (más económico) -// - Operaciones medianas (1k-10k tokens): Flash (balance costo/calidad) -// - Operaciones grandes (> 10k tokens): 3.0 Flash (mejor contexto, evita alucinaciones) -// - Releases/Issues: 3.0 Flash (máxima calidad de redacción) +// Smart Routing Strategy: +// - Small operations (< 1k tokens): Flash-Lite (most economical) +// - Medium operations (1k-10k tokens): Flash (balance cost/quality) +// - Large operations (> 10k tokens): 3.0 Flash (better context, avoids hallucinations) +// - Releases/Issues: 3.0 Flash (maximum writing quality) // -// SelectBestModel selecciona el modelo óptimo basado en la operación y cantidad de tokens +// SelectBestModel selects the optimal model based on the operation and token count func (m *ModelSelector) SelectBestModel(operation string, estimatedTokens int) string { if operation == "generate-release" || operation == "generate-issue" { return "gemini-3-pro-preview" @@ -27,7 +27,7 @@ func (m *ModelSelector) SelectBestModel(operation string, estimatedTokens int) s return "gemini-2.5-flash" } -// GetRationale retorna la clave de traducción que explica por qué se eligió un modelo +// GetRationale returns the translation key that explains why a model was chosen func (m *ModelSelector) GetRationale(selectedModel string) string { switch selectedModel { case "gemini-1.5-flash": diff --git a/internal/services/version_checker.go b/internal/services/version_checker.go index 096b61f..3e17bf8 100644 --- a/internal/services/version_checker.go +++ b/internal/services/version_checker.go @@ -6,6 +6,7 @@ import ( "compress/gzip" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -16,15 +17,14 @@ import ( "strings" "time" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/fatih/color" "github.com/google/go-github/v80/github" "golang.org/x/mod/semver" + + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" ) type VersionUpdater struct { currentVersion string - trans *i18n.Translations } type UpdateCache struct { @@ -32,24 +32,33 @@ type UpdateCache struct { LatestKnown string `json:"latest_known"` } -func NewVersionUpdater(version string, trans *i18n.Translations) *VersionUpdater { - return &VersionUpdater{ - currentVersion: version, - trans: trans, +type VersionOption func(*VersionUpdater) + +func WithCurrentVersion(version string) VersionOption { + return func(v *VersionUpdater) { + v.currentVersion = version + } +} + +func NewVersionUpdater(opts ...VersionOption) *VersionUpdater { + v := &VersionUpdater{} + for _, opt := range opts { + opt(v) } + return v } -func (v *VersionUpdater) CheckForUpdates(ctx context.Context) { +func (v *VersionUpdater) CheckForUpdates(ctx context.Context) (string, bool) { if os.Getenv("MATECOMMIT_DISABLE_UPDATE_CHECK") != "" { - return + return "", false } cache, err := v.loadCache() if err == nil && time.Since(cache.LastCheck) < 24*time.Hour { if cache.LatestKnown != "" && v.isUpdateAvailable(cache.LatestKnown) { - v.printUpdateNotification(cache.LatestKnown) + return cache.LatestKnown, true } - return + return "", false } client := github.NewClient(nil) @@ -58,7 +67,7 @@ func (v *VersionUpdater) CheckForUpdates(ctx context.Context) { release, _, err := client.Repositories.GetLatestRelease(ctx, "Tomas-vilte", "MateCommit") if err != nil { - return + return "", false } latestVersion := release.GetTagName() @@ -68,24 +77,28 @@ func (v *VersionUpdater) CheckForUpdates(ctx context.Context) { LatestKnown: latestVersion, }) - if v.isUpdateAvailable(latestVersion) { - v.printUpdateNotification(latestVersion) - } + return latestVersion, v.isUpdateAvailable(latestVersion) } func (v *VersionUpdater) UpdateCLI(ctx context.Context) error { method := v.detectInstallMethod() + var err error switch method { case "go": - return v.updateViaGo(ctx) + err = v.updateViaGo(ctx) case "brew": - return v.updateViaBrew(ctx) + err = v.updateViaBrew(ctx) case "binary": - return v.updateViaBinary(ctx) + err = v.updateViaBinary(ctx) default: - return fmt.Errorf("%s", v.trans.GetMessage("update.method_not_detected", 0, nil)) + return domainErrors.ErrUpdateFailed } + + if err != nil { + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to update application", err) + } + return nil } func (v *VersionUpdater) detectInstallMethod() string { @@ -115,26 +128,26 @@ func (v *VersionUpdater) detectInstallMethod() string { func (v *VersionUpdater) updateViaGo(ctx context.Context) error { if _, err := exec.LookPath("go"); err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.go_not_found", 0, nil)) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "go command not found", err) } cmd := exec.CommandContext(ctx, "go", "install", "github.com/Tomas-vilte/MateCommit@latest") output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("%s: %s", v.trans.GetMessage("update.error", 0, nil), string(output)) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "go install failed", fmt.Errorf("%s", string(output))) } return nil } func (v *VersionUpdater) updateViaBrew(ctx context.Context) error { if _, err := exec.LookPath("brew"); err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.brew_not_found", 0, nil)) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "brew command not found", err) } cmd := exec.CommandContext(ctx, "brew", "upgrade", "matecommit") output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("%s: %s", v.trans.GetMessage("update.error", 0, nil), string(output)) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "brew upgrade failed", fmt.Errorf("%s", string(output))) } return nil } @@ -143,7 +156,7 @@ func (v *VersionUpdater) updateViaBinary(ctx context.Context) error { client := github.NewClient(nil) release, _, err := client.Repositories.GetLatestRelease(ctx, "Tomas-vilte", "MateCommit") if err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.get_latest", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to get latest release info", err) } latestVersion := release.GetTagName() @@ -158,7 +171,7 @@ func (v *VersionUpdater) updateViaBinary(ctx context.Context) error { arch, ok := archMap[goarch] if !ok { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.arch_not_supported", 0, map[string]interface{}{"Arch": goarch})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, fmt.Sprintf("architecture %s not supported", goarch), nil) } var assetName string @@ -170,7 +183,7 @@ func (v *VersionUpdater) updateViaBinary(ctx context.Context) error { case "windows": assetName = fmt.Sprintf("matecommit_%s_windows_%s.zip", strings.TrimPrefix(latestVersion, "v"), arch) default: - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.os_not_supported", 0, map[string]interface{}{"OS": goos})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, fmt.Sprintf("operating system %s not supported", goos), nil) } var assetURL string @@ -182,16 +195,12 @@ func (v *VersionUpdater) updateViaBinary(ctx context.Context) error { } if assetURL == "" { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.binary_not_found_manual", 0, map[string]interface{}{ - "OS": goos, - "Arch": goarch, - "URL": release.GetHTMLURL(), - })) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "binary not found for this platform", nil) } tmpDir, err := os.MkdirTemp("", "matecommit-update-*") if err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.create_temp", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to create temporary directory", err) } defer func() { if err := os.RemoveAll(tmpDir); err != nil { @@ -199,13 +208,9 @@ func (v *VersionUpdater) updateViaBinary(ctx context.Context) error { } }() - fmt.Println(v.trans.GetMessage("update.downloading", 0, map[string]interface{}{ - "Version": latestVersion, - })) - resp, err := http.Get(assetURL) if err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.download", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to download update binary", err) } defer func() { if err := resp.Body.Close(); err != nil { @@ -214,70 +219,62 @@ func (v *VersionUpdater) updateViaBinary(ctx context.Context) error { }() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.download_http", 0, map[string]interface{}{"StatusCode": resp.StatusCode})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, fmt.Sprintf("download failed: HTTP %d", resp.StatusCode), nil) } archivePath := filepath.Join(tmpDir, assetName) archiveFile, err := os.Create(archivePath) if err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.create_temp_file", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to create temporary archive file", err) } _, err = io.Copy(archiveFile, resp.Body) _ = archiveFile.Close() if err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.save_binary", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to save update binary", err) } - fmt.Println(v.trans.GetMessage("update.extracting", 0, nil)) - var binaryPath string if strings.HasSuffix(assetName, ".tar.gz") { binaryPath, err = v.extractTarGz(archivePath, tmpDir) } else if strings.HasSuffix(assetName, ".zip") { binaryPath, err = v.extractZip(archivePath, tmpDir) } else { - return fmt.Errorf("%s", v.trans.GetMessage("archive_format_not_supported", 0, map[string]interface{}{"Format": assetName})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, fmt.Sprintf("unsupported archive format: %s", assetName), nil) } if err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.extract", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to extract archive", err) } currentExec, err := os.Executable() if err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.get_executable", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to get current executable path", err) } currentExec, err = filepath.EvalSymlinks(currentExec) if err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.resolve_symlinks", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to resolve symlinks for executable", err) } backupPath := currentExec + ".backup" if err := os.Rename(currentExec, backupPath); err != nil { - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.create_backup", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to create backup of current executable", err) } - fmt.Println(v.trans.GetMessage("update.installing", 0, nil)) - if err := v.copyFile(binaryPath, currentExec); err != nil { _ = os.Rename(backupPath, currentExec) - return fmt.Errorf("%s", v.trans.GetMessage("update.errors.install_binary", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to install new binary", err) } if err := os.Chmod(currentExec, 0755); err != nil { _ = os.Remove(currentExec) _ = os.Rename(backupPath, currentExec) - return fmt.Errorf("%s", v.trans.GetMessage("update.error.chmod", 0, map[string]interface{}{"Error": err})) + return domainErrors.NewAppError(domainErrors.TypeUpdate, "failed to set executable permissions", err) } _ = os.Remove(backupPath) - fmt.Println(v.trans.GetMessage("update.success", 0, map[string]interface{}{ - "Version": latestVersion, - })) - return nil } @@ -297,41 +294,6 @@ func (v *VersionUpdater) isUpdateAvailable(latest string) bool { return semver.Compare(latest, current) > 0 } -func (v *VersionUpdater) printUpdateNotification(latest string) { - yellow := color.New(color.FgYellow, color.Bold).SprintFunc() - green := color.New(color.FgGreen, color.Bold).SprintFunc() - - boxTop := v.trans.GetMessage("update.box_top", 0, nil) - boxBottom := v.trans.GetMessage("update.box_bottom", 0, nil) - - msgAvailable := v.trans.GetMessage("update.available", 0, map[string]interface{}{ - "Current": v.currentVersion, - "Latest": green(latest), - }) - - method := v.detectInstallMethod() - var updateCmd string - switch method { - case "brew": - updateCmd = green("brew upgrade matecommit") - case "go": - updateCmd = green("go install github.com/Tomas-vilte/MateCommit@latest") - default: - updateCmd = green("matecommit update") - } - - msgCommand := v.trans.GetMessage("update.command", 0, map[string]interface{}{ - "Command": updateCmd, - }) - - fmt.Printf("\n%s\n", yellow(boxTop)) - fmt.Printf("%s %s\n", yellow("│"), yellow("│")) - fmt.Printf("%s %s %s\n", yellow("│"), msgAvailable, yellow("│")) - fmt.Printf("%s %s %s\n", yellow("│"), msgCommand, yellow("│")) - fmt.Printf("%s %s\n", yellow("│"), yellow("│")) - fmt.Printf("%s\n\n", yellow(boxBottom)) -} - func (v *VersionUpdater) getCacheDir() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { @@ -440,7 +402,7 @@ func (v *VersionUpdater) extractTarGz(archivePath, destDir string) (string, erro } if binaryPath == "" { - return "", fmt.Errorf("%s", v.trans.GetMessage("binary_not_found_archive", 0, nil)) + return "", errors.New("binary_not_found_archive") } return binaryPath, nil @@ -497,7 +459,7 @@ func (v *VersionUpdater) extractZip(archivePath, destDir string) (string, error) } if binaryPath == "" { - return "", fmt.Errorf("%s", v.trans.GetMessage("binary_not_found_archive", 0, nil)) + return "", errors.New("binary_not_found_archive") } return binaryPath, nil diff --git a/internal/services/version_checker_test.go b/internal/services/version_checker_test.go index 262e8c9..2078da6 100644 --- a/internal/services/version_checker_test.go +++ b/internal/services/version_checker_test.go @@ -10,13 +10,11 @@ import ( "testing" "time" - "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestIsUpdateAvailable(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") tests := []struct { name string @@ -82,7 +80,7 @@ func TestIsUpdateAvailable(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - updater := NewVersionUpdater(tt.current, trans) + updater := NewVersionUpdater(WithCurrentVersion(tt.current)) got := updater.isUpdateAvailable(tt.latest) assert.Equal(t, tt.expected, got) }) @@ -90,7 +88,6 @@ func TestIsUpdateAvailable(t *testing.T) { } func TestDetectInstallMethod(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") tests := []struct { name string @@ -119,7 +116,7 @@ func TestDetectInstallMethod(t *testing.T) { t.Setenv("GOBIN", tt.gobin) } - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) method := updater.detectInstallMethod() assert.Contains(t, []string{"go", "brew", "binary", "unknown"}, method) @@ -128,8 +125,7 @@ func TestDetectInstallMethod(t *testing.T) { } func TestDetectInstallMethod_Binary(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) t.Setenv("GOPATH", "") t.Setenv("GOBIN", "") @@ -141,20 +137,18 @@ func TestDetectInstallMethod_Binary(t *testing.T) { } func TestUpdateCLI(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) t.Run("calls appropriate method", func(t *testing.T) { t.Setenv("GOPATH", "") t.Setenv("GOBIN", "") err := updater.UpdateCLI(context.Background()) - assert.Error(t, err) + assert.ErrorContains(t, err, "UPDATE: failed to update application") }) } func TestCacheOperations(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) cache := UpdateCache{ LastCheck: time.Now(), @@ -175,8 +169,7 @@ func TestCacheOperations(t *testing.T) { } func TestCheckForUpdates_WithDisableEnvVar(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) t.Setenv("MATECOMMIT_DISABLE_UPDATE_CHECK", "1") @@ -187,8 +180,7 @@ func TestCheckForUpdates_WithDisableEnvVar(t *testing.T) { } func TestCheckForUpdates_WithCache(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) cache := UpdateCache{ LastCheck: time.Now().Add(-1 * time.Hour), @@ -208,16 +200,15 @@ func TestCheckForUpdates_WithCache(t *testing.T) { } func TestUpdateViaBinary_RealCallFails(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) err := updater.updateViaBinary(context.Background()) assert.Error(t, err) + assert.Contains(t, err.Error(), "binary not found for this platform") } func TestExtractZip(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) tmpDir := t.TempDir() zipPath := filepath.Join(tmpDir, "test.zip") @@ -250,8 +241,7 @@ func TestExtractZip(t *testing.T) { } func TestExtractTarGz(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) tmpDir := t.TempDir() tarPath := filepath.Join(tmpDir, "test.tar.gz") @@ -288,16 +278,14 @@ func TestExtractTarGz(t *testing.T) { assert.Equal(t, "dummy content", string(content)) } func TestVersionUpdater_IsUpdateAvailable_EdgeCases(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) assert.False(t, updater.isUpdateAvailable("v1.0.0-rc.1")) - assert.True(t, NewVersionUpdater("v1.0.0-rc.1", trans).isUpdateAvailable("v1.0.0")) + assert.True(t, NewVersionUpdater(WithCurrentVersion("v1.0.0-rc.1")).isUpdateAvailable("v1.0.0")) } func TestVersionUpdater_LoadCache_InvalidJSON(t *testing.T) { - trans, _ := i18n.NewTranslations("en", "") - updater := NewVersionUpdater("v1.0.0", trans) + updater := NewVersionUpdater(WithCurrentVersion("v1.0.0")) cacheDir, _ := updater.getCacheDir() _ = os.MkdirAll(cacheDir, 0755) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index aa8f4be..1163832 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -1,18 +1,22 @@ package ui import ( + "errors" "fmt" + "io" "os" "os/exec" "strings" "time" + domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" + "github.com/Tomas-vilte/MateCommit/internal/i18n" "github.com/briandowns/spinner" "github.com/fatih/color" ) var ( - // Colores para diferentes tipos de mensajes + // Colors for different message types Success = color.New(color.FgGreen, color.Bold) Error = color.New(color.FgRed, color.Bold) Warning = color.New(color.FgYellow, color.Bold) @@ -20,7 +24,7 @@ var ( Accent = color.New(color.FgMagenta, color.Bold) Dim = color.New(color.FgHiBlack) - // Emojis con colores + // Emojis with colors SuccessEmoji = Success.Sprint("✓") ErrorEmoji = Error.Sprint("✗") WarningEmoji = Warning.Sprint("⚠") @@ -32,12 +36,12 @@ var ( var activeSpinner *SmartSpinner var suspendedSpinner *SmartSpinner -// SmartSpinner es un spinner con capacidades mejoradas +// SmartSpinner is a spinner with enhanced capabilities type SmartSpinner struct { spinner *spinner.Spinner } -// NewSmartSpinner crea un nuevo spinner con mensaje inicial +// NewSmartSpinner creates a new spinner with an initial message func NewSmartSpinner(initialMessage string) *SmartSpinner { s := spinner.New( spinner.CharSets[14], @@ -48,13 +52,13 @@ func NewSmartSpinner(initialMessage string) *SmartSpinner { return &SmartSpinner{spinner: s} } -// Start inicia el spinner y lo registra como el spinner activo globalmente. +// Start starts the spinner and registers it as the globally active spinner. func (s *SmartSpinner) Start() { activeSpinner = s s.spinner.Start() } -// Stop detiene el spinner y limpia el registro del spinner activo. +// Stop stops the spinner and clears the active spinner record. func (s *SmartSpinner) Stop() { s.spinner.Stop() if activeSpinner == s { @@ -65,15 +69,15 @@ func (s *SmartSpinner) Stop() { } } -// StopActiveSpinner detiene el spinner que esté activo actualmente en la sesión de terminal. +// StopActiveSpinner stops the currently active spinner in the terminal session. func StopActiveSpinner() { if activeSpinner != nil { activeSpinner.Stop() } } -// SuspendActiveSpinner detiene temporalmente el spinner activo sin borrar su referencia, -// permitiendo que sea reanudado después de una interacción del usuario. +// SuspendActiveSpinner temporarily stops the active spinner without deleting its reference, +// allowing it to be resumed after user interaction. func SuspendActiveSpinner() { if activeSpinner != nil { suspendedSpinner = activeSpinner @@ -82,7 +86,7 @@ func SuspendActiveSpinner() { } } -// ResumeSuspendedSpinner reanuda el spinner que fue suspendido previamente. +// ResumeSuspendedSpinner resumes the previously suspended spinner. func ResumeSuspendedSpinner() { if suspendedSpinner != nil { activeSpinner = suspendedSpinner @@ -97,12 +101,12 @@ func (s *SmartSpinner) UpdateMessage(msg string) { func (s *SmartSpinner) Success(msg string) { s.Stop() - PrintSuccess(msg) + PrintSuccess(os.Stdout, msg) } func (s *SmartSpinner) Error(msg string) { s.Stop() - PrintError(msg) + PrintError(os.Stdout, msg) } func (s *SmartSpinner) Warning(msg string) { @@ -116,7 +120,7 @@ func (s *SmartSpinner) Log(msg string) { s.Start() } -// SpinnerBuilder permite construir spinners con configuración flexible +// SpinnerBuilder allows building spinners with flexible configuration type SpinnerBuilder struct { message string charset int @@ -124,7 +128,7 @@ type SpinnerBuilder struct { speed time.Duration } -// NewSpinner crea un nuevo builder para spinners +// NewSpinner creates a new spinner builder func NewSpinner() *SpinnerBuilder { return &SpinnerBuilder{ charset: 14, @@ -133,31 +137,31 @@ func NewSpinner() *SpinnerBuilder { } } -// WithMessage establece el mensaje del spinner +// WithMessage sets the spinner message func (b *SpinnerBuilder) WithMessage(msg string) *SpinnerBuilder { b.message = msg return b } -// WithColor establece el color del spinner +// WithColor sets the spinner color func (b *SpinnerBuilder) WithColor(color string) *SpinnerBuilder { b.color = color return b } -// WithSpeed establece la velocidad del spinner +// WithSpeed sets the spinner speed func (b *SpinnerBuilder) WithSpeed(speed time.Duration) *SpinnerBuilder { b.speed = speed return b } -// WithCharset establece el charset del spinner +// WithCharset sets the spinner charset func (b *SpinnerBuilder) WithCharset(charset int) *SpinnerBuilder { b.charset = charset return b } -// Build construye el SmartSpinner con la configuración especificada +// Build constructs the SmartSpinner with the specified configuration func (b *SpinnerBuilder) Build() *SmartSpinner { s := spinner.New( spinner.CharSets[b.charset], @@ -168,12 +172,12 @@ func (b *SpinnerBuilder) Build() *SmartSpinner { return &SmartSpinner{spinner: s} } -func PrintSuccess(msg string) { - fmt.Printf("%s %s\n", SuccessEmoji, Success.Sprint(msg)) +func PrintSuccess(w io.Writer, msg string) { + _, _ = fmt.Fprintf(w, "%s ✓ %s\n", color.GreenString("Success:"), msg) } -func PrintError(msg string) { - fmt.Printf("%s %s\n", ErrorEmoji, Error.Sprint(msg)) +func PrintError(w io.Writer, msg string) { + _, _ = fmt.Fprintf(w, "%s ❌ %s\n", color.RedString("Error:"), msg) } func PrintWarning(msg string) { @@ -197,8 +201,70 @@ func PrintDuration(msg string, duration time.Duration) { } func PrintErrorWithSuggestion(errMsg, suggestion string) { - PrintError(errMsg) - fmt.Printf("\n%s %s\n", Info.Sprint("💡"), suggestion) + PrintError(os.Stdout, errMsg) + if suggestion != "" { + fmt.Printf("\n%s %s\n", Info.Sprint("💡"), suggestion) + } +} + +// HandleAppError handles an application error and displays it in a friendly way. +func HandleAppError(err error, t *i18n.Translations) { + if err == nil { + return + } + + var appErr *domainErrors.AppError + ok := errors.As(err, &appErr) + if !ok { + PrintError(os.Stdout, err.Error()) + return + } + + var msg, suggestion string + + switch appErr.Type { + case domainErrors.TypeConfiguration: + if appErr.Err != nil && strings.Contains(appErr.Err.Error(), "API key") { + msg = t.GetMessage("ui_error.ai_api_key_missing_generic", 0, nil) + } else { + msg = t.GetMessage("ui_error.config_missing", 0, nil) + } + suggestion = t.GetMessage("ui_error.run_config_init", 0, nil) + + case domainErrors.TypeGit: + if errors.Is(appErr, domainErrors.ErrNoChanges) { + msg = t.GetMessage("ui_error.no_changes_detected", 0, nil) + suggestion = t.GetMessage("ui_error.ensure_modified_files", 0, nil) + } else { + msg = appErr.Message + } + + case domainErrors.TypeAI: + msg = t.GetMessage("ui.error_generating_suggestions", 0, nil) + suggestion = appErr.Error() + + case domainErrors.TypeVCS: + msg = t.GetMessage("ui_error.github_token_missing", 0, nil) + suggestion = t.GetMessage("ui_error.run_config_init", 0, nil) + + case domainErrors.TypeUpdate: + msg = t.GetMessage("ui_error.update_failed", 0, nil) + + case domainErrors.TypeInternal: + msg = t.GetMessage("ui_error.internal_error", 0, nil) + if appErr.Err != nil { + suggestion = appErr.Err.Error() + } + + default: + msg = appErr.Error() + } + + if msg == "" { + msg = appErr.Error() + } + + PrintErrorWithSuggestion(msg, suggestion) } func PrintKeyValue(key, value string) { @@ -229,7 +295,7 @@ func ShowDiff(files []string) error { } if err != nil { - return fmt.Errorf("error al obtener diff: %w", err) + return fmt.Errorf("error getting diff: %w", err) } if len(output) > 0 { @@ -272,7 +338,7 @@ func WithSpinnerAndDuration(message string, fn func() error) error { return nil } -// ShowDiffStats muestra estadísticas de cambios (como git diff --stat) +// ShowDiffStats shows change statistics (like git diff --stat) func ShowDiffStats(files []string, headerMessage string) error { cmd := exec.Command("git", append([]string{"diff", "--stat", "--staged", "--"}, files...)...) output, err := cmd.CombinedOutput() @@ -294,7 +360,7 @@ func ShowDiffStats(files []string, headerMessage string) error { return nil } -// EditCommitMessage abre un editor para que el usuario edite el mensaje +// EditCommitMessage opens an editor for the user to edit the message func EditCommitMessage(initialMessage string, editorErrorMsg string) (string, error) { tmpFile, err := os.CreateTemp("", "commit-msg-*.txt") if err != nil { @@ -341,14 +407,14 @@ func EditCommitMessage(initialMessage string, editorErrorMsg string) (string, er return editedMessage, nil } -// FileChange representa un archivo modificado con sus estadísticas +// FileChange represents a modified file with its statistics type FileChange struct { Path string Additions int Deletions int } -// ShowFilesTree muestra los archivos modificados en formato árbol +// ShowFilesTree shows modified files in tree format func ShowFilesTree(files []string, headerMessage string) error { if len(files) == 0 { return nil @@ -370,7 +436,7 @@ func ShowFilesTree(files []string, headerMessage string) error { return nil } -// getFileStats obtiene las estadísticas de cambios para cada archivo +// getFileStats gets change statistics for each file func getFileStats(files []string) ([]FileChange, error) { cmd := exec.Command("git", append([]string{"diff", "--numstat", "--staged", "--"}, files...)...) output, err := cmd.CombinedOutput() @@ -417,7 +483,7 @@ func getFileStats(files []string) ([]FileChange, error) { return changes, nil } -// treeNode representa un nodo en el árbol de archivos +// treeNode represents a node in the file tree type treeNode struct { name string isFile bool @@ -425,7 +491,7 @@ type treeNode struct { children map[string]*treeNode } -// buildFileTree construye un árbol de directorios +// buildFileTree builds a directory tree func buildFileTree(changes []FileChange) *treeNode { root := &treeNode{ name: "", @@ -456,7 +522,7 @@ func buildFileTree(changes []FileChange) *treeNode { return root } -// printTree imprime el árbol recursivamente +// printTree prints the tree recursively func printTree(node *treeNode, prefix string, isLast bool) { if node.name != "" { connector := "├── " @@ -504,7 +570,7 @@ func printTree(node *treeNode, prefix string, isLast bool) { } } -// sortFileTree ordena las claves: directorios primero, luego archivos +// sortFileTree sorts the keys: directories first, then files func sortFileTree(keys []string, nodes map[string]*treeNode) { for i := 0; i < len(keys); i++ { for j := i + 1; j < len(keys); j++ { @@ -522,7 +588,7 @@ func sortFileTree(keys []string, nodes map[string]*treeNode) { } } -// parseInt convierte string a int, retorna 0 si falla +// parseInt converts string to int, returns 0 if it fails func parseInt(s string) int { var result int _, _ = fmt.Sscanf(s, "%d", &result) diff --git a/internal/version/version.go b/internal/version/version.go index 3a07f44..cb2785a 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,10 +1,10 @@ package version -// Version es la versión actual de MateCommit -// Esta versión debe actualizarse en cada release +// Version is the current version of MateCommit +// This version should be updated in each release const Version = "1.4.0" -// FullVersion retorna la versión con el prefijo v +// FullVersion returns the version with the v prefix func FullVersion() string { return "v" + Version } From e76b02541095fc47bb1018e05bcf12262f24c350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8thomas=C2=A8?= Date: Sun, 21 Dec 2025 02:39:32 -0300 Subject: [PATCH 2/2] refactor: simplify project structure to be more Go-idiomatic --- cmd/main.go | 38 +++++++++---------- go.mod | 4 +- .../{infrastructure => }/ai/cost_wrapper.go | 12 +++--- .../ai/cost_wrapper_test.go | 4 +- .../{infrastructure => }/ai/gemini/base.go | 2 +- .../ai/gemini/commit_summarizer_service.go | 10 ++--- .../gemini/commit_summarizer_service_test.go | 4 +- .../{infrastructure => }/ai/gemini/helper.go | 2 +- .../ai/gemini/helper_test.go | 0 .../ai/gemini/issue_content_generator.go | 10 ++--- .../ai/gemini/issue_content_generator_test.go | 4 +- .../pull_requests_summarizer_service.go | 10 ++--- .../pull_requests_summarizer_service_test.go | 4 +- .../ai/gemini/release_generator.go | 10 ++--- .../ai/gemini/release_generator_test.go | 4 +- internal/{infrastructure => }/ai/prompts.go | 2 +- .../{infrastructure => }/ai/prompts_test.go | 0 .../builder/binary_builder.go | 2 +- .../builder/binary_builder_test.go | 0 internal/{infrastructure => }/cache/cache.go | 0 .../{infrastructure => }/cache/cache_test.go | 0 .../{cli/command => commands}/cache/cache.go | 6 +-- .../completion/completion.go | 2 +- .../completion_helper/helper.go | 0 .../command => commands}/config/config.go | 4 +- .../config/config_test.go | 6 +-- .../command => commands}/config/doctor.go | 8 ++-- .../{cli/command => commands}/config/edit.go | 4 +- .../command => commands}/config/edit_test.go | 6 +-- .../{cli/command => commands}/config/init.go | 10 ++--- .../command => commands}/config/init_test.go | 6 +-- .../config/quick_setup.go | 6 +-- .../{cli/command => commands}/config/show.go | 4 +- .../command => commands}/config/show_test.go | 2 +- .../handler/suggestions.go | 8 ++-- .../handler/suggestions_test.go | 28 +++++++------- .../command => commands}/issues/issues.go | 10 ++--- .../issues/issues_test.go | 8 ++-- .../{cli/command => commands}/issues/mocks.go | 2 +- .../command => commands}/issues/templates.go | 6 +-- .../issues/templates_test.go | 2 +- .../pull_requests/summarize.go | 10 ++--- .../pull_requests/summarize_test.go | 8 ++-- .../command => commands}/release/create.go | 8 ++-- .../release/create_test.go | 6 +-- .../{cli/command => commands}/release/edit.go | 8 ++-- .../command => commands}/release/edit_test.go | 6 +-- .../command => commands}/release/formatter.go | 4 +- .../command => commands}/release/generate.go | 6 +-- .../release/generate_test.go | 6 +-- .../command => commands}/release/mocks.go | 2 +- .../command => commands}/release/preview.go | 6 +-- .../release/preview_test.go | 6 +-- .../command => commands}/release/publish.go | 6 +-- .../release/publish_test.go | 6 +-- .../{cli/command => commands}/release/push.go | 6 +-- .../command => commands}/release/push_test.go | 6 +-- .../command => commands}/release/release.go | 20 +++++----- .../release/version_helper.go | 0 .../{cli/command => commands}/stats/stats.go | 6 +-- .../command => commands}/stats/stats_test.go | 6 +-- .../suggests_commits/suggests_commits.go | 10 ++--- .../suggests_commits/suggests_commits_test.go | 8 ++-- .../{cli/command => commands}/update/cmd.go | 8 ++-- .../dependency/gomod_analyzer.go | 6 +-- .../dependency/gomod_analyzer_test.go | 2 +- .../dependency/package_json_analyzer.go | 6 +-- .../dependency/package_json_analyzer_test.go | 2 +- .../dependency/registry.go | 4 +- .../dependency/registry_test.go | 4 +- internal/{domain => }/errors/errors.go | 0 .../{infrastructure => }/git/git_service.go | 4 +- .../git/git_service_test.go | 2 +- .../vcs => }/github/github_service.go | 6 +-- .../vcs => }/github/github_service_test.go | 2 +- .../{infrastructure/vcs => }/github/mocks.go | 2 +- internal/i18n/locales/active.en.toml | 6 +-- internal/i18n/locales/active.es.toml | 6 +-- .../tickets => }/jira/jira_service.go | 4 +- .../jira/jira_service_integration_test.go | 0 .../tickets => }/jira/jira_service_test.go | 2 +- internal/{domain => }/models/commit.go | 0 internal/{domain => }/models/issue.go | 0 .../{domain => }/models/issue_generation.go | 0 .../{domain => }/models/issue_template.go | 0 internal/{domain => }/models/pr.go | 0 internal/{domain => }/models/release.go | 0 internal/{domain => }/models/ticket.go | 0 internal/{domain => }/models/usage.go | 0 internal/{domain => }/ports/ai_provider.go | 0 internal/{domain => }/ports/ai_service.go | 2 +- .../{domain => }/ports/dependency_analyzer.go | 2 +- internal/{domain => }/ports/ticket_manager.go | 2 +- internal/{domain => }/ports/vcs_client.go | 2 +- internal/providers/ai.go | 8 ++-- internal/providers/tickets.go | 6 +-- internal/providers/vcs.go | 8 ++-- internal/services/commit_service.go | 10 ++--- internal/services/commit_service_test.go | 6 +-- internal/services/issue_generator_service.go | 8 ++-- .../services/issue_generator_service_test.go | 4 +- internal/services/issue_template_service.go | 6 +-- .../services/issue_template_service_test.go | 4 +- internal/services/mocks.go | 2 +- internal/services/pull_request_service.go | 8 ++-- .../services/pull_request_service_test.go | 10 ++--- internal/services/release_service.go | 10 ++--- internal/services/release_service_test.go | 4 +- internal/services/version_checker.go | 4 +- internal/ui/token_stats.go | 4 +- internal/ui/ui.go | 4 +- 111 files changed, 290 insertions(+), 290 deletions(-) rename internal/{infrastructure => }/ai/cost_wrapper.go (92%) rename internal/{infrastructure => }/ai/cost_wrapper_test.go (98%) rename internal/{infrastructure => }/ai/gemini/base.go (94%) rename internal/{infrastructure => }/ai/gemini/commit_summarizer_service.go (96%) rename internal/{infrastructure => }/ai/gemini/commit_summarizer_service_test.go (99%) rename internal/{infrastructure => }/ai/gemini/helper.go (98%) rename internal/{infrastructure => }/ai/gemini/helper_test.go (100%) rename internal/{infrastructure => }/ai/gemini/issue_content_generator.go (94%) rename internal/{infrastructure => }/ai/gemini/issue_content_generator_test.go (98%) rename internal/{infrastructure => }/ai/gemini/pull_requests_summarizer_service.go (92%) rename internal/{infrastructure => }/ai/gemini/pull_requests_summarizer_service_test.go (97%) rename internal/{infrastructure => }/ai/gemini/release_generator.go (96%) rename internal/{infrastructure => }/ai/gemini/release_generator_test.go (98%) rename internal/{infrastructure => }/ai/prompts.go (99%) rename internal/{infrastructure => }/ai/prompts_test.go (100%) rename internal/{infrastructure => }/builder/binary_builder.go (98%) rename internal/{infrastructure => }/builder/binary_builder_test.go (100%) rename internal/{infrastructure => }/cache/cache.go (100%) rename internal/{infrastructure => }/cache/cache_test.go (100%) rename internal/{cli/command => commands}/cache/cache.go (85%) rename internal/{cli/command => commands}/completion/completion.go (98%) rename internal/{cli => commands}/completion_helper/helper.go (100%) rename internal/{cli/command => commands}/config/config.go (83%) rename internal/{cli/command => commands}/config/config_test.go (76%) rename internal/{cli/command => commands}/config/doctor.go (97%) rename internal/{cli/command => commands}/config/edit.go (90%) rename internal/{cli/command => commands}/config/edit_test.go (95%) rename internal/{cli/command => commands}/config/init.go (97%) rename internal/{cli/command => commands}/config/init_test.go (95%) rename internal/{cli/command => commands}/config/quick_setup.go (93%) rename internal/{cli/command => commands}/config/show.go (95%) rename internal/{cli/command => commands}/config/show_test.go (98%) rename internal/{cli/command => commands}/handler/suggestions.go (97%) rename internal/{cli/command => commands}/handler/suggestions_test.go (91%) rename internal/{cli/command => commands}/issues/issues.go (97%) rename internal/{cli/command => commands}/issues/issues_test.go (96%) rename internal/{cli/command => commands}/issues/mocks.go (98%) rename internal/{cli/command => commands}/issues/templates.go (93%) rename internal/{cli/command => commands}/issues/templates_test.go (97%) rename internal/{cli/command => commands}/pull_requests/summarize.go (91%) rename internal/{cli/command => commands}/pull_requests/summarize_test.go (92%) rename internal/{cli/command => commands}/release/create.go (96%) rename internal/{cli/command => commands}/release/create_test.go (97%) rename internal/{cli/command => commands}/release/edit.go (96%) rename internal/{cli/command => commands}/release/edit_test.go (98%) rename internal/{cli/command => commands}/release/formatter.go (97%) rename internal/{cli/command => commands}/release/generate.go (93%) rename internal/{cli/command => commands}/release/generate_test.go (94%) rename internal/{cli/command => commands}/release/mocks.go (98%) rename internal/{cli/command => commands}/release/preview.go (95%) rename internal/{cli/command => commands}/release/preview_test.go (93%) rename internal/{cli/command => commands}/release/publish.go (94%) rename internal/{cli/command => commands}/release/publish_test.go (96%) rename internal/{cli/command => commands}/release/push.go (90%) rename internal/{cli/command => commands}/release/push_test.go (91%) rename internal/{cli/command => commands}/release/release.go (85%) rename internal/{cli/command => commands}/release/version_helper.go (100%) rename internal/{cli/command => commands}/stats/stats.go (95%) rename internal/{cli/command => commands}/stats/stats_test.go (98%) rename internal/{cli/command => commands}/suggests_commits/suggests_commits.go (93%) rename internal/{cli/command => commands}/suggests_commits/suggests_commits_test.go (96%) rename internal/{cli/command => commands}/update/cmd.go (80%) rename internal/{infrastructure => }/dependency/gomod_analyzer.go (96%) rename internal/{infrastructure => }/dependency/gomod_analyzer_test.go (99%) rename internal/{infrastructure => }/dependency/package_json_analyzer.go (96%) rename internal/{infrastructure => }/dependency/package_json_analyzer_test.go (99%) rename internal/{infrastructure => }/dependency/registry.go (92%) rename internal/{infrastructure => }/dependency/registry_test.go (98%) rename internal/{domain => }/errors/errors.go (100%) rename internal/{infrastructure => }/git/git_service.go (98%) rename internal/{infrastructure => }/git/git_service_test.go (99%) rename internal/{infrastructure/vcs => }/github/github_service.go (99%) rename internal/{infrastructure/vcs => }/github/github_service_test.go (99%) rename internal/{infrastructure/vcs => }/github/mocks.go (99%) rename internal/{infrastructure/tickets => }/jira/jira_service.go (98%) rename internal/{infrastructure/tickets => }/jira/jira_service_integration_test.go (100%) rename internal/{infrastructure/tickets => }/jira/jira_service_test.go (99%) rename internal/{domain => }/models/commit.go (100%) rename internal/{domain => }/models/issue.go (100%) rename internal/{domain => }/models/issue_generation.go (100%) rename internal/{domain => }/models/issue_template.go (100%) rename internal/{domain => }/models/pr.go (100%) rename internal/{domain => }/models/release.go (100%) rename internal/{domain => }/models/ticket.go (100%) rename internal/{domain => }/models/usage.go (100%) rename internal/{domain => }/ports/ai_provider.go (100%) rename internal/{domain => }/ports/ai_service.go (95%) rename internal/{domain => }/ports/dependency_analyzer.go (90%) rename internal/{domain => }/ports/ticket_manager.go (62%) rename internal/{domain => }/ports/vcs_client.go (98%) diff --git a/cmd/main.go b/cmd/main.go index 1a61919..3eb0652 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,25 +7,25 @@ import ( "os" "strings" - "github.com/Tomas-vilte/MateCommit/internal/cli/command/cache" - "github.com/Tomas-vilte/MateCommit/internal/cli/command/completion" - "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/issues" - "github.com/Tomas-vilte/MateCommit/internal/cli/command/pull_requests" - "github.com/Tomas-vilte/MateCommit/internal/cli/command/release" - "github.com/Tomas-vilte/MateCommit/internal/cli/command/stats" - "github.com/Tomas-vilte/MateCommit/internal/cli/command/suggests_commits" - "github.com/Tomas-vilte/MateCommit/internal/cli/command/update" - 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/Tomas-vilte/MateCommit/internal/infrastructure/ai" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/git" - "github.com/Tomas-vilte/MateCommit/internal/providers" - "github.com/Tomas-vilte/MateCommit/internal/services" - "github.com/Tomas-vilte/MateCommit/internal/ui" - "github.com/Tomas-vilte/MateCommit/internal/version" + "github.com/thomas-vilte/matecommit/internal/commands/cache" + "github.com/thomas-vilte/matecommit/internal/commands/completion" + "github.com/thomas-vilte/matecommit/internal/commands/config" + "github.com/thomas-vilte/matecommit/internal/commands/handler" + "github.com/thomas-vilte/matecommit/internal/commands/issues" + "github.com/thomas-vilte/matecommit/internal/commands/pull_requests" + "github.com/thomas-vilte/matecommit/internal/commands/release" + "github.com/thomas-vilte/matecommit/internal/commands/stats" + "github.com/thomas-vilte/matecommit/internal/commands/suggests_commits" + "github.com/thomas-vilte/matecommit/internal/commands/update" + cfg "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/ai" + "github.com/thomas-vilte/matecommit/internal/git" + "github.com/thomas-vilte/matecommit/internal/providers" + "github.com/thomas-vilte/matecommit/internal/services" + "github.com/thomas-vilte/matecommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/version" "github.com/fatih/color" "github.com/urfave/cli/v3" ) diff --git a/go.mod b/go.mod index ea1d9d5..49e74c2 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Tomas-vilte/MateCommit +module github.com/thomas-vilte/matecommit go 1.24.0 @@ -16,6 +16,7 @@ require ( golang.org/x/oauth2 v0.34.0 golang.org/x/text v0.32.0 google.golang.org/genai v1.38.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -49,5 +50,4 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/infrastructure/ai/cost_wrapper.go b/internal/ai/cost_wrapper.go similarity index 92% rename from internal/infrastructure/ai/cost_wrapper.go rename to internal/ai/cost_wrapper.go index 82899d2..c1c1263 100644 --- a/internal/infrastructure/ai/cost_wrapper.go +++ b/internal/ai/cost_wrapper.go @@ -6,12 +6,12 @@ import ( "fmt" "time" - "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/cache" - cost2 "github.com/Tomas-vilte/MateCommit/internal/services/cost" - "github.com/Tomas-vilte/MateCommit/internal/services/routing" + "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/cache" + cost2 "github.com/thomas-vilte/matecommit/internal/services/cost" + "github.com/thomas-vilte/matecommit/internal/services/routing" ) type ConfirmationCallback func(result ConfirmationResult) (choice string, proceed bool) diff --git a/internal/infrastructure/ai/cost_wrapper_test.go b/internal/ai/cost_wrapper_test.go similarity index 98% rename from internal/infrastructure/ai/cost_wrapper_test.go rename to internal/ai/cost_wrapper_test.go index 245aa43..c79d862 100644 --- a/internal/infrastructure/ai/cost_wrapper_test.go +++ b/internal/ai/cost_wrapper_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/services/cost" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/services/cost" "github.com/stretchr/testify/mock" ) diff --git a/internal/infrastructure/ai/gemini/base.go b/internal/ai/gemini/base.go similarity index 94% rename from internal/infrastructure/ai/gemini/base.go rename to internal/ai/gemini/base.go index 259d883..b01ddc5 100644 --- a/internal/infrastructure/ai/gemini/base.go +++ b/internal/ai/gemini/base.go @@ -3,7 +3,7 @@ package gemini import ( "context" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/thomas-vilte/matecommit/internal/ports" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/commit_summarizer_service.go b/internal/ai/gemini/commit_summarizer_service.go similarity index 96% rename from internal/infrastructure/ai/gemini/commit_summarizer_service.go rename to internal/ai/gemini/commit_summarizer_service.go index a50a0a1..cc9f64b 100644 --- a/internal/infrastructure/ai/gemini/commit_summarizer_service.go +++ b/internal/ai/gemini/commit_summarizer_service.go @@ -7,11 +7,11 @@ import ( "regexp" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" + "github.com/thomas-vilte/matecommit/internal/config" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/ai" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/commit_summarizer_service_test.go b/internal/ai/gemini/commit_summarizer_service_test.go similarity index 99% rename from internal/infrastructure/ai/gemini/commit_summarizer_service_test.go rename to internal/ai/gemini/commit_summarizer_service_test.go index 967e0b5..36d6074 100644 --- a/internal/infrastructure/ai/gemini/commit_summarizer_service_test.go +++ b/internal/ai/gemini/commit_summarizer_service_test.go @@ -6,8 +6,8 @@ import ( "os" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/helper.go b/internal/ai/gemini/helper.go similarity index 98% rename from internal/infrastructure/ai/gemini/helper.go rename to internal/ai/gemini/helper.go index 3486468..b63c467 100644 --- a/internal/infrastructure/ai/gemini/helper.go +++ b/internal/ai/gemini/helper.go @@ -5,7 +5,7 @@ import ( "regexp" "strings" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/helper_test.go b/internal/ai/gemini/helper_test.go similarity index 100% rename from internal/infrastructure/ai/gemini/helper_test.go rename to internal/ai/gemini/helper_test.go diff --git a/internal/infrastructure/ai/gemini/issue_content_generator.go b/internal/ai/gemini/issue_content_generator.go similarity index 94% rename from internal/infrastructure/ai/gemini/issue_content_generator.go rename to internal/ai/gemini/issue_content_generator.go index b7522f3..70a5f69 100644 --- a/internal/infrastructure/ai/gemini/issue_content_generator.go +++ b/internal/ai/gemini/issue_content_generator.go @@ -6,11 +6,11 @@ import ( "fmt" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" + "github.com/thomas-vilte/matecommit/internal/config" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/ai" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/issue_content_generator_test.go b/internal/ai/gemini/issue_content_generator_test.go similarity index 98% rename from internal/infrastructure/ai/gemini/issue_content_generator_test.go rename to internal/ai/gemini/issue_content_generator_test.go index fe6571f..333d7de 100644 --- a/internal/infrastructure/ai/gemini/issue_content_generator_test.go +++ b/internal/ai/gemini/issue_content_generator_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/pull_requests_summarizer_service.go b/internal/ai/gemini/pull_requests_summarizer_service.go similarity index 92% rename from internal/infrastructure/ai/gemini/pull_requests_summarizer_service.go rename to internal/ai/gemini/pull_requests_summarizer_service.go index a0d9850..284a7e2 100644 --- a/internal/infrastructure/ai/gemini/pull_requests_summarizer_service.go +++ b/internal/ai/gemini/pull_requests_summarizer_service.go @@ -6,11 +6,11 @@ import ( "fmt" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" + "github.com/thomas-vilte/matecommit/internal/config" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/ai" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/pull_requests_summarizer_service_test.go b/internal/ai/gemini/pull_requests_summarizer_service_test.go similarity index 97% rename from internal/infrastructure/ai/gemini/pull_requests_summarizer_service_test.go rename to internal/ai/gemini/pull_requests_summarizer_service_test.go index 0ba6ca3..09c8e79 100644 --- a/internal/infrastructure/ai/gemini/pull_requests_summarizer_service_test.go +++ b/internal/ai/gemini/pull_requests_summarizer_service_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/release_generator.go b/internal/ai/gemini/release_generator.go similarity index 96% rename from internal/infrastructure/ai/gemini/release_generator.go rename to internal/ai/gemini/release_generator.go index beb0b9c..6d12169 100644 --- a/internal/infrastructure/ai/gemini/release_generator.go +++ b/internal/ai/gemini/release_generator.go @@ -6,11 +6,11 @@ import ( "fmt" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" + "github.com/thomas-vilte/matecommit/internal/config" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/ai" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/gemini/release_generator_test.go b/internal/ai/gemini/release_generator_test.go similarity index 98% rename from internal/infrastructure/ai/gemini/release_generator_test.go rename to internal/ai/gemini/release_generator_test.go index 53f1290..e7b0818 100644 --- a/internal/infrastructure/ai/gemini/release_generator_test.go +++ b/internal/ai/gemini/release_generator_test.go @@ -6,8 +6,8 @@ import ( "os" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "google.golang.org/genai" ) diff --git a/internal/infrastructure/ai/prompts.go b/internal/ai/prompts.go similarity index 99% rename from internal/infrastructure/ai/prompts.go rename to internal/ai/prompts.go index 8e633f3..68657fd 100644 --- a/internal/infrastructure/ai/prompts.go +++ b/internal/ai/prompts.go @@ -6,7 +6,7 @@ import ( "strings" "text/template" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" ) const ( diff --git a/internal/infrastructure/ai/prompts_test.go b/internal/ai/prompts_test.go similarity index 100% rename from internal/infrastructure/ai/prompts_test.go rename to internal/ai/prompts_test.go diff --git a/internal/infrastructure/builder/binary_builder.go b/internal/builder/binary_builder.go similarity index 98% rename from internal/infrastructure/builder/binary_builder.go rename to internal/builder/binary_builder.go index 6e5a996..af8fa21 100644 --- a/internal/infrastructure/builder/binary_builder.go +++ b/internal/builder/binary_builder.go @@ -13,7 +13,7 @@ import ( "strings" "time" - "github.com/Tomas-vilte/MateCommit/internal/domain/errors" + "github.com/thomas-vilte/matecommit/internal/errors" ) type BuildTarget struct { diff --git a/internal/infrastructure/builder/binary_builder_test.go b/internal/builder/binary_builder_test.go similarity index 100% rename from internal/infrastructure/builder/binary_builder_test.go rename to internal/builder/binary_builder_test.go diff --git a/internal/infrastructure/cache/cache.go b/internal/cache/cache.go similarity index 100% rename from internal/infrastructure/cache/cache.go rename to internal/cache/cache.go diff --git a/internal/infrastructure/cache/cache_test.go b/internal/cache/cache_test.go similarity index 100% rename from internal/infrastructure/cache/cache_test.go rename to internal/cache/cache_test.go diff --git a/internal/cli/command/cache/cache.go b/internal/commands/cache/cache.go similarity index 85% rename from internal/cli/command/cache/cache.go rename to internal/commands/cache/cache.go index 324ace2..85078f5 100644 --- a/internal/cli/command/cache/cache.go +++ b/internal/commands/cache/cache.go @@ -5,9 +5,9 @@ import ( "fmt" "time" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/cache" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/cache" "github.com/fatih/color" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/completion/completion.go b/internal/commands/completion/completion.go similarity index 98% rename from internal/cli/command/completion/completion.go rename to internal/commands/completion/completion.go index e68f43a..8e049b3 100644 --- a/internal/cli/command/completion/completion.go +++ b/internal/commands/completion/completion.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/completion_helper/helper.go b/internal/commands/completion_helper/helper.go similarity index 100% rename from internal/cli/completion_helper/helper.go rename to internal/commands/completion_helper/helper.go diff --git a/internal/cli/command/config/config.go b/internal/commands/config/config.go similarity index 83% rename from internal/cli/command/config/config.go rename to internal/commands/config/config.go index 26a671e..e027b76 100644 --- a/internal/cli/command/config/config.go +++ b/internal/commands/config/config.go @@ -1,8 +1,8 @@ package config import ( - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/config/config_test.go b/internal/commands/config/config_test.go similarity index 76% rename from internal/cli/command/config/config_test.go rename to internal/commands/config/config_test.go index b9777d5..f4c9f69 100644 --- a/internal/cli/command/config/config_test.go +++ b/internal/commands/config/config_test.go @@ -5,8 +5,8 @@ import ( "path/filepath" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/stretchr/testify/assert" ) @@ -21,7 +21,7 @@ func setupConfigTest(t *testing.T) (*config.Config, *i18n.Translations, string, Language: "en", } - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) cleanup := func() { diff --git a/internal/cli/command/config/doctor.go b/internal/commands/config/doctor.go similarity index 97% rename from internal/cli/command/config/doctor.go rename to internal/commands/config/doctor.go index 68d0d0d..4cadc4e 100644 --- a/internal/cli/command/config/doctor.go +++ b/internal/commands/config/doctor.go @@ -8,10 +8,10 @@ import ( "strings" "time" - "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/ui" + "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" ) diff --git a/internal/cli/command/config/edit.go b/internal/commands/config/edit.go similarity index 90% rename from internal/cli/command/config/edit.go rename to internal/commands/config/edit.go index ef5eac4..aa51ef1 100644 --- a/internal/cli/command/config/edit.go +++ b/internal/commands/config/edit.go @@ -6,8 +6,8 @@ import ( "os" "os/exec" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/config/edit_test.go b/internal/commands/config/edit_test.go similarity index 95% rename from internal/cli/command/config/edit_test.go rename to internal/commands/config/edit_test.go index 6989f33..14466fa 100644 --- a/internal/cli/command/config/edit_test.go +++ b/internal/commands/config/edit_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,7 +25,7 @@ func setupEditTest(t *testing.T) (*config.Config, *i18n.Translations, func()) { PathFile: fakeConfigPath, } - translations, err := i18n.NewTranslations("es", "../../../i18n/locales") + translations, err := i18n.NewTranslations("es", "../../i18n/locales") require.NoError(t, err) cleanup := func() { diff --git a/internal/cli/command/config/init.go b/internal/commands/config/init.go similarity index 97% rename from internal/cli/command/config/init.go rename to internal/commands/config/init.go index 97525a8..0dc4925 100644 --- a/internal/cli/command/config/init.go +++ b/internal/commands/config/init.go @@ -9,11 +9,11 @@ import ( "strings" "time" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "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/ui" + "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" ) diff --git a/internal/cli/command/config/init_test.go b/internal/commands/config/init_test.go similarity index 95% rename from internal/cli/command/config/init_test.go rename to internal/commands/config/init_test.go index b2e3fd5..15cb642 100644 --- a/internal/cli/command/config/init_test.go +++ b/internal/commands/config/init_test.go @@ -10,8 +10,8 @@ import ( "sync" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/urfave/cli/v3" @@ -25,7 +25,7 @@ func runInitCommandTest(t *testing.T, userInput string, fullMode bool) (output s Language: "en", } - translations, err := i18n.NewTranslations("en", "../../../i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") require.NoError(t, err) originalStdin := os.Stdin diff --git a/internal/cli/command/config/quick_setup.go b/internal/commands/config/quick_setup.go similarity index 93% rename from internal/cli/command/config/quick_setup.go rename to internal/commands/config/quick_setup.go index b1e1d7b..260eb62 100644 --- a/internal/cli/command/config/quick_setup.go +++ b/internal/commands/config/quick_setup.go @@ -7,9 +7,9 @@ import ( "os" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/ui" ) func runQuickSetup(ctx context.Context, reader *bufio.Reader, cfg *config.Config, t *i18n.Translations) error { diff --git a/internal/cli/command/config/show.go b/internal/commands/config/show.go similarity index 95% rename from internal/cli/command/config/show.go rename to internal/commands/config/show.go index 7facdca..b0a4c78 100644 --- a/internal/cli/command/config/show.go +++ b/internal/commands/config/show.go @@ -4,8 +4,8 @@ import ( "context" "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/config/show_test.go b/internal/commands/config/show_test.go similarity index 98% rename from internal/cli/command/config/show_test.go rename to internal/commands/config/show_test.go index dad974a..dede9c4 100644 --- a/internal/cli/command/config/show_test.go +++ b/internal/commands/config/show_test.go @@ -7,7 +7,7 @@ import ( "os" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/config" "github.com/stretchr/testify/assert" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/handler/suggestions.go b/internal/commands/handler/suggestions.go similarity index 97% rename from internal/cli/command/handler/suggestions.go rename to internal/commands/handler/suggestions.go index b8d659a..be7225b 100644 --- a/internal/cli/command/handler/suggestions.go +++ b/internal/commands/handler/suggestions.go @@ -6,10 +6,10 @@ import ( "os" "strings" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "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" ) diff --git a/internal/cli/command/handler/suggestions_test.go b/internal/commands/handler/suggestions_test.go similarity index 91% rename from internal/cli/command/handler/suggestions_test.go rename to internal/commands/handler/suggestions_test.go index ad91d3e..6f756aa 100644 --- a/internal/cli/command/handler/suggestions_test.go +++ b/internal/commands/handler/suggestions_test.go @@ -8,8 +8,8 @@ import ( "os" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -146,7 +146,7 @@ func TestNewSuggestionHandler(t *testing.T) { t.Run("should create new suggestion handler", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) // Act @@ -164,7 +164,7 @@ func TestSuggestionHandler_DisplaySuggestions(t *testing.T) { t.Run("should display suggestions correctly", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -201,7 +201,7 @@ func TestSuggestionHandler_HandleCommitSelection(t *testing.T) { t.Run("should handle valid selection", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -229,7 +229,7 @@ func TestSuggestionHandler_HandleCommitSelection(t *testing.T) { t.Run("should handle operation canceled (selection 0)", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -253,7 +253,7 @@ func TestSuggestionHandler_HandleCommitSelection(t *testing.T) { t.Run("should handle invalid selection number", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -277,7 +277,7 @@ func TestSuggestionHandler_HandleCommitSelection(t *testing.T) { t.Run("should handle invalid input", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -303,7 +303,7 @@ func TestSuggestionHandler_HandleSuggestions(t *testing.T) { t.Run("should process commit successfully", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -332,7 +332,7 @@ func TestSuggestionHandler_HandleSuggestions(t *testing.T) { t.Run("should handle error when adding file to staging", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -360,7 +360,7 @@ func TestSuggestionHandler_HandleSuggestions(t *testing.T) { t.Run("should handle error when creating commit", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -388,7 +388,7 @@ func TestSuggestionHandler_HandleSuggestions(t *testing.T) { t.Run("should handle cancel operation", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -414,7 +414,7 @@ func TestSuggestionHandler_ProcessCommit(t *testing.T) { t.Run("should trim commit title prefix correctly", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) @@ -440,7 +440,7 @@ func TestSuggestionHandler_ProcessCommit(t *testing.T) { t.Run("should handle multiple files correctly", func(t *testing.T) { // Arrange mockGit := new(mockGitService) - translations, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") assert.NoError(t, err) handler := NewSuggestionHandler(mockGit, nil, translations) diff --git a/internal/cli/command/issues/issues.go b/internal/commands/issues/issues.go similarity index 97% rename from internal/cli/command/issues/issues.go rename to internal/commands/issues/issues.go index 5d46c3d..39b0546 100644 --- a/internal/cli/command/issues/issues.go +++ b/internal/commands/issues/issues.go @@ -8,11 +8,11 @@ import ( "os/exec" "strings" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "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/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/issues/issues_test.go b/internal/commands/issues/issues_test.go similarity index 96% rename from internal/cli/command/issues/issues_test.go rename to internal/commands/issues/issues_test.go index 2bae32f..9e129b8 100644 --- a/internal/cli/command/issues/issues_test.go +++ b/internal/commands/issues/issues_test.go @@ -5,9 +5,9 @@ import ( "os" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/stretchr/testify/require" @@ -22,7 +22,7 @@ func setupIssuesTest(t *testing.T) (*MockIssueGeneratorService, *MockIssueTempla return mockGen, nil } - trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + trans, err := i18n.NewTranslations("en", "../../i18n/locales") require.NoError(t, err) cfg := &config.Config{Language: "en"} return mockGen, mockTemp, provider, trans, cfg diff --git a/internal/cli/command/issues/mocks.go b/internal/commands/issues/mocks.go similarity index 98% rename from internal/cli/command/issues/mocks.go rename to internal/commands/issues/mocks.go index f08d644..ac1c8bf 100644 --- a/internal/cli/command/issues/mocks.go +++ b/internal/commands/issues/mocks.go @@ -3,7 +3,7 @@ package issues import ( "context" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/mock" ) diff --git a/internal/cli/command/issues/templates.go b/internal/commands/issues/templates.go similarity index 93% rename from internal/cli/command/issues/templates.go rename to internal/commands/issues/templates.go index eb6bf12..c5c8d17 100644 --- a/internal/cli/command/issues/templates.go +++ b/internal/commands/issues/templates.go @@ -5,9 +5,9 @@ import ( "fmt" "os" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/issues/templates_test.go b/internal/commands/issues/templates_test.go similarity index 97% rename from internal/cli/command/issues/templates_test.go rename to internal/commands/issues/templates_test.go index c71cd12..c17142a 100644 --- a/internal/cli/command/issues/templates_test.go +++ b/internal/commands/issues/templates_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/pull_requests/summarize.go b/internal/commands/pull_requests/summarize.go similarity index 91% rename from internal/cli/command/pull_requests/summarize.go rename to internal/commands/pull_requests/summarize.go index 740d46d..962a272 100644 --- a/internal/cli/command/pull_requests/summarize.go +++ b/internal/commands/pull_requests/summarize.go @@ -4,11 +4,11 @@ import ( "context" "fmt" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - cfg "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "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/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/pull_requests/summarize_test.go b/internal/commands/pull_requests/summarize_test.go similarity index 92% rename from internal/cli/command/pull_requests/summarize_test.go rename to internal/commands/pull_requests/summarize_test.go index 18590fd..aa88439 100644 --- a/internal/cli/command/pull_requests/summarize_test.go +++ b/internal/commands/pull_requests/summarize_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/stretchr/testify/require" @@ -27,7 +27,7 @@ func setupSummarizeTest(t *testing.T) (*MockPRService, *i18n.Translations, *conf cfg := &config.Config{} - translations, err := i18n.NewTranslations("en", "../../../i18n/locales") + translations, err := i18n.NewTranslations("en", "../../i18n/locales") require.NoError(t, err) return mockPRService, translations, cfg diff --git a/internal/cli/command/release/create.go b/internal/commands/release/create.go similarity index 96% rename from internal/cli/command/release/create.go rename to internal/commands/release/create.go index 10d60bd..804447c 100644 --- a/internal/cli/command/release/create.go +++ b/internal/commands/release/create.go @@ -7,10 +7,10 @@ import ( "os" "strings" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - cfg "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" + cfg "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/release/create_test.go b/internal/commands/release/create_test.go similarity index 97% rename from internal/cli/command/release/create_test.go rename to internal/commands/release/create_test.go index bec909d..7cd83cc 100644 --- a/internal/cli/command/release/create_test.go +++ b/internal/commands/release/create_test.go @@ -7,8 +7,8 @@ import ( "errors" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/stretchr/testify/require" @@ -16,7 +16,7 @@ import ( ) func runCreateTest(t *testing.T, userInput string, args []string, mockService *MockReleaseService) error { - trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + trans, err := i18n.NewTranslations("en", "../../i18n/locales") if err != nil { t.Logf("Warning: using empty translations by error: %v", err) trans = &i18n.Translations{} diff --git a/internal/cli/command/release/edit.go b/internal/commands/release/edit.go similarity index 96% rename from internal/cli/command/release/edit.go rename to internal/commands/release/edit.go index 9e32005..d3fcfd6 100644 --- a/internal/cli/command/release/edit.go +++ b/internal/commands/release/edit.go @@ -6,10 +6,10 @@ import ( "os" "os/exec" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "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/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/release/edit_test.go b/internal/commands/release/edit_test.go similarity index 98% rename from internal/cli/command/release/edit_test.go rename to internal/commands/release/edit_test.go index 36c1413..3df59b4 100644 --- a/internal/cli/command/release/edit_test.go +++ b/internal/commands/release/edit_test.go @@ -8,8 +8,8 @@ import ( "path/filepath" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/stretchr/testify/require" @@ -17,7 +17,7 @@ import ( ) func getTestTranslations(t *testing.T) *i18n.Translations { - trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + trans, err := i18n.NewTranslations("en", "../../i18n/locales") if err != nil { t.Logf("Warning: using empty translations: %v", err) trans = &i18n.Translations{} diff --git a/internal/cli/command/release/formatter.go b/internal/commands/release/formatter.go similarity index 97% rename from internal/cli/command/release/formatter.go rename to internal/commands/release/formatter.go index 5049269..86e1e61 100644 --- a/internal/cli/command/release/formatter.go +++ b/internal/commands/release/formatter.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/i18n" ) // FormatReleaseMarkdown generates the full release markdown with all sections diff --git a/internal/cli/command/release/generate.go b/internal/commands/release/generate.go similarity index 93% rename from internal/cli/command/release/generate.go rename to internal/commands/release/generate.go index 267ccfd..2fb8b86 100644 --- a/internal/cli/command/release/generate.go +++ b/internal/commands/release/generate.go @@ -5,9 +5,9 @@ import ( "fmt" "os" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/release/generate_test.go b/internal/commands/release/generate_test.go similarity index 94% rename from internal/cli/command/release/generate_test.go rename to internal/commands/release/generate_test.go index 54656af..62cc64c 100644 --- a/internal/cli/command/release/generate_test.go +++ b/internal/commands/release/generate_test.go @@ -7,8 +7,8 @@ import ( "path/filepath" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/stretchr/testify/require" @@ -16,7 +16,7 @@ import ( ) func runGenerateTest(t *testing.T, args []string, mockService *MockReleaseService) error { - trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + trans, err := i18n.NewTranslations("en", "../../i18n/locales") if err != nil { t.Logf("Warning: using empty translations: %v", err) trans = &i18n.Translations{} diff --git a/internal/cli/command/release/mocks.go b/internal/commands/release/mocks.go similarity index 98% rename from internal/cli/command/release/mocks.go rename to internal/commands/release/mocks.go index ed2d2f7..c46cce9 100644 --- a/internal/cli/command/release/mocks.go +++ b/internal/commands/release/mocks.go @@ -3,7 +3,7 @@ package release import ( "context" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/mock" ) diff --git a/internal/cli/command/release/preview.go b/internal/commands/release/preview.go similarity index 95% rename from internal/cli/command/release/preview.go rename to internal/commands/release/preview.go index fcc2746..954e977 100644 --- a/internal/cli/command/release/preview.go +++ b/internal/commands/release/preview.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/release/preview_test.go b/internal/commands/release/preview_test.go similarity index 93% rename from internal/cli/command/release/preview_test.go rename to internal/commands/release/preview_test.go index d4c1d14..d407720 100644 --- a/internal/cli/command/release/preview_test.go +++ b/internal/commands/release/preview_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/stretchr/testify/require" @@ -14,7 +14,7 @@ import ( ) func runPreviewTest(t *testing.T, args []string, mockService *MockReleaseService) error { - trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + trans, err := i18n.NewTranslations("en", "../../i18n/locales") if err != nil { t.Logf("Warning: using empty translations: %v", err) trans = &i18n.Translations{} diff --git a/internal/cli/command/release/publish.go b/internal/commands/release/publish.go similarity index 94% rename from internal/cli/command/release/publish.go rename to internal/commands/release/publish.go index 6637215..f5ac2e0 100644 --- a/internal/cli/command/release/publish.go +++ b/internal/commands/release/publish.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/release/publish_test.go b/internal/commands/release/publish_test.go similarity index 96% rename from internal/cli/command/release/publish_test.go rename to internal/commands/release/publish_test.go index 3a757c7..af65e98 100644 --- a/internal/cli/command/release/publish_test.go +++ b/internal/commands/release/publish_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/stretchr/testify/require" @@ -14,7 +14,7 @@ import ( ) func runPublishTest(t *testing.T, args []string, mockService *MockReleaseService) error { - trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + trans, err := i18n.NewTranslations("en", "../../i18n/locales") if err != nil { t.Logf("Warning: using empty translations: %v", err) trans = &i18n.Translations{} diff --git a/internal/cli/command/release/push.go b/internal/commands/release/push.go similarity index 90% rename from internal/cli/command/release/push.go rename to internal/commands/release/push.go index edffb86..b59d83e 100644 --- a/internal/cli/command/release/push.go +++ b/internal/commands/release/push.go @@ -4,9 +4,9 @@ import ( "context" "fmt" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/commands/completion_helper" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/release/push_test.go b/internal/commands/release/push_test.go similarity index 91% rename from internal/cli/command/release/push_test.go rename to internal/commands/release/push_test.go index 6e094d1..1734bab 100644 --- a/internal/cli/command/release/push_test.go +++ b/internal/commands/release/push_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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/stretchr/testify/require" @@ -14,7 +14,7 @@ import ( ) func runPushTest(t *testing.T, args []string, mockService *MockReleaseService, mockGit *MockGitService) error { - trans, err := i18n.NewTranslations("en", "../../../../internal/i18n/locales") + trans, err := i18n.NewTranslations("en", "../../i18n/locales") if err != nil { t.Logf("Warning: using empty translations: %v", err) trans = &i18n.Translations{} diff --git a/internal/cli/command/release/release.go b/internal/commands/release/release.go similarity index 85% rename from internal/cli/command/release/release.go rename to internal/commands/release/release.go index e06702f..2cd5d70 100644 --- a/internal/cli/command/release/release.go +++ b/internal/commands/release/release.go @@ -4,14 +4,14 @@ import ( "context" "fmt" - cfg "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/git" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" - "github.com/Tomas-vilte/MateCommit/internal/providers" - "github.com/Tomas-vilte/MateCommit/internal/services" + "github.com/thomas-vilte/matecommit/internal/ai/gemini" + cfg "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/git" + "github.com/thomas-vilte/matecommit/internal/github" + "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/services" "github.com/urfave/cli/v3" ) @@ -90,13 +90,13 @@ func (r *ReleaseCommandFactory) createReleaseService(ctx context.Context, t *i18 } var notesGen ports.ReleaseNotesGenerator - if r.config.AIConfig.ActiveAI != "" { + 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) } - gen, err := providers.NewReleaseNotesGenerator(ctx, r.config, nil, owner, repo) + gen, err := gemini.NewReleaseNotesGenerator(ctx, r.config, nil, owner, repo) if err != nil { return nil, err } diff --git a/internal/cli/command/release/version_helper.go b/internal/commands/release/version_helper.go similarity index 100% rename from internal/cli/command/release/version_helper.go rename to internal/commands/release/version_helper.go diff --git a/internal/cli/command/stats/stats.go b/internal/commands/stats/stats.go similarity index 95% rename from internal/cli/command/stats/stats.go rename to internal/commands/stats/stats.go index 5077439..af2cfa1 100644 --- a/internal/cli/command/stats/stats.go +++ b/internal/commands/stats/stats.go @@ -5,9 +5,9 @@ import ( "fmt" "time" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/services/cost" + "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" ) diff --git a/internal/cli/command/stats/stats_test.go b/internal/commands/stats/stats_test.go similarity index 98% rename from internal/cli/command/stats/stats_test.go rename to internal/commands/stats/stats_test.go index f2f3414..3e5a2ea 100644 --- a/internal/cli/command/stats/stats_test.go +++ b/internal/commands/stats/stats_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/services/cost" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/services/cost" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -368,7 +368,7 @@ func setupTestManager(t *testing.T, tempDir string, records []cost.ActivityRecor func setupTestTranslations(t *testing.T) *i18n.Translations { t.Helper() - localesPath := filepath.Join("..", "..", "..", "i18n", "locales") + localesPath := filepath.Join("..", "..", "i18n", "locales") trans, err := i18n.NewTranslations("en", localesPath) require.NoError(t, err, "should not fail to create test translations") diff --git a/internal/cli/command/suggests_commits/suggests_commits.go b/internal/commands/suggests_commits/suggests_commits.go similarity index 93% rename from internal/cli/command/suggests_commits/suggests_commits.go rename to internal/commands/suggests_commits/suggests_commits.go index 543ee2a..847ffda 100644 --- a/internal/cli/command/suggests_commits/suggests_commits.go +++ b/internal/commands/suggests_commits/suggests_commits.go @@ -6,11 +6,11 @@ import ( "os" "time" - "github.com/Tomas-vilte/MateCommit/internal/cli/completion_helper" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "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/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/cli/command/suggests_commits/suggests_commits_test.go b/internal/commands/suggests_commits/suggests_commits_test.go similarity index 96% rename from internal/cli/command/suggests_commits/suggests_commits_test.go rename to internal/commands/suggests_commits/suggests_commits_test.go index 6c74e67..0e2f383 100644 --- a/internal/cli/command/suggests_commits/suggests_commits_test.go +++ b/internal/commands/suggests_commits/suggests_commits_test.go @@ -7,9 +7,9 @@ import ( "path/filepath" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "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" ) @@ -47,7 +47,7 @@ func setupTestEnv(t *testing.T) (*config.Config, *i18n.Translations, func()) { SuggestionsCount: 3, } - translations, err := i18n.NewTranslations("es", "../../../i18n/locales") + translations, err := i18n.NewTranslations("es", "../../i18n/locales") if err != nil { t.Fatal(err) } diff --git a/internal/cli/command/update/cmd.go b/internal/commands/update/cmd.go similarity index 80% rename from internal/cli/command/update/cmd.go rename to internal/commands/update/cmd.go index ad7dbcd..bd99117 100644 --- a/internal/cli/command/update/cmd.go +++ b/internal/commands/update/cmd.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/i18n" - "github.com/Tomas-vilte/MateCommit/internal/services" - "github.com/Tomas-vilte/MateCommit/internal/ui" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/services" + "github.com/thomas-vilte/matecommit/internal/ui" "github.com/urfave/cli/v3" ) diff --git a/internal/infrastructure/dependency/gomod_analyzer.go b/internal/dependency/gomod_analyzer.go similarity index 96% rename from internal/infrastructure/dependency/gomod_analyzer.go rename to internal/dependency/gomod_analyzer.go index 12d486e..3ab6845 100644 --- a/internal/infrastructure/dependency/gomod_analyzer.go +++ b/internal/dependency/gomod_analyzer.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" ) var _ ports.DependencyAnalyzer = (*GoModAnalyzer)(nil) diff --git a/internal/infrastructure/dependency/gomod_analyzer_test.go b/internal/dependency/gomod_analyzer_test.go similarity index 99% rename from internal/infrastructure/dependency/gomod_analyzer_test.go rename to internal/dependency/gomod_analyzer_test.go index 4844440..af0c127 100644 --- a/internal/infrastructure/dependency/gomod_analyzer_test.go +++ b/internal/dependency/gomod_analyzer_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) diff --git a/internal/infrastructure/dependency/package_json_analyzer.go b/internal/dependency/package_json_analyzer.go similarity index 96% rename from internal/infrastructure/dependency/package_json_analyzer.go rename to internal/dependency/package_json_analyzer.go index 3d7b60a..dbad9aa 100644 --- a/internal/infrastructure/dependency/package_json_analyzer.go +++ b/internal/dependency/package_json_analyzer.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" ) var _ ports.DependencyAnalyzer = (*PackageJsonAnalyzer)(nil) diff --git a/internal/infrastructure/dependency/package_json_analyzer_test.go b/internal/dependency/package_json_analyzer_test.go similarity index 99% rename from internal/infrastructure/dependency/package_json_analyzer_test.go rename to internal/dependency/package_json_analyzer_test.go index 8fcd99f..868b6bc 100644 --- a/internal/infrastructure/dependency/package_json_analyzer_test.go +++ b/internal/dependency/package_json_analyzer_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) diff --git a/internal/infrastructure/dependency/registry.go b/internal/dependency/registry.go similarity index 92% rename from internal/infrastructure/dependency/registry.go rename to internal/dependency/registry.go index 75e35f7..d4c4073 100644 --- a/internal/infrastructure/dependency/registry.go +++ b/internal/dependency/registry.go @@ -3,8 +3,8 @@ package dependency import ( "context" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" ) type AnalyzerRegistry struct { diff --git a/internal/infrastructure/dependency/registry_test.go b/internal/dependency/registry_test.go similarity index 98% rename from internal/infrastructure/dependency/registry_test.go rename to internal/dependency/registry_test.go index b467fce..9e1c89d 100644 --- a/internal/infrastructure/dependency/registry_test.go +++ b/internal/dependency/registry_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/ports" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) diff --git a/internal/domain/errors/errors.go b/internal/errors/errors.go similarity index 100% rename from internal/domain/errors/errors.go rename to internal/errors/errors.go diff --git a/internal/infrastructure/git/git_service.go b/internal/git/git_service.go similarity index 98% rename from internal/infrastructure/git/git_service.go rename to internal/git/git_service.go index 106f672..4b0c7ab 100644 --- a/internal/infrastructure/git/git_service.go +++ b/internal/git/git_service.go @@ -7,8 +7,8 @@ import ( "regexp" "strings" - "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" ) type GitService struct{} diff --git a/internal/infrastructure/git/git_service_test.go b/internal/git/git_service_test.go similarity index 99% rename from internal/infrastructure/git/git_service_test.go rename to internal/git/git_service_test.go index 19384a2..ec624c5 100644 --- a/internal/infrastructure/git/git_service_test.go +++ b/internal/git/git_service_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" "github.com/stretchr/testify/assert" ) diff --git a/internal/infrastructure/vcs/github/github_service.go b/internal/github/github_service.go similarity index 99% rename from internal/infrastructure/vcs/github/github_service.go rename to internal/github/github_service.go index 7b9b300..6c07046 100644 --- a/internal/infrastructure/vcs/github/github_service.go +++ b/internal/github/github_service.go @@ -12,9 +12,9 @@ import ( "strings" "time" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/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" "golang.org/x/oauth2" ) diff --git a/internal/infrastructure/vcs/github/github_service_test.go b/internal/github/github_service_test.go similarity index 99% rename from internal/infrastructure/vcs/github/github_service_test.go rename to internal/github/github_service_test.go index 3863938..ba979df 100644 --- a/internal/infrastructure/vcs/github/github_service_test.go +++ b/internal/github/github_service_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/google/go-github/v80/github" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/internal/infrastructure/vcs/github/mocks.go b/internal/github/mocks.go similarity index 99% rename from internal/infrastructure/vcs/github/mocks.go rename to internal/github/mocks.go index 1b4a508..4c0a9df 100644 --- a/internal/infrastructure/vcs/github/mocks.go +++ b/internal/github/mocks.go @@ -4,7 +4,7 @@ import ( "context" "os" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/builder" + "github.com/thomas-vilte/matecommit/internal/builder" "github.com/google/go-github/v80/github" "github.com/stretchr/testify/mock" ) diff --git a/internal/i18n/locales/active.en.toml b/internal/i18n/locales/active.en.toml index f05369a..1db22f0 100644 --- a/internal/i18n/locales/active.en.toml +++ b/internal/i18n/locales/active.en.toml @@ -11,7 +11,7 @@ Examples: """ [suggest_command_usage] -other = "💡 Generate commit message suggestions" +other = "Generate commit message suggestions" [suggest_command_description] other = "Analyze your changes and suggest appropriate commit messages" @@ -44,7 +44,7 @@ other = "🔍 Analyzing changes..." other = "❌ Error generating suggestions: {{.Error}}" [config_command_usage] -other = "⚙️ Manage configuration" +other = "Manage configuration" [config_init_usage] other = "🚀 Initialize configuration wizard" @@ -297,7 +297,7 @@ error_analyzing = "Error analyzing release: {{.Error}}" 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" +command_usage = "Manage releases and changelogs" push_usage = "Push a tag to remote" output_flag = "Output file (default: RELEASE_NOTES.md)" auto_flag = "Skip confirmation" diff --git a/internal/i18n/locales/active.es.toml b/internal/i18n/locales/active.es.toml index 46024f5..4015f4d 100644 --- a/internal/i18n/locales/active.es.toml +++ b/internal/i18n/locales/active.es.toml @@ -11,7 +11,7 @@ Ejemplos: """ [suggest_command_usage] -other = "💡 Generar sugerencias de mensajes de commit" +other = "Generar sugerencias de mensajes de commit" [suggest_command_description] other = "Analiza tus cambios y sugiere mensajes de commit adecuados" @@ -44,7 +44,7 @@ other = "🔍 Analizando cambios..." other = "❌ Error al generar sugerencias: {{.Error}}" [config_command_usage] -other = "⚙️ Administrar configuración" +other = "Administrar configuración" [config_init_usage] other = "🚀 Iniciar asistente de configuración" @@ -304,7 +304,7 @@ error_analyzing = "Error al analizar release: {{.Error}}" error_generating_notes = "Error al generar notas: {{.Error}}" error_creating_tag = "Error al crear tag: {{.Error}}" error_writing_file = "Error al escribir archivo: {{.Error}}" -command_usage = "🚀 Gestionar releases y changelogs del proyecto" +command_usage = "Gestionar releases y changelogs del proyecto" push_usage = "Subir un tag al remoto" output_flag = "Archivo de salida (default: RELEASE_NOTES.md)" auto_flag = "Saltar confirmación" diff --git a/internal/infrastructure/tickets/jira/jira_service.go b/internal/jira/jira_service.go similarity index 98% rename from internal/infrastructure/tickets/jira/jira_service.go rename to internal/jira/jira_service.go index 67c3f8b..dcee14f 100644 --- a/internal/infrastructure/tickets/jira/jira_service.go +++ b/internal/jira/jira_service.go @@ -8,8 +8,8 @@ import ( "regexp" "strings" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" ) // Constants for acceptance criteria patterns diff --git a/internal/infrastructure/tickets/jira/jira_service_integration_test.go b/internal/jira/jira_service_integration_test.go similarity index 100% rename from internal/infrastructure/tickets/jira/jira_service_integration_test.go rename to internal/jira/jira_service_integration_test.go diff --git a/internal/infrastructure/tickets/jira/jira_service_test.go b/internal/jira/jira_service_test.go similarity index 99% rename from internal/infrastructure/tickets/jira/jira_service_test.go rename to internal/jira/jira_service_test.go index a8ad6b6..e781169 100644 --- a/internal/infrastructure/tickets/jira/jira_service_test.go +++ b/internal/jira/jira_service_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/internal/domain/models/commit.go b/internal/models/commit.go similarity index 100% rename from internal/domain/models/commit.go rename to internal/models/commit.go diff --git a/internal/domain/models/issue.go b/internal/models/issue.go similarity index 100% rename from internal/domain/models/issue.go rename to internal/models/issue.go diff --git a/internal/domain/models/issue_generation.go b/internal/models/issue_generation.go similarity index 100% rename from internal/domain/models/issue_generation.go rename to internal/models/issue_generation.go diff --git a/internal/domain/models/issue_template.go b/internal/models/issue_template.go similarity index 100% rename from internal/domain/models/issue_template.go rename to internal/models/issue_template.go diff --git a/internal/domain/models/pr.go b/internal/models/pr.go similarity index 100% rename from internal/domain/models/pr.go rename to internal/models/pr.go diff --git a/internal/domain/models/release.go b/internal/models/release.go similarity index 100% rename from internal/domain/models/release.go rename to internal/models/release.go diff --git a/internal/domain/models/ticket.go b/internal/models/ticket.go similarity index 100% rename from internal/domain/models/ticket.go rename to internal/models/ticket.go diff --git a/internal/domain/models/usage.go b/internal/models/usage.go similarity index 100% rename from internal/domain/models/usage.go rename to internal/models/usage.go diff --git a/internal/domain/ports/ai_provider.go b/internal/ports/ai_provider.go similarity index 100% rename from internal/domain/ports/ai_provider.go rename to internal/ports/ai_provider.go diff --git a/internal/domain/ports/ai_service.go b/internal/ports/ai_service.go similarity index 95% rename from internal/domain/ports/ai_service.go rename to internal/ports/ai_service.go index a534f90..c2a0688 100644 --- a/internal/domain/ports/ai_service.go +++ b/internal/ports/ai_service.go @@ -3,7 +3,7 @@ package ports import ( "context" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" ) // CommitSummarizer is an interface that defines the service to generate commit suggestions. diff --git a/internal/domain/ports/dependency_analyzer.go b/internal/ports/dependency_analyzer.go similarity index 90% rename from internal/domain/ports/dependency_analyzer.go rename to internal/ports/dependency_analyzer.go index a2e01a3..9d08783 100644 --- a/internal/domain/ports/dependency_analyzer.go +++ b/internal/ports/dependency_analyzer.go @@ -3,7 +3,7 @@ package ports import ( "context" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" ) // DependencyAnalyzer defines the interface to analyze dependencies for different languages diff --git a/internal/domain/ports/ticket_manager.go b/internal/ports/ticket_manager.go similarity index 62% rename from internal/domain/ports/ticket_manager.go rename to internal/ports/ticket_manager.go index 4abda98..da9263d 100644 --- a/internal/domain/ports/ticket_manager.go +++ b/internal/ports/ticket_manager.go @@ -1,6 +1,6 @@ package ports -import "github.com/Tomas-vilte/MateCommit/internal/domain/models" +import "github.com/thomas-vilte/matecommit/internal/models" type TickerManager interface { GetTicketInfo(ticketID string) (*models.TicketInfo, error) diff --git a/internal/domain/ports/vcs_client.go b/internal/ports/vcs_client.go similarity index 98% rename from internal/domain/ports/vcs_client.go rename to internal/ports/vcs_client.go index 6a63a63..e06ff8b 100644 --- a/internal/domain/ports/vcs_client.go +++ b/internal/ports/vcs_client.go @@ -3,7 +3,7 @@ package ports import ( "context" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" ) // VCSClient defines common methods to interact with the APIs of version control systems. diff --git a/internal/providers/ai.go b/internal/providers/ai.go index b1f2812..ae6d571 100644 --- a/internal/providers/ai.go +++ b/internal/providers/ai.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai/gemini" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/ai" + "github.com/thomas-vilte/matecommit/internal/ai/gemini" ) // NewCommitSummarizer creates a CommitSummarizer based on the configured provider diff --git a/internal/providers/tickets.go b/internal/providers/tickets.go index a2e7d48..daf7dcb 100644 --- a/internal/providers/tickets.go +++ b/internal/providers/tickets.go @@ -5,9 +5,9 @@ import ( "fmt" "net/http" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/tickets/jira" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/jira" ) // NewTicketManager creates a TicketManager based on the configured provider diff --git a/internal/providers/vcs.go b/internal/providers/vcs.go index 4e35e5c..6a65cc4 100644 --- a/internal/providers/vcs.go +++ b/internal/providers/vcs.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/git" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/ports" + "github.com/thomas-vilte/matecommit/internal/git" + "github.com/thomas-vilte/matecommit/internal/github" ) // NewVCSClient creates a VCSClient based on the configuration and automatic detection of the remote diff --git a/internal/services/commit_service.go b/internal/services/commit_service.go index 7ac9142..8feb687 100644 --- a/internal/services/commit_service.go +++ b/internal/services/commit_service.go @@ -7,11 +7,11 @@ import ( "strconv" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" + "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/github" ) // commitGitService defines only the methods needed by CommitService. diff --git a/internal/services/commit_service_test.go b/internal/services/commit_service_test.go index 04ba063..ac7a9f1 100644 --- a/internal/services/commit_service_test.go +++ b/internal/services/commit_service_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) diff --git a/internal/services/issue_generator_service.go b/internal/services/issue_generator_service.go index 47da662..4b5794f 100644 --- a/internal/services/issue_generator_service.go +++ b/internal/services/issue_generator_service.go @@ -6,10 +6,10 @@ import ( "regexp" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" + "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" ) // issueGitService defines only the methods needed by IssueGeneratorService. diff --git a/internal/services/issue_generator_service_test.go b/internal/services/issue_generator_service_test.go index 195aed6..16b54ba 100644 --- a/internal/services/issue_generator_service_test.go +++ b/internal/services/issue_generator_service_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) diff --git a/internal/services/issue_template_service.go b/internal/services/issue_template_service.go index 7162ed5..d2d9045 100644 --- a/internal/services/issue_template_service.go +++ b/internal/services/issue_template_service.go @@ -6,9 +6,9 @@ import ( "path/filepath" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/models" "gopkg.in/yaml.v3" ) diff --git a/internal/services/issue_template_service_test.go b/internal/services/issue_template_service_test.go index 0a5c643..85aa6f6 100644 --- a/internal/services/issue_template_service_test.go +++ b/internal/services/issue_template_service_test.go @@ -5,8 +5,8 @@ import ( "path/filepath" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/internal/services/mocks.go b/internal/services/mocks.go index 46ad7c3..a78d10b 100644 --- a/internal/services/mocks.go +++ b/internal/services/mocks.go @@ -3,7 +3,7 @@ package services import ( "context" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/mock" ) diff --git a/internal/services/pull_request_service.go b/internal/services/pull_request_service.go index 771c95c..c1f2324 100644 --- a/internal/services/pull_request_service.go +++ b/internal/services/pull_request_service.go @@ -5,10 +5,10 @@ import ( "fmt" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/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. diff --git a/internal/services/pull_request_service_test.go b/internal/services/pull_request_service_test.go index c0bb741..9d4f62e 100644 --- a/internal/services/pull_request_service_test.go +++ b/internal/services/pull_request_service_test.go @@ -7,11 +7,11 @@ import ( "strings" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/ai/gemini" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/vcs/github" + "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/gemini" + "github.com/thomas-vilte/matecommit/internal/github" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/internal/services/release_service.go b/internal/services/release_service.go index 33f06e7..9db9f57 100644 --- a/internal/services/release_service.go +++ b/internal/services/release_service.go @@ -8,11 +8,11 @@ import ( "strconv" "strings" - "github.com/Tomas-vilte/MateCommit/internal/config" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/domain/ports" - "github.com/Tomas-vilte/MateCommit/internal/infrastructure/dependency" + "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/dependency" ) var ( diff --git a/internal/services/release_service_test.go b/internal/services/release_service_test.go index e249819..a3ec1b1 100644 --- a/internal/services/release_service_test.go +++ b/internal/services/release_service_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/Tomas-vilte/MateCommit/internal/config" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" + "github.com/thomas-vilte/matecommit/internal/config" + "github.com/thomas-vilte/matecommit/internal/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/internal/services/version_checker.go b/internal/services/version_checker.go index 3e17bf8..3c5ef74 100644 --- a/internal/services/version_checker.go +++ b/internal/services/version_checker.go @@ -20,7 +20,7 @@ import ( "github.com/google/go-github/v80/github" "golang.org/x/mod/semver" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" ) type VersionUpdater struct { @@ -131,7 +131,7 @@ func (v *VersionUpdater) updateViaGo(ctx context.Context) error { return domainErrors.NewAppError(domainErrors.TypeUpdate, "go command not found", err) } - cmd := exec.CommandContext(ctx, "go", "install", "github.com/Tomas-vilte/MateCommit@latest") + cmd := exec.CommandContext(ctx, "go", "install", "github.com/thomas-vilte/matecommit@latest") output, err := cmd.CombinedOutput() if err != nil { return domainErrors.NewAppError(domainErrors.TypeUpdate, "go install failed", fmt.Errorf("%s", string(output))) diff --git a/internal/ui/token_stats.go b/internal/ui/token_stats.go index 2d18f2b..cb8a01f 100644 --- a/internal/ui/token_stats.go +++ b/internal/ui/token_stats.go @@ -2,8 +2,8 @@ package ui import ( "fmt" - "github.com/Tomas-vilte/MateCommit/internal/domain/models" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + "github.com/thomas-vilte/matecommit/internal/models" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/fatih/color" ) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 1163832..fd50ffd 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -9,8 +9,8 @@ import ( "strings" "time" - domainErrors "github.com/Tomas-vilte/MateCommit/internal/domain/errors" - "github.com/Tomas-vilte/MateCommit/internal/i18n" + domainErrors "github.com/thomas-vilte/matecommit/internal/errors" + "github.com/thomas-vilte/matecommit/internal/i18n" "github.com/briandowns/spinner" "github.com/fatih/color" )