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