Git Message Drafter
at development 115 lines 3.4 kB view raw
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}