1# index.py -- File parser/writer for the git index file 2# Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@jelmer.uk> 3# 4# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU 5# General Public License as public by the Free Software Foundation; version 2.0 6# or (at your option) any later version. You can redistribute it and/or 7# modify it under the terms of either of these two licenses. 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# 15# You should have received a copy of the licenses; if not, see 16# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License 17# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache 18# License, Version 2.0. 19# 20 21"""Parser for the git index file format.""" 22 23import collections 24import errno 25import os 26import stat 27import struct 28import sys 29 30from dulwich.file import GitFile 31from dulwich.objects import ( 32 Blob, 33 S_IFGITLINK, 34 S_ISGITLINK, 35 Tree, 36 hex_to_sha, 37 sha_to_hex, 38 ) 39from dulwich.pack import ( 40 SHA1Reader, 41 SHA1Writer, 42 ) 43 44 45IndexEntry = collections.namedtuple( 46 'IndexEntry', [ 47 'ctime', 'mtime', 'dev', 'ino', 'mode', 'uid', 'gid', 'size', 'sha', 48 'flags']) 49 50 51FLAG_STAGEMASK = 0x3000 52FLAG_VALID = 0x8000 53FLAG_EXTENDED = 0x4000 54 55 56def pathsplit(path): 57 """Split a /-delimited path into a directory part and a basename. 58 59 Args: 60 path: The path to split. 61 Returns: 62 Tuple with directory name and basename 63 """ 64 try: 65 (dirname, basename) = path.rsplit(b"/", 1) 66 except ValueError: 67 return (b"", path) 68 else: 69 return (dirname, basename) 70 71 72def pathjoin(*args): 73 """Join a /-delimited path. 74 75 """ 76 return b"/".join([p for p in args if p]) 77 78 79def read_cache_time(f): 80 """Read a cache time. 81 82 Args: 83 f: File-like object to read from 84 Returns: 85 Tuple with seconds and nanoseconds 86 """ 87 return struct.unpack(">LL", f.read(8)) 88 89 90def write_cache_time(f, t): 91 """Write a cache time. 92 93 Args: 94 f: File-like object to write to 95 t: Time to write (as int, float or tuple with secs and nsecs) 96 """ 97 if isinstance(t, int): 98 t = (t, 0) 99 elif isinstance(t, float): 100 (secs, nsecs) = divmod(t, 1.0) 101 t = (int(secs), int(nsecs * 1000000000)) 102 elif not isinstance(t, tuple): 103 raise TypeError(t) 104 f.write(struct.pack(">LL", *t)) 105 106 107def read_cache_entry(f): 108 """Read an entry from a cache file. 109 110 Args: 111 f: File-like object to read from 112 Returns: 113 tuple with: device, inode, mode, uid, gid, size, sha, flags 114 """ 115 beginoffset = f.tell() 116 ctime = read_cache_time(f) 117 mtime = read_cache_time(f) 118 (dev, ino, mode, uid, gid, size, sha, flags, ) = \ 119 struct.unpack(">LLLLLL20sH", f.read(20 + 4 * 6 + 2)) 120 name = f.read((flags & 0x0fff)) 121 # Padding: 122 real_size = ((f.tell() - beginoffset + 8) & ~7) 123 f.read((beginoffset + real_size) - f.tell()) 124 return (name, ctime, mtime, dev, ino, mode, uid, gid, size, 125 sha_to_hex(sha), flags & ~0x0fff) 126 127 128def write_cache_entry(f, entry): 129 """Write an index entry to a file. 130 131 Args: 132 f: File object 133 entry: Entry to write, tuple with: 134 (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) 135 """ 136 beginoffset = f.tell() 137 (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = entry 138 write_cache_time(f, ctime) 139 write_cache_time(f, mtime) 140 flags = len(name) | (flags & ~0x0fff) 141 f.write(struct.pack( 142 b'>LLLLLL20sH', dev & 0xFFFFFFFF, ino & 0xFFFFFFFF, 143 mode, uid, gid, size, hex_to_sha(sha), flags)) 144 f.write(name) 145 real_size = ((f.tell() - beginoffset + 8) & ~7) 146 f.write(b'\0' * ((beginoffset + real_size) - f.tell())) 147 148 149def read_index(f): 150 """Read an index file, yielding the individual entries.""" 151 header = f.read(4) 152 if header != b'DIRC': 153 raise AssertionError("Invalid index file header: %r" % header) 154 (version, num_entries) = struct.unpack(b'>LL', f.read(4 * 2)) 155 assert version in (1, 2) 156 for i in range(num_entries): 157 yield read_cache_entry(f) 158 159 160def read_index_dict(f): 161 """Read an index file and return it as a dictionary. 162 163 Args: 164 f: File object to read from 165 """ 166 ret = {} 167 for x in read_index(f): 168 ret[x[0]] = IndexEntry(*x[1:]) 169 return ret 170 171 172def write_index(f, entries): 173 """Write an index file. 174 175 Args: 176 f: File-like object to write to 177 entries: Iterable over the entries to write 178 """ 179 f.write(b'DIRC') 180 f.write(struct.pack(b'>LL', 2, len(entries))) 181 for x in entries: 182 write_cache_entry(f, x) 183 184 185def write_index_dict(f, entries): 186 """Write an index file based on the contents of a dictionary. 187 188 """ 189 entries_list = [] 190 for name in sorted(entries): 191 entries_list.append((name,) + tuple(entries[name])) 192 write_index(f, entries_list) 193 194 195def cleanup_mode(mode): 196 """Cleanup a mode value. 197 198 This will return a mode that can be stored in a tree object. 199 200 Args: 201 mode: Mode to clean up. 202 """ 203 if stat.S_ISLNK(mode): 204 return stat.S_IFLNK 205 elif stat.S_ISDIR(mode): 206 return stat.S_IFDIR 207 elif S_ISGITLINK(mode): 208 return S_IFGITLINK 209 ret = stat.S_IFREG | 0o644 210 if mode & 0o100: 211 ret |= 0o111 212 return ret 213 214 215class Index(object): 216 """A Git Index file.""" 217 218 def __init__(self, filename): 219 """Open an index file. 220 221 Args: 222 filename: Path to the index file 223 """ 224 self._filename = filename 225 self.clear() 226 self.read() 227 228 @property 229 def path(self): 230 return self._filename 231 232 def __repr__(self): 233 return "%s(%r)" % (self.__class__.__name__, self._filename) 234 235 def write(self): 236 """Write current contents of index to disk.""" 237 f = GitFile(self._filename, 'wb') 238 try: 239 f = SHA1Writer(f) 240 write_index_dict(f, self._byname) 241 finally: 242 f.close() 243 244 def read(self): 245 """Read current contents of index from disk.""" 246 if not os.path.exists(self._filename): 247 return 248 f = GitFile(self._filename, 'rb') 249 try: 250 f = SHA1Reader(f) 251 for x in read_index(f): 252 self[x[0]] = IndexEntry(*x[1:]) 253 # FIXME: Additional data? 254 f.read(os.path.getsize(self._filename)-f.tell()-20) 255 f.check_sha() 256 finally: 257 f.close() 258 259 def __len__(self): 260 """Number of entries in this index file.""" 261 return len(self._byname) 262 263 def __getitem__(self, name): 264 """Retrieve entry by relative path. 265 266 Returns: tuple with (ctime, mtime, dev, ino, mode, uid, gid, size, sha, 267 flags) 268 """ 269 return self._byname[name] 270 271 def __iter__(self): 272 """Iterate over the paths in this index.""" 273 return iter(self._byname) 274 275 def get_sha1(self, path): 276 """Return the (git object) SHA1 for the object at a path.""" 277 return self[path].sha 278 279 def get_mode(self, path): 280 """Return the POSIX file mode for the object at a path.""" 281 return self[path].mode 282 283 def iterobjects(self): 284 """Iterate over path, sha, mode tuples for use with commit_tree.""" 285 for path in self: 286 entry = self[path] 287 yield path, entry.sha, cleanup_mode(entry.mode) 288 289 def iterblobs(self): 290 import warnings 291 warnings.warn('Use iterobjects() instead.', PendingDeprecationWarning) 292 return self.iterobjects() 293 294 def clear(self): 295 """Remove all contents from this index.""" 296 self._byname = {} 297 298 def __setitem__(self, name, x): 299 assert isinstance(name, bytes) 300 assert len(x) == 10 301 # Remove the old entry if any 302 self._byname[name] = IndexEntry(*x) 303 304 def __delitem__(self, name): 305 assert isinstance(name, bytes) 306 del self._byname[name] 307 308 def iteritems(self): 309 return self._byname.items() 310 311 def items(self): 312 return self._byname.items() 313 314 def update(self, entries): 315 for name, value in entries.items(): 316 self[name] = value 317 318 def changes_from_tree(self, object_store, tree, want_unchanged=False): 319 """Find the differences between the contents of this index and a tree. 320 321 Args: 322 object_store: Object store to use for retrieving tree contents 323 tree: SHA1 of the root tree 324 want_unchanged: Whether unchanged files should be reported 325 Returns: Iterator over tuples with (oldpath, newpath), (oldmode, 326 newmode), (oldsha, newsha) 327 """ 328 def lookup_entry(path): 329 entry = self[path] 330 return entry.sha, cleanup_mode(entry.mode) 331 for (name, mode, sha) in changes_from_tree( 332 self._byname.keys(), lookup_entry, object_store, tree, 333 want_unchanged=want_unchanged): 334 yield (name, mode, sha) 335 336 def commit(self, object_store): 337 """Create a new tree from an index. 338 339 Args: 340 object_store: Object store to save the tree in 341 Returns: 342 Root tree SHA 343 """ 344 return commit_tree(object_store, self.iterobjects()) 345 346 347def commit_tree(object_store, blobs): 348 """Commit a new tree. 349 350 Args: 351 object_store: Object store to add trees to 352 blobs: Iterable over blob path, sha, mode entries 353 Returns: 354 SHA1 of the created tree. 355 """ 356 357 trees = {b'': {}} 358 359 def add_tree(path): 360 if path in trees: 361 return trees[path] 362 dirname, basename = pathsplit(path) 363 t = add_tree(dirname) 364 assert isinstance(basename, bytes) 365 newtree = {} 366 t[basename] = newtree 367 trees[path] = newtree 368 return newtree 369 370 for path, sha, mode in blobs: 371 tree_path, basename = pathsplit(path) 372 tree = add_tree(tree_path) 373 tree[basename] = (mode, sha) 374 375 def build_tree(path): 376 tree = Tree() 377 for basename, entry in trees[path].items(): 378 if isinstance(entry, dict): 379 mode = stat.S_IFDIR 380 sha = build_tree(pathjoin(path, basename)) 381 else: 382 (mode, sha) = entry 383 tree.add(basename, mode, sha) 384 object_store.add_object(tree) 385 return tree.id 386 return build_tree(b'') 387 388 389def commit_index(object_store, index): 390 """Create a new tree from an index. 391 392 Args: 393 object_store: Object store to save the tree in 394 index: Index file 395 Note: This function is deprecated, use index.commit() instead. 396 Returns: Root tree sha. 397 """ 398 return commit_tree(object_store, index.iterobjects()) 399 400 401def changes_from_tree(names, lookup_entry, object_store, tree, 402 want_unchanged=False): 403 """Find the differences between the contents of a tree and 404 a working copy. 405 406 Args: 407 names: Iterable of names in the working copy 408 lookup_entry: Function to lookup an entry in the working copy 409 object_store: Object store to use for retrieving tree contents 410 tree: SHA1 of the root tree, or None for an empty tree 411 want_unchanged: Whether unchanged files should be reported 412 Returns: Iterator over tuples with (oldpath, newpath), (oldmode, newmode), 413 (oldsha, newsha) 414 """ 415 # TODO(jelmer): Support a include_trees option 416 other_names = set(names) 417 418 if tree is not None: 419 for (name, mode, sha) in object_store.iter_tree_contents(tree): 420 try: 421 (other_sha, other_mode) = lookup_entry(name) 422 except KeyError: 423 # Was removed 424 yield ((name, None), (mode, None), (sha, None)) 425 else: 426 other_names.remove(name) 427 if (want_unchanged or other_sha != sha or other_mode != mode): 428 yield ((name, name), (mode, other_mode), (sha, other_sha)) 429 430 # Mention added files 431 for name in other_names: 432 try: 433 (other_sha, other_mode) = lookup_entry(name) 434 except KeyError: 435 pass 436 else: 437 yield ((None, name), (None, other_mode), (None, other_sha)) 438 439 440def index_entry_from_stat(stat_val, hex_sha, flags, mode=None): 441 """Create a new index entry from a stat value. 442 443 Args: 444 stat_val: POSIX stat_result instance 445 hex_sha: Hex sha of the object 446 flags: Index flags 447 """ 448 if mode is None: 449 mode = cleanup_mode(stat_val.st_mode) 450 451 return IndexEntry( 452 stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev, 453 stat_val.st_ino, mode, stat_val.st_uid, 454 stat_val.st_gid, stat_val.st_size, hex_sha, flags) 455 456 457def build_file_from_blob(blob, mode, target_path, honor_filemode=True): 458 """Build a file or symlink on disk based on a Git object. 459 460 Args: 461 obj: The git object 462 mode: File mode 463 target_path: Path to write to 464 honor_filemode: An optional flag to honor core.filemode setting in 465 config file, default is core.filemode=True, change executable bit 466 Returns: stat object for the file 467 """ 468 try: 469 oldstat = os.lstat(target_path) 470 except OSError as e: 471 if e.errno == errno.ENOENT: 472 oldstat = None 473 else: 474 raise 475 contents = blob.as_raw_string() 476 if stat.S_ISLNK(mode): 477 # FIXME: This will fail on Windows. What should we do instead? 478 if oldstat: 479 os.unlink(target_path) 480 if sys.platform == 'win32' and sys.version_info[0] == 3: 481 # os.readlink on Python3 on Windows requires a unicode string. 482 # TODO(jelmer): Don't assume tree_encoding == fs_encoding 483 tree_encoding = sys.getfilesystemencoding() 484 contents = contents.decode(tree_encoding) 485 target_path = target_path.decode(tree_encoding) 486 os.symlink(contents, target_path) 487 else: 488 if oldstat is not None and oldstat.st_size == len(contents): 489 with open(target_path, 'rb') as f: 490 if f.read() == contents: 491 return oldstat 492 493 with open(target_path, 'wb') as f: 494 # Write out file 495 f.write(contents) 496 497 if honor_filemode: 498 os.chmod(target_path, mode) 499 500 return os.lstat(target_path) 501 502 503INVALID_DOTNAMES = (b".git", b".", b"..", b"") 504 505 506def validate_path_element_default(element): 507 return element.lower() not in INVALID_DOTNAMES 508 509 510def validate_path_element_ntfs(element): 511 stripped = element.rstrip(b". ").lower() 512 if stripped in INVALID_DOTNAMES: 513 return False 514 if stripped == b"git~1": 515 return False 516 return True 517 518 519def validate_path(path, element_validator=validate_path_element_default): 520 """Default path validator that just checks for .git/.""" 521 parts = path.split(b"/") 522 for p in parts: 523 if not element_validator(p): 524 return False 525 else: 526 return True 527 528 529def build_index_from_tree(root_path, index_path, object_store, tree_id, 530 honor_filemode=True, 531 validate_path_element=validate_path_element_default): 532 """Generate and materialize index from a tree 533 534 Args: 535 tree_id: Tree to materialize 536 root_path: Target dir for materialized index files 537 index_path: Target path for generated index 538 object_store: Non-empty object store holding tree contents 539 honor_filemode: An optional flag to honor core.filemode setting in 540 config file, default is core.filemode=True, change executable bit 541 validate_path_element: Function to validate path elements to check 542 out; default just refuses .git and .. directories. 543 544 Note: existing index is wiped and contents are not merged 545 in a working dir. Suitable only for fresh clones. 546 """ 547 548 index = Index(index_path) 549 if not isinstance(root_path, bytes): 550 root_path = root_path.encode(sys.getfilesystemencoding()) 551 552 for entry in object_store.iter_tree_contents(tree_id): 553 if not validate_path(entry.path, validate_path_element): 554 continue 555 full_path = _tree_to_fs_path(root_path, entry.path) 556 557 if not os.path.exists(os.path.dirname(full_path)): 558 os.makedirs(os.path.dirname(full_path)) 559 560 # TODO(jelmer): Merge new index into working tree 561 if S_ISGITLINK(entry.mode): 562 if not os.path.isdir(full_path): 563 os.mkdir(full_path) 564 st = os.lstat(full_path) 565 # TODO(jelmer): record and return submodule paths 566 else: 567 obj = object_store[entry.sha] 568 st = build_file_from_blob( 569 obj, entry.mode, full_path, honor_filemode=honor_filemode) 570 # Add file to index 571 if not honor_filemode or S_ISGITLINK(entry.mode): 572 # we can not use tuple slicing to build a new tuple, 573 # because on windows that will convert the times to 574 # longs, which causes errors further along 575 st_tuple = (entry.mode, st.st_ino, st.st_dev, st.st_nlink, 576 st.st_uid, st.st_gid, st.st_size, st.st_atime, 577 st.st_mtime, st.st_ctime) 578 st = st.__class__(st_tuple) 579 index[entry.path] = index_entry_from_stat(st, entry.sha, 0) 580 581 index.write() 582 583 584def blob_from_path_and_stat(fs_path, st): 585 """Create a blob from a path and a stat object. 586 587 Args: 588 fs_path: Full file system path to file 589 st: A stat object 590 Returns: A `Blob` object 591 """ 592 assert isinstance(fs_path, bytes) 593 blob = Blob() 594 if not stat.S_ISLNK(st.st_mode): 595 with open(fs_path, 'rb') as f: 596 blob.data = f.read() 597 else: 598 if sys.platform == 'win32' and sys.version_info[0] == 3: 599 # os.readlink on Python3 on Windows requires a unicode string. 600 # TODO(jelmer): Don't assume tree_encoding == fs_encoding 601 tree_encoding = sys.getfilesystemencoding() 602 fs_path = fs_path.decode(tree_encoding) 603 blob.data = os.readlink(fs_path).encode(tree_encoding) 604 else: 605 blob.data = os.readlink(fs_path) 606 return blob 607 608 609def read_submodule_head(path): 610 """Read the head commit of a submodule. 611 612 Args: 613 path: path to the submodule 614 Returns: HEAD sha, None if not a valid head/repository 615 """ 616 from dulwich.errors import NotGitRepository 617 from dulwich.repo import Repo 618 # Repo currently expects a "str", so decode if necessary. 619 # TODO(jelmer): Perhaps move this into Repo() ? 620 if not isinstance(path, str): 621 path = path.decode(sys.getfilesystemencoding()) 622 try: 623 repo = Repo(path) 624 except NotGitRepository: 625 return None 626 try: 627 return repo.head() 628 except KeyError: 629 return None 630 631 632def _has_directory_changed(tree_path, entry): 633 """Check if a directory has changed after getting an error. 634 635 When handling an error trying to create a blob from a path, call this 636 function. It will check if the path is a directory. If it's a directory 637 and a submodule, check the submodule head to see if it's has changed. If 638 not, consider the file as changed as Git tracked a file and not a 639 directory. 640 641 Return true if the given path should be considered as changed and False 642 otherwise or if the path is not a directory. 643 """ 644 # This is actually a directory 645 if os.path.exists(os.path.join(tree_path, b'.git')): 646 # Submodule 647 head = read_submodule_head(tree_path) 648 if entry.sha != head: 649 return True 650 else: 651 # The file was changed to a directory, so consider it removed. 652 return True 653 654 return False 655 656 657def get_unstaged_changes(index, root_path, filter_blob_callback=None): 658 """Walk through an index and check for differences against working tree. 659 660 Args: 661 index: index to check 662 root_path: path in which to find files 663 Returns: iterator over paths with unstaged changes 664 """ 665 # For each entry in the index check the sha1 & ensure not staged 666 if not isinstance(root_path, bytes): 667 root_path = root_path.encode(sys.getfilesystemencoding()) 668 669 for tree_path, entry in index.iteritems(): 670 full_path = _tree_to_fs_path(root_path, tree_path) 671 try: 672 st = os.lstat(full_path) 673 if stat.S_ISDIR(st.st_mode): 674 if _has_directory_changed(tree_path, entry): 675 yield tree_path 676 continue 677 678 blob = blob_from_path_and_stat(full_path, st) 679 680 if filter_blob_callback is not None: 681 blob = filter_blob_callback(blob, tree_path) 682 except EnvironmentError as e: 683 if e.errno == errno.ENOENT: 684 # The file was removed, so we assume that counts as 685 # different from whatever file used to exist. 686 yield tree_path 687 else: 688 raise 689 else: 690 if blob.id != entry.sha: 691 yield tree_path 692 693 694os_sep_bytes = os.sep.encode('ascii') 695 696 697def _tree_to_fs_path(root_path, tree_path): 698 """Convert a git tree path to a file system path. 699 700 Args: 701 root_path: Root filesystem path 702 tree_path: Git tree path as bytes 703 704 Returns: File system path. 705 """ 706 assert isinstance(tree_path, bytes) 707 if os_sep_bytes != b'/': 708 sep_corrected_path = tree_path.replace(b'/', os_sep_bytes) 709 else: 710 sep_corrected_path = tree_path 711 return os.path.join(root_path, sep_corrected_path) 712 713 714def _fs_to_tree_path(fs_path, fs_encoding=None): 715 """Convert a file system path to a git tree path. 716 717 Args: 718 fs_path: File system path. 719 fs_encoding: File system encoding 720 721 Returns: Git tree path as bytes 722 """ 723 if fs_encoding is None: 724 fs_encoding = sys.getfilesystemencoding() 725 if not isinstance(fs_path, bytes): 726 fs_path_bytes = fs_path.encode(fs_encoding) 727 else: 728 fs_path_bytes = fs_path 729 if os_sep_bytes != b'/': 730 tree_path = fs_path_bytes.replace(os_sep_bytes, b'/') 731 else: 732 tree_path = fs_path_bytes 733 return tree_path 734 735 736def index_entry_from_path(path, object_store=None): 737 """Create an index from a filesystem path. 738 739 This returns an index value for files, symlinks 740 and tree references. for directories and 741 non-existant files it returns None 742 743 Args: 744 path: Path to create an index entry for 745 object_store: Optional object store to 746 save new blobs in 747 Returns: An index entry; None for directories 748 """ 749 assert isinstance(path, bytes) 750 st = os.lstat(path) 751 if stat.S_ISDIR(st.st_mode): 752 if os.path.exists(os.path.join(path, b'.git')): 753 head = read_submodule_head(path) 754 if head is None: 755 return None 756 return index_entry_from_stat( 757 st, head, 0, mode=S_IFGITLINK) 758 return None 759 760 blob = blob_from_path_and_stat(path, st) 761 if object_store is not None: 762 object_store.add_object(blob) 763 return index_entry_from_stat(st, blob.id, 0) 764 765 766def iter_fresh_entries(paths, root_path, object_store=None): 767 """Iterate over current versions of index entries on disk. 768 769 Args: 770 paths: Paths to iterate over 771 root_path: Root path to access from 772 store: Optional store to save new blobs in 773 Returns: Iterator over path, index_entry 774 """ 775 for path in paths: 776 p = _tree_to_fs_path(root_path, path) 777 try: 778 entry = index_entry_from_path(p, object_store=object_store) 779 except EnvironmentError as e: 780 if e.errno in (errno.ENOENT, errno.EISDIR): 781 entry = None 782 else: 783 raise 784 yield path, entry 785 786 787def iter_fresh_blobs(index, root_path): 788 """Iterate over versions of blobs on disk referenced by index. 789 790 Don't use this function; it removes missing entries from index. 791 792 Args: 793 index: Index file 794 root_path: Root path to access from 795 include_deleted: Include deleted entries with sha and 796 mode set to None 797 Returns: Iterator over path, sha, mode 798 """ 799 import warnings 800 warnings.warn(PendingDeprecationWarning, 801 "Use iter_fresh_objects instead.") 802 for entry in iter_fresh_objects( 803 index, root_path, include_deleted=True): 804 if entry[1] is None: 805 del index[entry[0]] 806 else: 807 yield entry 808 809 810def iter_fresh_objects(paths, root_path, include_deleted=False, 811 object_store=None): 812 """Iterate over versions of objecs on disk referenced by index. 813 814 Args: 815 index: Index file 816 root_path: Root path to access from 817 include_deleted: Include deleted entries with sha and 818 mode set to None 819 object_store: Optional object store to report new items to 820 Returns: Iterator over path, sha, mode 821 """ 822 for path, entry in iter_fresh_entries(paths, root_path, 823 object_store=object_store): 824 if entry is None: 825 if include_deleted: 826 yield path, None, None 827 else: 828 entry = IndexEntry(*entry) 829 yield path, entry.sha, cleanup_mode(entry.mode) 830 831 832def refresh_index(index, root_path): 833 """Refresh the contents of an index. 834 835 This is the equivalent to running 'git commit -a'. 836 837 Args: 838 index: Index to update 839 root_path: Root filesystem path 840 """ 841 for path, entry in iter_fresh_entries(index, root_path): 842 index[path] = path 843