Skip to content

Commit

Permalink
Merge branch 'multi-line-footnotes' into 'master'
Browse files Browse the repository at this point in the history
Replace Reference with Footnote

See merge request mkjeldsen/commitmsgfmt!11
  • Loading branch information
commonquail committed Sep 28, 2018
2 parents 16fff21 + 6155000 commit 5eb92a4
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 48 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 9 additions & 7 deletions doc/commitmsgfmt.1.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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

Expand Down
40 changes: 32 additions & 8 deletions src/commitmsgfmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down Expand Up @@ -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]
Expand Down
77 changes: 46 additions & 31 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -27,7 +27,7 @@ pub fn parse(input: &str, comment_char: char) -> Vec<Token> {
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(
Expand Down Expand Up @@ -81,11 +81,20 @@ pub fn parse(input: &str, comment_char: char) -> Vec<Token> {
} 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());
Expand Down Expand Up @@ -489,20 +498,21 @@ Signed-off-by: Jane Doe <[email protected]>
}

#[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]
"
Expand All @@ -511,22 +521,26 @@ 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()),
],
);
}

#[test]
fn reference_order_is_unchanged() {
fn footnote_order_is_unchanged() {
// Naive solution is technically trivial but may not match semantics.
assert_eq!(
parse(
Expand All @@ -543,42 +557,43 @@ 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(
"
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(
"
Expand All @@ -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()),
],
);
}
Expand Down

0 comments on commit 5eb92a4

Please sign in to comment.