diff --git a/main.go b/main.go index da9ab7cedfaea036da8c486e0ee88879f51eef29..00e67aa8ec3773fe5ece3008153bfe69a8c891bb 100644 --- a/main.go +++ b/main.go @@ -67,11 +67,12 @@ formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \ } if len(trailers) > 0 { - commitMsg.WriteString("\n\n") - for _, trailer := range trailers { - commitMsg.WriteString(trailer) - commitMsg.WriteString("\n") + trailersBlock, err := buildTrailersBlock(trailers) + if err != nil { + return fmt.Errorf("failed to build trailers: %w", err) } + commitMsg.WriteString("\n\n") + commitMsg.WriteString(trailersBlock) } fmt.Print(commitMsg.String()) @@ -82,7 +83,7 @@ formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \ func init() { rootCmd.Flags().StringVarP(&commitType, "type", "t", "", "commit type (required)") rootCmd.Flags().StringVarP(&message, "message", "m", "", "commit message (required)") - rootCmd.Flags().StringSliceVarP(&trailers, "trailer", "T", []string{}, "trailer in 'Sentence-case-key: value' format (optional, repeatable)") + rootCmd.Flags().StringArrayVarP(&trailers, "trailer", "T", []string{}, "trailer in 'Sentence-case-key: value' format (optional, repeatable)") rootCmd.Flags().StringVarP(&body, "body", "b", "", "commit body (optional)") rootCmd.Flags().StringVarP(&scope, "scope", "s", "", "commit scope (optional)") rootCmd.Flags().BoolVarP(&breakingChange, "breaking", "B", false, "mark as breaking change (optional)") diff --git a/trailers.go b/trailers.go new file mode 100644 index 0000000000000000000000000000000000000000..82b6b7299866e223d3a43fa3dca4334dbd2dabde --- /dev/null +++ b/trailers.go @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package main + +import ( + "fmt" + "strings" + "unicode" +) + +func validateTrailer(trailer string) error { + if trailer == "" { + return fmt.Errorf("trailer cannot be empty") + } + + lines := strings.Split(trailer, "\n") + firstLine := lines[0] + + if len(firstLine) > 0 && unicode.IsSpace(rune(firstLine[0])) { + return fmt.Errorf("trailer key cannot start with whitespace: %q", trailer) + } + + colonIdx := strings.Index(firstLine, ":") + if colonIdx == -1 { + return fmt.Errorf("trailer must contain ':' separator: %q", trailer) + } + + key := firstLine[:colonIdx] + + if strings.TrimSpace(key) != key { + return fmt.Errorf("trailer key cannot have leading or trailing whitespace: %q", key) + } + + if strings.ContainsAny(key, " \t") { + return fmt.Errorf("trailer key cannot contain whitespace: %q", key) + } + + if key == "" { + return fmt.Errorf("trailer key cannot be empty: %q", trailer) + } + + for i, line := range lines[1:] { + if line == "" { + continue + } + if len(line) > 0 && !unicode.IsSpace(rune(line[0])) { + return fmt.Errorf("trailer continuation line %d must start with whitespace: %q", i+2, line) + } + } + + return nil +} + +func buildTrailersBlock(trailers []string) (string, error) { + if len(trailers) == 0 { + return "", nil + } + + for _, trailer := range trailers { + if err := validateTrailer(trailer); err != nil { + return "", err + } + } + + return strings.Join(trailers, "\n"), nil +}