@@ -71,7 +71,7 @@ The body (`-b` flag) processing pipeline:
- **Blank lines**: Preserved as-is
3. **Word wrapping algorithm**: Greedy wrapping splits on word boundaries, never mid-word
4. **Hanging indent logic**: For bullets/numbered lists, first line gets the marker, continuation lines get spaces equal to marker width
-5. **Spacing**: Body separated from subject by one blank line, from trailers by one blank line
+5. **Spacing**: Body separated from subject by one blank line, from breaking change footer (if present) by one blank line, from trailers by one blank line
Example wrapped bullet:
@@ -106,12 +106,37 @@ Co-authored-by: This is a very long value, with spaces and
- `-t` / `--type`: Commit type (required) - e.g., `feat`, `fix`, `refactor`
- `-m` / `--message`: Commit message (required) - the description after the colon
- `-s` / `--scope`: Commit scope (optional) - goes in parentheses after type
-- `-B` / `--breaking`: Boolean flag for breaking changes (adds `!` to subject)
+- `-B` / `--breaking`: String flag for breaking change description (optional) - adds `!` to subject AND creates `BREAKING CHANGE:` footer
- `-b` / `--body`: String flag for commit body text (can use heredoc for multiline)
- `-T` / `--trailer`: Repeatable flag accepting full trailer strings in `Key: value` format (not separate key/value args)
+- `-a` / `--amend`: Boolean flag to amend the previous commit instead of creating a new one
-Trailer format detail: Each `-T` flag takes a complete trailer string like `-T "Co-authored-by: Name <email>"`, NOT separate key and value arguments.
+Breaking change detail: The `-B` flag value becomes the description in a `BREAKING CHANGE:` footer (with space, per Conventional Commits spec). This footer is distinct from git trailers and allows spaces in the key. It's inserted between the body and trailers.
+
+Trailer format detail: Each `-T` flag takes a complete trailer string like `-T "Assisted-by: Claude Sonnet 4.5 via Crush"`, NOT separate key and value arguments.
### Final Output
-The formatted commit message must be piped to `git commit -F -` to read from stdin.
+The formatted commit message structure:
+
+1. Subject: `type(scope)!: message` (or `type!: message` if no scope)
+2. Blank line
+3. Body (if `-b` provided)
+4. Blank line (if body or breaking change present)
+5. `BREAKING CHANGE: description` footer (if `-B` provided)
+6. Blank line (if trailers present)
+7. Trailers (if `-T` provided)
+
+Example with all components:
+```
+feat!: restructure config
+
+Improves readability and supports comments
+
+BREAKING CHANGE: Configuration format changed from JSON to TOML.
+Migrate by running: ./migrate-config.sh
+
+Assisted-by: Claude Sonnet 4.5 via Crush
+```
+
+The formatted commit message must be piped to `git commit -F -` to read from stdin. When the `-a`/`--amend` flag is used, it pipes to `git commit --amend -F -` instead.
@@ -22,7 +22,8 @@ var (
trailers []string
body string
scope string
- breakingChange bool
+ breakingChange string
+ amend bool
)
var rootCmd = &cobra.Command{
@@ -32,18 +33,19 @@ var rootCmd = &cobra.Command{
the Conventional Commits specification with proper subject length validation,
body wrapping, and trailer formatting.`,
Example: `
-# With co-author
-formatted-commit -t feat -m "do a thing" -T "Crush <crush@charm.land>"
+# With assisted-by trailer
+formatted-commit -t feat -m "do a thing" -T "Assisted-by: Veldt via Crush"
-# Breaking change with longer body
-formatted-commit -t feat -m "do a thing that borks a thing" -B "$(cat <<'EOF'
-Multi-line
-- Body
-- Here
+# Breaking change with description
+formatted-commit -t feat -m "remove deprecated API" \
+ -B "The old /v1/users endpoint is removed. Use /v2/users instead."
-This is what borked because of new shiny, this is how migrate
+# Breaking change with multi-line description using heredoc
+formatted-commit -t feat -m "restructure config format" -B "$(cat <<'EOF'
+Configuration format changed from JSON to TOML.
+Migrate by running: ./migrate-config.sh
EOF
-)"
+)" -b "Improves readability and supports comments"
# Including scope for more precise changes
formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \
@@ -55,10 +57,6 @@ formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \
return err
}
- if breakingChange && !hasBreakingChangeFooter(body) {
- return fmt.Errorf("breaking change flag (-B) requires a BREAKING CHANGE: or CHANGES: footer at the end of the body. It instructs users how to resolve the breaking changes resulting from this commit")
- }
-
var commitMsg strings.Builder
commitMsg.WriteString(subject)
@@ -71,6 +69,16 @@ formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \
commitMsg.WriteString(formattedBody)
}
+ if breakingChange != "" {
+ formattedBreaking, err := formatBody(breakingChange)
+ if err != nil {
+ return fmt.Errorf("failed to format breaking change: %w", err)
+ }
+ commitMsg.WriteString("\n\n")
+ commitMsg.WriteString("BREAKING CHANGE: ")
+ commitMsg.WriteString(formattedBreaking)
+ }
+
if len(trailers) > 0 {
trailersBlock, err := buildTrailersBlock(trailers)
if err != nil {
@@ -80,7 +88,13 @@ formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \
commitMsg.WriteString(trailersBlock)
}
- gitCmd := exec.Command("git", "commit", "-F", "-")
+ gitArgs := []string{"commit"}
+ if amend {
+ gitArgs = append(gitArgs, "--amend")
+ }
+ gitArgs = append(gitArgs, "-F", "-")
+
+ gitCmd := exec.Command("git", gitArgs...)
gitCmd.Stdout = os.Stdout
gitCmd.Stderr = os.Stderr
@@ -115,7 +129,8 @@ func init() {
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)")
+ rootCmd.Flags().StringVarP(&breakingChange, "breaking", "B", "", "breaking change description (optional, adds BREAKING CHANGE footer)")
+ rootCmd.Flags().BoolVarP(&amend, "amend", "a", false, "amend the previous commit (optional)")
if err := rootCmd.MarkFlagRequired("type"); err != nil {
panic(err)
@@ -125,7 +140,7 @@ func init() {
}
}
-func buildAndValidateSubject(commitType, scope, message string, breaking bool) (string, error) {
+func buildAndValidateSubject(commitType, scope, message string, breaking string) (string, error) {
var subject strings.Builder
subject.WriteString(commitType)
@@ -136,7 +151,7 @@ func buildAndValidateSubject(commitType, scope, message string, breaking bool) (
subject.WriteString(")")
}
- if breaking {
+ if breaking != "" {
subject.WriteString("!")
}
@@ -155,17 +170,6 @@ func buildAndValidateSubject(commitType, scope, message string, breaking bool) (
return result, nil
}
-func hasBreakingChangeFooter(body string) bool {
- lines := strings.Split(body, "\n")
- for _, line := range lines {
- trimmed := strings.TrimSpace(line)
- if strings.HasPrefix(trimmed, "BREAKING CHANGE:") || strings.HasPrefix(trimmed, "BREAKING CHANGES:") {
- return true
- }
- }
- return false
-}
-
func main() {
ctx := context.Background()