fork of go-gitdiff with jj support

Make patch date parsing simpler and stricter (#15)

Removed the distinction between parsed and raw dates and simply return
an error for unsupported date formats. Arbitrary date support was
probably a premature optimization and would be better supported by a
method to register custom date parsing functions.

Simple time.Time fields make the structure easier to use and mean that
a zero value always indicates the patch did not include a date.

authored by Billy Keyes and committed by GitHub d3116e7b f3b83ad7

Changed files
+79 -118
gitdiff
+38 -41
gitdiff/patch_header.go
··· 25 25 // not included in the header. 26 26 SHA string 27 27 28 - // The author details of the patch. Nil if author information is not 29 - // included in the header. 28 + // The author details of the patch. If these details are not included in 29 + // the header, Author is nil and AuthorDate is the zero time. 30 30 Author *PatchIdentity 31 - AuthorDate *PatchDate 31 + AuthorDate time.Time 32 32 33 - // The committer details of the patch. Nil if committer information is not 34 - // included in the header. 33 + // The committer details of the patch. If these details are not included in 34 + // the header, Committer is nil and CommitterDate is the zero time. 35 35 Committer *PatchIdentity 36 - CommitterDate *PatchDate 36 + CommitterDate time.Time 37 37 38 38 // The title and body of the commit message describing the changes in the 39 39 // patch. Empty if no message is included in the header. ··· 104 104 return PatchIdentity{Name: name, Email: email}, nil 105 105 } 106 106 107 - // PatchDate is the timestamp when a patch was authored or committed. It 108 - // contains a raw string version of the date and a parsed version if the date 109 - // is in a known format. 110 - type PatchDate struct { 111 - Parsed time.Time 112 - Raw string 113 - } 114 - 115 - // IsParsed returns true if the PatchDate has a parsed time. 116 - func (d PatchDate) IsParsed() bool { 117 - return !d.Parsed.IsZero() 118 - } 119 - 120 - // ParsePatchDate parses a patch date string. If s is in a supported format, 121 - // the PatchDate has both the Raw and Parsed initialized. 122 - // 123 - // ParsePatchDate supports the iso, rfc, short, raw, unix, and default formats 124 - // (with local variants) used by the --date flag in Git. 125 - func ParsePatchDate(s string) PatchDate { 107 + // ParsePatchDate parses a patch date string. It returns the parsed time or an 108 + // error if s has an unknown format. ParsePatchDate supports the iso, rfc, 109 + // short, raw, unix, and default formats (with local variants) used by the 110 + // --date flag in Git. 111 + func ParsePatchDate(s string) (time.Time, error) { 126 112 const ( 127 113 isoFormat = "2006-01-02 15:04:05 -0700" 128 114 isoStrictFormat = "2006-01-02T15:04:05-07:00" ··· 132 118 defaultLocalFormat = "Mon Jan 2 15:04:05 2006" 133 119 ) 134 120 135 - d := PatchDate{Raw: s} 121 + if s == "" { 122 + return time.Time{}, nil 123 + } 136 124 137 125 for _, fmt := range []string{ 138 126 isoFormat, ··· 143 131 defaultLocalFormat, 144 132 } { 145 133 if t, err := time.ParseInLocation(fmt, s, time.Local); err == nil { 146 - d.Parsed = t 147 - return d 134 + return t, nil 148 135 } 149 136 } 150 137 151 138 // unix format 152 139 if unix, err := strconv.ParseInt(s, 10, 64); err == nil { 153 - d.Parsed = time.Unix(unix, 0) 154 - return d 140 + return time.Unix(unix, 0), nil 155 141 } 156 142 157 143 // raw format ··· 159 145 unix, uerr := strconv.ParseInt(s[:space], 10, 64) 160 146 zone, zerr := time.Parse("-0700", s[space+1:]) 161 147 if uerr == nil && zerr == nil { 162 - d.Parsed = time.Unix(unix, 0).In(zone.Location()) 163 - return d 148 + return time.Unix(unix, 0).In(zone.Location()), nil 164 149 } 165 150 } 166 151 167 - return d 152 + return time.Time{}, fmt.Errorf("unknown date format: %s", s) 168 153 } 169 154 170 155 // ParsePatchHeader parses a preamble string as returned by Parse into a ··· 251 236 h.Committer = &u 252 237 253 238 case strings.HasPrefix(line, datePrefix): 254 - d := ParsePatchDate(strings.TrimSpace(line[len(datePrefix):])) 255 - h.AuthorDate = &d 239 + d, err := ParsePatchDate(strings.TrimSpace(line[len(datePrefix):])) 240 + if err != nil { 241 + return nil, err 242 + } 243 + h.AuthorDate = d 256 244 257 245 case strings.HasPrefix(line, authorDatePrefix): 258 - d := ParsePatchDate(strings.TrimSpace(line[len(authorDatePrefix):])) 259 - h.AuthorDate = &d 246 + d, err := ParsePatchDate(strings.TrimSpace(line[len(authorDatePrefix):])) 247 + if err != nil { 248 + return nil, err 249 + } 250 + h.AuthorDate = d 260 251 261 252 case strings.HasPrefix(line, commitDatePrefix): 262 - d := ParsePatchDate(strings.TrimSpace(line[len(commitDatePrefix):])) 263 - h.CommitterDate = &d 253 + d, err := ParsePatchDate(strings.TrimSpace(line[len(commitDatePrefix):])) 254 + if err != nil { 255 + return nil, err 256 + } 257 + h.CommitterDate = d 264 258 } 265 259 } 266 260 if s.Err() != nil { ··· 358 352 359 353 date := msg.Header.Get("Date") 360 354 if date != "" { 361 - d := ParsePatchDate(date) 362 - h.AuthorDate = &d 355 + d, err := ParsePatchDate(date) 356 + if err != nil { 357 + return nil, err 358 + } 359 + h.AuthorDate = d 363 360 } 364 361 365 362 h.Title = msg.Header.Get("Subject")
+41 -77
gitdiff/patch_header_test.go
··· 69 69 70 70 tests := map[string]struct { 71 71 Input string 72 - Output PatchDate 72 + Output time.Time 73 + Err interface{} 73 74 }{ 74 75 "default": { 75 - Input: "Thu Apr 9 01:07:06 2020 -0700", 76 - Output: PatchDate{ 77 - Parsed: expected, 78 - Raw: "Thu Apr 9 01:07:06 2020 -0700", 79 - }, 76 + Input: "Thu Apr 9 01:07:06 2020 -0700", 77 + Output: expected, 80 78 }, 81 79 "defaultLocal": { 82 - Input: "Thu Apr 9 01:07:06 2020", 83 - Output: PatchDate{ 84 - Parsed: time.Date(2020, 4, 9, 1, 7, 6, 0, time.Local), 85 - Raw: "Thu Apr 9 01:07:06 2020", 86 - }, 80 + Input: "Thu Apr 9 01:07:06 2020", 81 + Output: time.Date(2020, 4, 9, 1, 7, 6, 0, time.Local), 87 82 }, 88 83 "iso": { 89 - Input: "2020-04-09 01:07:06 -0700", 90 - Output: PatchDate{ 91 - Parsed: expected, 92 - Raw: "2020-04-09 01:07:06 -0700", 93 - }, 84 + Input: "2020-04-09 01:07:06 -0700", 85 + Output: expected, 94 86 }, 95 87 "isoStrict": { 96 - Input: "2020-04-09T01:07:06-07:00", 97 - Output: PatchDate{ 98 - Parsed: expected, 99 - Raw: "2020-04-09T01:07:06-07:00", 100 - }, 88 + Input: "2020-04-09T01:07:06-07:00", 89 + Output: expected, 101 90 }, 102 91 "rfc": { 103 - Input: "Thu, 9 Apr 2020 01:07:06 -0700", 104 - Output: PatchDate{ 105 - Parsed: expected, 106 - Raw: "Thu, 9 Apr 2020 01:07:06 -0700", 107 - }, 92 + Input: "Thu, 9 Apr 2020 01:07:06 -0700", 93 + Output: expected, 108 94 }, 109 95 "short": { 110 - Input: "2020-04-09", 111 - Output: PatchDate{ 112 - Parsed: time.Date(2020, 4, 9, 0, 0, 0, 0, time.Local), 113 - Raw: "2020-04-09", 114 - }, 96 + Input: "2020-04-09", 97 + Output: time.Date(2020, 4, 9, 0, 0, 0, 0, time.Local), 115 98 }, 116 99 "raw": { 117 - Input: "1586419626 -0700", 118 - Output: PatchDate{ 119 - Parsed: expected, 120 - Raw: "1586419626 -0700", 121 - }, 100 + Input: "1586419626 -0700", 101 + Output: expected, 122 102 }, 123 103 "unix": { 124 - Input: "1586419626", 125 - Output: PatchDate{ 126 - Parsed: expected, 127 - Raw: "1586419626", 128 - }, 104 + Input: "1586419626", 105 + Output: expected, 129 106 }, 130 107 "unknownFormat": { 131 108 Input: "4/9/2020 01:07:06 PDT", 132 - Output: PatchDate{ 133 - Raw: "4/9/2020 01:07:06 PDT", 134 - }, 109 + Err: "unknown date format", 135 110 }, 136 111 "empty": { 137 - Input: "", 138 - Output: PatchDate{}, 112 + Input: "", 139 113 }, 140 114 } 141 115 142 116 for name, test := range tests { 143 117 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) 118 + d, err := ParsePatchDate(test.Input) 119 + if test.Err != nil { 120 + assertError(t, test.Err, err, "parsing date") 121 + return 147 122 } 148 - if !test.Output.Parsed.Equal(d.Parsed) { 149 - t.Errorf("incorrect parsed date: expected %v, actual %v", test.Output.Parsed, d.Parsed) 123 + if err != nil { 124 + t.Fatalf("unexpected error parsing date: %v", err) 125 + } 126 + if !test.Output.Equal(d) { 127 + t.Errorf("incorrect parsed date: expected %v, actual %v", test.Output, d) 150 128 } 151 129 }) 152 130 } ··· 158 136 Name: "Morton Haypenny", 159 137 Email: "mhaypenny@example.com", 160 138 } 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 - } 139 + expectedDate := time.Date(2020, 04, 11, 15, 21, 23, 0, time.FixedZone("PDT", -7*60*60)) 165 140 expectedTitle := "A sample commit to test header parsing" 166 141 expectedBody := "The medium format shows the body, which\nmay wrap on to multiple lines.\n\nAnother body line." 167 142 ··· 258 233 Another body line. 259 234 `, 260 235 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 - Body: expectedBody, 236 + SHA: expectedSHA, 237 + Author: expectedIdentity, 238 + AuthorDate: expectedDate, 239 + Title: "[PATCH] " + expectedTitle, 240 + Body: expectedBody, 269 241 }, 270 242 }, 271 243 "unwrapTitle": { ··· 346 318 } 347 319 348 320 assertPatchIdentity(t, "author", exp.Author, act.Author) 349 - assertPatchDate(t, "author", exp.AuthorDate, act.AuthorDate) 321 + if !exp.AuthorDate.Equal(act.AuthorDate) { 322 + t.Errorf("incorrect parsed author date: expected %v, but got %v", exp.AuthorDate, act.AuthorDate) 323 + } 350 324 351 325 assertPatchIdentity(t, "committer", exp.Committer, act.Committer) 352 - assertPatchDate(t, "committer", exp.CommitterDate, act.CommitterDate) 326 + if !exp.CommitterDate.Equal(act.CommitterDate) { 327 + t.Errorf("incorrect parsed committer date: expected %v, but got %v", exp.CommitterDate, act.CommitterDate) 328 + } 353 329 354 330 if exp.Title != act.Title { 355 331 t.Errorf("incorrect parsed title:\n expected: %q\n actual: %q", exp.Title, act.Title) ··· 372 348 t.Errorf("incorrect parsed %s, expected %+v, bot got %+v", kind, exp, act) 373 349 } 374 350 } 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 - }