mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Exclude more commit trailers from auto-wrapping
We already excluded the most commonly used commit trailers from being
auto-wrapped when typing or rewording a commit message, but this was
limited to two hard-coded ones ("Signed-off-by:" and "Co-authored-by:").
Extend this mechanism to use a heuristic to prevent more trailers from
wrapping; the heuristic kicks in for any "Key: Value" line if Key
contains a dash, or the value looks like a URL (so that it also catches
things like "Bug: https://my-bug-tracker/345").
To avoid mistaking a "Key: Value"-looking line in the message body for a
trailer, only apply the heuristic in the last paragraph of the message,
i.e. the block of lines at the end that is separated from the body by a
blank line. Each line there is judged on its own, so a line that isn't
recognized as a trailer still wraps without affecting the real trailers
next to it.
This commit is contained in:
parent
9811bc7ea8
commit
b6deefacd2
|
|
@ -87,6 +87,12 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
|
|||
result = append(result, cells[startOfLine:to]...)
|
||||
}
|
||||
|
||||
// Commit message trailers ("Signed-off-by:" and the like) must not be
|
||||
// auto-wrapped. They are only recognized in the last paragraph of the
|
||||
// message, so that a trailer-looking line in the message body isn't treated
|
||||
// as one; see startOfTrailerBlock.
|
||||
trailerBlockStart := startOfTrailerBlock(content)
|
||||
|
||||
for currentPos, c := range cells {
|
||||
if c.char == "\n" {
|
||||
appendCellsSinceLineStart(currentPos + 1)
|
||||
|
|
@ -98,7 +104,9 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
|
|||
trailerMatcher.reset()
|
||||
} else {
|
||||
currentLineWidth += c.width
|
||||
if c.char == " " && !footNoteMatcher.isFootNote() && !trailerMatcher.isTrailer() {
|
||||
inTrailerBlock := c.contentIndex >= trailerBlockStart
|
||||
if c.char == " " && !footNoteMatcher.isFootNote() &&
|
||||
!(inTrailerBlock && trailerMatcher.isTrailer(content[c.contentIndex+len(c.char):])) {
|
||||
indexOfLastWhitespace = currentPos + 1
|
||||
} else if autoWrapWidth > 0 && currentLineWidth > autoWrapWidth && indexOfLastWhitespace >= 0 {
|
||||
wrapAt := indexOfLastWhitespace
|
||||
|
|
@ -118,7 +126,9 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
|
|||
}
|
||||
|
||||
footNoteMatcher.addCharacter(c.char)
|
||||
trailerMatcher.addCharacter(c.char)
|
||||
if inTrailerBlock {
|
||||
trailerMatcher.addCharacter(c.char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,6 +137,21 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
|
|||
return result, softLineBreakIndices
|
||||
}
|
||||
|
||||
// startOfTrailerBlock returns the byte index into content at which the trailer
|
||||
// block begins, i.e. the start of the last paragraph (the run of lines at the
|
||||
// end of the message that is separated from the body by a blank line). Trailers
|
||||
// are only looked for from this index onwards, so that a trailer-looking line in
|
||||
// the middle of the message body isn't mistaken for a trailer. Trailing blank
|
||||
// lines are ignored, and a message that consists of a single paragraph is
|
||||
// treated as its own trailer block.
|
||||
func startOfTrailerBlock(content string) int {
|
||||
end := len(strings.TrimRight(content, "\n"))
|
||||
if blankLine := strings.LastIndex(content[:end], "\n\n"); blankLine >= 0 {
|
||||
return blankLine + len("\n\n")
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var footNoteRe = regexp.MustCompile(`^\[\d+\]:\s*$`)
|
||||
|
||||
type footNoteMatcher struct {
|
||||
|
|
@ -171,15 +196,11 @@ func (self *footNoteMatcher) reset() {
|
|||
self.didFailToMatch = false
|
||||
}
|
||||
|
||||
var supportedTrailers = []string{
|
||||
"Signed-off-by:",
|
||||
"Co-authored-by:",
|
||||
}
|
||||
|
||||
type trailerMatcher struct {
|
||||
lineStr strings.Builder
|
||||
didFailToMatch bool
|
||||
didMatch bool
|
||||
didFailToMatch bool
|
||||
didMatch bool
|
||||
keyContainsDash bool
|
||||
keyEndsWithColon bool
|
||||
}
|
||||
|
||||
func (self *trailerMatcher) addCharacter(chr string) {
|
||||
|
|
@ -194,19 +215,11 @@ func (self *trailerMatcher) addCharacter(chr string) {
|
|||
return
|
||||
}
|
||||
|
||||
if self.lineStr.Len() == 0 {
|
||||
// If this is the first character, see if it could possibly match any supported trailer; if
|
||||
// not, we can fail early and stop tracking further characters for this line.
|
||||
if !anyOf(supportedTrailers, func(trailer string) bool { return trailer[0] == chr[0] }) {
|
||||
self.didFailToMatch = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.lineStr.WriteString(chr)
|
||||
self.keyContainsDash = self.keyContainsDash || chr == "-"
|
||||
self.keyEndsWithColon = chr == ":"
|
||||
}
|
||||
|
||||
func (self *trailerMatcher) isTrailer() bool {
|
||||
func (self *trailerMatcher) isTrailer(remainingContent string) bool {
|
||||
if self.didFailToMatch {
|
||||
return false
|
||||
}
|
||||
|
|
@ -215,8 +228,9 @@ func (self *trailerMatcher) isTrailer() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
line := self.lineStr.String()
|
||||
if anyOf(supportedTrailers, func(trailer string) bool { return line == trailer }) {
|
||||
remainingContent = strings.TrimLeft(remainingContent, WHITESPACES)
|
||||
if self.keyEndsWithColon && (self.keyContainsDash ||
|
||||
strings.HasPrefix(remainingContent, "http://") || strings.HasPrefix(remainingContent, "https://")) {
|
||||
self.didMatch = true
|
||||
return true
|
||||
}
|
||||
|
|
@ -226,19 +240,10 @@ func (self *trailerMatcher) isTrailer() bool {
|
|||
}
|
||||
|
||||
func (self *trailerMatcher) reset() {
|
||||
self.lineStr.Reset()
|
||||
self.didFailToMatch = false
|
||||
self.didMatch = false
|
||||
}
|
||||
|
||||
func anyOf(strings []string, predicate func(s string) bool) bool {
|
||||
for _, s := range strings {
|
||||
if predicate(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
self.keyContainsDash = false
|
||||
self.keyEndsWithColon = false
|
||||
}
|
||||
|
||||
func (self *TextArea) updateCells() {
|
||||
|
|
|
|||
|
|
@ -945,18 +945,63 @@ func Test_AutoWrapContent(t *testing.T) {
|
|||
expectedSoftLineBreaks: []int{16, 21},
|
||||
},
|
||||
{
|
||||
name: "don't break at space after trailer",
|
||||
content: "abc\nSigned-off-by: John Doe <john@doe.com>\nCo-authored-by: Jane Smith <jane@smith.com>\n",
|
||||
name: "don't break at space after trailer at beginning of message",
|
||||
content: "Signed-off-by: John Doe <john@doe.com>\nDepends-on: Some dependency with spaces\n",
|
||||
autoWrapWidth: 10,
|
||||
expectedWrappedContent: "abc\nSigned-off-by: John Doe <john@doe.com>\nCo-authored-by: Jane Smith <jane@smith.com>\n",
|
||||
expectedWrappedContent: "Signed-off-by: John Doe <john@doe.com>\nDepends-on: Some dependency with spaces\n",
|
||||
expectedSoftLineBreaks: []int{},
|
||||
},
|
||||
{
|
||||
name: "do break at space after trailer if there is no space after the colon",
|
||||
content: "abc\nSigned-off-by:John Doe <john@doe.com>\n",
|
||||
name: "don't break at space after trailer in a trailer block at the end of a message",
|
||||
content: "abc\n\nSigned-off-by: John Doe <john@doe.com>\nDepends-on: Some dependency with spaces\n",
|
||||
autoWrapWidth: 10,
|
||||
expectedWrappedContent: "abc\nSigned-off-by:John \nDoe \n<john@doe.com>\n",
|
||||
expectedSoftLineBreaks: []int{23, 27},
|
||||
expectedWrappedContent: "abc\n\nSigned-off-by: John Doe <john@doe.com>\nDepends-on: Some dependency with spaces\n",
|
||||
expectedSoftLineBreaks: []int{},
|
||||
},
|
||||
{
|
||||
name: "don't break at space after trailer with URL value",
|
||||
content: "abc\n\nBug: https://example.com/a/very/long/path\nIssue: http://example.com/a/very/long/path\n",
|
||||
autoWrapWidth: 10,
|
||||
expectedWrappedContent: "abc\n\nBug: https://example.com/a/very/long/path\nIssue: http://example.com/a/very/long/path\n",
|
||||
expectedSoftLineBreaks: []int{},
|
||||
},
|
||||
{
|
||||
name: "do break at space if trailer is not in a trailer block at the end",
|
||||
content: "abc\n\nSigned-off-by: John Doe <john@doe.com>\n\nMore text here\n",
|
||||
autoWrapWidth: 10,
|
||||
expectedWrappedContent: "abc\n\nSigned-off-by: \nJohn Doe \n<john@doe.com>\n\nMore text \nhere\n",
|
||||
expectedSoftLineBreaks: []int{20, 29, 55},
|
||||
},
|
||||
{
|
||||
// Each line in the trailer block is judged on its own, so a line
|
||||
// that isn't recognized as a trailer wraps without affecting the
|
||||
// real trailers around it.
|
||||
name: "keep a trailer next to a non-trailer line in the same block",
|
||||
content: "abc\n\nFixes: a long description that wraps\nSigned-off-by: John Doe <john@doe.com>\n",
|
||||
autoWrapWidth: 10,
|
||||
expectedWrappedContent: "abc\n\nFixes: a \nlong \ndescription \nthat wraps\nSigned-off-by: John Doe <john@doe.com>\n",
|
||||
expectedSoftLineBreaks: []int{14, 19, 31},
|
||||
},
|
||||
{
|
||||
name: "don't break at space after trailer when the block ends in a blank line",
|
||||
content: "abc\n\nSigned-off-by: John Doe <john@doe.com>\n\n",
|
||||
autoWrapWidth: 10,
|
||||
expectedWrappedContent: "abc\n\nSigned-off-by: John Doe <john@doe.com>\n\n",
|
||||
expectedSoftLineBreaks: []int{},
|
||||
},
|
||||
{
|
||||
name: "do break normal text after non-hyphenated key",
|
||||
content: "However: in this commit blah blah blah\n",
|
||||
autoWrapWidth: 10,
|
||||
expectedWrappedContent: "However: \nin this \ncommit \nblah blah \nblah\n",
|
||||
expectedSoftLineBreaks: []int{9, 17, 24, 34},
|
||||
},
|
||||
{
|
||||
name: "do break at space after trailer if there is no space after the colon",
|
||||
content: "abc\n\nSigned-off-by:John Doe <john@doe.com>\n",
|
||||
autoWrapWidth: 10,
|
||||
expectedWrappedContent: "abc\n\nSigned-off-by:John \nDoe \n<john@doe.com>\n",
|
||||
expectedSoftLineBreaks: []int{24, 28},
|
||||
},
|
||||
{
|
||||
name: "hard line breaks",
|
||||
|
|
|
|||
Loading…
Reference in a new issue