this repo has no description
0
fork

Configure Feed

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

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.

+202 -6
+8 -4
README.md
··· 37 37 ## Known Issues and Differences From Git 38 38 39 39 1. Certain types of invalid input that I believe are accepted by `git apply` 40 - generate errors in this library. These include: 40 + generate errors. These include: 41 41 42 42 - Numbers immediately followed by non-numeric characters 43 43 - Trailing characters on a line after valid or expected content ··· 46 46 Unicode file names are handled; these are bugs, so please report any issues 47 47 of this type. 48 48 49 - 3. When reading headers, this library does not validate that OIDs present on an 50 - `index` line are shorter than or equal to the maximum hash length, as this 51 - requires knowing if the repository used SHA1 or SHA256 hashes. 49 + 3. When reading headers, there is no validation that OIDs present on an `index` 50 + line are shorter than or equal to the maximum hash length, as this requires 51 + knowing if the repository used SHA1 or SHA256 hashes. 52 + 53 + 4. When reading "traditional" patches (those not produced by `git`), prefixes 54 + are not stripped from file names (`git apply` attempts to remove prefixes 55 + that match the current repository directory/prefix.)
+30
gitdiff/file_header.go
··· 5 5 "os" 6 6 "strconv" 7 7 "strings" 8 + "time" 8 9 ) 9 10 10 11 // parseGitHeaderName extracts a default file name from the Git file header ··· 280 281 } 281 282 return b.String() 282 283 } 284 + 285 + // hasEpochTimestamp returns true if the string ends with a POSIX-formatted 286 + // timestamp for the UNIX epoch after a tab character. According to git, this 287 + // is used by GNU diff to mark creations and deletions. 288 + func hasEpochTimestamp(s string) bool { 289 + const posixTimeLayout = "2006-01-02 15:04:05.9 -0700" 290 + 291 + start := strings.IndexRune(s, '\t') 292 + if start < 0 { 293 + return false 294 + } 295 + 296 + ts := strings.TrimSuffix(s[start+1:], "\n") 297 + 298 + // a valid timestamp can have optional ':' in zone specifier 299 + // remove that if it exists so we have a single format 300 + if ts[len(ts)-3] == ':' { 301 + ts = ts[:len(ts)-3] + ts[len(ts)-2:] 302 + } 303 + 304 + t, err := time.Parse(posixTimeLayout, ts) 305 + if err != nil { 306 + return false 307 + } 308 + if !t.Equal(time.Unix(0, 0)) { 309 + return false 310 + } 311 + return true 312 + }
+49
gitdiff/file_header_test.go
··· 391 391 }) 392 392 } 393 393 } 394 + 395 + func TestHasEpochTimestamp(t *testing.T) { 396 + tests := map[string]struct { 397 + Input string 398 + Output bool 399 + }{ 400 + "utcTimestamp": { 401 + Input: "+++ file.txt\t1970-01-01 00:00:00 +0000\n", 402 + Output: true, 403 + }, 404 + "utcZoneWithColon": { 405 + Input: "+++ file.txt\t1970-01-01 00:00:00 +00:00\n", 406 + Output: true, 407 + }, 408 + "utcZoneWithMilliseconds": { 409 + Input: "+++ file.txt\t1970-01-01 00:00:00.000000 +00:00\n", 410 + Output: true, 411 + }, 412 + "westTimestamp": { 413 + Input: "+++ file.txt\t1969-12-31 16:00:00 -0800\n", 414 + Output: true, 415 + }, 416 + "eastTimestamp": { 417 + Input: "+++ file.txt\t1970-01-01 04:00:00 +0400\n", 418 + Output: true, 419 + }, 420 + "noTab": { 421 + Input: "+++ file.txt 1970-01-01 00:00:00 +0000\n", 422 + Output: false, 423 + }, 424 + "invalidFormat": { 425 + Input: "+++ file.txt\t1970-01-01T00:00:00Z\n", 426 + Output: false, 427 + }, 428 + "notEpoch": { 429 + Input: "+++ file.txt\t2019-03-21 12:34:56.789 -0700\n", 430 + Output: false, 431 + }, 432 + } 433 + 434 + for name, test := range tests { 435 + t.Run(name, func(t *testing.T) { 436 + output := hasEpochTimestamp(test.Input) 437 + if output != test.Output { 438 + t.Errorf("incorrect output: expected %t, actual %t", test.Output, output) 439 + } 440 + }) 441 + } 442 + }
+35 -2
gitdiff/parser.go
··· 36 36 return files, nil 37 37 } 38 38 39 + // TODO(bkeyes): consider exporting the parser type with configuration 40 + // this would enable OID validation, p-value guessing, and prefix stripping 41 + // by allowing users to set or override defaults 42 + 39 43 type parser struct { 40 44 r *bufio.Reader 41 45 lineno int64 ··· 168 172 return nil 169 173 } 170 174 171 - func (p *parser) ParseTraditionalFileHeader(f *File, oldFile, newFile string) error { 172 - panic("TODO(bkeyes): unimplemented") 175 + func (p *parser) ParseTraditionalFileHeader(f *File, oldLine, newLine string) error { 176 + oldName, _, err := parseName(strings.TrimPrefix(oldLine, oldFilePrefix), '\t', 0) 177 + if err != nil { 178 + return p.Errorf("file header: %v", err) 179 + } 180 + 181 + newName, _, err := parseName(strings.TrimPrefix(newLine, newFilePrefix), '\t', 0) 182 + if err != nil { 183 + return p.Errorf("file header: %v", err) 184 + } 185 + 186 + switch { 187 + case oldName == devNull || hasEpochTimestamp(oldLine): 188 + f.IsNew = true 189 + f.NewName = newName 190 + case newName == devNull || hasEpochTimestamp(newLine): 191 + f.IsDelete = true 192 + f.OldName = oldName 193 + default: 194 + // if old name is a prefix of new name, use that instead 195 + // this avoids picking variants like "file.bak" or "file~" 196 + if strings.HasPrefix(newName, oldName) { 197 + f.OldName = oldName 198 + f.NewName = oldName 199 + } else { 200 + f.OldName = newName 201 + f.NewName = newName 202 + } 203 + } 204 + return nil 173 205 } 174 206 175 207 // Line reads and returns the next line. The first call to Line after a call to ··· 197 229 } 198 230 199 231 // Errorf generates an error and appends the current line information. 232 + // TODO(bkeyes): add linedelta to allow changing lineno per-error 200 233 func (p *parser) Errorf(msg string, args ...interface{}) error { 201 234 return fmt.Errorf("gitdiff: line %d: %s", p.lineno, fmt.Sprintf(msg, args...)) 202 235 }
+80
gitdiff/parser_test.go
··· 286 286 }) 287 287 } 288 288 } 289 + 290 + func TestParseTraditionalFileHeader(t *testing.T) { 291 + tests := map[string]struct { 292 + OldLine string 293 + NewLine string 294 + Output *File 295 + Err bool 296 + }{ 297 + "fileContentChange": { 298 + OldLine: "--- dir/file_old.txt\t2019-03-21 23:00:00.0 -0700\n", 299 + NewLine: "+++ dir/file_new.txt\t2019-03-21 23:30:00.0 -0700\n", 300 + Output: &File{ 301 + OldName: "dir/file_new.txt", 302 + NewName: "dir/file_new.txt", 303 + }, 304 + }, 305 + "newFile": { 306 + OldLine: "--- /dev/null\t1969-12-31 17:00:00.0 -0700\n", 307 + NewLine: "+++ dir/file.txt\t2019-03-21 23:30:00.0 -0700\n", 308 + Output: &File{ 309 + NewName: "dir/file.txt", 310 + IsNew: true, 311 + }, 312 + }, 313 + "newFileTimestamp": { 314 + OldLine: "--- dir/file.txt\t1969-12-31 17:00:00.0 -0700\n", 315 + NewLine: "+++ dir/file.txt\t2019-03-21 23:30:00.0 -0700\n", 316 + Output: &File{ 317 + NewName: "dir/file.txt", 318 + IsNew: true, 319 + }, 320 + }, 321 + "deleteFile": { 322 + OldLine: "--- dir/file.txt\t2019-03-21 23:30:00.0 -0700\n", 323 + NewLine: "+++ /dev/null\t1969-12-31 17:00:00.0 -0700\n", 324 + Output: &File{ 325 + OldName: "dir/file.txt", 326 + IsDelete: true, 327 + }, 328 + }, 329 + "deleteFileTimestamp": { 330 + OldLine: "--- dir/file.txt\t2019-03-21 23:30:00.0 -0700\n", 331 + NewLine: "+++ dir/file.txt\t1969-12-31 17:00:00.0 -0700\n", 332 + Output: &File{ 333 + OldName: "dir/file.txt", 334 + IsDelete: true, 335 + }, 336 + }, 337 + "useShortestPrefixName": { 338 + OldLine: "--- dir/file.txt\t2019-03-21 23:00:00.0 -0700\n", 339 + NewLine: "+++ dir/file.txt~\t2019-03-21 23:30:00.0 -0700\n", 340 + Output: &File{ 341 + OldName: "dir/file.txt", 342 + NewName: "dir/file.txt", 343 + }, 344 + }, 345 + } 346 + 347 + for name, test := range tests { 348 + t.Run(name, func(t *testing.T) { 349 + p := &parser{r: bufio.NewReader(strings.NewReader(""))} 350 + 351 + var f File 352 + err := p.ParseTraditionalFileHeader(&f, test.OldLine, test.NewLine) 353 + if test.Err { 354 + if err == nil { 355 + t.Fatalf("expected error parsing traditional file header, got nil") 356 + } 357 + return 358 + } 359 + if err != nil { 360 + t.Fatalf("unexpected error parsing traditional file header: %v", err) 361 + } 362 + 363 + if test.Output != nil && !reflect.DeepEqual(f, *test.Output) { 364 + t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", *test.Output, f) 365 + } 366 + }) 367 + } 368 + }