blob: eaea6ca0490b340692cea2365946b709d3305f3f [file] [log] [blame]
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +00001package claudetool
2
3import (
4 "context"
5 "fmt"
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +00006 "log/slog"
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +00007 "os/exec"
8 "strings"
9
10 "sketch.dev/llm/conversation"
11)
12
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000013// CommitMessageStyleHint provides example commit messages representative of this repository's style.
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000014func CommitMessageStyleHint(ctx context.Context, repoRoot string) (string, error) {
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000015 commitSHAs, analysis, err := representativeCommitSHAs(ctx, repoRoot)
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000016 if err != nil {
17 return "", err
18 }
19
20 buf := new(strings.Builder)
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000021 if len(commitSHAs) == 0 {
22 return "", nil
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000023 }
24
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000025 if analysis != "" {
26 fmt.Fprintf(buf, "<commit_message_style_analysis>%s</commit_message_style_analysis>\n\n", analysis)
27 }
28
Josh Bleecher Snyder586ecb12025-05-22 21:04:33 -070029 args := []string{"show", "-s", "--format='<commit_message_style_example>%n%B</commit_message_style_example>'"}
30 args = append(args, commitSHAs...)
31 cmd := exec.Command("git", args...)
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000032 cmd.Dir = repoRoot
33 out, err := cmd.CombinedOutput()
34 if err == nil {
Josh Bleecher Snyderefa8f432025-05-28 18:04:41 -070035 // Filter out git trailers from examples
36 cleanedExamples := filterGitTrailers(string(out))
37 fmt.Fprintf(buf, "<commit_message_style_examples>%s</commit_message_style_examples>\n\n", cleanedExamples)
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000038 } else {
39 slog.DebugContext(ctx, "failed to get commit messages", "shas", commitSHAs, "out", string(out), "err", err)
40 }
41
42 fmt.Fprint(buf, "IMPORTANT: Follow this commit message style for ALL git commits you create.\n")
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000043 return buf.String(), nil
44}
45
46// representativeCommitSHAs analyze recent commits and selects some representative ones.
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000047// It returns a list of commit SHAs and the analysis text.
48func representativeCommitSHAs(ctx context.Context, repoRoot string) ([]string, string, error) {
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000049 cmd := exec.Command("git", "log", "-n", "25", `--format=<commit_message hash="%H">%n%B</commit_message>`)
50 cmd.Dir = repoRoot
51 out, err := cmd.CombinedOutput()
52 if err != nil {
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000053 return nil, "", fmt.Errorf("git log failed: %w\n%s", err, out)
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000054 }
55 commits := strings.TrimSpace(string(out))
56 if commits == "" {
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000057 return nil, "", fmt.Errorf("no commits found in repository")
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000058 }
59
60 info := conversation.ToolCallInfoFromContext(ctx)
61 sub := info.Convo.SubConvo()
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +000062 sub.Hidden = true
Josh Bleecher Snyder593ca642025-05-07 05:19:32 -070063 sub.PromptCaching = false
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000064
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000065 sub.SystemPrompt = `Analyze the provided git commit messages to identify consistent patterns, including but not limited to:
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000066- Formatting conventions
67- Language and tone
68- Structure and organization
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000069- Length and detail
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000070- Special notations or tags
Josh Bleecher Snyder7ce7b022025-05-16 11:35:51 -070071- Capitalization and punctuation
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000072
Josh Bleecher Snyderefa8f432025-05-28 18:04:41 -070073Do NOT mention in any way Change-ID or Co-authored-by git trailer lines in your analysis, not even their existence.
74Those are added automatically by git hooks; they are NOT part of the commit message style.
Josh Bleecher Snydere10f0e62025-05-21 10:57:09 -070075
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000076First, provide a concise analysis of the predominant patterns.
77Then select up to 3 commit hashes that best exemplify the repository's commit style.
78Finally, output these selected commit hashes, one per line, without commentary.
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000079`
80
81 resp, err := sub.SendUserTextMessage(commits)
82 if err != nil {
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000083 return nil, "", fmt.Errorf("error from Claude: %w", err)
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000084 }
85
86 if len(resp.Content) != 1 {
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000087 return nil, "", fmt.Errorf("unexpected response: %v", resp)
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000088 }
89 response := resp.Content[0].Text
90
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000091 // Split into analysis and commit hashes
92 var analysisLines []string
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000093 var result []string
94 for line := range strings.Lines(response) {
95 line = strings.TrimSpace(line)
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000096 if isHexString(line) && (len(line) >= 7 && len(line) <= 40) {
97 result = append(result, line)
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +000098 } else {
99 analysisLines = append(analysisLines, line)
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +0000100 }
101 }
102
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +0000103 analysis := strings.Join(analysisLines, "\n")
104
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +0000105 result = result[:min(len(result), 3)]
Josh Bleecher Snyder924a7702025-05-06 02:14:40 +0000106 return result, analysis, nil
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +0000107}
108
Josh Bleecher Snyderefa8f432025-05-28 18:04:41 -0700109// filterGitTrailers removes git trailers (Co-authored-by, Change-ID) from commit message examples
110func filterGitTrailers(input string) string {
111 buf := new(strings.Builder)
112 for line := range strings.Lines(input) {
113 lowerLine := strings.ToLower(line)
114 if strings.HasPrefix(lowerLine, "co-authored-by:") || strings.HasPrefix(lowerLine, "change-id:") {
115 continue
116 }
117 buf.WriteString(line)
118 }
119
120 return buf.String()
121}
122
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +0000123// isHexString reports whether a string only contains hexadecimal characters
124func isHexString(s string) bool {
125 for _, c := range s {
126 if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
127 return false
128 }
129 }
130 return true
131}