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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/cli/command/pull_requests/summarize.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (c *SummarizeCommand) CreateCommand(t *i18n.Translations, _ *cfg.Config) *c
}
prNumber := command.Int("pr-number")

summary, err := prService.SummarizePR(ctx, int(prNumber))
summary, err := prService.SummarizePR(ctx, prNumber)
if err != nil {
return fmt.Errorf(t.GetMessage("error.pr_summary_error", 0, nil)+": %w", err)
}
Expand Down
11 changes: 7 additions & 4 deletions internal/domain/models/pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package models
type (
// PRData contiene la información extraída de una Pull Request.
PRData struct {
ID int
Creator string
Commits []Commit
Diff string
ID int
Creator string
Commits []Commit
Diff string
BranchName string
RelatedIssues []Issue
PRDescription string
}

// Commit representa un commit incluido en el PR.
Expand Down
2 changes: 2 additions & 0 deletions internal/domain/ports/vcs_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ type VCSClient interface {
GetIssue(ctx context.Context, issueNumber int) (*models.Issue, error)
// GetFileAtTag obtiene el contenido de un archivo en un tag específico
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(ctx context.Context, branchName string, commits []string, prDescription string) ([]models.Issue, error)
}
13 changes: 13 additions & 0 deletions internal/i18n/locales/active.en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,16 @@ branch_not_detected = "Could not detect branch name"
get_repo_url = "Error getting repository URL: {{.Error}}"
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"
13 changes: 13 additions & 0 deletions internal/i18n/locales/active.es.toml
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,16 @@ branch_not_detected = "No se pudo detectar el nombre de la branch"
get_repo_url = "Error al obtener la URL del repositorio: {{.Error}}"
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"
97 changes: 97 additions & 0 deletions internal/infrastructure/ai/prompts.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package ai

import (
"fmt"
"strings"

"github.com/Tomas-vilte/MateCommit/internal/domain/models"
)

// Issue reference instructions
const (
issueReferenceInstructionsES = `Si hay un issue asociado (#%d), DEBES incluir la referencia en el título del commit:
Expand Down Expand Up @@ -503,3 +510,93 @@ func GetIssueReferenceInstructions(lang string) string {
return issueReferenceInstructionsEN
}
}

const (
prIssueContextInstructionsES = `
**IMPORTANTE - Contexto de Issues/Tickets:**
Este PR está relacionado con los siguientes issues:
%s

**INSTRUCCIONES OBLIGATORIAS:**
1. DEBES incluir AL INICIO del resumen (primeras líneas) las referencias de cierre:
- Si resuelve bugs: "Fixes #N"
- Si implementa features: "Closes #N"
- Si solo relaciona: "Relates to #N"
- Formato: "Closes #39, Fixes #41" (separados por comas)

2. En la sección de cambios clave, menciona explícitamente cómo cada cambio aborda el issue

3. Usa el formato correcto para que GitHub auto-enlace los issues en la sección "Development"

**Ejemplo de formato correcto:**
Closes #39

- **Primer cambio clave:**
- Propósito: Resolver el problema reportado en #39...
- Impacto técnico: ...
`

prIssueContextInstructionsEN = `
**IMPORTANT - Issue/Ticket Context:**
This PR is related to the following issues:
%s

**MANDATORY INSTRUCTIONS:**
1. You MUST include at the BEGINNING of the summary (first lines) the closing references:
- If fixing bugs: "Fixes #N"
- If implementing features: "Closes #N"
- If just relating: "Relates to #N"
- Format: "Closes #39, Fixes #41" (comma separated)

2. In the key changes section, explicitly mention how each change addresses the issue

3. Use the correct format so GitHub auto-links the issues in the "Development" section

**Example of correct format:**
Closes #39

- **First key change:**
- Purpose: Resolve the problem reported in #39...
- Technical impact: ...
`
)

// GetPRIssueContextInstructions devuelve las instrucciones de contexto de issues para PRs
func GetPRIssueContextInstructions(locale string) string {
if locale == "es" {
return prIssueContextInstructionsES
}
return prIssueContextInstructionsEN
}

// FormatIssuesForPrompt formatea la lista de issues para incluir en el prompt
func FormatIssuesForPrompt(issues []models.Issue, locale string) string {
if len(issues) == 0 {
return ""
}

var result strings.Builder
for _, issue := range issues {
if locale == "es" {
result.WriteString(fmt.Sprintf("- Issue #%d: %s\n", issue.Number, issue.Title))
if issue.Description != "" {
desc := issue.Description
if len(desc) > 200 {
desc = desc[:200] + "..."
}
result.WriteString(fmt.Sprintf(" Descripción: %s\n", desc))
}
} else {
result.WriteString(fmt.Sprintf("- Issue #%d: %s\n", issue.Number, issue.Title))
if issue.Description != "" {
desc := issue.Description
if len(desc) > 200 {
desc = desc[:200] + "..."
}
result.WriteString(fmt.Sprintf(" Description: %s\n", desc))
}
}
}

return result.String()
}
5 changes: 5 additions & 0 deletions internal/infrastructure/dependency/gomod_analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ func (m *MockVCSClient) GetIssue(ctx context.Context, issueNumber int) (*models.
return args.Get(0).(*models.Issue), args.Error(1)
}

func (m *MockVCSClient) GetPRIssues(ctx context.Context, branchName string, commits []string, prDescription string) ([]models.Issue, error) {
args := m.Called(ctx, branchName, commits, prDescription)
return args.Get(0).([]models.Issue), args.Error(1)
}

func TestGoModAnalyzer_Name(t *testing.T) {
analyzer := NewGoModAnalyzer()
assert.Equal(t, "go.mod", analyzer.Name())
Expand Down
2 changes: 1 addition & 1 deletion internal/infrastructure/factory/pr_service_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ func (f *prServiceFactory) CreatePRService(ctx context.Context) (ports.PRService
return nil, fmt.Errorf("proveedor de VCS no compatible: %s", provider)
}

return services.NewPRService(vcsClient, f.aiService, f.trans), nil
return services.NewPRService(vcsClient, f.aiService, f.trans, f.config), nil
}
90 changes: 86 additions & 4 deletions internal/infrastructure/vcs/github/github_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"context"
"fmt"
"net/http"
"regexp"
"sort"
"strconv"
"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/infrastructure/httpclient"
"github.com/google/go-github/v80/github"
"golang.org/x/oauth2"
)
Expand Down Expand Up @@ -52,6 +55,8 @@ type GitHubClient struct {
owner string
repo string
trans *i18n.Translations
token string
httpClient httpclient.HTTPClient
}

var allowedLabels = map[string]struct {
Expand Down Expand Up @@ -82,6 +87,8 @@ func NewGitHubClient(owner, repo, token string, trans *i18n.Translations) *GitHu
owner: owner,
repo: repo,
trans: trans,
token: token,
httpClient: httpClient,
}
}

Expand All @@ -102,6 +109,8 @@ func NewGitHubClientWithServices(
owner: owner,
repo: repo,
trans: trans,
token: "",
httpClient: &http.Client{},
}
}

Expand Down Expand Up @@ -176,10 +185,12 @@ func (ghc *GitHubClient) GetPR(ctx context.Context, prNumber int) (models.PRData
}

return models.PRData{
ID: prNumber,
Creator: pr.GetUser().GetLogin(),
Commits: prCommits,
Diff: diff,
ID: prNumber,
Creator: pr.GetUser().GetLogin(),
Commits: prCommits,
Diff: diff,
BranchName: pr.GetHead().GetRef(),
PRDescription: pr.GetBody(),
}, nil
}

Expand Down Expand Up @@ -527,6 +538,77 @@ func (ghc *GitHubClient) GetFileAtTag(ctx context.Context, tag, filepath string)
return content, nil
}

func (ghc *GitHubClient) GetPRIssues(ctx context.Context, branchName string, commits []string, prDescription string) ([]models.Issue, error) {
issueNumbers := make(map[int]bool)

branchPatterns := []string{
`#(\d+)`,
`issue[/-](\d+)`,
`^(\d+)-`,
`/(\d+)-`,
`-(\d+)-`,
}

for _, pattern := range branchPatterns {
re := regexp.MustCompile(pattern)
if matches := re.FindStringSubmatch(branchName); len(matches) > 1 {
if num, err := strconv.Atoi(matches[1]); err == nil {
issueNumbers[num] = true
}
}
}

if prDescription != "" {
descPatterns := []string{
`(?i)(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)`,
`#(\d+)`,
}

for _, pattern := range descPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(prDescription, -1)
for _, match := range matches {
if len(match) > 1 {
if num, err := strconv.Atoi(match[1]); err == nil {
issueNumbers[num] = true
}
}
}
}
}

commitPatterns := []string{
`(?i)(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)`,
`\(#(\d+)\)`,
`#(\d+)`,
}

for _, commit := range commits {
for _, pattern := range commitPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(commit, -1)
for _, match := range matches {
if len(match) > 1 {
if num, err := strconv.Atoi(match[1]); err == nil {
issueNumbers[num] = true
}
}
}
}
}

var issues []models.Issue
for issueNum := range issueNumbers {
issue, err := ghc.GetIssue(ctx, issueNum)
if err != nil {
continue
}
issues = append(issues, *issue)
}

return issues, nil
}

func (ghc *GitHubClient) labelExists(existingLabels []string, target string) bool {
for _, l := range existingLabels {
if strings.EqualFold(l, target) {
Expand Down
Loading
Loading