diff --git a/CHANGELOG.md b/CHANGELOG.md index 28fc624..110f93e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ understanding of patterns often seen in commit messages. place, and breaking is actively detrimental to URLs. This change alone yields a 6-8 times speed-up as reported by `time(1)`. +- _References_ may span multiple lines, subsequent lines following the same + indentation rules as _list items_. The syntactical unit has been renamed to + _footnote_ to better capture the new functionality. + ## 1.1.0 - 2018-08-25 - #3: If the `core.commentChar` setting is set to an explicit character, use diff --git a/README.md b/README.md index d4ef7d5..1a73530 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ In summary, `commitmsgfmt` - properly indents continuation lines in numbered and unnumbered lists, recognizing several different list styles; -- exempts comments; text indented at least 4 spaces or 1 tab; "trailers" - (`Signed-off-by:`); and IEEE-style references from reformatting; +- exempts comments; text indented at least 4 spaces or 1 tab; and "trailers" + (`Signed-off-by:`); - assumes UTF-8 encoded input but can gracefully degrade to ISO-8859-1 ("latin1") which has been observed in the Linux kernel; diff --git a/doc/commitmsgfmt.1.adoc b/doc/commitmsgfmt.1.adoc index 86a0cbe..324362a 100644 --- a/doc/commitmsgfmt.1.adoc +++ b/doc/commitmsgfmt.1.adoc @@ -100,12 +100,11 @@ not a hard rule, and {self} tries not be a stick. Except for the subject line, items can appear in any order. -=== Reference +=== Footnotes A line that starts with a left bracket (*[*) followed by one or more non-whitespace characters, a right bracket (*]*), an optional colon (*:*), and -a space is considered a _reference_, similar in style to the one IEEE -whitepapers often use: +a space is considered a _footnote_: ---- Official Git Web site [1][git] @@ -114,10 +113,13 @@ Official Git Web site [1][git] [git]: https://git-scm.com/ ---- -References are not wrapped, on account of frequently being URLs. References are -not considered to have a natural order and correspondingly are not sorted, and -the identifier between the brackets is not cross-checked with the rest of the -message. +This syntax is inspired by the IEEE whitepaper reference style. + +Footnotes are wrapped like _paragraphs_, with continuation lines space-indented +to match the first line. Additional spaces after the mandatory space are +stripped. Footnotes are not considered to have a natural order and +correspondingly are not sorted, and the identifier between the brackets is not +cross-checked with the rest of the message. === Trailer diff --git a/src/commitmsgfmt.rs b/src/commitmsgfmt.rs index 10dca71..0af7134 100644 --- a/src/commitmsgfmt.rs +++ b/src/commitmsgfmt.rs @@ -51,7 +51,14 @@ impl CommitMsgFmt { self.wrap_paragraph_into(&mut buf, &p, None); buf.push('\n'); } - Reference(ref s) | Subject(ref s) | Trailer(ref s) => { + Footnote(ref key, ref rest) => { + buf.push_str(&key); + buf.push(' '); + let continuation = " ".repeat(key.graphemes(true).count() + 1); + self.wrap_paragraph_into(&mut buf, &rest.trim(), Some(&continuation)); + buf.push('\n'); + } + Subject(ref s) | Trailer(ref s) => { buf.push_str(s.as_str()); buf.push('\n'); } @@ -177,20 +184,37 @@ foo } #[test] - fn formats_references() { - // References don't wrap. Many are just URLs, which don't wrap nicely, - // or short sentences that don't qualify for wrapping, but some - // references are entire paragraphs of prose and ought to be wrapped. - // See: git -C ../git/ log --format=%b | grep '^\[[^]]\+\] ' + fn formats_footnotes() { let msg = " foo [2] note [1] note -[reference] reference extending beyond line-wrapping limit +[3] foo bar baz qux https://a.really-long-url.example +[4] https://a.really-long-url.example +[footnote] footnote extending + beyond line-wrapping + limit +[ä] multi-code-point footnote key +"; + let expected = " +foo + +[2] note +[1] note +[3] foo bar baz qux + https://a.really-long-url.example +[4] https://a.really-long-url.example +[footnote] footnote + extending + beyond + line-wrapping + limit +[ä] multi-code-point + footnote key "; - assert_eq!(filter(10, &msg), msg, "print references literally"); + assert_eq!(filter(20, &msg), expected); } #[test] diff --git a/src/parser.rs b/src/parser.rs index 6d3cd2f..deec7ce 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -10,7 +10,7 @@ pub struct ListIndent(pub String); #[derive(Debug, PartialEq)] pub enum Token { Comment(String), - Reference(String), + Footnote(String, String), ListItem(ListIndent, ListType, String), Literal(String), Paragraph(String), @@ -27,7 +27,7 @@ pub fn parse(input: &str, comment_char: char) -> Vec { let mut has_scissors = false; let lines = input.lines(); let blank_or_empty = Regex::new(r"^\s*$").unwrap(); - let reference = Regex::new(r"^\[[^]]+\]:? .+$").unwrap(); + let footnote = Regex::new(r"^\[[^]]+\]:? .+$").unwrap(); let trailer = Regex::new(r"^\p{Alphabetic}[-\w]+: .+$").unwrap(); let indented = Regex::new(r"^(?:\t| {4,})").unwrap(); let list_item = Regex::new( @@ -81,11 +81,20 @@ pub fn parse(input: &str, comment_char: char) -> Vec { } else if !has_subject { parse_subject(line, &mut toks); has_subject = true; - } else if reference.is_match(line) { - toks.push(Token::Reference(line.to_owned())); + } else if footnote.is_match(line) { + debug_assert!(footnote.as_str().contains(' ')); + let mut splitter = line.splitn(2, ' '); + let key = splitter.next().unwrap().to_owned(); + let rest = splitter.next().unwrap().trim().to_owned(); + toks.push(Token::Footnote(key, rest)); } else if trailer.is_match(line) { toks.push(Token::Trailer(line.to_owned())); } else if let Some(y) = match toks.last_mut() { + Some(&mut Token::Footnote(_, ref mut b)) => { + b.push(' '); + b.push_str(line.trim()); + None + } Some(&mut Token::Paragraph(ref mut b)) => { b.push(' '); b.push_str(line.trim()); @@ -489,20 +498,21 @@ Signed-off-by: Jane Doe } #[test] - fn references_are_left_bracket_ident_right_bracket_space_text() { + fn footnotes_are_left_bracket_ident_right_bracket_space_text() { assert_eq!( parse( " subject -[1] reference -[re-fe] rence +[1] footnote +[fo-ot] note +[ä] multi-code-point footnote key -[@]: reference +[@]: footnote -[] not a reference +[] not a footnote -[1]not a reference +[1]not a footnote [1] " @@ -511,14 +521,18 @@ subject VerticalSpace, Subject("subject".to_owned()), VerticalSpace, - Reference("[1] reference".to_owned()), - Reference("[re-fe] rence".to_owned()), + Footnote("[1]".to_owned(), "footnote".to_owned()), + Footnote("[fo-ot]".to_owned(), "note".to_owned()), + Footnote( + "[ä]".to_owned(), + "multi-code-point footnote key".to_owned() + ), VerticalSpace, - Reference("[@]: reference".to_owned()), + Footnote("[@]:".to_owned(), "footnote".to_owned()), VerticalSpace, - Paragraph("[] not a reference".to_owned()), + Paragraph("[] not a footnote".to_owned()), VerticalSpace, - Paragraph("[1]not a reference".to_owned()), + Paragraph("[1]not a footnote".to_owned()), VerticalSpace, Paragraph("[1]".to_owned()), ], @@ -526,7 +540,7 @@ subject } #[test] - fn reference_order_is_unchanged() { + fn footnote_order_is_unchanged() { // Naive solution is technically trivial but may not match semantics. assert_eq!( parse( @@ -543,20 +557,16 @@ subject VerticalSpace, Subject("subject".to_owned()), VerticalSpace, - Reference("[2] bar".to_owned()), - Reference("[b] a".to_owned()), - Reference("[a] b".to_owned()), - Reference("[1] foo".to_owned()), + Footnote("[2]".to_owned(), "bar".to_owned()), + Footnote("[b]".to_owned(), "a".to_owned()), + Footnote("[a]".to_owned(), "b".to_owned()), + Footnote("[1]".to_owned(), "foo".to_owned()), ], ); } #[test] - fn bug_references_are_single_line_only() { - // Some references are prose and should be treated accordingly. - // Regrettably this clashes with references that should not wrap. Maybe - // fixable by identifying unwrappable words instead of unwrappable - // lines. + fn footnotes_may_span_multiple_lines() { assert_eq!( parse( " @@ -564,21 +574,26 @@ subject [1] foo bar +[2] foo + bar +[3] foo + bar " ), [ VerticalSpace, Subject("subject".to_owned()), VerticalSpace, - Reference("[1] foo".to_owned()), - Paragraph("bar".to_owned()), + Footnote("[1]".to_owned(), "foo bar".to_owned()), + Footnote("[2]".to_owned(), "foo bar".to_owned()), + Footnote("[3]".to_owned(), "foo bar".to_owned()), ], ); } #[test] - fn bug_reference_idents_are_not_disambiguated() { - // Nice-to-have but not really our job. Requires tracking all references + fn bug_footnote_idents_are_not_disambiguated() { + // Nice-to-have but not really our job. Requires tracking all footnotes. assert_eq!( parse( " @@ -592,8 +607,8 @@ subject VerticalSpace, Subject("subject".to_owned()), VerticalSpace, - Reference("[1] foo".to_owned()), - Reference("[1] bar".to_owned()), + Footnote("[1]".to_owned(), "foo".to_owned()), + Footnote("[1]".to_owned(), "bar".to_owned()), ], ); }