github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/index.go (about) 1 package git 2 3 import ( 4 "crypto/sha1" 5 "encoding/binary" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "os" 12 "sort" 13 "strings" 14 ) 15 16 var InvalidIndex error = errors.New("Invalid index") 17 18 // index file is defined as Network byte Order (Big Endian) 19 20 // 12 byte header: 21 // 4 bytes: D I R C (stands for "Dir cache") 22 // 4-byte version number (can be 2, 3 or 4) 23 // 32bit number of index entries 24 type fixedGitIndex struct { 25 Signature [4]byte // 4 26 Version uint32 // 8 27 NumberIndexEntries uint32 // 12 28 } 29 30 type Index struct { 31 fixedGitIndex // 12 32 Objects []*IndexEntry 33 } 34 35 type V3IndexExtensions struct { 36 Flags uint16 37 } 38 39 type IndexEntry struct { 40 FixedIndexEntry 41 *V3IndexExtensions 42 PathName IndexPath 43 } 44 45 func (ie IndexEntry) Stage() Stage { 46 return Stage((ie.FixedIndexEntry.Flags >> 12) & 0x3) 47 } 48 49 func (ie IndexEntry) SkipWorktree() bool { 50 if ie.ExtendedFlag() == false || ie.V3IndexExtensions == nil { 51 return false 52 } 53 return (ie.V3IndexExtensions.Flags>>14)&0x1 == 1 54 } 55 func (ie *IndexEntry) SetSkipWorktree(value bool) { 56 if value { 57 // If it's being set, we need to set the extended 58 // flag. If it's not being set, we don't care, but 59 // we don't change it in case intend-to-add is set. 60 ie.FixedIndexEntry.SetExtendedFlag(true) 61 } 62 63 if ie.V3IndexExtensions == nil { 64 ie.V3IndexExtensions = &V3IndexExtensions{} 65 } 66 if value == true { 67 ie.V3IndexExtensions.Flags |= (0x1 << 14) 68 } else { 69 ie.V3IndexExtensions.Flags &^= (0x1 << 14) 70 } 71 } 72 73 func NewIndex() *Index { 74 return &Index{ 75 fixedGitIndex: fixedGitIndex{ 76 Signature: [4]byte{'D', 'I', 'R', 'C'}, 77 Version: 2, 78 NumberIndexEntries: 0, 79 }, 80 Objects: make([]*IndexEntry, 0), 81 } 82 } 83 84 type FixedIndexEntry struct { 85 Ctime uint32 // 16 86 Ctimenano uint32 // 20 87 88 Mtime int64 // 24 89 90 Dev uint32 // 32 91 Ino uint32 // 36 92 93 Mode EntryMode // 40 94 95 Uid uint32 // 44 96 Gid uint32 // 48 97 98 Fsize uint32 // 52 99 100 Sha1 Sha1 // 72 101 102 Flags uint16 // 74 103 } 104 105 func (i FixedIndexEntry) ExtendedFlag() bool { 106 return ((i.Flags >> 14) & 0x1) == 1 107 } 108 func (i *FixedIndexEntry) SetExtendedFlag(val bool) { 109 if val { 110 i.Flags |= (0x1 << 14) 111 } else { 112 i.Flags &^= (0x1 << 14) 113 } 114 } 115 116 // Refreshes the stat information for this entry using the file 117 // file 118 func (i *FixedIndexEntry) RefreshStat(f File) error { 119 log.Printf("Refreshing stat info for %v\n", f) 120 // FIXME: Add other stat info here too, but these are the 121 // most important ones and the onlye ones that the os package 122 // exposes in a cross-platform way. 123 stat, err := f.Lstat() 124 if err != nil { 125 return err 126 } 127 fmtime, err := f.MTime() 128 if err != nil { 129 return err 130 } 131 i.Mtime = fmtime 132 i.Fsize = uint32(stat.Size()) 133 i.Ctime, i.Ctimenano = f.CTime() 134 i.Ino = f.INode() 135 return nil 136 } 137 138 // Refreshes the stat information for this entry in the index against 139 // the stat info on the filesystem for things that we know about. 140 func (i *FixedIndexEntry) CompareStat(f File) error { 141 log.Printf("Comparing stat info for %v\n", f) 142 // FIXME: Add other stat info here too, but these are the 143 // most important ones and the onlye ones that the os package 144 // exposes in a cross-platform way. 145 stat, err := f.Lstat() 146 if err != nil { 147 return err 148 } 149 fmtime, err := f.MTime() 150 if err != nil { 151 return err 152 } 153 if i.Mtime != fmtime { 154 return fmt.Errorf("MTime does not match for %v", f) 155 } 156 if f.IsSymlink() { 157 dst, err := os.Readlink(string(f)) 158 if err != nil { 159 return err 160 } 161 if int(i.Fsize) != len(dst) { 162 return fmt.Errorf("Size does not match for symlink %v", f) 163 } 164 } else { 165 if i.Fsize != uint32(stat.Size()) { 166 return fmt.Errorf("Size does not match for %v", f) 167 } 168 } 169 ctime, ctimenano := f.CTime() 170 if i.Ctime != ctime || i.Ctimenano != ctimenano { 171 return fmt.Errorf("CTime does not match for %v", f) 172 } 173 if i.Ino != f.INode() { 174 return fmt.Errorf("INode does not match for %v", f) 175 } 176 return nil 177 } 178 179 // Refreshes the stat information for an index entry by comparing 180 // it against the path in the index. 181 func (ie *IndexEntry) RefreshStat(c *Client) error { 182 f, err := ie.PathName.FilePath(c) 183 if err != nil { 184 return err 185 } 186 return ie.FixedIndexEntry.RefreshStat(f) 187 } 188 189 // Reads the index file from the GitDir and returns a Index object. 190 // If the index file does not exist, it will return a new empty Index. 191 func (d GitDir) ReadIndex() (*Index, error) { 192 var file *os.File 193 var err error 194 if ifile := os.Getenv("GIT_INDEX_FILE"); ifile != "" { 195 log.Println("Using index file", ifile) 196 file, err = os.Open(ifile) 197 } else { 198 199 file, err = d.Open("index") 200 } 201 202 if err != nil { 203 if os.IsNotExist(err) { 204 // Is the file doesn't exist, treat it 205 // as a new empty index. 206 return &Index{ 207 fixedGitIndex{ 208 [4]byte{'D', 'I', 'R', 'C'}, 209 2, // version 2 210 0, // no entries 211 }, 212 make([]*IndexEntry, 0), 213 }, nil 214 } 215 return nil, err 216 } 217 defer file.Close() 218 219 var i fixedGitIndex 220 binary.Read(file, binary.BigEndian, &i) 221 if i.Signature != [4]byte{'D', 'I', 'R', 'C'} { 222 return nil, InvalidIndex 223 } 224 if i.Version < 2 || i.Version > 4 { 225 return nil, InvalidIndex 226 } 227 log.Println("Index version", i.Version) 228 229 var idx uint32 230 indexes := make([]*IndexEntry, i.NumberIndexEntries, i.NumberIndexEntries) 231 for idx = 0; idx < i.NumberIndexEntries; idx += 1 { 232 index, err := ReadIndexEntry(file, i.Version) 233 if err != nil { 234 log.Println(err) 235 } else { 236 log.Println("Read entry for ", index.PathName) 237 indexes[idx] = index 238 } 239 } 240 return &Index{i, indexes}, nil 241 } 242 243 func ReadIndexEntry(file *os.File, indexVersion uint32) (*IndexEntry, error) { 244 log.Printf("Reading index entry from %v assuming index version %d\n", file.Name(), indexVersion) 245 if indexVersion < 2 || indexVersion > 3 { 246 return nil, fmt.Errorf("Unsupported index version.") 247 } 248 var f FixedIndexEntry 249 var name []byte 250 if err := binary.Read(file, binary.BigEndian, &f); err != nil { 251 return nil, err 252 } 253 254 var v3e *V3IndexExtensions 255 if f.ExtendedFlag() { 256 if indexVersion < 3 { 257 return nil, InvalidIndex 258 } 259 v3e = &V3IndexExtensions{} 260 if err := binary.Read(file, binary.BigEndian, v3e); err != nil { 261 return nil, err 262 } 263 } 264 265 var nameLength uint16 266 nameLength = f.Flags & 0x0FFF 267 268 if nameLength&0xFFF != 0xFFF { 269 name = make([]byte, nameLength, nameLength) 270 n, err := file.Read(name) 271 if err != nil { 272 panic("I don't know what to do") 273 } 274 if n != int(nameLength) { 275 panic(fmt.Sprintf("Error reading the name read %d (got :%v)", n, string(name[:n]))) 276 } 277 278 // I don't understand where this +4 comes from, but it seems to work 279 // out with how the c git implementation calculates the padding.. 280 // 281 // The definition of the index file format at: 282 // https://github.com/git/git/blob/master/Documentation/technical/index-format.txt 283 // claims that there should be "1-8 nul bytes as necessary to pad the entry to a multiple of eight 284 // bytes while keeping the name NUL-terminated." 285 // 286 // The fixed size of the header is 82 bytes if you add up all the types. 287 // the length of the name is nameLength bytes, so according to the spec 288 // this *should* be 8 - ((82 + nameLength) % 8) bytes of padding. 289 // But reading existant index files, there seems to be an extra 4 bytes 290 // incorporated into the index size calculation. 291 sz := uint16(82) 292 293 if f.ExtendedFlag() { 294 // Add 2 bytes if the extended flag is set for the V3 extensions 295 sz += 2 296 } 297 expectedOffset := 8 - ((sz + nameLength + 4) % 8) 298 file.Seek(int64(expectedOffset), 1) 299 } else { 300 301 nbyte := make([]byte, 1, 1) 302 303 // This algorithm isn't very efficient, reading one byte at a time, but we 304 // reserve a big space for name to make it slightly more efficient, since 305 // we know it's a large name 306 name = make([]byte, 0, 8192) 307 for _, err := file.Read(nbyte); nbyte[0] != 0; _, err = file.Read(nbyte) { 308 if err != nil { 309 return nil, err 310 } 311 name = append(name, nbyte...) 312 } 313 off, err := file.Seek(0, io.SeekCurrent) 314 if err != nil { 315 return nil, err 316 } 317 // The mystery 4 appears again. 318 padding := 8 - ((off + 4) % 8) 319 if _, err := file.Seek(padding, io.SeekCurrent); err != nil { 320 return nil, err 321 } 322 } 323 return &IndexEntry{f, v3e, IndexPath(name)}, nil 324 } 325 326 // A Stage represents a git merge stage in the index. 327 type Stage uint8 328 329 // Valid merge stages. 330 const ( 331 Stage0 = Stage(iota) 332 Stage1 333 Stage2 334 Stage3 335 ) 336 337 func (g *Index) SetSkipWorktree(c *Client, path IndexPath, value bool) error { 338 for _, entry := range g.Objects { 339 if entry.PathName == path { 340 if entry.Stage() != Stage0 { 341 return fmt.Errorf("Can not set skip worktree on unmerged paths") 342 } 343 entry.SetSkipWorktree(value) 344 break 345 } 346 } 347 if g.Version <= 2 && value { 348 g.Version = 3 349 } 350 return nil 351 } 352 353 // Adds an entry to the index with Sha1 s and stage stage during a merge. 354 // If an entry already exists for this pathname/stage, it will be overwritten, 355 // otherwise it will be added if createEntry is true, and return an error if not. 356 // 357 // As a special case, if something is added as Stage0, then Stage1-3 entries 358 // will be removed. 359 func (g *Index) AddStage(c *Client, path IndexPath, mode EntryMode, s Sha1, stage Stage, size uint32, mtime int64, opts UpdateIndexOptions) error { 360 if stage == Stage0 { 361 defer g.RemoveUnmergedStages(c, path) 362 } 363 364 replaceEntriesCheck := func() error { 365 if stage != Stage0 { 366 return nil 367 } 368 // If replace is true then we search for any entries that 369 // should be replaced with this one. 370 newObjects := make([]*IndexEntry, 0, len(g.Objects)) 371 for _, e := range g.Objects { 372 if strings.HasPrefix(string(e.PathName), string(path)+"/") { 373 if !opts.Replace { 374 return fmt.Errorf("There is an existing file %s under %s, should it be replaced?", e.PathName, path) 375 } 376 continue 377 } else if strings.HasPrefix(string(path), string(e.PathName)+"/") { 378 if !opts.Replace { 379 return fmt.Errorf("There is a parent file %s above %s, should it be replaced?", e.PathName, path) 380 } 381 continue 382 } 383 384 newObjects = append(newObjects, e) 385 } 386 387 g.Objects = newObjects 388 389 return nil 390 } 391 392 // Update the existing stage, if it exists. 393 for _, entry := range g.Objects { 394 if entry.PathName == path && entry.Stage() == stage { 395 if err := replaceEntriesCheck(); err != nil { 396 return err 397 } 398 399 file, _ := path.FilePath(c) 400 if file.Exists() && stage == Stage0 { 401 // FIXME: mtime/fsize/etc and ctime should either all be 402 // from the filesystem, or all come from the caller 403 // For now we just refresh the stat, and then overwrite with 404 // the stuff from the caller. 405 log.Println("Refreshing stat for", path) 406 if err := entry.RefreshStat(c); err != nil { 407 return err 408 } 409 } 410 entry.Sha1 = s 411 entry.Mtime = mtime 412 entry.Fsize = size 413 return nil 414 } 415 } 416 417 if !opts.Add { 418 return fmt.Errorf("%v not found in index", path) 419 } 420 // There was no path/stage combo already in the index. Add it. 421 422 // According to the git documentation: 423 // Flags is 424 // A 16-bit 'flags' field split into (high to low bits) 425 // 426 // 1-bit assume-valid flag 427 // 428 // 1-bit extended flag (must be zero in version 2) 429 // 430 // 2-bit stage (during merge) 431 // 12-bit name length if the length is less than 0xFFF; otherwise 0xFFF 432 // is stored in this field. 433 434 // So we'll construct the flags based on what we know. 435 436 var flags = uint16(stage) << 12 // start with the stage. 437 // Add the name length. 438 if len(path) >= 0x0FFF { 439 flags |= 0x0FFF 440 } else { 441 flags |= (uint16(len(path)) & 0x0FFF) 442 } 443 444 if err := replaceEntriesCheck(); err != nil { 445 return err 446 } 447 newentry := &IndexEntry{ 448 FixedIndexEntry{ 449 0, //uint32(csec), 450 0, //uint32(cnano), 451 mtime, 452 0, //uint32(stat.Dev), 453 0, //uint32(stat.Ino), 454 mode, 455 0, //stat.Uid, 456 0, //stat.Gid, 457 size, 458 s, 459 flags, 460 }, 461 &V3IndexExtensions{}, 462 path, 463 } 464 newentry.RefreshStat(c) 465 466 g.Objects = append(g.Objects, newentry) 467 g.NumberIndexEntries += 1 468 sort.Sort(ByPath(g.Objects)) 469 return nil 470 } 471 472 // Remove any unmerged (non-stage 0) stage from the index for the given path 473 func (g *Index) RemoveUnmergedStages(c *Client, path IndexPath) error { 474 // There are likely 3 things being deleted, so make a new slice 475 newobjects := make([]*IndexEntry, 0, len(g.Objects)) 476 for _, entry := range g.Objects { 477 stage := entry.Stage() 478 if entry.PathName == path && stage == Stage0 { 479 newobjects = append(newobjects, entry) 480 } else if entry.PathName == path && stage != Stage0 { 481 // do not add it, it's the wrong stage. 482 } else { 483 // It's a different Pathname, keep it. 484 newobjects = append(newobjects, entry) 485 } 486 } 487 g.Objects = newobjects 488 g.NumberIndexEntries = uint32(len(newobjects)) 489 return nil 490 } 491 492 // Adds a file to the index, without writing it to disk. 493 // To write it to disk after calling this, use GitIndex.WriteIndex 494 // 495 // This will do the following: 496 // 1. write git object blob of file contents to .git/objects 497 // 2. normalize os.File name to path relative to gitRoot 498 // 3. search GitIndex for normalized name 499 // if GitIndexEntry found 500 // update GitIndexEntry to point to the new object blob 501 // else 502 // add new GitIndexEntry if not found and createEntry is true, error otherwise 503 // 504 func (g *Index) AddFile(c *Client, file File, opts UpdateIndexOptions) error { 505 name, err := file.IndexPath(c) 506 if err != nil { 507 return err 508 } 509 510 mtime, err := file.MTime() 511 if err != nil { 512 return err 513 } 514 515 fsize := uint32(0) 516 fstat, err := file.Lstat() 517 if err == nil { 518 fsize = uint32(fstat.Size()) 519 } 520 if fstat.IsDir() { 521 return fmt.Errorf("Must add a file, not a directory.") 522 } 523 var mode EntryMode 524 525 var hash Sha1 526 if file.IsSymlink() { 527 mode = ModeSymlink 528 contents, err := os.Readlink(string(file)) 529 if err != nil { 530 return err 531 } 532 hash1, err := c.WriteObject("blob", []byte(contents)) 533 if err != nil { 534 fmt.Fprintf(os.Stderr, "Error storing object: %s", err) 535 } 536 hash = hash1 537 } else { 538 mode = ModeBlob 539 contents, err := ioutil.ReadFile(string(file)) 540 if err != nil { 541 return err 542 } 543 hash1, err := c.WriteObject("blob", contents) 544 if err != nil { 545 fmt.Fprintf(os.Stderr, "Error storing object: %s", err) 546 return err 547 } 548 hash = hash1 549 } 550 551 return g.AddStage( 552 c, 553 name, 554 mode, 555 hash, 556 Stage0, 557 fsize, 558 mtime, 559 opts, 560 ) 561 } 562 563 type IndexStageEntry struct { 564 IndexPath 565 Stage 566 } 567 568 func (i *Index) GetStageMap() map[IndexStageEntry]*IndexEntry { 569 r := make(map[IndexStageEntry]*IndexEntry) 570 for _, entry := range i.Objects { 571 r[IndexStageEntry{entry.PathName, entry.Stage()}] = entry 572 } 573 return r 574 } 575 576 type UnmergedPath struct { 577 Stage1, Stage2, Stage3 *IndexEntry 578 } 579 580 func (i *Index) GetUnmerged() map[IndexPath]*UnmergedPath { 581 r := make(map[IndexPath]*UnmergedPath) 582 for _, entry := range i.Objects { 583 if entry.Stage() != Stage0 { 584 e, ok := r[entry.PathName] 585 if !ok { 586 e = &UnmergedPath{} 587 r[entry.PathName] = e 588 } 589 switch entry.Stage() { 590 case Stage1: 591 e.Stage1 = entry 592 case Stage2: 593 e.Stage2 = entry 594 case Stage3: 595 e.Stage3 = entry 596 } 597 } 598 } 599 return r 600 } 601 602 // Remove the first instance of file from the index. (This will usually 603 // be stage 0.) 604 func (g *Index) RemoveFile(file IndexPath) { 605 for i, entry := range g.Objects { 606 if entry.PathName == file { 607 g.Objects = append(g.Objects[:i], g.Objects[i+1:]...) 608 g.NumberIndexEntries -= 1 609 return 610 } 611 } 612 } 613 614 // This will write a new index file to w by doing the following: 615 // 1. Sort the objects in g.Index to ascending order based on name and update 616 // g.NumberIndexEntries 617 // 2. Write g.fixedGitIndex to w 618 // 3. for each entry in g.Objects, write it to w. 619 // 4. Write the Sha1 of the contents of what was written 620 func (g Index) WriteIndex(file io.Writer) error { 621 sort.Sort(ByPath(g.Objects)) 622 g.NumberIndexEntries = uint32(len(g.Objects)) 623 s := sha1.New() 624 w := io.MultiWriter(file, s) 625 binary.Write(w, binary.BigEndian, g.fixedGitIndex) 626 for _, entry := range g.Objects { 627 if err := binary.Write(w, binary.BigEndian, entry.FixedIndexEntry); err != nil { 628 return err 629 } 630 if entry.ExtendedFlag() { 631 if g.Version == 2 || entry.V3IndexExtensions == nil { 632 return InvalidIndex 633 } 634 if err := binary.Write(w, binary.BigEndian, *entry.V3IndexExtensions); err != nil { 635 return err 636 } 637 } 638 if err := binary.Write(w, binary.BigEndian, []byte(entry.PathName)); err != nil { 639 return err 640 } 641 sz := 82 642 if entry.ExtendedFlag() { 643 sz += 2 644 } 645 padding := 8 - ((sz + len(entry.PathName) + 4) % 8) 646 p := make([]byte, padding) 647 if err := binary.Write(w, binary.BigEndian, p); err != nil { 648 return err 649 } 650 } 651 binary.Write(w, binary.BigEndian, s.Sum(nil)) 652 return nil 653 } 654 655 // Looks up the Sha1 of path currently stored in the index. 656 // Will return the 0 Sha1 if not found. 657 func (g Index) GetSha1(path IndexPath) Sha1 { 658 for _, entry := range g.Objects { 659 if entry.PathName == path { 660 return entry.Sha1 661 } 662 } 663 return Sha1{} 664 } 665 666 // Implement the sort interface on *GitIndexEntry, so that 667 // it's easy to sort by name. 668 type ByPath []*IndexEntry 669 670 func (g ByPath) Len() int { return len(g) } 671 func (g ByPath) Swap(i, j int) { g[i], g[j] = g[j], g[i] } 672 func (g ByPath) Less(i, j int) bool { 673 if g[i].PathName == g[j].PathName { 674 return g[i].Stage() < g[j].Stage() 675 } 676 ibytes := []byte(g[i].PathName) 677 jbytes := []byte(g[j].PathName) 678 for k := range ibytes { 679 if k >= len(jbytes) { 680 // We reached the end of j and there was stuff 681 // leftover in i, so i > j 682 return false 683 } 684 685 // If a character is not equal, return if it's 686 // less or greater 687 if ibytes[k] < jbytes[k] { 688 return true 689 } else if ibytes[k] > jbytes[k] { 690 return false 691 } 692 } 693 // Everything equal up to the end of i, and there is stuff 694 // left in j, so i < j 695 return true 696 } 697 698 // Replaces the index of Client with the the tree from the provided Treeish. 699 // if PreserveStatInfo is true, the stat information in the index won't be 700 // modified for existing entries. 701 func (g *Index) ResetIndex(c *Client, tree Treeish) error { 702 newEntries, err := expandGitTreeIntoIndexes(c, tree, true, false, false) 703 if err != nil { 704 return err 705 } 706 g.NumberIndexEntries = uint32(len(newEntries)) 707 g.Objects = newEntries 708 return nil 709 } 710 711 func (g Index) String() string { 712 ret := "" 713 714 for _, i := range g.Objects { 715 ret += fmt.Sprintf("%v %v %v\n", i.Mode, i.Sha1, i.PathName) 716 } 717 return ret 718 } 719 720 type IndexMap map[IndexPath]*IndexEntry 721 722 func (i *Index) GetMap() IndexMap { 723 r := make(IndexMap) 724 for _, entry := range i.Objects { 725 r[entry.PathName] = entry 726 } 727 return r 728 } 729 730 func (im IndexMap) Contains(path IndexPath) bool { 731 if _, ok := im[path]; ok { 732 return true 733 } 734 735 // Check of there is a directory named path in the IndexMap 736 return im.HasDir(path) 737 } 738 739 func (im IndexMap) HasDir(path IndexPath) bool { 740 for _, im := range im { 741 if strings.HasPrefix(string(im.PathName), string(path+"/")) { 742 return true 743 } 744 } 745 return false 746 }