Implement traditional file header parsing

While I think this works, this is mostly for completeness. I (and I
expect most other users, if any) intend to use this with git patches.
This commit is contained in:
Billy Keyes
2019-03-21 22:47:52 -07:00
parent 412e7a39b1
commit cf2f946a66
5 changed files with 202 additions and 6 deletions
+30
View File
@@ -5,6 +5,7 @@ import (
"os"
"strconv"
"strings"
"time"
)
// parseGitHeaderName extracts a default file name from the Git file header
@@ -280,3 +281,32 @@ func cleanName(name string, drop int) string {
}
return b.String()
}
// hasEpochTimestamp returns true if the string ends with a POSIX-formatted
// timestamp for the UNIX epoch after a tab character. According to git, this
// is used by GNU diff to mark creations and deletions.
func hasEpochTimestamp(s string) bool {
const posixTimeLayout = "2006-01-02 15:04:05.9 -0700"
start := strings.IndexRune(s, '\t')
if start < 0 {
return false
}
ts := strings.TrimSuffix(s[start+1:], "\n")
// a valid timestamp can have optional ':' in zone specifier
// remove that if it exists so we have a single format
if ts[len(ts)-3] == ':' {
ts = ts[:len(ts)-3] + ts[len(ts)-2:]
}
t, err := time.Parse(posixTimeLayout, ts)
if err != nil {
return false
}
if !t.Equal(time.Unix(0, 0)) {
return false
}
return true
}
+49
View File
@@ -391,3 +391,52 @@ func TestParseGitHeaderName(t *testing.T) {
})
}
}
func TestHasEpochTimestamp(t *testing.T) {
tests := map[string]struct {
Input string
Output bool
}{
"utcTimestamp": {
Input: "+++ file.txt\t1970-01-01 00:00:00 +0000\n",
Output: true,
},
"utcZoneWithColon": {
Input: "+++ file.txt\t1970-01-01 00:00:00 +00:00\n",
Output: true,
},
"utcZoneWithMilliseconds": {
Input: "+++ file.txt\t1970-01-01 00:00:00.000000 +00:00\n",
Output: true,
},
"westTimestamp": {
Input: "+++ file.txt\t1969-12-31 16:00:00 -0800\n",
Output: true,
},
"eastTimestamp": {
Input: "+++ file.txt\t1970-01-01 04:00:00 +0400\n",
Output: true,
},
"noTab": {
Input: "+++ file.txt 1970-01-01 00:00:00 +0000\n",
Output: false,
},
"invalidFormat": {
Input: "+++ file.txt\t1970-01-01T00:00:00Z\n",
Output: false,
},
"notEpoch": {
Input: "+++ file.txt\t2019-03-21 12:34:56.789 -0700\n",
Output: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
output := hasEpochTimestamp(test.Input)
if output != test.Output {
t.Errorf("incorrect output: expected %t, actual %t", test.Output, output)
}
})
}
}
+35 -2
View File
@@ -36,6 +36,10 @@ func Parse(r io.Reader) (files []*File, err error) {
return files, nil
}
// TODO(bkeyes): consider exporting the parser type with configuration
// this would enable OID validation, p-value guessing, and prefix stripping
// by allowing users to set or override defaults
type parser struct {
r *bufio.Reader
lineno int64
@@ -168,8 +172,36 @@ func (p *parser) ParseGitFileHeader(f *File, header string) error {
return nil
}
func (p *parser) ParseTraditionalFileHeader(f *File, oldFile, newFile string) error {
panic("TODO(bkeyes): unimplemented")
func (p *parser) ParseTraditionalFileHeader(f *File, oldLine, newLine string) error {
oldName, _, err := parseName(strings.TrimPrefix(oldLine, oldFilePrefix), '\t', 0)
if err != nil {
return p.Errorf("file header: %v", err)
}
newName, _, err := parseName(strings.TrimPrefix(newLine, newFilePrefix), '\t', 0)
if err != nil {
return p.Errorf("file header: %v", err)
}
switch {
case oldName == devNull || hasEpochTimestamp(oldLine):
f.IsNew = true
f.NewName = newName
case newName == devNull || hasEpochTimestamp(newLine):
f.IsDelete = true
f.OldName = oldName
default:
// if old name is a prefix of new name, use that instead
// this avoids picking variants like "file.bak" or "file~"
if strings.HasPrefix(newName, oldName) {
f.OldName = oldName
f.NewName = oldName
} else {
f.OldName = newName
f.NewName = newName
}
}
return nil
}
// Line reads and returns the next line. The first call to Line after a call to
@@ -197,6 +229,7 @@ func (p *parser) PeekLine() (line string, err error) {
}
// Errorf generates an error and appends the current line information.
// TODO(bkeyes): add linedelta to allow changing lineno per-error
func (p *parser) Errorf(msg string, args ...interface{}) error {
return fmt.Errorf("gitdiff: line %d: %s", p.lineno, fmt.Sprintf(msg, args...))
}
+80
View File
@@ -286,3 +286,83 @@ index deadbeef
})
}
}
func TestParseTraditionalFileHeader(t *testing.T) {
tests := map[string]struct {
OldLine string
NewLine string
Output *File
Err bool
}{
"fileContentChange": {
OldLine: "--- dir/file_old.txt\t2019-03-21 23:00:00.0 -0700\n",
NewLine: "+++ dir/file_new.txt\t2019-03-21 23:30:00.0 -0700\n",
Output: &File{
OldName: "dir/file_new.txt",
NewName: "dir/file_new.txt",
},
},
"newFile": {
OldLine: "--- /dev/null\t1969-12-31 17:00:00.0 -0700\n",
NewLine: "+++ dir/file.txt\t2019-03-21 23:30:00.0 -0700\n",
Output: &File{
NewName: "dir/file.txt",
IsNew: true,
},
},
"newFileTimestamp": {
OldLine: "--- dir/file.txt\t1969-12-31 17:00:00.0 -0700\n",
NewLine: "+++ dir/file.txt\t2019-03-21 23:30:00.0 -0700\n",
Output: &File{
NewName: "dir/file.txt",
IsNew: true,
},
},
"deleteFile": {
OldLine: "--- dir/file.txt\t2019-03-21 23:30:00.0 -0700\n",
NewLine: "+++ /dev/null\t1969-12-31 17:00:00.0 -0700\n",
Output: &File{
OldName: "dir/file.txt",
IsDelete: true,
},
},
"deleteFileTimestamp": {
OldLine: "--- dir/file.txt\t2019-03-21 23:30:00.0 -0700\n",
NewLine: "+++ dir/file.txt\t1969-12-31 17:00:00.0 -0700\n",
Output: &File{
OldName: "dir/file.txt",
IsDelete: true,
},
},
"useShortestPrefixName": {
OldLine: "--- dir/file.txt\t2019-03-21 23:00:00.0 -0700\n",
NewLine: "+++ dir/file.txt~\t2019-03-21 23:30:00.0 -0700\n",
Output: &File{
OldName: "dir/file.txt",
NewName: "dir/file.txt",
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
p := &parser{r: bufio.NewReader(strings.NewReader(""))}
var f File
err := p.ParseTraditionalFileHeader(&f, test.OldLine, test.NewLine)
if test.Err {
if err == nil {
t.Fatalf("expected error parsing traditional file header, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error parsing traditional file header: %v", err)
}
if test.Output != nil && !reflect.DeepEqual(f, *test.Output) {
t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", *test.Output, f)
}
})
}
}