Git Message Drafter
1package pkg
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "os"
9 "os/exec"
10 "strings"
11)
12
13// RunGit executes a git command with the given arguments in the specified repository root.
14func RunGit(ctx context.Context, gitBinary, repoRoot string, args ...string) (string, error) {
15 cmd := exec.CommandContext(ctx, gitBinary, args...)
16 cmd.Dir = repoRoot
17
18 var out, stderr bytes.Buffer
19 cmd.Stdout = &out
20 cmd.Stderr = &stderr
21
22 if err := cmd.Run(); err != nil {
23 return "", fmt.Errorf("%w: git %s: %s", err, strings.Join(args, " "), strings.TrimSpace(stderr.String()))
24 }
25
26 return out.String(), nil
27}
28
29// commitMessage executes 'git commit -F -' using the provided message via stdin.
30func commitMessage(ctx context.Context, gitBinary, repoRoot, message string) error {
31 cmd := exec.CommandContext(ctx, gitBinary, "commit", "-F", "-")
32 cmd.Dir = repoRoot
33 cmd.Stdin = strings.NewReader(message)
34 cmd.Stdout = os.Stdout
35 cmd.Stderr = os.Stderr
36 return cmd.Run()
37}
38
39// amendCommitMessage executes 'git commit --amend -F -' using the provided message via stdin.
40func amendCommitMessage(ctx context.Context, gitBinary, repoRoot, message string) error {
41 cmd := exec.CommandContext(ctx, gitBinary, "commit", "--amend", "-F", "-")
42 cmd.Dir = repoRoot
43 cmd.Stdin = strings.NewReader(message)
44 cmd.Stdout = os.Stdout
45 cmd.Stderr = os.Stderr
46 return cmd.Run()
47}
48
49// loadLastCommitContext retrieves the file list and diff of the HEAD commit.
50func loadLastCommitContext(ctx context.Context, gitBinary, repoRoot string) (string, string, error) {
51 if _, err := RunGit(ctx, gitBinary, repoRoot, "rev-parse", "--verify", "HEAD"); err != nil {
52 return "", "", errors.New("no commits found; cannot use -amend without an existing HEAD commit")
53 }
54
55 files, err := RunGit(ctx, gitBinary, repoRoot, "show", "--name-only", "--pretty=format:", "HEAD")
56 if err != nil {
57 return "", "", fmt.Errorf("failed to read last commit files: %w", err)
58 }
59
60 diff, err := RunGit(ctx, gitBinary, repoRoot, "show", "--unified=0", "--no-color", "--pretty=format:", "HEAD")
61 if err != nil {
62 return "", "", fmt.Errorf("failed to read last commit diff: %w", err)
63 }
64
65 if strings.TrimSpace(files) == "" && strings.TrimSpace(diff) == "" {
66 return "", "", errors.New("last commit has no readable content for message generation")
67 }
68
69 return files, diff, nil
70}
71
72// loadCommitHistory fetches the subjects of recent commits to use as style reference.
73func loadCommitHistory(ctx context.Context, gitBinary, repoRoot string, historyCount, maxHistoryBytes int) string {
74 if historyCount <= 0 {
75 return ""
76 }
77
78 history, err := RunGit(
79 ctx,
80 gitBinary,
81 repoRoot,
82 "log",
83 "--no-merges",
84 "-n",
85 fmt.Sprintf("%d", historyCount),
86 "--pretty=format:%s",
87 )
88 if err != nil {
89 return ""
90 }
91
92 history = strings.TrimSpace(history)
93 if history == "" {
94 return ""
95 }
96
97 if maxHistoryBytes > 0 && len(history) > maxHistoryBytes {
98 history = history[:maxHistoryBytes]
99 history = strings.TrimRight(history, "\n")
100 }
101
102 return history
103}
104
105// gitRepoRoot returns the absolute path to the root of the git repository.
106func gitRepoRoot(ctx context.Context, gitBinary string) (string, error) {
107 cmd := exec.CommandContext(ctx, gitBinary, "rev-parse", "--show-toplevel")
108 var out, stderr bytes.Buffer
109 cmd.Stdout = &out
110 cmd.Stderr = &stderr
111 if err := cmd.Run(); err != nil {
112 return "", fmt.Errorf("%w: %s", err, strings.TrimSpace(stderr.String()))
113 }
114 return strings.TrimSpace(out.String()), nil
115}