Skip to content

Commit d3116e7

Browse files
authored
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.
1 parent f3b83ad commit d3116e7

File tree

2 files changed

+79
-118
lines changed

2 files changed

+79
-118
lines changed

gitdiff/patch_header.go

+38-41
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ type PatchHeader struct {
2525
// not included in the header.
2626
SHA string
2727

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.
3030
Author *PatchIdentity
31-
AuthorDate *PatchDate
31+
AuthorDate time.Time
3232

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.
3535
Committer *PatchIdentity
36-
CommitterDate *PatchDate
36+
CommitterDate time.Time
3737

3838
// The title and body of the commit message describing the changes in the
3939
// patch. Empty if no message is included in the header.
@@ -104,25 +104,11 @@ func ParsePatchIdentity(s string) (PatchIdentity, error) {
104104
return PatchIdentity{Name: name, Email: email}, nil
105105
}
106106

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) {
126112
const (
127113
isoFormat = "2006-01-02 15:04:05 -0700"
128114
isoStrictFormat = "2006-01-02T15:04:05-07:00"
@@ -132,7 +118,9 @@ func ParsePatchDate(s string) PatchDate {
132118
defaultLocalFormat = "Mon Jan 2 15:04:05 2006"
133119
)
134120

135-
d := PatchDate{Raw: s}
121+
if s == "" {
122+
return time.Time{}, nil
123+
}
136124

137125
for _, fmt := range []string{
138126
isoFormat,
@@ -143,28 +131,25 @@ func ParsePatchDate(s string) PatchDate {
143131
defaultLocalFormat,
144132
} {
145133
if t, err := time.ParseInLocation(fmt, s, time.Local); err == nil {
146-
d.Parsed = t
147-
return d
134+
return t, nil
148135
}
149136
}
150137

151138
// unix format
152139
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
155141
}
156142

157143
// raw format
158144
if space := strings.IndexByte(s, ' '); space > 0 {
159145
unix, uerr := strconv.ParseInt(s[:space], 10, 64)
160146
zone, zerr := time.Parse("-0700", s[space+1:])
161147
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
164149
}
165150
}
166151

167-
return d
152+
return time.Time{}, fmt.Errorf("unknown date format: %s", s)
168153
}
169154

170155
// ParsePatchHeader parses a preamble string as returned by Parse into a
@@ -251,16 +236,25 @@ func parseHeaderPretty(prettyLine string, r io.Reader) (*PatchHeader, error) {
251236
h.Committer = &u
252237

253238
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
256244

257245
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
260251

261252
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
264258
}
265259
}
266260
if s.Err() != nil {
@@ -358,8 +352,11 @@ func parseHeaderMail(mailLine string, r io.Reader) (*PatchHeader, error) {
358352

359353
date := msg.Header.Get("Date")
360354
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
363360
}
364361

365362
h.Title = msg.Header.Get("Subject")

gitdiff/patch_header_test.go

+41-77
Original file line numberDiff line numberDiff line change
@@ -69,84 +69,62 @@ func TestParsePatchDate(t *testing.T) {
6969

7070
tests := map[string]struct {
7171
Input string
72-
Output PatchDate
72+
Output time.Time
73+
Err interface{}
7374
}{
7475
"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,
8078
},
8179
"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),
8782
},
8883
"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,
9486
},
9587
"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,
10190
},
10291
"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,
10894
},
10995
"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),
11598
},
11699
"raw": {
117-
Input: "1586419626 -0700",
118-
Output: PatchDate{
119-
Parsed: expected,
120-
Raw: "1586419626 -0700",
121-
},
100+
Input: "1586419626 -0700",
101+
Output: expected,
122102
},
123103
"unix": {
124-
Input: "1586419626",
125-
Output: PatchDate{
126-
Parsed: expected,
127-
Raw: "1586419626",
128-
},
104+
Input: "1586419626",
105+
Output: expected,
129106
},
130107
"unknownFormat": {
131108
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",
135110
},
136111
"empty": {
137-
Input: "",
138-
Output: PatchDate{},
112+
Input: "",
139113
},
140114
}
141115

142116
for name, test := range tests {
143117
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
147122
}
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)
150128
}
151129
})
152130
}
@@ -158,10 +136,7 @@ func TestParsePatchHeader(t *testing.T) {
158136
Name: "Morton Haypenny",
159137
160138
}
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))
165140
expectedTitle := "A sample commit to test header parsing"
166141
expectedBody := "The medium format shows the body, which\nmay wrap on to multiple lines.\n\nAnother body line."
167142

@@ -258,14 +233,11 @@ may wrap on to multiple lines.
258233
Another body line.
259234
`,
260235
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,
269241
},
270242
},
271243
"unwrapTitle": {
@@ -346,10 +318,14 @@ Author: Morton Haypenny <[email protected]>
346318
}
347319

348320
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+
}
350324

351325
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+
}
353329

354330
if exp.Title != act.Title {
355331
t.Errorf("incorrect parsed title:\n expected: %q\n actual: %q", exp.Title, act.Title)
@@ -372,15 +348,3 @@ func assertPatchIdentity(t *testing.T, kind string, exp, act *PatchIdentity) {
372348
t.Errorf("incorrect parsed %s, expected %+v, bot got %+v", kind, exp, act)
373349
}
374350
}
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-
}

0 commit comments

Comments
 (0)