mirror of
https://github.com/bluekeyes/go-gitdiff
synced 2026-06-08 13:18:30 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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...))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user