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}