fork of go-gitdiff with jj support

Remove email decorations from patch titles (#17)

Primarily to get rid of [PATCH] at the front, but while we're here
just be generally compatible with `git am`:

* Remove `re` and variations
* Remove whitespace
* Remove anything in brackets

But only at the very beginning of the subject.

Store anything removed in this way in PatchHeader.SubjectPrefix.

Inspired by
https://github.com/git/git/blob/master/mailinfo.c:cleanup_subject()

Signed-off-by: George Dunlap <george.dunlap@citrix.com>
Co-authored-by: George Dunlap <george.dunlap@citrix.com>

authored by George Dunlap George Dunlap and committed by GitHub 379b8934 864dd3fb

Changed files
+112 -6
gitdiff
+63 -5
gitdiff/patch_header.go
··· 39 39 // patch. Empty if no message is included in the header. 40 40 Title string 41 41 Body string 42 + 43 + // If the preamble looks like an email, ParsePatchHeader will 44 + // remove prefixes such as `Re: ` and `[PATCH v3 5/17]` from the 45 + // Title and place them here. 46 + SubjectPrefix string 42 47 } 43 48 44 49 // Message returns the commit message for the header. The message consists of ··· 160 165 // formats used by git diff, git log, and git show and the UNIX mailbox format 161 166 // used by git format-patch. 162 167 // 163 - // ParsePatchHeader makes no assumptions about the format of the patch title or 164 - // message other than trimming whitespace and condensing blank lines. In 165 - // particular, it does not remove the extra content that git format-patch adds 166 - // to make emailed patches friendlier, like subject prefixes or commit stats. 168 + // If ParsePatchHeader detect that it is handling an email, it will 169 + // remove extra content at the beginning of the title line, such as 170 + // `[PATCH]` or `Re:` in the same way that `git mailinfo` does. 171 + // SubjectPrefix will be set to the value of this removed string. 172 + // (`git mailinfo` is the core part of `git am` that pulls information 173 + // out of an individual mail.) Unline `git mailinfo`, 174 + // ParsePatchHeader does not at the moment remove commit states or 175 + // other extraneous matter after a `---` line. 167 176 func ParsePatchHeader(s string) (*PatchHeader, error) { 168 177 r := bufio.NewReader(strings.NewReader(s)) 169 178 ··· 359 368 h.AuthorDate = d 360 369 } 361 370 362 - h.Title = msg.Header.Get("Subject") 371 + subject := msg.Header.Get("Subject") 372 + h.SubjectPrefix, h.Title = parseSubject(subject) 363 373 364 374 s := bufio.NewScanner(msg.Body) 365 375 h.Body = scanMessageBody(s, "") ··· 369 379 370 380 return h, nil 371 381 } 382 + 383 + // Takes an email subject and returns the patch prefix and commit 384 + // title. i.e., `[PATCH v3 3/5] Implement foo` would return `[PATCH 385 + // v3 3/5] ` and `Implement foo` 386 + func parseSubject(s string) (string, string) { 387 + // This is meant to be compatible with 388 + // https://github.com/git/git/blob/master/mailinfo.c:cleanup_subject(). 389 + // If compatibility with `git am` drifts, go there to see if there 390 + // are any updates. 391 + 392 + at := 0 393 + for at < len(s) { 394 + switch s[at] { 395 + case 'r', 'R': 396 + // Detect re:, Re:, rE: and RE: 397 + if at+2 < len(s) && 398 + (s[at+1] == 'e' || s[at+1] == 'E') && 399 + s[at+2] == ':' { 400 + at += 3 401 + continue 402 + } 403 + 404 + case ' ', '\t', ':': 405 + // Delete whitespace and duplicate ':' characters 406 + at++ 407 + continue 408 + 409 + case '[': 410 + // Look for closing parenthesis 411 + j := at + 1 412 + for ; j < len(s); j++ { 413 + if s[j] == ']' { 414 + break 415 + } 416 + } 417 + 418 + if j < len(s) { 419 + at = j + 1 420 + continue 421 + } 422 + } 423 + 424 + // Only loop if we actually removed something 425 + break 426 + } 427 + 428 + return s[:at], s[at:] 429 + }
+49 -1
gitdiff/patch_header_test.go
··· 236 236 SHA: expectedSHA, 237 237 Author: expectedIdentity, 238 238 AuthorDate: expectedDate, 239 - Title: "[PATCH] " + expectedTitle, 239 + Title: expectedTitle, 240 240 Body: expectedBody, 241 241 }, 242 242 }, ··· 348 348 t.Errorf("incorrect parsed %s, expected %+v, bot got %+v", kind, exp, act) 349 349 } 350 350 } 351 + 352 + func TestCleanupSubject(t *testing.T) { 353 + exp := "A sample commit to test header parsing" 354 + tests := map[string]string{ 355 + "plain": "", 356 + "patch": "[PATCH] ", 357 + "patchv5": "[PATCH v5] ", 358 + "patchrfc": "[PATCH RFC] ", 359 + "patchnospace": "[PATCH]", 360 + "space": " ", 361 + "re": "re: ", 362 + "Re": "Re: ", 363 + "RE": "rE: ", 364 + "rere": "re: re: ", 365 + } 366 + 367 + for name, prefix := range tests { 368 + gotprefix, gottitle := parseSubject(prefix + exp) 369 + if gottitle != exp { 370 + t.Errorf("%s: Incorrect parsing of prefix %s: got title %s, wanted %s", 371 + name, prefix, gottitle, exp) 372 + } 373 + if gotprefix != prefix { 374 + t.Errorf("%s: Incorrect parsing of prefix %s: got prefix %s", 375 + name, prefix, gotprefix) 376 + } 377 + } 378 + 379 + moretests := map[string]struct { 380 + in, eprefix, etitle string 381 + }{ 382 + "Reimplement": {"Reimplement something", "", "Reimplement something"}, 383 + "patch-reimplement": {"[PATCH v5] Reimplement something", "[PATCH v5] ", "Reimplement something"}, 384 + "Openbracket": {"[Just to annoy people", "", "[Just to annoy people"}, 385 + } 386 + 387 + for name, test := range moretests { 388 + prefix, title := parseSubject(test.in) 389 + if title != test.etitle { 390 + t.Errorf("%s: Incorrect parsing of %s: got title %s, wanted %s", 391 + name, test.in, title, test.etitle) 392 + } 393 + if prefix != test.eprefix { 394 + t.Errorf("%s: Incorrect parsing of %s: got prefix %s, wanted %s", 395 + name, test.in, title, test.etitle) 396 + } 397 + } 398 + }