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		var commitMsg strings.Builder
 58		commitMsg.WriteString(subject)
 59
 60		if body != "" {
 61			formattedBody, err := formatBody(body)
 62			if err != nil {
 63				return fmt.Errorf("failed to format body: %w", err)
 64			}
 65			commitMsg.WriteString("\n\n")
 66			commitMsg.WriteString(formattedBody)
 67		}
 68
 69		if len(trailers) > 0 {
 70			trailersBlock, err := buildTrailersBlock(trailers)
 71			if err != nil {
 72				return fmt.Errorf("failed to build trailers: %w", err)
 73			}
 74			commitMsg.WriteString("\n\n")
 75			commitMsg.WriteString(trailersBlock)
 76		}
 77
 78		fmt.Print(commitMsg.String())
 79		return nil
 80	},
 81}
 82
 83func init() {
 84	rootCmd.Flags().StringVarP(&commitType, "type", "t", "", "commit type (required)")
 85	rootCmd.Flags().StringVarP(&message, "message", "m", "", "commit message (required)")
 86	rootCmd.Flags().StringArrayVarP(&trailers, "trailer", "T", []string{}, "trailer in 'Sentence-case-key: value' format (optional, repeatable)")
 87	rootCmd.Flags().StringVarP(&body, "body", "b", "", "commit body (optional)")
 88	rootCmd.Flags().StringVarP(&scope, "scope", "s", "", "commit scope (optional)")
 89	rootCmd.Flags().BoolVarP(&breakingChange, "breaking", "B", false, "mark as breaking change (optional)")
 90
 91	if err := rootCmd.MarkFlagRequired("type"); err != nil {
 92		panic(err)
 93	}
 94	if err := rootCmd.MarkFlagRequired("message"); err != nil {
 95		panic(err)
 96	}
 97}
 98
 99func buildAndValidateSubject(commitType, scope, message string, breaking bool) (string, error) {
100	var subject strings.Builder
101
102	subject.WriteString(commitType)
103
104	if scope != "" {
105		subject.WriteString("(")
106		subject.WriteString(scope)
107		subject.WriteString(")")
108	}
109
110	if breaking {
111		subject.WriteString("!")
112	}
113
114	subject.WriteString(": ")
115	subject.WriteString(message)
116
117	result := subject.String()
118	length := len(result)
119
120	if length > 50 {
121		exceededBy := length - 50
122		truncated := result[:50] + "…"
123		return "", fmt.Errorf("subject exceeds 50 character limit by %d:\n%s", exceededBy, truncated)
124	}
125
126	return result, nil
127}
128
129func main() {
130	ctx := context.Background()
131
132	var version string
133	if info, ok := debug.ReadBuildInfo(); ok {
134		version = info.Main.Version
135	}
136	if version == "" || version == "(devel)" {
137		version = "dev"
138	}
139
140	if err := fang.Execute(ctx, rootCmd,
141		fang.WithVersion(version),
142		fang.WithoutCompletions(),
143	); err != nil {
144		os.Exit(1)
145	}
146}