main.go

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