index.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. # index.py -- File parser/writer for the git index file
  2. # Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
  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. """Parser for the git index file format."""
  21. import collections
  22. import errno
  23. import os
  24. import stat
  25. import struct
  26. import sys
  27. from dulwich.file import GitFile
  28. from dulwich.objects import (
  29. Blob,
  30. S_IFGITLINK,
  31. S_ISGITLINK,
  32. Tree,
  33. hex_to_sha,
  34. sha_to_hex,
  35. )
  36. from dulwich.pack import (
  37. SHA1Reader,
  38. SHA1Writer,
  39. )
  40. IndexEntry = collections.namedtuple(
  41. 'IndexEntry', [
  42. 'ctime', 'mtime', 'dev', 'ino', 'mode', 'uid', 'gid', 'size', 'sha',
  43. 'flags'])
  44. FLAG_STAGEMASK = 0x3000
  45. FLAG_VALID = 0x8000
  46. FLAG_EXTENDED = 0x4000
  47. def pathsplit(path):
  48. """Split a /-delimited path into a directory part and a basename.
  49. :param path: The path to split.
  50. :return: Tuple with directory name and basename
  51. """
  52. try:
  53. (dirname, basename) = path.rsplit(b"/", 1)
  54. except ValueError:
  55. return (b"", path)
  56. else:
  57. return (dirname, basename)
  58. def pathjoin(*args):
  59. """Join a /-delimited path.
  60. """
  61. return b"/".join([p for p in args if p])
  62. def read_cache_time(f):
  63. """Read a cache time.
  64. :param f: File-like object to read from
  65. :return: Tuple with seconds and nanoseconds
  66. """
  67. return struct.unpack(">LL", f.read(8))
  68. def write_cache_time(f, t):
  69. """Write a cache time.
  70. :param f: File-like object to write to
  71. :param t: Time to write (as int, float or tuple with secs and nsecs)
  72. """
  73. if isinstance(t, int):
  74. t = (t, 0)
  75. elif isinstance(t, float):
  76. (secs, nsecs) = divmod(t, 1.0)
  77. t = (int(secs), int(nsecs * 1000000000))
  78. elif not isinstance(t, tuple):
  79. raise TypeError(t)
  80. f.write(struct.pack(">LL", *t))
  81. def read_cache_entry(f):
  82. """Read an entry from a cache file.
  83. :param f: File-like object to read from
  84. :return: tuple with: device, inode, mode, uid, gid, size, sha, flags
  85. """
  86. beginoffset = f.tell()
  87. ctime = read_cache_time(f)
  88. mtime = read_cache_time(f)
  89. (dev, ino, mode, uid, gid, size, sha, flags, ) = \
  90. struct.unpack(">LLLLLL20sH", f.read(20 + 4 * 6 + 2))
  91. name = f.read((flags & 0x0fff))
  92. # Padding:
  93. real_size = ((f.tell() - beginoffset + 8) & ~7)
  94. f.read((beginoffset + real_size) - f.tell())
  95. return (name, ctime, mtime, dev, ino, mode, uid, gid, size,
  96. sha_to_hex(sha), flags & ~0x0fff)
  97. def write_cache_entry(f, entry):
  98. """Write an index entry to a file.
  99. :param f: File object
  100. :param entry: Entry to write, tuple with:
  101. (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
  102. """
  103. beginoffset = f.tell()
  104. (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = entry
  105. write_cache_time(f, ctime)
  106. write_cache_time(f, mtime)
  107. flags = len(name) | (flags & ~0x0fff)
  108. f.write(struct.pack(
  109. b'>LLLLLL20sH', dev & 0xFFFFFFFF, ino & 0xFFFFFFFF,
  110. mode, uid, gid, size, hex_to_sha(sha), flags))
  111. f.write(name)
  112. real_size = ((f.tell() - beginoffset + 8) & ~7)
  113. f.write(b'\0' * ((beginoffset + real_size) - f.tell()))
  114. def read_index(f):
  115. """Read an index file, yielding the individual entries."""
  116. header = f.read(4)
  117. if header != b'DIRC':
  118. raise AssertionError("Invalid index file header: %r" % header)
  119. (version, num_entries) = struct.unpack(b'>LL', f.read(4 * 2))
  120. assert version in (1, 2)
  121. for i in range(num_entries):
  122. yield read_cache_entry(f)
  123. def read_index_dict(f):
  124. """Read an index file and return it as a dictionary.
  125. :param f: File object to read from
  126. """
  127. ret = {}
  128. for x in read_index(f):
  129. ret[x[0]] = IndexEntry(*x[1:])
  130. return ret
  131. def write_index(f, entries):
  132. """Write an index file.
  133. :param f: File-like object to write to
  134. :param entries: Iterable over the entries to write
  135. """
  136. f.write(b'DIRC')
  137. f.write(struct.pack(b'>LL', 2, len(entries)))
  138. for x in entries:
  139. write_cache_entry(f, x)
  140. def write_index_dict(f, entries):
  141. """Write an index file based on the contents of a dictionary.
  142. """
  143. entries_list = []
  144. for name in sorted(entries):
  145. entries_list.append((name,) + tuple(entries[name]))
  146. write_index(f, entries_list)
  147. def cleanup_mode(mode):
  148. """Cleanup a mode value.
  149. This will return a mode that can be stored in a tree object.
  150. :param mode: Mode to clean up.
  151. """
  152. if stat.S_ISLNK(mode):
  153. return stat.S_IFLNK
  154. elif stat.S_ISDIR(mode):
  155. return stat.S_IFDIR
  156. elif S_ISGITLINK(mode):
  157. return S_IFGITLINK
  158. ret = stat.S_IFREG | 0o644
  159. ret |= (mode & 0o111)
  160. return ret
  161. class Index(object):
  162. """A Git Index file."""
  163. def __init__(self, filename):
  164. """Open an index file.
  165. :param filename: Path to the index file
  166. """
  167. self._filename = filename
  168. self.clear()
  169. self.read()
  170. @property
  171. def path(self):
  172. return self._filename
  173. def __repr__(self):
  174. return "%s(%r)" % (self.__class__.__name__, self._filename)
  175. def write(self):
  176. """Write current contents of index to disk."""
  177. f = GitFile(self._filename, 'wb')
  178. try:
  179. f = SHA1Writer(f)
  180. write_index_dict(f, self._byname)
  181. finally:
  182. f.close()
  183. def read(self):
  184. """Read current contents of index from disk."""
  185. if not os.path.exists(self._filename):
  186. return
  187. f = GitFile(self._filename, 'rb')
  188. try:
  189. f = SHA1Reader(f)
  190. for x in read_index(f):
  191. self[x[0]] = IndexEntry(*x[1:])
  192. # FIXME: Additional data?
  193. f.read(os.path.getsize(self._filename)-f.tell()-20)
  194. f.check_sha()
  195. finally:
  196. f.close()
  197. def __len__(self):
  198. """Number of entries in this index file."""
  199. return len(self._byname)
  200. def __getitem__(self, name):
  201. """Retrieve entry by relative path.
  202. :return: tuple with (ctime, mtime, dev, ino, mode, uid, gid, size, sha,
  203. flags)
  204. """
  205. return self._byname[name]
  206. def __iter__(self):
  207. """Iterate over the paths in this index."""
  208. return iter(self._byname)
  209. def get_sha1(self, path):
  210. """Return the (git object) SHA1 for the object at a path."""
  211. return self[path].sha
  212. def get_mode(self, path):
  213. """Return the POSIX file mode for the object at a path."""
  214. return self[path].mode
  215. def iterblobs(self):
  216. """Iterate over path, sha, mode tuples for use with commit_tree."""
  217. for path in self:
  218. entry = self[path]
  219. yield path, entry.sha, cleanup_mode(entry.mode)
  220. def clear(self):
  221. """Remove all contents from this index."""
  222. self._byname = {}
  223. def __setitem__(self, name, x):
  224. assert isinstance(name, bytes)
  225. assert len(x) == 10
  226. # Remove the old entry if any
  227. self._byname[name] = IndexEntry(*x)
  228. def __delitem__(self, name):
  229. assert isinstance(name, bytes)
  230. del self._byname[name]
  231. def iteritems(self):
  232. return self._byname.items()
  233. def update(self, entries):
  234. for name, value in entries.items():
  235. self[name] = value
  236. def changes_from_tree(self, object_store, tree, want_unchanged=False):
  237. """Find the differences between the contents of this index and a tree.
  238. :param object_store: Object store to use for retrieving tree contents
  239. :param tree: SHA1 of the root tree
  240. :param want_unchanged: Whether unchanged files should be reported
  241. :return: Iterator over tuples with (oldpath, newpath), (oldmode,
  242. newmode), (oldsha, newsha)
  243. """
  244. def lookup_entry(path):
  245. entry = self[path]
  246. return entry.sha, entry.mode
  247. for (name, mode, sha) in changes_from_tree(
  248. self._byname.keys(), lookup_entry, object_store, tree,
  249. want_unchanged=want_unchanged):
  250. yield (name, mode, sha)
  251. def commit(self, object_store):
  252. """Create a new tree from an index.
  253. :param object_store: Object store to save the tree in
  254. :return: Root tree SHA
  255. """
  256. return commit_tree(object_store, self.iterblobs())
  257. def commit_tree(object_store, blobs):
  258. """Commit a new tree.
  259. :param object_store: Object store to add trees to
  260. :param blobs: Iterable over blob path, sha, mode entries
  261. :return: SHA1 of the created tree.
  262. """
  263. trees = {b'': {}}
  264. def add_tree(path):
  265. if path in trees:
  266. return trees[path]
  267. dirname, basename = pathsplit(path)
  268. t = add_tree(dirname)
  269. assert isinstance(basename, bytes)
  270. newtree = {}
  271. t[basename] = newtree
  272. trees[path] = newtree
  273. return newtree
  274. for path, sha, mode in blobs:
  275. tree_path, basename = pathsplit(path)
  276. tree = add_tree(tree_path)
  277. tree[basename] = (mode, sha)
  278. def build_tree(path):
  279. tree = Tree()
  280. for basename, entry in trees[path].items():
  281. if isinstance(entry, dict):
  282. mode = stat.S_IFDIR
  283. sha = build_tree(pathjoin(path, basename))
  284. else:
  285. (mode, sha) = entry
  286. tree.add(basename, mode, sha)
  287. object_store.add_object(tree)
  288. return tree.id
  289. return build_tree(b'')
  290. def commit_index(object_store, index):
  291. """Create a new tree from an index.
  292. :param object_store: Object store to save the tree in
  293. :param index: Index file
  294. :note: This function is deprecated, use index.commit() instead.
  295. :return: Root tree sha.
  296. """
  297. return commit_tree(object_store, index.iterblobs())
  298. def changes_from_tree(names, lookup_entry, object_store, tree,
  299. want_unchanged=False):
  300. """Find the differences between the contents of a tree and
  301. a working copy.
  302. :param names: Iterable of names in the working copy
  303. :param lookup_entry: Function to lookup an entry in the working copy
  304. :param object_store: Object store to use for retrieving tree contents
  305. :param tree: SHA1 of the root tree, or None for an empty tree
  306. :param want_unchanged: Whether unchanged files should be reported
  307. :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode),
  308. (oldsha, newsha)
  309. """
  310. # TODO(jelmer): Support a include_trees option
  311. other_names = set(names)
  312. if tree is not None:
  313. for (name, mode, sha) in object_store.iter_tree_contents(tree):
  314. try:
  315. (other_sha, other_mode) = lookup_entry(name)
  316. except KeyError:
  317. # Was removed
  318. yield ((name, None), (mode, None), (sha, None))
  319. else:
  320. other_names.remove(name)
  321. if (want_unchanged or other_sha != sha or other_mode != mode):
  322. yield ((name, name), (mode, other_mode), (sha, other_sha))
  323. # Mention added files
  324. for name in other_names:
  325. try:
  326. (other_sha, other_mode) = lookup_entry(name)
  327. except KeyError:
  328. pass
  329. else:
  330. yield ((None, name), (None, other_mode), (None, other_sha))
  331. def index_entry_from_stat(stat_val, hex_sha, flags, mode=None):
  332. """Create a new index entry from a stat value.
  333. :param stat_val: POSIX stat_result instance
  334. :param hex_sha: Hex sha of the object
  335. :param flags: Index flags
  336. """
  337. if mode is None:
  338. mode = cleanup_mode(stat_val.st_mode)
  339. return (stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev,
  340. stat_val.st_ino, mode, stat_val.st_uid,
  341. stat_val.st_gid, stat_val.st_size, hex_sha, flags)
  342. def build_file_from_blob(blob, mode, target_path, honor_filemode=True):
  343. """Build a file or symlink on disk based on a Git object.
  344. :param obj: The git object
  345. :param mode: File mode
  346. :param target_path: Path to write to
  347. :param honor_filemode: An optional flag to honor core.filemode setting in
  348. config file, default is core.filemode=True, change executable bit
  349. :return: stat object for the file
  350. """
  351. try:
  352. oldstat = os.lstat(target_path)
  353. except OSError as e:
  354. if e.errno == errno.ENOENT:
  355. oldstat = None
  356. else:
  357. raise
  358. contents = blob.as_raw_string()
  359. if stat.S_ISLNK(mode):
  360. # FIXME: This will fail on Windows. What should we do instead?
  361. if oldstat:
  362. os.unlink(target_path)
  363. if sys.platform == 'win32' and sys.version_info[0] == 3:
  364. # os.readlink on Python3 on Windows requires a unicode string.
  365. # TODO(jelmer): Don't assume tree_encoding == fs_encoding
  366. tree_encoding = sys.getfilesystemencoding()
  367. contents = contents.decode(tree_encoding)
  368. target_path = target_path.decode(tree_encoding)
  369. os.symlink(contents, target_path)
  370. else:
  371. if oldstat is not None and oldstat.st_size == len(contents):
  372. with open(target_path, 'rb') as f:
  373. if f.read() == contents:
  374. return oldstat
  375. with open(target_path, 'wb') as f:
  376. # Write out file
  377. f.write(contents)
  378. if honor_filemode:
  379. os.chmod(target_path, mode)
  380. return os.lstat(target_path)
  381. INVALID_DOTNAMES = (b".git", b".", b"..", b"")
  382. def validate_path_element_default(element):
  383. return element.lower() not in INVALID_DOTNAMES
  384. def validate_path_element_ntfs(element):
  385. stripped = element.rstrip(b". ").lower()
  386. if stripped in INVALID_DOTNAMES:
  387. return False
  388. if stripped == b"git~1":
  389. return False
  390. return True
  391. def validate_path(path, element_validator=validate_path_element_default):
  392. """Default path validator that just checks for .git/."""
  393. parts = path.split(b"/")
  394. for p in parts:
  395. if not element_validator(p):
  396. return False
  397. else:
  398. return True
  399. def build_index_from_tree(root_path, index_path, object_store, tree_id,
  400. honor_filemode=True,
  401. validate_path_element=validate_path_element_default):
  402. """Generate and materialize index from a tree
  403. :param tree_id: Tree to materialize
  404. :param root_path: Target dir for materialized index files
  405. :param index_path: Target path for generated index
  406. :param object_store: Non-empty object store holding tree contents
  407. :param honor_filemode: An optional flag to honor core.filemode setting in
  408. config file, default is core.filemode=True, change executable bit
  409. :param validate_path_element: Function to validate path elements to check
  410. out; default just refuses .git and .. directories.
  411. :note:: existing index is wiped and contents are not merged
  412. in a working dir. Suitable only for fresh clones.
  413. """
  414. index = Index(index_path)
  415. if not isinstance(root_path, bytes):
  416. root_path = root_path.encode(sys.getfilesystemencoding())
  417. for entry in object_store.iter_tree_contents(tree_id):
  418. if not validate_path(entry.path, validate_path_element):
  419. continue
  420. full_path = _tree_to_fs_path(root_path, entry.path)
  421. if not os.path.exists(os.path.dirname(full_path)):
  422. os.makedirs(os.path.dirname(full_path))
  423. # TODO(jelmer): Merge new index into working tree
  424. if S_ISGITLINK(entry.mode):
  425. if not os.path.isdir(full_path):
  426. os.mkdir(full_path)
  427. st = os.lstat(full_path)
  428. # TODO(jelmer): record and return submodule paths
  429. else:
  430. obj = object_store[entry.sha]
  431. st = build_file_from_blob(
  432. obj, entry.mode, full_path, honor_filemode=honor_filemode)
  433. # Add file to index
  434. if not honor_filemode or S_ISGITLINK(entry.mode):
  435. # we can not use tuple slicing to build a new tuple,
  436. # because on windows that will convert the times to
  437. # longs, which causes errors further along
  438. st_tuple = (entry.mode, st.st_ino, st.st_dev, st.st_nlink,
  439. st.st_uid, st.st_gid, st.st_size, st.st_atime,
  440. st.st_mtime, st.st_ctime)
  441. st = st.__class__(st_tuple)
  442. index[entry.path] = index_entry_from_stat(st, entry.sha, 0)
  443. index.write()
  444. def blob_from_path_and_stat(fs_path, st):
  445. """Create a blob from a path and a stat object.
  446. :param fs_path: Full file system path to file
  447. :param st: A stat object
  448. :return: A `Blob` object
  449. """
  450. assert isinstance(fs_path, bytes)
  451. blob = Blob()
  452. if not stat.S_ISLNK(st.st_mode):
  453. with open(fs_path, 'rb') as f:
  454. blob.data = f.read()
  455. else:
  456. if sys.platform == 'win32' and sys.version_info[0] == 3:
  457. # os.readlink on Python3 on Windows requires a unicode string.
  458. # TODO(jelmer): Don't assume tree_encoding == fs_encoding
  459. tree_encoding = sys.getfilesystemencoding()
  460. fs_path = fs_path.decode(tree_encoding)
  461. blob.data = os.readlink(fs_path).encode(tree_encoding)
  462. else:
  463. blob.data = os.readlink(fs_path)
  464. return blob
  465. def get_unstaged_changes(index, root_path):
  466. """Walk through an index and check for differences against working tree.
  467. :param index: index to check
  468. :param root_path: path in which to find files
  469. :return: iterator over paths with unstaged changes
  470. """
  471. # For each entry in the index check the sha1 & ensure not staged
  472. if not isinstance(root_path, bytes):
  473. root_path = root_path.encode(sys.getfilesystemencoding())
  474. for tree_path, entry in index.iteritems():
  475. full_path = _tree_to_fs_path(root_path, tree_path)
  476. try:
  477. blob = blob_from_path_and_stat(full_path, os.lstat(full_path))
  478. except OSError as e:
  479. if e.errno != errno.ENOENT:
  480. raise
  481. # The file was removed, so we assume that counts as
  482. # different from whatever file used to exist.
  483. yield tree_path
  484. except IOError as e:
  485. if e.errno != errno.EISDIR:
  486. raise
  487. # This is actually a directory
  488. if os.path.exists(os.path.join(tree_path, '.git')):
  489. # Submodule
  490. from dulwich.errors import NotGitRepository
  491. from dulwich.repo import Repo
  492. try:
  493. if entry.sha != Repo(tree_path).head():
  494. yield tree_path
  495. except NotGitRepository:
  496. yield tree_path
  497. else:
  498. # The file was changed to a directory, so consider it removed.
  499. yield tree_path
  500. else:
  501. if blob.id != entry.sha:
  502. yield tree_path
  503. os_sep_bytes = os.sep.encode('ascii')
  504. def _tree_to_fs_path(root_path, tree_path):
  505. """Convert a git tree path to a file system path.
  506. :param root_path: Root filesystem path
  507. :param tree_path: Git tree path as bytes
  508. :return: File system path.
  509. """
  510. assert isinstance(tree_path, bytes)
  511. if os_sep_bytes != b'/':
  512. sep_corrected_path = tree_path.replace(b'/', os_sep_bytes)
  513. else:
  514. sep_corrected_path = tree_path
  515. return os.path.join(root_path, sep_corrected_path)
  516. def _fs_to_tree_path(fs_path, fs_encoding=None):
  517. """Convert a file system path to a git tree path.
  518. :param fs_path: File system path.
  519. :param fs_encoding: File system encoding
  520. :return: Git tree path as bytes
  521. """
  522. if fs_encoding is None:
  523. fs_encoding = sys.getfilesystemencoding()
  524. if not isinstance(fs_path, bytes):
  525. fs_path_bytes = fs_path.encode(fs_encoding)
  526. else:
  527. fs_path_bytes = fs_path
  528. if os_sep_bytes != b'/':
  529. tree_path = fs_path_bytes.replace(os_sep_bytes, b'/')
  530. else:
  531. tree_path = fs_path_bytes
  532. return tree_path
  533. def iter_fresh_entries(index, root_path):
  534. """Iterate over current versions of index entries on disk.
  535. :param index: Index file
  536. :param root_path: Root path to access from
  537. :return: Iterator over path, index_entry
  538. """
  539. for path in set(index):
  540. p = _tree_to_fs_path(root_path, path)
  541. try:
  542. st = os.lstat(p)
  543. blob = blob_from_path_and_stat(p, st)
  544. except OSError as e:
  545. if e.errno == errno.ENOENT:
  546. del index[path]
  547. else:
  548. raise
  549. except IOError as e:
  550. if e.errno == errno.EISDIR:
  551. del index[path]
  552. else:
  553. raise
  554. else:
  555. yield path, index_entry_from_stat(st, blob.id, 0)
  556. def iter_fresh_blobs(index, root_path):
  557. """Iterate over versions of blobs on disk referenced by index.
  558. :param index: Index file
  559. :param root_path: Root path to access from
  560. :return: Iterator over path, sha, mode
  561. """
  562. for path, entry in iter_fresh_entries(index, root_path):
  563. entry = IndexEntry(*entry)
  564. yield path, entry.sha, cleanup_mode(entry.mode)
  565. def refresh_index(index, root_path):
  566. """Refresh the contents of an index.
  567. This is the equivalent to running 'git commit -a'.
  568. :param index: Index to update
  569. :param root_path: Root filesystem path
  570. """
  571. for path, entry in iter_fresh_entries(index, root_path):
  572. index[path] = path