blob: 7c93f0dbd80a88a00afc571c348b96a02ebd07ce [file] [log] [blame]
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +00001package claudetool
2
3import (
4 "context"
5 "fmt"
6 "os/exec"
7 "strings"
8
9 "sketch.dev/llm/conversation"
10)
11
12// CommitMessageStyleHint explains how to find commit messages representative of this repository's style.
13func CommitMessageStyleHint(ctx context.Context, repoRoot string) (string, error) {
14 commitSHAs, err := representativeCommitSHAs(ctx, repoRoot)
15 if err != nil {
16 return "", err
17 }
18
19 buf := new(strings.Builder)
20 if len(commitSHAs) > 0 {
21 fmt.Fprint(buf, "To see representative commit messages for this repository, run:\n\n")
22 fmt.Fprintf(buf, "git show -s --format='<commit_message>%%n%%B</commit_message>' %s\n\n", strings.Join(commitSHAs, " "))
23 fmt.Fprint(buf, "Please run this EXACT command and follow their style when writing commit messages.\n")
24 }
25
26 return buf.String(), nil
27}
28
29// representativeCommitSHAs analyze recent commits and selects some representative ones.
30func representativeCommitSHAs(ctx context.Context, repoRoot string) ([]string, error) {
31 cmd := exec.Command("git", "log", "-n", "25", `--format=<commit_message hash="%H">%n%B</commit_message>`)
32 cmd.Dir = repoRoot
33 out, err := cmd.CombinedOutput()
34 if err != nil {
35 return nil, fmt.Errorf("git log failed: %w\n%s", err, out)
36 }
37 commits := strings.TrimSpace(string(out))
38 if commits == "" {
39 return nil, fmt.Errorf("no commits found in repository")
40 }
41
42 info := conversation.ToolCallInfoFromContext(ctx)
43 sub := info.Convo.SubConvo()
Josh Bleecher Snyder593ca642025-05-07 05:19:32 -070044 sub.PromptCaching = false
Josh Bleecher Snyderd7970e62025-05-01 01:56:28 +000045
46 sub.SystemPrompt = `You are an expert Git commit analyzer.
47
48Your task is to analyze the provided commit messages and select the most representative examples that demonstrate this repository's commit style.
49
50Identify consistent patterns in:
51- Formatting conventions
52- Language and tone
53- Structure and organization
54- Special notations or tags
55
56Select up to 3 commit hashes that best exemplify the repository's commit style.
57
58Provide ONLY the commit hashes, one per line. No additional text, formatting, or commentary.
59`
60
61 resp, err := sub.SendUserTextMessage(commits)
62 if err != nil {
63 return nil, fmt.Errorf("error from Claude: %w", err)
64 }
65
66 if len(resp.Content) != 1 {
67 return nil, fmt.Errorf("unexpected response: %v", resp)
68 }
69 response := resp.Content[0].Text
70
71 var result []string
72 for line := range strings.Lines(response) {
73 line = strings.TrimSpace(line)
74 if line == "" {
75 continue
76 }
77 if isHexString(line) && (len(line) >= 7 && len(line) <= 40) {
78 result = append(result, line)
79 }
80 }
81
82 result = result[:min(len(result), 3)]
83 return result, nil
84}
85
86// isHexString reports whether a string only contains hexadecimal characters
87func isHexString(s string) bool {
88 for _, c := range s {
89 if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
90 return false
91 }
92 }
93 return true
94}