@@ -8,10 +8,12 @@ go 1.25.3
require (
github.com/charmbracelet/fang v0.4.3
+ github.com/microcosm-cc/bluemonday v1.0.27
github.com/spf13/cobra v1.10.1
)
require (
+ github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef // indirect
@@ -21,6 +23,7 @@ require (
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
+ github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
@@ -32,6 +35,7 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.24.0 // indirect
@@ -1,5 +1,7 @@
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
github.com/charmbracelet/fang v0.4.3 h1:qXeMxnL4H6mSKBUhDefHu8NfikFbP/MBNTfqTrXvzmY=
@@ -25,12 +27,16 @@ github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soH
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
@@ -57,6 +63,10 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
@@ -37,11 +37,11 @@ reuse:
build:
# Building formatted-commit
- CGO_ENABLED=0 GOOS={{GOOS}} GOARCH={{GOARCH}} go build -o formatted-commit -ldflags "-s -w -X main.version={{VERSION}}" ./main.go
+ CGO_ENABLED=0 GOOS={{GOOS}} GOARCH={{GOARCH}} go build -o formatted-commit -ldflags "-s -w -X main.version={{VERSION}}"
run *FLAGS:
# Running formatted-commit
- CGO_ENABLED=0 GOOS={{GOOS}} GOARCH={{GOARCH}} go run -ldflags "-s -w -X main.version={{VERSION}}" ./main.go {{FLAGS}}
+ CGO_ENABLED=0 GOOS={{GOOS}} GOARCH={{GOARCH}} go run -ldflags "-s -w -X main.version={{VERSION}}" . {{FLAGS}}
pack:
# Packing formatted-commit
@@ -54,8 +54,27 @@ formatted-commit -t refactor -s "web/git-bug" -m "fancy shmancy" \
return err
}
- _ = subject
+ var commitMsg strings.Builder
+ commitMsg.WriteString(subject)
+
+ if body != "" {
+ formattedBody, err := formatBody(body)
+ if err != nil {
+ return fmt.Errorf("failed to format body: %w", err)
+ }
+ commitMsg.WriteString("\n\n")
+ commitMsg.WriteString(formattedBody)
+ }
+
+ if len(trailers) > 0 {
+ commitMsg.WriteString("\n\n")
+ for _, trailer := range trailers {
+ commitMsg.WriteString(trailer)
+ commitMsg.WriteString("\n")
+ }
+ }
+ fmt.Print(commitMsg.String())
return nil
},
}
@@ -0,0 +1,148 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package main
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/microcosm-cc/bluemonday"
+)
+
+var numberedListRegex = regexp.MustCompile(`^\d+\.\s`)
+
+func formatBody(body string) (string, error) {
+ p := bluemonday.UGCPolicy()
+ sanitized := p.Sanitize(body)
+
+ lines := strings.Split(sanitized, "\n")
+ var result []string
+
+ for _, line := range lines {
+ trimmed := strings.TrimSpace(line)
+ if trimmed == "" {
+ result = append(result, "")
+ continue
+ }
+
+ if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") {
+ marker := trimmed[:2]
+ content := trimmed[2:]
+ wrapped := wrapWithHangingIndent(marker, " ", content, 72)
+ result = append(result, wrapped)
+ continue
+ }
+
+ if numberedListRegex.MatchString(trimmed) {
+ parts := strings.SplitN(trimmed, " ", 2)
+ marker := parts[0] + " "
+ content := ""
+ if len(parts) > 1 {
+ content = parts[1]
+ }
+ indent := strings.Repeat(" ", len(marker))
+ wrapped := wrapWithHangingIndent(marker, indent, content, 72)
+ result = append(result, wrapped)
+ continue
+ }
+
+ result = append(result, wordWrap(trimmed, 72))
+ }
+
+ return strings.Join(result, "\n"), nil
+}
+
+func wrapWithHangingIndent(firstPrefix, contPrefix, text string, width int) string {
+ firstWidth := width - len(firstPrefix)
+ contWidth := width - len(contPrefix)
+
+ words := strings.Fields(text)
+ if len(words) == 0 {
+ return firstPrefix
+ }
+
+ var lines []string
+ var currentLine strings.Builder
+ var currentWidth int
+ isFirstLine := true
+
+ for _, word := range words {
+ wordLen := len(word)
+ maxWidth := firstWidth
+ if !isFirstLine {
+ maxWidth = contWidth
+ }
+
+ if currentLine.Len() == 0 {
+ currentLine.WriteString(word)
+ currentWidth = wordLen
+ } else if currentWidth+1+wordLen <= maxWidth {
+ currentLine.WriteString(" ")
+ currentLine.WriteString(word)
+ currentWidth += 1 + wordLen
+ } else {
+ if isFirstLine {
+ lines = append(lines, firstPrefix+currentLine.String())
+ isFirstLine = false
+ } else {
+ lines = append(lines, contPrefix+currentLine.String())
+ }
+ currentLine.Reset()
+ currentLine.WriteString(word)
+ currentWidth = wordLen
+ }
+ }
+
+ if currentLine.Len() > 0 {
+ if isFirstLine {
+ lines = append(lines, firstPrefix+currentLine.String())
+ } else {
+ lines = append(lines, contPrefix+currentLine.String())
+ }
+ }
+
+ return strings.Join(lines, "\n")
+}
+
+func wordWrap(text string, width int) string {
+ words := strings.Fields(text)
+ if len(words) == 0 {
+ return ""
+ }
+
+ var result strings.Builder
+ var currentLine strings.Builder
+ var currentWidth int
+
+ for _, word := range words {
+ wordLen := len(word)
+
+ if currentLine.Len() == 0 {
+ currentLine.WriteString(word)
+ currentWidth = wordLen
+ } else if currentWidth+1+wordLen <= width {
+ currentLine.WriteString(" ")
+ currentLine.WriteString(word)
+ currentWidth += 1 + wordLen
+ } else {
+ if result.Len() > 0 {
+ result.WriteString("\n")
+ }
+ result.WriteString(currentLine.String())
+ currentLine.Reset()
+ currentLine.WriteString(word)
+ currentWidth = wordLen
+ }
+ }
+
+ if currentLine.Len() > 0 {
+ if result.Len() > 0 {
+ result.WriteString("\n")
+ }
+ result.WriteString(currentLine.String())
+ }
+
+ return result.String()
+}