@@ -165,34 +165,71 @@ func ParsePatchDate(s string) (time.Time, error) {
165
165
return time.Time {}, fmt .Errorf ("unknown date format: %s" , s )
166
166
}
167
167
168
- // ParsePatchHeader parses a preamble string as returned by Parse into a
168
+ // A PatchHeaderOption modifies the behavior of ParsePatchHeader.
169
+ type PatchHeaderOption func (* patchHeaderOptions )
170
+
171
+ // SubjectCleanMode controls how ParsePatchHeader cleans subject lines when
172
+ // parsing mail-formatted patches.
173
+ type SubjectCleanMode int
174
+
175
+ const (
176
+ // SubjectCleanWhitespace removes leading and trailing whitespace.
177
+ SubjectCleanWhitespace SubjectCleanMode = iota
178
+
179
+ // SubjectCleanAll removes leading and trailing whitespace, leading "Re:",
180
+ // "re:", and ":" strings, and leading strings enclosed by '[' and ']'.
181
+ // This is the default behavior of git (see `git mailinfo`) and this
182
+ // package.
183
+ SubjectCleanAll
184
+
185
+ // SubjectCleanPatchOnly is the same as SubjectCleanAll, but only removes
186
+ // leading strings enclosed by '[' and ']' if they start with "PATCH".
187
+ SubjectCleanPatchOnly
188
+ )
189
+
190
+ // WithSubjectCleanMode sets the SubjectCleanMode for header parsing. By
191
+ // default, uses SubjectCleanAll.
192
+ func WithSubjectCleanMode (m SubjectCleanMode ) PatchHeaderOption {
193
+ return func (opts * patchHeaderOptions ) {
194
+ opts .subjectCleanMode = m
195
+ }
196
+ }
197
+
198
+ type patchHeaderOptions struct {
199
+ subjectCleanMode SubjectCleanMode
200
+ }
201
+
202
+ // ParsePatchHeader parses the preamble string returned by [Parse] into a
169
203
// PatchHeader. Due to the variety of header formats, some fields of the parsed
170
204
// PatchHeader may be unset after parsing.
171
205
//
172
206
// Supported formats are the short, medium, full, fuller, and email pretty
173
- // formats used by git diff, git log, and git show and the UNIX mailbox format
174
- // used by git format-patch.
207
+ // formats used by ` git diff`, ` git log` , and ` git show` and the UNIX mailbox
208
+ // format used by ` git format-patch` .
175
209
//
176
- // If ParsePatchHeader detects that it is handling an email, it will
177
- // remove extra content at the beginning of the title line, such as
178
- // `[PATCH]` or `Re:` in the same way that `git mailinfo` does.
179
- // SubjectPrefix will be set to the value of this removed string.
180
- // (`git mailinfo` is the core part of `git am` that pulls information
181
- // out of an individual mail.)
210
+ // When parsing mail-formatted headers, ParsePatchHeader tries to remove
211
+ // email-specific content from the title and body:
182
212
//
183
- // Additionally, if ParsePatchHeader detects that it's handling an
184
- // email, it will remove a `---` line and put anything after it into
185
- // BodyAppendix.
213
+ // - Based on the SubjectCleanMode, remove prefixes like reply markers and
214
+ // "[PATCH]" strings from the subject, saving any removed content in the
215
+ // SubjectPrefix field. Parsing always discards leading and trailing
216
+ // whitespace from the subject line. The default mode is SubjectCleanAll.
186
217
//
187
- // Those wishing the effect of a plain `git am` should use
188
- // `PatchHeader.Title + "\n" + PatchHeader.Body` (or
189
- // `PatchHeader.Message()`). Those wishing to retain the subject
190
- // prefix and appendix material should use `PatchHeader.SubjectPrefix
191
- // + PatchHeader.Title + "\n" + PatchHeader.Body + "\n" +
192
- // PatchHeader.BodyAppendix`.
193
- func ParsePatchHeader (header string ) (* PatchHeader , error ) {
194
- header = strings .TrimSpace (header )
218
+ // - If the body contains a "---" line (3 hyphens), remove that line and any
219
+ // content after it from the body and save it in the BodyAppendix field.
220
+ //
221
+ // ParsePatchHeader tries to process content it does not understand wthout
222
+ // returning errors, but will return errors if well-identified content like
223
+ // dates or identies uses unknown or invalid formats.
224
+ func ParsePatchHeader (header string , options ... PatchHeaderOption ) (* PatchHeader , error ) {
225
+ opts := patchHeaderOptions {
226
+ subjectCleanMode : SubjectCleanAll , // match git defaults
227
+ }
228
+ for _ , optFn := range options {
229
+ optFn (& opts )
230
+ }
195
231
232
+ header = strings .TrimSpace (header )
196
233
if header == "" {
197
234
return & PatchHeader {}, nil
198
235
}
@@ -208,12 +245,12 @@ func ParsePatchHeader(header string) (*PatchHeader, error) {
208
245
209
246
switch {
210
247
case strings .HasPrefix (firstLine , mailHeaderPrefix ):
211
- return parseHeaderMail (firstLine , strings .NewReader (rest ))
248
+ return parseHeaderMail (firstLine , strings .NewReader (rest ), opts )
212
249
213
250
case strings .HasPrefix (firstLine , mailMinimumHeaderPrefix ):
214
251
// With a minimum header, the first line is part of the actual mail
215
252
// content and needs to be parsed as part of the "rest"
216
- return parseHeaderMail ("" , strings .NewReader (header ))
253
+ return parseHeaderMail ("" , strings .NewReader (header ), opts )
217
254
218
255
case strings .HasPrefix (firstLine , prettyHeaderPrefix ):
219
256
return parseHeaderPretty (firstLine , strings .NewReader (rest ))
@@ -366,7 +403,7 @@ func scanMessageBody(s *bufio.Scanner, indent string, separateAppendix bool) (st
366
403
return body .String (), appendix .String ()
367
404
}
368
405
369
- func parseHeaderMail (mailLine string , r io.Reader ) (* PatchHeader , error ) {
406
+ func parseHeaderMail (mailLine string , r io.Reader , opts patchHeaderOptions ) (* PatchHeader , error ) {
370
407
msg , err := mail .ReadMessage (r )
371
408
if err != nil {
372
409
return nil , err
@@ -403,7 +440,7 @@ func parseHeaderMail(mailLine string, r io.Reader) (*PatchHeader, error) {
403
440
}
404
441
405
442
subject := msg .Header .Get ("Subject" )
406
- h .SubjectPrefix , h .Title = parseSubject (subject )
443
+ h .SubjectPrefix , h .Title = cleanSubject (subject , opts . subjectCleanMode )
407
444
408
445
s := bufio .NewScanner (msg .Body )
409
446
h .Body , h .BodyAppendix = scanMessageBody (s , "" , true )
@@ -414,23 +451,24 @@ func parseHeaderMail(mailLine string, r io.Reader) (*PatchHeader, error) {
414
451
return h , nil
415
452
}
416
453
417
- // Takes an email subject and returns the patch prefix and commit
418
- // title. i.e., `[PATCH v3 3/5] Implement foo` would return `[PATCH
419
- // v3 3/5] ` and `Implement foo`
420
- func parseSubject (s string ) (string , string ) {
421
- // This is meant to be compatible with
422
- // https://github.com/git/git/blob/master/mailinfo.c:cleanup_subject().
423
- // If compatibility with `git am` drifts, go there to see if there
424
- // are any updates.
454
+ func cleanSubject (s string , mode SubjectCleanMode ) (prefix string , subject string ) {
455
+ switch mode {
456
+ case SubjectCleanAll , SubjectCleanPatchOnly :
457
+ case SubjectCleanWhitespace :
458
+ return "" , strings .TrimSpace (decodeSubject (s ))
459
+ default :
460
+ panic (fmt .Sprintf ("unknown clean mode: %d" , mode ))
461
+ }
462
+
463
+ // Based on the algorithm from Git in mailinfo.c:cleanup_subject()
464
+ // If compatibility with `git am` drifts, go there to see if there are any updates.
425
465
426
466
at := 0
427
467
for at < len (s ) {
428
468
switch s [at ] {
429
469
case 'r' , 'R' :
430
470
// Detect re:, Re:, rE: and RE:
431
- if at + 2 < len (s ) &&
432
- (s [at + 1 ] == 'e' || s [at + 1 ] == 'E' ) &&
433
- s [at + 2 ] == ':' {
471
+ if at + 2 < len (s ) && (s [at + 1 ] == 'e' || s [at + 1 ] == 'E' ) && s [at + 2 ] == ':' {
434
472
at += 3
435
473
continue
436
474
}
@@ -441,25 +479,21 @@ func parseSubject(s string) (string, string) {
441
479
continue
442
480
443
481
case '[' :
444
- // Look for closing parenthesis
445
- j := at + 1
446
- for ; j < len (s ); j ++ {
447
- if s [j ] == ']' {
448
- break
482
+ if i := strings .IndexByte (s [at :], ']' ); i > 0 {
483
+ if mode == SubjectCleanAll || strings .Contains (s [at :at + i + 1 ], "PATCH" ) {
484
+ at += i + 1
485
+ continue
449
486
}
450
487
}
451
-
452
- if j < len (s ) {
453
- at = j + 1
454
- continue
455
- }
456
488
}
457
489
458
- // Only loop if we actually removed something
490
+ // Nothing was removed, end processing
459
491
break
460
492
}
461
493
462
- return s [:at ], decodeSubject (s [at :])
494
+ prefix = strings .TrimLeftFunc (s [:at ], unicode .IsSpace )
495
+ subject = strings .TrimRightFunc (decodeSubject (s [at :]), unicode .IsSpace )
496
+ return
463
497
}
464
498
465
499
// Decodes a subject line. Currently only supports quoted-printable UTF-8. This format is the result
0 commit comments