this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add ParsePatchHeader and related types (#6)

This function parses patch headers (the preamble returned by the
existing Parse function) to extract information about the commit that
generated the patch. This is useful when patches are an interchange
format and this library is applying commits generated elsewhere.

Because of the variety of header formats, parsing is fairly lenient and
best-effort, although certain invalid input does cause errors.

This also extracts some test utilities from the apply tests for reuse.

authored by

Billy Keyes and committed by
GitHub
f0da09b1 c2035ade

+778 -21
+1 -21
gitdiff/apply_test.go
··· 6 6 "io" 7 7 "io/ioutil" 8 8 "path/filepath" 9 - "strings" 10 9 "testing" 11 10 ) 12 11 ··· 221 220 var dst bytes.Buffer 222 221 err = apply(&dst, applier, files[0]) 223 222 if at.Err != nil { 224 - at.assertError(t, err) 223 + assertError(t, at.Err, err, "applying fragment") 225 224 return 226 225 } 227 226 if err != nil { ··· 235 234 236 235 if !bytes.Equal(out, dst.Bytes()) { 237 236 t.Errorf("incorrect result after apply\nexpected:\n%x\nactual:\n%x", out, dst.Bytes()) 238 - } 239 - } 240 - 241 - func (at applyTest) assertError(t *testing.T, err error) { 242 - if err == nil { 243 - t.Fatalf("expected error applying fragment, but got nil") 244 - } 245 - 246 - switch terr := at.Err.(type) { 247 - case string: 248 - if !strings.Contains(err.Error(), terr) { 249 - t.Fatalf("incorrect apply error: %q does not contain %q", err.Error(), terr) 250 - } 251 - case error: 252 - if !errors.Is(err, terr) { 253 - t.Fatalf("incorrect apply error: expected: %T (%v), actual: %T (%v)", terr, terr, err, err) 254 - } 255 - default: 256 - t.Fatalf("unsupported expected error type: %T", terr) 257 237 } 258 238 } 259 239
+30
gitdiff/assert_test.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "errors" 5 + "strings" 6 + "testing" 7 + ) 8 + 9 + func assertError(t *testing.T, expected interface{}, actual error, action string) { 10 + if actual == nil { 11 + t.Fatalf("expected error %s, but got nil", action) 12 + } 13 + 14 + switch exp := expected.(type) { 15 + case bool: 16 + if !exp { 17 + t.Fatalf("unexpected error %s: %v", action, actual) 18 + } 19 + case string: 20 + if !strings.Contains(actual.Error(), exp) { 21 + t.Fatalf("incorrect error %s: %q does not contain %q", action, actual.Error(), exp) 22 + } 23 + case error: 24 + if !errors.Is(actual, exp) { 25 + t.Fatalf("incorrect error %s: expected %T (%v), actual: %T (%v)", action, exp, exp, actual, actual) 26 + } 27 + default: 28 + t.Fatalf("unsupported expected error type: %T", exp) 29 + } 30 + }
+361
gitdiff/patch_header.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "bufio" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "net/mail" 9 + "strconv" 10 + "strings" 11 + "time" 12 + "unicode" 13 + ) 14 + 15 + const ( 16 + mailHeaderPrefix = "From " 17 + prettyHeaderPrefix = "commit " 18 + ) 19 + 20 + // PatchHeader is a parsed version of the preamble content that appears before 21 + // the first diff in a patch. It includes metadata about the patch, such as the 22 + // author and a subject. 23 + type PatchHeader struct { 24 + // The SHA of the commit the patch was generated from. Empty if the SHA is 25 + // not included in the header. 26 + SHA string 27 + 28 + // The author details of the patch. Nil if author information is not 29 + // included in the header. 30 + Author *PatchIdentity 31 + AuthorDate *PatchDate 32 + 33 + // The committer details of the patch. Nil if committer information is not 34 + // included in the header. 35 + Committer *PatchIdentity 36 + CommitterDate *PatchDate 37 + 38 + // The title and message summarizing the changes in the patch. Empty if a 39 + // title or message is not included in the header. 40 + Title string 41 + Message string 42 + } 43 + 44 + // PatchIdentity identifies a person who authored or committed a patch. 45 + type PatchIdentity struct { 46 + Name string 47 + Email string 48 + } 49 + 50 + func (i PatchIdentity) String() string { 51 + name := i.Name 52 + if name == "" { 53 + name = `""` 54 + } 55 + return fmt.Sprintf("%s <%s>", name, i.Email) 56 + } 57 + 58 + // ParsePatchIdentity parses a patch identity string. A valid string contains a 59 + // non-empty name followed by an email address in angle brackets. Like Git, 60 + // ParsePatchIdentity does not require that the email addresses is valid or 61 + // properly formatted, only that it is non-empty. The name must not contain a 62 + // left angle bracket, '<', and the email address must not contain a right 63 + // angle bracket, '>'. 64 + func ParsePatchIdentity(s string) (PatchIdentity, error) { 65 + var emailStart, emailEnd int 66 + for i, c := range s { 67 + if c == '<' && emailStart == 0 { 68 + emailStart = i + 1 69 + } 70 + if c == '>' && emailStart > 0 { 71 + emailEnd = i 72 + break 73 + } 74 + } 75 + if emailStart > 0 && emailEnd == 0 { 76 + return PatchIdentity{}, fmt.Errorf("invalid identity string: unclosed email section: %s", s) 77 + } 78 + 79 + var name, email string 80 + if emailStart > 0 { 81 + name = strings.TrimSpace(s[:emailStart-1]) 82 + } 83 + if emailStart > 0 && emailEnd > 0 { 84 + email = strings.TrimSpace(s[emailStart:emailEnd]) 85 + } 86 + if name == "" || email == "" { 87 + return PatchIdentity{}, fmt.Errorf("invalid identity string: %s", s) 88 + } 89 + 90 + return PatchIdentity{Name: name, Email: email}, nil 91 + } 92 + 93 + // PatchDate is the timestamp when a patch was authored or committed. It 94 + // contains a raw string version of the date and a parsed version if the date 95 + // is in a known format. 96 + type PatchDate struct { 97 + Parsed time.Time 98 + Raw string 99 + } 100 + 101 + // IsParsed returns true if the PatchDate has a parsed time. 102 + func (d PatchDate) IsParsed() bool { 103 + return !d.Parsed.IsZero() 104 + } 105 + 106 + // ParsePatchDate parses a patch date string. If s is in a supported format, 107 + // the PatchDate has both the Raw and Parsed initialized. 108 + // 109 + // ParsePatchDate supports the iso, rfc, short, raw, unix, and default formats 110 + // (with local variants) used by the --date flag in Git. 111 + func ParsePatchDate(s string) PatchDate { 112 + const ( 113 + isoFormat = "2006-01-02 15:04:05 -0700" 114 + isoStrictFormat = "2006-01-02T15:04:05-07:00" 115 + rfc2822Format = "Mon, 02 Jan 2006 15:04:05 -0700" 116 + shortFormat = "2006-01-02" 117 + defaultFormat = "Mon Jan 02 15:04:05 2006 -0700" 118 + defaultLocalFormat = "Mon Jan 02 15:04:05 2006" 119 + ) 120 + 121 + d := PatchDate{Raw: s} 122 + 123 + for _, fmt := range []string{ 124 + isoFormat, 125 + isoStrictFormat, 126 + rfc2822Format, 127 + shortFormat, 128 + defaultFormat, 129 + defaultLocalFormat, 130 + } { 131 + if t, err := time.ParseInLocation(fmt, s, time.Local); err == nil { 132 + d.Parsed = t 133 + return d 134 + } 135 + } 136 + 137 + // unix format 138 + if unix, err := strconv.ParseInt(s, 10, 64); err == nil { 139 + d.Parsed = time.Unix(unix, 0) 140 + return d 141 + } 142 + 143 + // raw format 144 + if space := strings.IndexByte(s, ' '); space > 0 { 145 + unix, uerr := strconv.ParseInt(s[:space], 10, 64) 146 + zone, zerr := time.Parse("-0700", s[space+1:]) 147 + if uerr == nil && zerr == nil { 148 + d.Parsed = time.Unix(unix, 0).In(zone.Location()) 149 + return d 150 + } 151 + } 152 + 153 + return d 154 + } 155 + 156 + // ParsePatchHeader parses a preamble string as returned by Parse into a 157 + // PatchHeader. Due to the variety of header formats, some fields of the parsed 158 + // PatchHeader may be unset after parsing. 159 + // 160 + // Supported formats are the short, medium, full, fuller, and email pretty 161 + // formats used by git diff, git log, and git show and the UNIX mailbox format 162 + // used by git format-patch. 163 + // 164 + // ParsePatchHeader makes no assumptions about the format of the patch title or 165 + // message other than removing leading and trailing whitespace on each line and 166 + // condensing blank lines. In particular, it does not remove the extra content 167 + // that git format-patch adds to make emailed patches friendlier, like subject 168 + // prefixes or commit stats. 169 + func ParsePatchHeader(s string) (*PatchHeader, error) { 170 + r := bufio.NewReader(strings.NewReader(s)) 171 + 172 + var line string 173 + for { 174 + var err error 175 + line, err = r.ReadString('\n') 176 + if err == io.EOF { 177 + break 178 + } 179 + if err != nil { 180 + return nil, err 181 + } 182 + 183 + line = strings.TrimSpace(line) 184 + if len(line) > 0 { 185 + break 186 + } 187 + } 188 + 189 + switch { 190 + case strings.HasPrefix(line, mailHeaderPrefix): 191 + return parseHeaderMail(line, r) 192 + case strings.HasPrefix(line, prettyHeaderPrefix): 193 + return parseHeaderPretty(line, r) 194 + } 195 + return nil, errors.New("unrecognized patch header format") 196 + } 197 + 198 + func parseHeaderPretty(prettyLine string, r io.Reader) (*PatchHeader, error) { 199 + const ( 200 + authorPrefix = "Author:" 201 + commitPrefix = "Commit:" 202 + datePrefix = "Date:" 203 + authorDatePrefix = "AuthorDate:" 204 + commitDatePrefix = "CommitDate:" 205 + ) 206 + 207 + h := &PatchHeader{} 208 + 209 + prettyLine = prettyLine[len(prettyHeaderPrefix):] 210 + if i := strings.IndexByte(prettyLine, ' '); i > 0 { 211 + h.SHA = prettyLine[:i] 212 + } else { 213 + h.SHA = prettyLine 214 + } 215 + 216 + s := bufio.NewScanner(r) 217 + for s.Scan() { 218 + line := s.Text() 219 + 220 + // emtpy line marks end of fields, remaining lines are title/message 221 + if strings.TrimSpace(line) == "" { 222 + break 223 + } 224 + 225 + switch { 226 + case strings.HasPrefix(line, authorPrefix): 227 + u, err := ParsePatchIdentity(line[len(authorPrefix):]) 228 + if err != nil { 229 + return nil, err 230 + } 231 + h.Author = &u 232 + 233 + case strings.HasPrefix(line, commitPrefix): 234 + u, err := ParsePatchIdentity(line[len(commitPrefix):]) 235 + if err != nil { 236 + return nil, err 237 + } 238 + h.Committer = &u 239 + 240 + case strings.HasPrefix(line, datePrefix): 241 + d := ParsePatchDate(strings.TrimSpace(line[len(datePrefix):])) 242 + h.AuthorDate = &d 243 + 244 + case strings.HasPrefix(line, authorDatePrefix): 245 + d := ParsePatchDate(strings.TrimSpace(line[len(authorDatePrefix):])) 246 + h.AuthorDate = &d 247 + 248 + case strings.HasPrefix(line, commitDatePrefix): 249 + d := ParsePatchDate(strings.TrimSpace(line[len(commitDatePrefix):])) 250 + h.CommitterDate = &d 251 + } 252 + } 253 + if s.Err() != nil { 254 + return nil, s.Err() 255 + } 256 + 257 + title, indent := scanPatchTitle(s) 258 + if s.Err() != nil { 259 + return nil, s.Err() 260 + } 261 + h.Title = title 262 + 263 + if title != "" { 264 + msg := scanPatchMessage(s, indent) 265 + if s.Err() != nil { 266 + return nil, s.Err() 267 + } 268 + h.Message = msg 269 + } 270 + 271 + return h, nil 272 + } 273 + 274 + func scanPatchTitle(s *bufio.Scanner) (title string, indent string) { 275 + var b strings.Builder 276 + for i := 0; s.Scan(); i++ { 277 + line := s.Text() 278 + trimLine := strings.TrimSpace(line) 279 + if trimLine == "" { 280 + break 281 + } 282 + 283 + if i == 0 { 284 + if start := strings.IndexFunc(line, func(c rune) bool { return !unicode.IsSpace(c) }); start > 0 { 285 + indent = line[:start] 286 + } 287 + } 288 + if b.Len() > 0 { 289 + b.WriteByte(' ') 290 + } 291 + b.WriteString(trimLine) 292 + } 293 + return b.String(), indent 294 + } 295 + 296 + func scanPatchMessage(s *bufio.Scanner, indent string) string { 297 + var b strings.Builder 298 + var empty int 299 + for i := 0; s.Scan(); i++ { 300 + line := s.Text() 301 + if strings.TrimSpace(line) == "" { 302 + empty++ 303 + continue 304 + } 305 + 306 + if b.Len() > 0 { 307 + b.WriteByte('\n') 308 + if empty > 0 { 309 + b.WriteByte('\n') 310 + } 311 + } 312 + empty = 0 313 + 314 + line = strings.TrimRightFunc(line, unicode.IsSpace) 315 + line = strings.TrimPrefix(line, indent) 316 + b.WriteString(line) 317 + } 318 + return b.String() 319 + } 320 + 321 + func parseHeaderMail(mailLine string, r io.Reader) (*PatchHeader, error) { 322 + msg, err := mail.ReadMessage(r) 323 + if err != nil { 324 + return nil, err 325 + } 326 + 327 + h := &PatchHeader{} 328 + 329 + mailLine = mailLine[len(mailHeaderPrefix):] 330 + if i := strings.IndexByte(mailLine, ' '); i > 0 { 331 + h.SHA = mailLine[:i] 332 + } 333 + 334 + addrs, err := msg.Header.AddressList("From") 335 + if err != nil && !errors.Is(err, mail.ErrHeaderNotPresent) { 336 + return nil, err 337 + } 338 + if len(addrs) > 0 { 339 + addr := addrs[0] 340 + if addr.Name == "" { 341 + return nil, fmt.Errorf("invalid user string: %s", addr) 342 + } 343 + h.Author = &PatchIdentity{Name: addr.Name, Email: addr.Address} 344 + } 345 + 346 + date := msg.Header.Get("Date") 347 + if date != "" { 348 + d := ParsePatchDate(date) 349 + h.AuthorDate = &d 350 + } 351 + 352 + h.Title = msg.Header.Get("Subject") 353 + 354 + s := bufio.NewScanner(msg.Body) 355 + h.Message = scanPatchMessage(s, "") 356 + if s.Err() != nil { 357 + return nil, s.Err() 358 + } 359 + 360 + return h, nil 361 + }
+386
gitdiff/patch_header_test.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "testing" 5 + "time" 6 + ) 7 + 8 + func TestParsePatchIdentity(t *testing.T) { 9 + tests := map[string]struct { 10 + Input string 11 + Output PatchIdentity 12 + Err interface{} 13 + }{ 14 + "simple": { 15 + Input: "Morton Haypenny <mhaypenny@example.com>", 16 + Output: PatchIdentity{ 17 + Name: "Morton Haypenny", 18 + Email: "mhaypenny@example.com", 19 + }, 20 + }, 21 + "extraWhitespace": { 22 + Input: " Morton Haypenny <mhaypenny@example.com > ", 23 + Output: PatchIdentity{ 24 + Name: "Morton Haypenny", 25 + Email: "mhaypenny@example.com", 26 + }, 27 + }, 28 + "trailingCharacters": { 29 + Input: "Morton Haypenny <mhaypenny@example.com> unrelated garbage", 30 + Output: PatchIdentity{ 31 + Name: "Morton Haypenny", 32 + Email: "mhaypenny@example.com", 33 + }, 34 + }, 35 + "missingName": { 36 + Input: "<mhaypenny@example.com>", 37 + Err: "invalid identity", 38 + }, 39 + "missingEmail": { 40 + Input: "Morton Haypenny", 41 + Err: "invalid identity", 42 + }, 43 + "unclosedEmail": { 44 + Input: "Morton Haypenny <mhaypenny@example.com", 45 + Err: "unclosed email", 46 + }, 47 + } 48 + 49 + for name, test := range tests { 50 + t.Run(name, func(t *testing.T) { 51 + id, err := ParsePatchIdentity(test.Input) 52 + if test.Err != nil { 53 + assertError(t, test.Err, err, "parsing identity") 54 + return 55 + } 56 + if err != nil { 57 + t.Fatalf("unexpected error parsing identity: %v", err) 58 + } 59 + 60 + if test.Output != id { 61 + t.Errorf("incorrect identity: expected %#v, actual %#v", test.Output, id) 62 + } 63 + }) 64 + } 65 + } 66 + 67 + func TestParsePatchDate(t *testing.T) { 68 + expected := time.Date(2020, 04, 11, 22, 21, 23, 0, time.UTC) 69 + 70 + tests := map[string]struct { 71 + Input string 72 + Output PatchDate 73 + }{ 74 + "default": { 75 + Input: "Sat Apr 11 15:21:23 2020 -0700", 76 + Output: PatchDate{ 77 + Parsed: expected, 78 + Raw: "Sat Apr 11 15:21:23 2020 -0700", 79 + }, 80 + }, 81 + "defaultLocal": { 82 + Input: "Sat Apr 11 15:21:23 2020", 83 + Output: PatchDate{ 84 + Parsed: time.Date(2020, 04, 11, 15, 21, 23, 0, time.Local), 85 + Raw: "Sat Apr 11 15:21:23 2020", 86 + }, 87 + }, 88 + "iso": { 89 + Input: "2020-04-11 15:21:23 -0700", 90 + Output: PatchDate{ 91 + Parsed: expected, 92 + Raw: "2020-04-11 15:21:23 -0700", 93 + }, 94 + }, 95 + "isoStrict": { 96 + Input: "2020-04-11T15:21:23-07:00", 97 + Output: PatchDate{ 98 + Parsed: expected, 99 + Raw: "2020-04-11T15:21:23-07:00", 100 + }, 101 + }, 102 + "rfc": { 103 + Input: "Sat, 11 Apr 2020 15:21:23 -0700", 104 + Output: PatchDate{ 105 + Parsed: expected, 106 + Raw: "Sat, 11 Apr 2020 15:21:23 -0700", 107 + }, 108 + }, 109 + "short": { 110 + Input: "2020-04-11", 111 + Output: PatchDate{ 112 + Parsed: time.Date(2020, 04, 11, 0, 0, 0, 0, time.Local), 113 + Raw: "2020-04-11", 114 + }, 115 + }, 116 + "raw": { 117 + Input: "1586643683 -0700", 118 + Output: PatchDate{ 119 + Parsed: expected, 120 + Raw: "1586643683 -0700", 121 + }, 122 + }, 123 + "unix": { 124 + Input: "1586643683", 125 + Output: PatchDate{ 126 + Parsed: expected, 127 + Raw: "1586643683", 128 + }, 129 + }, 130 + "unknownFormat": { 131 + Input: "4/11/2020 15:21:23 PDT", 132 + Output: PatchDate{ 133 + Raw: "4/11/2020 15:21:23 PDT", 134 + }, 135 + }, 136 + "empty": { 137 + Input: "", 138 + Output: PatchDate{}, 139 + }, 140 + } 141 + 142 + for name, test := range tests { 143 + t.Run(name, func(t *testing.T) { 144 + d := ParsePatchDate(test.Input) 145 + if test.Output.Raw != d.Raw { 146 + t.Errorf("incorrect raw date: expected %q, actual %q", test.Output.Raw, d.Raw) 147 + } 148 + if !test.Output.Parsed.Equal(d.Parsed) { 149 + t.Errorf("incorrect parsed date: expected %v, actual %v", test.Output.Parsed, d.Parsed) 150 + } 151 + }) 152 + } 153 + } 154 + 155 + func TestParsePatchHeader(t *testing.T) { 156 + expectedSHA := "61f5cd90bed4d204ee3feb3aa41ee91d4734855b" 157 + expectedIdentity := &PatchIdentity{ 158 + Name: "Morton Haypenny", 159 + Email: "mhaypenny@example.com", 160 + } 161 + expectedDate := &PatchDate{ 162 + Parsed: time.Date(2020, 04, 11, 15, 21, 23, 0, time.FixedZone("PDT", -7*60*60)), 163 + Raw: "Sat Apr 11 15:21:23 2020 -0700", 164 + } 165 + expectedTitle := "A sample commit to test header parsing" 166 + expectedMsg := "The medium format shows the body, which\nmay wrap on to multiple lines.\n\nAnother body line." 167 + 168 + tests := map[string]struct { 169 + Input string 170 + Header PatchHeader 171 + Err interface{} 172 + }{ 173 + "prettyShort": { 174 + Input: `commit 61f5cd90bed4d204ee3feb3aa41ee91d4734855b 175 + Author: Morton Haypenny <mhaypenny@example.com> 176 + 177 + A sample commit to test header parsing 178 + `, 179 + Header: PatchHeader{ 180 + SHA: expectedSHA, 181 + Author: expectedIdentity, 182 + Title: expectedTitle, 183 + }, 184 + }, 185 + "prettyMedium": { 186 + Input: `commit 61f5cd90bed4d204ee3feb3aa41ee91d4734855b 187 + Author: Morton Haypenny <mhaypenny@example.com> 188 + Date: Sat Apr 11 15:21:23 2020 -0700 189 + 190 + A sample commit to test header parsing 191 + 192 + The medium format shows the body, which 193 + may wrap on to multiple lines. 194 + 195 + Another body line. 196 + `, 197 + Header: PatchHeader{ 198 + SHA: expectedSHA, 199 + Author: expectedIdentity, 200 + AuthorDate: expectedDate, 201 + Title: expectedTitle, 202 + Message: expectedMsg, 203 + }, 204 + }, 205 + "prettyFull": { 206 + Input: `commit 61f5cd90bed4d204ee3feb3aa41ee91d4734855b 207 + Author: Morton Haypenny <mhaypenny@example.com> 208 + Commit: Morton Haypenny <mhaypenny@example.com> 209 + 210 + A sample commit to test header parsing 211 + 212 + The medium format shows the body, which 213 + may wrap on to multiple lines. 214 + 215 + Another body line. 216 + `, 217 + Header: PatchHeader{ 218 + SHA: expectedSHA, 219 + Author: expectedIdentity, 220 + Committer: expectedIdentity, 221 + Title: expectedTitle, 222 + Message: expectedMsg, 223 + }, 224 + }, 225 + "prettyFuller": { 226 + Input: `commit 61f5cd90bed4d204ee3feb3aa41ee91d4734855b 227 + Author: Morton Haypenny <mhaypenny@example.com> 228 + AuthorDate: Sat Apr 11 15:21:23 2020 -0700 229 + Commit: Morton Haypenny <mhaypenny@example.com> 230 + CommitDate: Sat Apr 11 15:21:23 2020 -0700 231 + 232 + A sample commit to test header parsing 233 + 234 + The medium format shows the body, which 235 + may wrap on to multiple lines. 236 + 237 + Another body line. 238 + `, 239 + Header: PatchHeader{ 240 + SHA: expectedSHA, 241 + Author: expectedIdentity, 242 + AuthorDate: expectedDate, 243 + Committer: expectedIdentity, 244 + CommitterDate: expectedDate, 245 + Title: expectedTitle, 246 + Message: expectedMsg, 247 + }, 248 + }, 249 + "mailbox": { 250 + Input: `From 61f5cd90bed4d204ee3feb3aa41ee91d4734855b Mon Sep 17 00:00:00 2001 251 + From: Morton Haypenny <mhaypenny@example.com> 252 + Date: Sat, 11 Apr 2020 15:21:23 -0700 253 + Subject: [PATCH] A sample commit to test header parsing 254 + 255 + The medium format shows the body, which 256 + may wrap on to multiple lines. 257 + 258 + Another body line. 259 + `, 260 + Header: PatchHeader{ 261 + SHA: expectedSHA, 262 + Author: expectedIdentity, 263 + AuthorDate: &PatchDate{ 264 + Parsed: expectedDate.Parsed, 265 + Raw: "Sat, 11 Apr 2020 15:21:23 -0700", 266 + }, 267 + Title: "[PATCH] " + expectedTitle, 268 + Message: expectedMsg, 269 + }, 270 + }, 271 + "unwrapTitle": { 272 + Input: `commit 61f5cd90bed4d204ee3feb3aa41ee91d4734855b 273 + Author: Morton Haypenny <mhaypenny@example.com> 274 + Date: Sat Apr 11 15:21:23 2020 -0700 275 + 276 + A sample commit to test header parsing with a long 277 + title that is wrapped. 278 + `, 279 + Header: PatchHeader{ 280 + SHA: expectedSHA, 281 + Author: expectedIdentity, 282 + AuthorDate: expectedDate, 283 + Title: expectedTitle + " with a long title that is wrapped.", 284 + }, 285 + }, 286 + "normalizeBodySpace": { 287 + Input: `commit 61f5cd90bed4d204ee3feb3aa41ee91d4734855b 288 + Author: Morton Haypenny <mhaypenny@example.com> 289 + Date: Sat Apr 11 15:21:23 2020 -0700 290 + 291 + A sample commit to test header parsing 292 + 293 + 294 + The medium format shows the body, which 295 + may wrap on to multiple lines. 296 + 297 + 298 + Another body line. 299 + 300 + 301 + `, 302 + Header: PatchHeader{ 303 + SHA: expectedSHA, 304 + Author: expectedIdentity, 305 + AuthorDate: expectedDate, 306 + Title: expectedTitle, 307 + Message: expectedMsg, 308 + }, 309 + }, 310 + "ignoreLeadingBlankLines": { 311 + Input: ` 312 + 313 + ` + " " + ` 314 + commit 61f5cd90bed4d204ee3feb3aa41ee91d4734855b 315 + Author: Morton Haypenny <mhaypenny@example.com> 316 + 317 + A sample commit to test header parsing 318 + `, 319 + Header: PatchHeader{ 320 + SHA: expectedSHA, 321 + Author: expectedIdentity, 322 + Title: expectedTitle, 323 + }, 324 + }, 325 + } 326 + 327 + for name, test := range tests { 328 + t.Run(name, func(t *testing.T) { 329 + h, err := ParsePatchHeader(test.Input) 330 + if test.Err != nil { 331 + assertError(t, test.Err, err, "parsing patch header") 332 + return 333 + } 334 + if err != nil { 335 + t.Fatalf("unexpected error parsing patch header: %v", err) 336 + } 337 + if h == nil { 338 + t.Fatalf("expected non-nil header, but got nil") 339 + } 340 + 341 + exp := test.Header 342 + act := *h 343 + 344 + if exp.SHA != act.SHA { 345 + t.Errorf("incorrect parsed SHA: expected %q, actual %q", exp.SHA, act.SHA) 346 + } 347 + 348 + assertPatchIdentity(t, "author", exp.Author, act.Author) 349 + assertPatchDate(t, "author", exp.AuthorDate, act.AuthorDate) 350 + 351 + assertPatchIdentity(t, "committer", exp.Committer, act.Committer) 352 + assertPatchDate(t, "committer", exp.CommitterDate, act.CommitterDate) 353 + 354 + if exp.Title != act.Title { 355 + t.Errorf("incorrect parsed title:\n expected: %q\n actual: %q", exp.Title, act.Title) 356 + } 357 + if exp.Message != act.Message { 358 + t.Errorf("incorrect parsed message:\n expected: %q\n actual: %q", exp.Message, act.Message) 359 + } 360 + }) 361 + } 362 + } 363 + 364 + func assertPatchIdentity(t *testing.T, kind string, exp, act *PatchIdentity) { 365 + switch { 366 + case exp == nil && act == nil: 367 + case exp == nil && act != nil: 368 + t.Errorf("incorrect parsed %s: expected nil, but got %+v", kind, act) 369 + case exp != nil && act == nil: 370 + t.Errorf("incorrect parsed %s: expected %+v, but got nil", kind, exp) 371 + case exp.Name != act.Name || exp.Email != act.Email: 372 + t.Errorf("incorrect parsed %s, expected %+v, bot got %+v", kind, exp, act) 373 + } 374 + } 375 + 376 + func assertPatchDate(t *testing.T, kind string, exp, act *PatchDate) { 377 + switch { 378 + case exp == nil && act == nil: 379 + case exp == nil && act != nil: 380 + t.Errorf("incorrect parsed %s date: expected nil, but got %+v", kind, act) 381 + case exp != nil && act == nil: 382 + t.Errorf("incorrect parsed %s date: expected %+v, but got nil", kind, exp) 383 + case exp.Raw != act.Raw || !exp.Parsed.Equal(act.Parsed): 384 + t.Errorf("incorrect parsed %s date, expected %+v, bot got %+v", kind, exp, act) 385 + } 386 + }