package services import ( "database/sql" "fmt" "strings" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" goopenai "github.com/sashabaranov/go-openai" ) // GeneratePR generates a PR description as Markdown from commit history and diff. // It streams the LLM output via callback ("content" events) and returns the full markdown. func GeneratePR(db *sql.DB, repoPath, base, head string, userID int64, callback StreamCallback) (string, error) { // Read LLM config (per-user) config, err := GetLLMConfig(db, userID) if err != nil { return "", err } // Open repo repo, err := OpenRepo(repoPath) if err != nil { return "", fmt.Errorf("open repo: %w", err) } // Get commits between base and head commits, err := GetCommitLog(repo, head, 100) if err != nil { return "", fmt.Errorf("get commits: %w", err) } // Filter commits to only those reachable from head but not from base baseHash, err := repo.ResolveRevision(plumbing.Revision(base)) if err == nil { baseCommit, bcErr := repo.CommitObject(*baseHash) if bcErr == nil { baseSet := make(map[string]bool) baseQueue := []*object.Commit{baseCommit} for len(baseQueue) > 0 { c := baseQueue[0] baseQueue = baseQueue[1:] if baseSet[c.Hash.String()] { continue } baseSet[c.Hash.String()] = true for _, p := range c.ParentHashes { pc, err := repo.CommitObject(p) if err == nil { baseQueue = append(baseQueue, pc) } } } var filtered []CommitInfo for _, ci := range commits { if !baseSet[ci.Hash] { filtered = append(filtered, ci) } } if len(filtered) > 0 { commits = filtered } } } // Get diff diff, err := GetDiff(repo, base, head) if err != nil { return "", fmt.Errorf("get diff: %w", err) } // Build prompt var commitLines []string for _, c := range commits { commitLines = append(commitLines, fmt.Sprintf("- %s %s", c.ShortHash, c.Message)) } commitStr := strings.Join(commitLines, "\n") if commitStr == "" { commitStr = "(no commits)" } // Truncate diff if too large (approx 60k chars to stay within token limits) if len(diff) > 60000 { diff = diff[:60000] + "\n\n... [diff truncated due to size]" } prompt := fmt.Sprintf(`你是一个专业的技术文档撰写助手。根据以下 Git 变更信息,生成一份 PR 描述。 ## Commit 记录 %s ## 代码变更 (Diff) %s 请直接输出 Markdown 格式的 PR 描述,包含以下部分: # 标题 **类型**: feat|fix|refactor|docs|chore|style|test|perf ## 概述 一段话概述变更内容 ## 详细说明 按模块分组的详细变更说明 ## 影响范围 影响范围说明 格式要求: - 引用文件名、函数名、变量名等代码标识时,必须用反引号包裹 - 直接写出实际的代码名称,不要用任何占位符替代 - 不要输出 JSON,直接输出 Markdown`, commitStr, diff) messages := []goopenai.ChatCompletionMessage{ {Role: goopenai.ChatMessageRoleUser, Content: prompt}, } // Call LLM with streaming — content events carry markdown chunks in real-time fullResponse, err := ChatStream(config, messages, callback) if err != nil { return "", fmt.Errorf("LLM call: %w", err) } // Signal completion if callback != nil { callback("done", map[string]interface{}{"content": ""}) } return fullResponse, nil }