1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package main
6
7import (
8 "context"
9 "fmt"
10 "os"
11 "runtime/debug"
12 "strings"
13
14 "github.com/charmbracelet/fang"
15 "github.com/spf13/cobra"
16)
17
18var (
19 commitType string
20 message string
21 trailers []string
22 body string
23 scope string
24 breakingChange bool
25)
26
27var rootCmd = &cobra.Command{
28 Use: "formatted-commit",
29 Short: "Create conventionally formatted Git commits",
30 Long: `formatted-commit helps you create well-formatted Git commits that follow
31the Conventional Commits specification with proper subject length validation,
32body wrapping, and trailer formatting.`,
33 Example: `
34# With co-author
35formatted-commit -t feat -m "do a thing" -T "Crush <crush@charm.land>"
36
37# Breaking change with longer body
38formatted-commit -t feat -m "do a thing that borks a thing" -B "$(cat <<'EOF'
39Multi-line
40- Body
41- Here
42
43This is what borked because of new shiny, this is how migrate
44EOF
45)"
46
47# Including scope for more precise changes
48formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \
49 -b "Had to do a weird thing because..."
50`,
51 RunE: func(cmd *cobra.Command, args []string) error {
52 subject, err := buildAndValidateSubject(commitType, scope, message, breakingChange)
53 if err != nil {
54 return err
55 }
56
57 _ = subject
58
59 return nil
60 },
61}
62
63func init() {
64 rootCmd.Flags().StringVarP(&commitType, "type", "t", "", "commit type (required)")
65 rootCmd.Flags().StringVarP(&message, "message", "m", "", "commit message (required)")
66 rootCmd.Flags().StringSliceVarP(&trailers, "trailer", "T", []string{}, "trailer in 'Sentence-case-key: value' format (optional, repeatable)")
67 rootCmd.Flags().StringVarP(&body, "body", "b", "", "commit body (optional)")
68 rootCmd.Flags().StringVarP(&scope, "scope", "s", "", "commit scope (optional)")
69 rootCmd.Flags().BoolVarP(&breakingChange, "breaking", "B", false, "mark as breaking change (optional)")
70
71 if err := rootCmd.MarkFlagRequired("type"); err != nil {
72 panic(err)
73 }
74 if err := rootCmd.MarkFlagRequired("message"); err != nil {
75 panic(err)
76 }
77}
78
79func buildAndValidateSubject(commitType, scope, message string, breaking bool) (string, error) {
80 var subject strings.Builder
81
82 subject.WriteString(commitType)
83
84 if scope != "" {
85 subject.WriteString("(")
86 subject.WriteString(scope)
87 subject.WriteString(")")
88 }
89
90 if breaking {
91 subject.WriteString("!")
92 }
93
94 subject.WriteString(": ")
95 subject.WriteString(message)
96
97 result := subject.String()
98 length := len(result)
99
100 if length > 50 {
101 exceededBy := length - 50
102 truncated := result[:50] + "…"
103 return "", fmt.Errorf("subject exceeds 50 character limit by %d:\n%s", exceededBy, truncated)
104 }
105
106 return result, nil
107}
108
109func main() {
110 ctx := context.Background()
111
112 var version string
113 if info, ok := debug.ReadBuildInfo(); ok {
114 version = info.Main.Version
115 }
116 if version == "" || version == "(devel)" {
117 version = "dev"
118 }
119
120 if err := fang.Execute(ctx, rootCmd,
121 fang.WithVersion(version),
122 fang.WithoutCompletions(),
123 ); err != nil {
124 os.Exit(1)
125 }
126}