Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/git/index/base.py: 39%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

500 statements  

1# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 

2# 

3# This module is part of GitPython and is released under the 

4# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 

5 

6"""Module containing :class:`IndexFile`, an Index implementation facilitating all kinds 

7of index manipulations such as querying and merging.""" 

8 

9__all__ = ["IndexFile", "CheckoutError", "StageType"] 

10 

11import contextlib 

12import datetime 

13import glob 

14from io import BytesIO 

15import os 

16import os.path as osp 

17from stat import S_ISLNK 

18import subprocess 

19import sys 

20import tempfile 

21 

22from gitdb.base import IStream 

23from gitdb.db import MemoryDB 

24 

25from git.compat import defenc, force_bytes 

26import git.diff as git_diff 

27from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError 

28from git.objects import Blob, Commit, Object, Submodule, Tree 

29from git.objects.util import Serializable 

30from git.util import ( 

31 LazyMixin, 

32 LockedFD, 

33 join_path_native, 

34 file_contents_ro, 

35 to_native_path_linux, 

36 unbare_repo, 

37 to_bin_sha, 

38) 

39 

40from .fun import ( 

41 S_IFGITLINK, 

42 aggressive_tree_merge, 

43 entry_key, 

44 read_cache, 

45 run_commit_hook, 

46 stat_mode_to_index_mode, 

47 write_cache, 

48 write_tree_from_cache, 

49) 

50from .typ import BaseIndexEntry, IndexEntry, StageType 

51from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir 

52 

53# typing ----------------------------------------------------------------------------- 

54 

55from typing import ( 

56 Any, 

57 BinaryIO, 

58 Callable, 

59 Dict, 

60 Generator, 

61 IO, 

62 Iterable, 

63 Iterator, 

64 List, 

65 NoReturn, 

66 Sequence, 

67 TYPE_CHECKING, 

68 Tuple, 

69 Union, 

70) 

71 

72from git.types import Literal, PathLike 

73 

74if TYPE_CHECKING: 

75 from subprocess import Popen 

76 

77 from git.refs.reference import Reference 

78 from git.repo import Repo 

79 from git.util import Actor 

80 

81 

82Treeish = Union[Tree, Commit, str, bytes] 

83 

84# ------------------------------------------------------------------------------------ 

85 

86 

87@contextlib.contextmanager 

88def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, None, None]: 

89 """Create a named temporary file git subprocesses can open, deleting it afterward. 

90 

91 :param directory: 

92 The directory in which the file is created. 

93 

94 :return: 

95 A context manager object that creates the file and provides its name on entry, 

96 and deletes it on exit. 

97 """ 

98 if sys.platform == "win32": 

99 fd, name = tempfile.mkstemp(dir=directory) 

100 os.close(fd) 

101 try: 

102 yield name 

103 finally: 

104 os.remove(name) 

105 else: 

106 with tempfile.NamedTemporaryFile(dir=directory) as ctx: 

107 yield ctx.name 

108 

109 

110class IndexFile(LazyMixin, git_diff.Diffable, Serializable): 

111 """An Index that can be manipulated using a native implementation in order to save 

112 git command function calls wherever possible. 

113 

114 This provides custom merging facilities allowing to merge without actually changing 

115 your index or your working tree. This way you can perform your own test merges based 

116 on the index only without having to deal with the working copy. This is useful in 

117 case of partial working trees. 

118 

119 Entries: 

120 

121 The index contains an entries dict whose keys are tuples of type 

122 :class:`~git.index.typ.IndexEntry` to facilitate access. 

123 

124 You may read the entries dict or manipulate it using IndexEntry instance, i.e.:: 

125 

126 index.entries[index.entry_key(index_entry_instance)] = index_entry_instance 

127 

128 Make sure you use :meth:`index.write() <write>` once you are done manipulating the 

129 index directly before operating on it using the git command. 

130 """ 

131 

132 __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path") 

133 

134 _VERSION = 2 

135 """The latest version we support.""" 

136 

137 S_IFGITLINK = S_IFGITLINK 

138 """Flags for a submodule.""" 

139 

140 def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None: 

141 """Initialize this Index instance, optionally from the given `file_path`. 

142 

143 If no `file_path` is given, we will be created from the current index file. 

144 

145 If a stream is not given, the stream will be initialized from the current 

146 repository's index on demand. 

147 """ 

148 self.repo = repo 

149 self.version = self._VERSION 

150 self._extension_data = b"" 

151 self._file_path: PathLike = file_path or self._index_path() 

152 

153 def _set_cache_(self, attr: str) -> None: 

154 if attr == "entries": 

155 try: 

156 fd = os.open(self._file_path, os.O_RDONLY) 

157 except OSError: 

158 # In new repositories, there may be no index, which means we are empty. 

159 self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {} 

160 return 

161 # END exception handling 

162 

163 try: 

164 stream = file_contents_ro(fd, stream=True, allow_mmap=True) 

165 finally: 

166 os.close(fd) 

167 

168 self._deserialize(stream) 

169 else: 

170 super()._set_cache_(attr) 

171 

172 def _index_path(self) -> PathLike: 

173 if self.repo.git_dir: 

174 return join_path_native(self.repo.git_dir, "index") 

175 else: 

176 raise GitCommandError("No git directory given to join index path") 

177 

178 @property 

179 def path(self) -> PathLike: 

180 """:return: Path to the index file we are representing""" 

181 return self._file_path 

182 

183 def _delete_entries_cache(self) -> None: 

184 """Safely clear the entries cache so it can be recreated.""" 

185 try: 

186 del self.entries 

187 except AttributeError: 

188 # It failed in Python 2.6.5 with AttributeError. 

189 # FIXME: Look into whether we can just remove this except clause now. 

190 pass 

191 # END exception handling 

192 

193 # { Serializable Interface 

194 

195 def _deserialize(self, stream: IO) -> "IndexFile": 

196 """Initialize this instance with index values read from the given stream.""" 

197 self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream) 

198 return self 

199 

200 def _entries_sorted(self) -> List[IndexEntry]: 

201 """:return: List of entries, in a sorted fashion, first by path, then by stage""" 

202 return sorted(self.entries.values(), key=lambda e: (e.path, e.stage)) 

203 

204 def _serialize(self, stream: IO, ignore_extension_data: bool = False) -> "IndexFile": 

205 entries = self._entries_sorted() 

206 extension_data = self._extension_data # type: Union[None, bytes] 

207 if ignore_extension_data: 

208 extension_data = None 

209 write_cache(entries, stream, extension_data) 

210 return self 

211 

212 # } END serializable interface 

213 

214 def write( 

215 self, 

216 file_path: Union[None, PathLike] = None, 

217 ignore_extension_data: bool = False, 

218 ) -> None: 

219 """Write the current state to our file path or to the given one. 

220 

221 :param file_path: 

222 If ``None``, we will write to our stored file path from which we have been 

223 initialized. Otherwise we write to the given file path. Please note that 

224 this will change the `file_path` of this index to the one you gave. 

225 

226 :param ignore_extension_data: 

227 If ``True``, the TREE type extension data read in the index will not be 

228 written to disk. NOTE that no extension data is actually written. Use this 

229 if you have altered the index and would like to use 

230 :manpage:`git-write-tree(1)` afterwards to create a tree representing your 

231 written changes. If this data is present in the written index, 

232 :manpage:`git-write-tree(1)` will instead write the stored/cached tree. 

233 Alternatively, use :meth:`write_tree` to handle this case automatically. 

234 """ 

235 # Make sure we have our entries read before getting a write lock. 

236 # Otherwise it would be done when streaming. 

237 # This can happen if one doesn't change the index, but writes it right away. 

238 self.entries # noqa: B018 

239 lfd = LockedFD(file_path or self._file_path) 

240 stream = lfd.open(write=True, stream=True) 

241 

242 try: 

243 self._serialize(stream, ignore_extension_data) 

244 except BaseException: 

245 lfd.rollback() 

246 raise 

247 

248 lfd.commit() 

249 

250 # Make sure we represent what we have written. 

251 if file_path is not None: 

252 self._file_path = file_path 

253 

254 @post_clear_cache 

255 @default_index 

256 def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexFile": 

257 """Merge the given `rhs` treeish into the current index, possibly taking 

258 a common base treeish into account. 

259 

260 As opposed to the :func:`from_tree` method, this allows you to use an already 

261 existing tree as the left side of the merge. 

262 

263 :param rhs: 

264 Treeish reference pointing to the 'other' side of the merge. 

265 

266 :param base: 

267 Optional treeish reference pointing to the common base of `rhs` and this 

268 index which equals lhs. 

269 

270 :return: 

271 self (containing the merge and possibly unmerged entries in case of 

272 conflicts) 

273 

274 :raise git.exc.GitCommandError: 

275 If there is a merge conflict. The error will be raised at the first 

276 conflicting path. If you want to have proper merge resolution to be done by 

277 yourself, you have to commit the changed index (or make a valid tree from 

278 it) and retry with a three-way :meth:`index.from_tree <from_tree>` call. 

279 """ 

280 # -i : ignore working tree status 

281 # --aggressive : handle more merge cases 

282 # -m : do an actual merge 

283 args: List[Union[Treeish, str]] = ["--aggressive", "-i", "-m"] 

284 if base is not None: 

285 args.append(base) 

286 args.append(rhs) 

287 

288 self.repo.git.read_tree(args) 

289 return self 

290 

291 @classmethod 

292 def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile": 

293 """Merge the given treeish revisions into a new index which is returned. 

294 

295 This method behaves like ``git-read-tree --aggressive`` when doing the merge. 

296 

297 :param repo: 

298 The repository treeish are located in. 

299 

300 :param tree_sha: 

301 20 byte or 40 byte tree sha or tree objects. 

302 

303 :return: 

304 New :class:`IndexFile` instance. Its path will be undefined. 

305 If you intend to write such a merged Index, supply an alternate 

306 ``file_path`` to its :meth:`write` method. 

307 """ 

308 tree_sha_bytes: List[bytes] = [to_bin_sha(str(t)) for t in tree_sha] 

309 base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes) 

310 

311 inst = cls(repo) 

312 # Convert to entries dict. 

313 entries: Dict[Tuple[PathLike, int], IndexEntry] = dict( 

314 zip( 

315 ((e.path, e.stage) for e in base_entries), 

316 (IndexEntry.from_base(e) for e in base_entries), 

317 ) 

318 ) 

319 

320 inst.entries = entries 

321 return inst 

322 

323 @classmethod 

324 def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile": 

325 R"""Merge the given treeish revisions into a new index which is returned. 

326 The original index will remain unaltered. 

327 

328 :param repo: 

329 The repository treeish are located in. 

330 

331 :param treeish: 

332 One, two or three :class:`~git.objects.tree.Tree` objects, 

333 :class:`~git.objects.commit.Commit`\s or 40 byte hexshas. 

334 

335 The result changes according to the amount of trees: 

336 

337 1. If 1 Tree is given, it will just be read into a new index. 

338 2. If 2 Trees are given, they will be merged into a new index using a two 

339 way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other' 

340 one. It behaves like a fast-forward. 

341 3. If 3 Trees are given, a 3-way merge will be performed with the first tree 

342 being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' 

343 tree, tree 3 is the 'other' one. 

344 

345 :param kwargs: 

346 Additional arguments passed to :manpage:`git-read-tree(1)`. 

347 

348 :return: 

349 New :class:`IndexFile` instance. It will point to a temporary index location 

350 which does not exist anymore. If you intend to write such a merged Index, 

351 supply an alternate ``file_path`` to its :meth:`write` method. 

352 

353 :note: 

354 In the three-way merge case, ``--aggressive`` will be specified to 

355 automatically resolve more cases in a commonly correct manner. Specify 

356 ``trivial=True`` as a keyword argument to override that. 

357 

358 As the underlying :manpage:`git-read-tree(1)` command takes into account the 

359 current index, it will be temporarily moved out of the way to prevent any 

360 unexpected interference. 

361 """ 

362 if len(treeish) == 0 or len(treeish) > 3: 

363 raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish)) 

364 

365 arg_list: List[Union[Treeish, str]] = [] 

366 # Ignore that the working tree and index possibly are out of date. 

367 if len(treeish) > 1: 

368 # Drop unmerged entries when reading our index and merging. 

369 arg_list.append("--reset") 

370 # Handle non-trivial cases the way a real merge does. 

371 arg_list.append("--aggressive") 

372 # END merge handling 

373 

374 # Create the temporary file in the .git directory to be sure renaming 

375 # works - /tmp/ directories could be on another device. 

376 with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index: 

377 arg_list.append("--index-output=%s" % tmp_index) 

378 arg_list.extend(treeish) 

379 

380 # Move the current index out of the way - otherwise the merge may fail as it 

381 # considers existing entries. Moving it essentially clears the index. 

382 # Unfortunately there is no 'soft' way to do it. 

383 # The TemporaryFileSwap ensures the original file gets put back. 

384 with TemporaryFileSwap(join_path_native(repo.git_dir, "index")): 

385 repo.git.read_tree(*arg_list, **kwargs) 

386 index = cls(repo, tmp_index) 

387 index.entries # noqa: B018 # Force it to read the file as we will delete the temp-file. 

388 return index 

389 # END index merge handling 

390 

391 # UTILITIES 

392 

393 @unbare_repo 

394 def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]: 

395 """Expand the directories in list of paths to the corresponding paths 

396 accordingly. 

397 

398 :note: 

399 git will add items multiple times even if a glob overlapped with manually 

400 specified paths or if paths where specified multiple times - we respect that 

401 and do not prune. 

402 """ 

403 

404 def raise_exc(e: Exception) -> NoReturn: 

405 raise e 

406 

407 r = str(self.repo.working_tree_dir) 

408 rs = r + os.sep 

409 for path in paths: 

410 abs_path = str(path) 

411 if not osp.isabs(abs_path): 

412 abs_path = osp.join(r, path) 

413 # END make absolute path 

414 

415 try: 

416 st = os.lstat(abs_path) # Handles non-symlinks as well. 

417 except OSError: 

418 # The lstat call may fail as the path may contain globs as well. 

419 pass 

420 else: 

421 if S_ISLNK(st.st_mode): 

422 yield abs_path.replace(rs, "") 

423 continue 

424 # END check symlink 

425 

426 # If the path is not already pointing to an existing file, resolve globs if possible. 

427 if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path): 

428 resolved_paths = glob.glob(abs_path) 

429 # not abs_path in resolved_paths: 

430 # A glob() resolving to the same path we are feeding it with is a 

431 # glob() that failed to resolve. If we continued calling ourselves 

432 # we'd endlessly recurse. If the condition below evaluates to true 

433 # then we are likely dealing with a file whose name contains wildcard 

434 # characters. 

435 if abs_path not in resolved_paths: 

436 for f in self._iter_expand_paths(glob.glob(abs_path)): 

437 yield str(f).replace(rs, "") 

438 continue 

439 # END glob handling 

440 try: 

441 for root, _dirs, files in os.walk(abs_path, onerror=raise_exc): 

442 for rela_file in files: 

443 # Add relative paths only. 

444 yield osp.join(root.replace(rs, ""), rela_file) 

445 # END for each file in subdir 

446 # END for each subdirectory 

447 except OSError: 

448 # It was a file or something that could not be iterated. 

449 yield abs_path.replace(rs, "") 

450 # END path exception handling 

451 # END for each path 

452 

453 def _write_path_to_stdin( 

454 self, 

455 proc: "Popen", 

456 filepath: PathLike, 

457 item: PathLike, 

458 fmakeexc: Callable[..., GitError], 

459 fprogress: Callable[[PathLike, bool, PathLike], None], 

460 read_from_stdout: bool = True, 

461 ) -> Union[None, str]: 

462 """Write path to ``proc.stdin`` and make sure it processes the item, including 

463 progress. 

464 

465 :return: 

466 stdout string 

467 

468 :param read_from_stdout: 

469 If ``True``, ``proc.stdout`` will be read after the item was sent to stdin. 

470 In that case, it will return ``None``. 

471 

472 :note: 

473 There is a bug in :manpage:`git-update-index(1)` that prevents it from 

474 sending reports just in time. This is why we have a version that tries to 

475 read stdout and one which doesn't. In fact, the stdout is not important as 

476 the piped-in files are processed anyway and just in time. 

477 

478 :note: 

479 Newlines are essential here, git's behaviour is somewhat inconsistent on 

480 this depending on the version, hence we try our best to deal with newlines 

481 carefully. Usually the last newline will not be sent, instead we will close 

482 stdin to break the pipe. 

483 """ 

484 fprogress(filepath, False, item) 

485 rval: Union[None, str] = None 

486 

487 if proc.stdin is not None: 

488 try: 

489 proc.stdin.write(("%s\n" % filepath).encode(defenc)) 

490 except IOError as e: 

491 # Pipe broke, usually because some error happened. 

492 raise fmakeexc() from e 

493 # END write exception handling 

494 proc.stdin.flush() 

495 

496 if read_from_stdout and proc.stdout is not None: 

497 rval = proc.stdout.readline().strip() 

498 fprogress(filepath, True, item) 

499 return rval 

500 

501 def iter_blobs( 

502 self, predicate: Callable[[Tuple[StageType, Blob]], bool] = lambda t: True 

503 ) -> Iterator[Tuple[StageType, Blob]]: 

504 """ 

505 :return: 

506 Iterator yielding tuples of :class:`~git.objects.blob.Blob` objects and 

507 stages, tuple(stage, Blob). 

508 

509 :param predicate: 

510 Function(t) returning ``True`` if tuple(stage, Blob) should be yielded by 

511 the iterator. A default filter, the `~git.index.typ.BlobFilter`, allows you 

512 to yield blobs only if they match a given list of paths. 

513 """ 

514 for entry in self.entries.values(): 

515 blob = entry.to_blob(self.repo) 

516 blob.size = entry.size 

517 output = (entry.stage, blob) 

518 if predicate(output): 

519 yield output 

520 # END for each entry 

521 

522 def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]: 

523 """ 

524 :return: 

525 Dict(path : list(tuple(stage, Blob, ...))), being a dictionary associating a 

526 path in the index with a list containing sorted stage/blob pairs. 

527 

528 :note: 

529 Blobs that have been removed in one side simply do not exist in the given 

530 stage. That is, a file removed on the 'other' branch whose entries are at 

531 stage 3 will not have a stage 3 entry. 

532 """ 

533 is_unmerged_blob = lambda t: t[0] != 0 

534 path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {} 

535 for stage, blob in self.iter_blobs(is_unmerged_blob): 

536 path_map.setdefault(blob.path, []).append((stage, blob)) 

537 # END for each unmerged blob 

538 for line in path_map.values(): 

539 line.sort() 

540 

541 return path_map 

542 

543 @classmethod 

544 def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[PathLike, StageType]: 

545 return entry_key(*entry) 

546 

547 def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile": 

548 """Resolve the blobs given in blob iterator. 

549 

550 This will effectively remove the index entries of the respective path at all 

551 non-null stages and add the given blob as new stage null blob. 

552 

553 For each path there may only be one blob, otherwise a :exc:`ValueError` will be 

554 raised claiming the path is already at stage 0. 

555 

556 :raise ValueError: 

557 If one of the blobs already existed at stage 0. 

558 

559 :return: 

560 self 

561 

562 :note: 

563 You will have to write the index manually once you are done, i.e. 

564 ``index.resolve_blobs(blobs).write()``. 

565 """ 

566 for blob in iter_blobs: 

567 stage_null_key = (blob.path, 0) 

568 if stage_null_key in self.entries: 

569 raise ValueError("Path %r already exists at stage 0" % str(blob.path)) 

570 # END assert blob is not stage 0 already 

571 

572 # Delete all possible stages. 

573 for stage in (1, 2, 3): 

574 try: 

575 del self.entries[(blob.path, stage)] 

576 except KeyError: 

577 pass 

578 # END ignore key errors 

579 # END for each possible stage 

580 

581 self.entries[stage_null_key] = IndexEntry.from_blob(blob) 

582 # END for each blob 

583 

584 return self 

585 

586 def update(self) -> "IndexFile": 

587 """Reread the contents of our index file, discarding all cached information 

588 we might have. 

589 

590 :note: 

591 This is a possibly dangerous operations as it will discard your changes to 

592 :attr:`index.entries <entries>`. 

593 

594 :return: 

595 self 

596 """ 

597 self._delete_entries_cache() 

598 # Allows to lazily reread on demand. 

599 return self 

600 

601 def write_tree(self) -> Tree: 

602 """Write this index to a corresponding :class:`~git.objects.tree.Tree` object 

603 into the repository's object database and return it. 

604 

605 :return: 

606 :class:`~git.objects.tree.Tree` object representing this index. 

607 

608 :note: 

609 The tree will be written even if one or more objects the tree refers to does 

610 not yet exist in the object database. This could happen if you added entries 

611 to the index directly. 

612 

613 :raise ValueError: 

614 If there are no entries in the cache. 

615 

616 :raise git.exc.UnmergedEntriesError: 

617 """ 

618 # We obtain no lock as we just flush our contents to disk as tree. 

619 # If we are a new index, the entries access will load our data accordingly. 

620 mdb = MemoryDB() 

621 entries = self._entries_sorted() 

622 binsha, tree_items = write_tree_from_cache(entries, mdb, slice(0, len(entries))) 

623 

624 # Copy changed trees only. 

625 mdb.stream_copy(mdb.sha_iter(), self.repo.odb) 

626 

627 # Note: Additional deserialization could be saved if write_tree_from_cache would 

628 # return sorted tree entries. 

629 root_tree = Tree(self.repo, binsha, path="") 

630 root_tree._cache = tree_items 

631 return root_tree 

632 

633 def _process_diff_args( 

634 self, 

635 args: List[Union[PathLike, "git_diff.Diffable"]], 

636 ) -> List[Union[PathLike, "git_diff.Diffable"]]: 

637 try: 

638 args.pop(args.index(self)) 

639 except IndexError: 

640 pass 

641 # END remove self 

642 return args 

643 

644 def _to_relative_path(self, path: PathLike) -> PathLike: 

645 """ 

646 :return: 

647 Version of path relative to our git directory or raise :exc:`ValueError` if 

648 it is not within our git directory. 

649 

650 :raise ValueError: 

651 """ 

652 if not osp.isabs(path): 

653 return path 

654 if self.repo.bare: 

655 raise InvalidGitRepositoryError("require non-bare repository") 

656 if not str(path).startswith(str(self.repo.working_tree_dir)): 

657 raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir)) 

658 return os.path.relpath(path, self.repo.working_tree_dir) 

659 

660 def _preprocess_add_items( 

661 self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]] 

662 ) -> Tuple[List[PathLike], List[BaseIndexEntry]]: 

663 """Split the items into two lists of path strings and BaseEntries.""" 

664 paths = [] 

665 entries = [] 

666 # if it is a string put in list 

667 if isinstance(items, (str, os.PathLike)): 

668 items = [items] 

669 

670 for item in items: 

671 if isinstance(item, (str, os.PathLike)): 

672 paths.append(self._to_relative_path(item)) 

673 elif isinstance(item, (Blob, Submodule)): 

674 entries.append(BaseIndexEntry.from_blob(item)) 

675 elif isinstance(item, BaseIndexEntry): 

676 entries.append(item) 

677 else: 

678 raise TypeError("Invalid Type: %r" % item) 

679 # END for each item 

680 return paths, entries 

681 

682 def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry: 

683 """Store file at filepath in the database and return the base index entry. 

684 

685 :note: 

686 This needs the :func:`~git.index.util.git_working_dir` decorator active! 

687 This must be ensured in the calling code. 

688 """ 

689 st = os.lstat(filepath) # Handles non-symlinks as well. 

690 if S_ISLNK(st.st_mode): 

691 # In PY3, readlink is a string, but we need bytes. 

692 # In PY2, it was just OS encoded bytes, we assumed UTF-8. 

693 open_stream: Callable[[], BinaryIO] = lambda: BytesIO(force_bytes(os.readlink(filepath), encoding=defenc)) 

694 else: 

695 open_stream = lambda: open(filepath, "rb") 

696 with open_stream() as stream: 

697 fprogress(filepath, False, filepath) 

698 istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream)) 

699 fprogress(filepath, True, filepath) 

700 return BaseIndexEntry( 

701 ( 

702 stat_mode_to_index_mode(st.st_mode), 

703 istream.binsha, 

704 0, 

705 to_native_path_linux(filepath), 

706 ) 

707 ) 

708 

709 @unbare_repo 

710 @git_working_dir 

711 def _entries_for_paths( 

712 self, 

713 paths: List[str], 

714 path_rewriter: Union[Callable, None], 

715 fprogress: Callable, 

716 entries: List[BaseIndexEntry], 

717 ) -> List[BaseIndexEntry]: 

718 entries_added: List[BaseIndexEntry] = [] 

719 if path_rewriter: 

720 for path in paths: 

721 if osp.isabs(path): 

722 abspath = path 

723 gitrelative_path = path[len(str(self.repo.working_tree_dir)) + 1 :] 

724 else: 

725 gitrelative_path = path 

726 if self.repo.working_tree_dir: 

727 abspath = osp.join(self.repo.working_tree_dir, gitrelative_path) 

728 # END obtain relative and absolute paths 

729 

730 blob = Blob( 

731 self.repo, 

732 Blob.NULL_BIN_SHA, 

733 stat_mode_to_index_mode(os.stat(abspath).st_mode), 

734 to_native_path_linux(gitrelative_path), 

735 ) 

736 # TODO: variable undefined 

737 entries.append(BaseIndexEntry.from_blob(blob)) 

738 # END for each path 

739 del paths[:] 

740 # END rewrite paths 

741 

742 # HANDLE PATHS 

743 assert len(entries_added) == 0 

744 for filepath in self._iter_expand_paths(paths): 

745 entries_added.append(self._store_path(filepath, fprogress)) 

746 # END for each filepath 

747 # END path handling 

748 return entries_added 

749 

750 def add( 

751 self, 

752 items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]], 

753 force: bool = True, 

754 fprogress: Callable = lambda *args: None, 

755 path_rewriter: Union[Callable[..., PathLike], None] = None, 

756 write: bool = True, 

757 write_extension_data: bool = False, 

758 ) -> List[BaseIndexEntry]: 

759 R"""Add files from the working tree, specific blobs, or 

760 :class:`~git.index.typ.BaseIndexEntry`\s to the index. 

761 

762 :param items: 

763 Multiple types of items are supported, types can be mixed within one call. 

764 Different types imply a different handling. File paths may generally be 

765 relative or absolute. 

766 

767 - path string 

768 

769 Strings denote a relative or absolute path into the repository pointing 

770 to an existing file, e.g., ``CHANGES``, `lib/myfile.ext``, 

771 ``/home/gitrepo/lib/myfile.ext``. 

772 

773 Absolute paths must start with working tree directory of this index's 

774 repository to be considered valid. For example, if it was initialized 

775 with a non-normalized path, like ``/root/repo/../repo``, absolute paths 

776 to be added must start with ``/root/repo/../repo``. 

777 

778 Paths provided like this must exist. When added, they will be written 

779 into the object database. 

780 

781 PathStrings may contain globs, such as ``lib/__init__*``. Or they can be 

782 directories like ``lib``, which will add all the files within the 

783 directory and subdirectories. 

784 

785 This equals a straight :manpage:`git-add(1)`. 

786 

787 They are added at stage 0. 

788 

789 - :class:~`git.objects.blob.Blob` or 

790 :class:`~git.objects.submodule.base.Submodule` object 

791 

792 Blobs are added as they are assuming a valid mode is set. 

793 

794 The file they refer to may or may not exist in the file system, but must 

795 be a path relative to our repository. 

796 

797 If their sha is null (40*0), their path must exist in the file system 

798 relative to the git repository as an object will be created from the 

799 data at the path. 

800 

801 The handling now very much equals the way string paths are processed, 

802 except that the mode you have set will be kept. This allows you to 

803 create symlinks by settings the mode respectively and writing the target 

804 of the symlink directly into the file. This equals a default Linux 

805 symlink which is not dereferenced automatically, except that it can be 

806 created on filesystems not supporting it as well. 

807 

808 Please note that globs or directories are not allowed in 

809 :class:`~git.objects.blob.Blob` objects. 

810 

811 They are added at stage 0. 

812 

813 - :class:`~git.index.typ.BaseIndexEntry` or type 

814 

815 Handling equals the one of :class:~`git.objects.blob.Blob` objects, but 

816 the stage may be explicitly set. Please note that Index Entries require 

817 binary sha's. 

818 

819 :param force: 

820 **CURRENTLY INEFFECTIVE** 

821 If ``True``, otherwise ignored or excluded files will be added anyway. As 

822 opposed to the :manpage:`git-add(1)` command, we enable this flag by default 

823 as the API user usually wants the item to be added even though they might be 

824 excluded. 

825 

826 :param fprogress: 

827 Function with signature ``f(path, done=False, item=item)`` called for each 

828 path to be added, one time once it is about to be added where ``done=False`` 

829 and once after it was added where ``done=True``. 

830 

831 ``item`` is set to the actual item we handle, either a path or a 

832 :class:`~git.index.typ.BaseIndexEntry`. 

833 

834 Please note that the processed path is not guaranteed to be present in the 

835 index already as the index is currently being processed. 

836 

837 :param path_rewriter: 

838 Function, with signature ``(string) func(BaseIndexEntry)``, returning a path 

839 for each passed entry which is the path to be actually recorded for the 

840 object created from :attr:`entry.path <git.index.typ.BaseIndexEntry.path>`. 

841 This allows you to write an index which is not identical to the layout of 

842 the actual files on your hard-disk. If not ``None`` and `items` contain 

843 plain paths, these paths will be converted to Entries beforehand and passed 

844 to the path_rewriter. Please note that ``entry.path`` is relative to the git 

845 repository. 

846 

847 :param write: 

848 If ``True``, the index will be written once it was altered. Otherwise the 

849 changes only exist in memory and are not available to git commands. 

850 

851 :param write_extension_data: 

852 If ``True``, extension data will be written back to the index. This can lead 

853 to issues in case it is containing the 'TREE' extension, which will cause 

854 the :manpage:`git-commit(1)` command to write an old tree, instead of a new 

855 one representing the now changed index. 

856 

857 This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the 

858 'TREE' extension altogether. You should set it to ``True`` if you intend to 

859 use :meth:`IndexFile.commit` exclusively while maintaining support for 

860 third-party extensions. Besides that, you can usually safely ignore the 

861 built-in extensions when using GitPython on repositories that are not 

862 handled manually at all. 

863 

864 All current built-in extensions are listed here: 

865 https://git-scm.com/docs/index-format 

866 

867 :return: 

868 List of :class:`~git.index.typ.BaseIndexEntry`\s representing the entries 

869 just actually added. 

870 

871 :raise OSError: 

872 If a supplied path did not exist. Please note that 

873 :class:`~git.index.typ.BaseIndexEntry` objects that do not have a null sha 

874 will be added even if their paths do not exist. 

875 """ 

876 # Sort the entries into strings and Entries. 

877 # Blobs are converted to entries automatically. 

878 # Paths can be git-added. For everything else we use git-update-index. 

879 paths, entries = self._preprocess_add_items(items) 

880 entries_added: List[BaseIndexEntry] = [] 

881 # This code needs a working tree, so we try not to run it unless required. 

882 # That way, we are OK on a bare repository as well. 

883 # If there are no paths, the rewriter has nothing to do either. 

884 if paths: 

885 entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries)) 

886 

887 # HANDLE ENTRIES 

888 if entries: 

889 null_mode_entries = [e for e in entries if e.mode == 0] 

890 if null_mode_entries: 

891 raise ValueError( 

892 "At least one Entry has a null-mode - please use index.remove to remove files for clarity" 

893 ) 

894 # END null mode should be remove 

895 

896 # HANDLE ENTRY OBJECT CREATION 

897 # Create objects if required, otherwise go with the existing shas. 

898 null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA] 

899 if null_entries_indices: 

900 

901 @git_working_dir 

902 def handle_null_entries(self: "IndexFile") -> None: 

903 for ei in null_entries_indices: 

904 null_entry = entries[ei] 

905 new_entry = self._store_path(null_entry.path, fprogress) 

906 

907 # Update null entry. 

908 entries[ei] = BaseIndexEntry( 

909 ( 

910 null_entry.mode, 

911 new_entry.binsha, 

912 null_entry.stage, 

913 null_entry.path, 

914 ) 

915 ) 

916 # END for each entry index 

917 

918 # END closure 

919 

920 handle_null_entries(self) 

921 # END null_entry handling 

922 

923 # REWRITE PATHS 

924 # If we have to rewrite the entries, do so now, after we have generated all 

925 # object sha's. 

926 if path_rewriter: 

927 for i, e in enumerate(entries): 

928 entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e))) 

929 # END for each entry 

930 # END handle path rewriting 

931 

932 # Just go through the remaining entries and provide progress info. 

933 for i, entry in enumerate(entries): 

934 progress_sent = i in null_entries_indices 

935 if not progress_sent: 

936 fprogress(entry.path, False, entry) 

937 fprogress(entry.path, True, entry) 

938 # END handle progress 

939 # END for each entry 

940 entries_added.extend(entries) 

941 # END if there are base entries 

942 

943 # FINALIZE 

944 # Add the new entries to this instance. 

945 for entry in entries_added: 

946 self.entries[(entry.path, 0)] = IndexEntry.from_base(entry) 

947 

948 if write: 

949 self.write(ignore_extension_data=not write_extension_data) 

950 # END handle write 

951 

952 return entries_added 

953 

954 def _items_to_rela_paths( 

955 self, 

956 items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]], 

957 ) -> List[PathLike]: 

958 """Returns a list of repo-relative paths from the given items which 

959 may be absolute or relative paths, entries or blobs.""" 

960 paths = [] 

961 # If string, put in list. 

962 if isinstance(items, (str, os.PathLike)): 

963 items = [items] 

964 

965 for item in items: 

966 if isinstance(item, (BaseIndexEntry, (Blob, Submodule))): 

967 paths.append(self._to_relative_path(item.path)) 

968 elif isinstance(item, (str, os.PathLike)): 

969 paths.append(self._to_relative_path(item)) 

970 else: 

971 raise TypeError("Invalid item type: %r" % item) 

972 # END for each item 

973 return paths 

974 

975 @post_clear_cache 

976 @default_index 

977 def remove( 

978 self, 

979 items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]], 

980 working_tree: bool = False, 

981 **kwargs: Any, 

982 ) -> List[str]: 

983 R"""Remove the given items from the index and optionally from the working tree 

984 as well. 

985 

986 :param items: 

987 Multiple types of items are supported which may be be freely mixed. 

988 

989 - path string 

990 

991 Remove the given path at all stages. If it is a directory, you must 

992 specify the ``r=True`` keyword argument to remove all file entries below 

993 it. If absolute paths are given, they will be converted to a path 

994 relative to the git repository directory containing the working tree 

995 

996 The path string may include globs, such as ``*.c``. 

997 

998 - :class:~`git.objects.blob.Blob` object 

999 

1000 Only the path portion is used in this case. 

1001 

1002 - :class:`~git.index.typ.BaseIndexEntry` or compatible type 

1003 

1004 The only relevant information here is the path. The stage is ignored. 

1005 

1006 :param working_tree: 

1007 If ``True``, the entry will also be removed from the working tree, 

1008 physically removing the respective file. This may fail if there are 

1009 uncommitted changes in it. 

1010 

1011 :param kwargs: 

1012 Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as 

1013 ``r`` to allow recursive removal. 

1014 

1015 :return: 

1016 List(path_string, ...) list of repository relative paths that have been 

1017 removed effectively. 

1018 

1019 This is interesting to know in case you have provided a directory or globs. 

1020 Paths are relative to the repository. 

1021 """ 

1022 args = [] 

1023 if not working_tree: 

1024 args.append("--cached") 

1025 args.append("--") 

1026 

1027 # Preprocess paths. 

1028 paths = self._items_to_rela_paths(items) 

1029 removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines() 

1030 

1031 # Process output to gain proper paths. 

1032 # rm 'path' 

1033 return [p[4:-1] for p in removed_paths] 

1034 

1035 @post_clear_cache 

1036 @default_index 

1037 def move( 

1038 self, 

1039 items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]], 

1040 skip_errors: bool = False, 

1041 **kwargs: Any, 

1042 ) -> List[Tuple[str, str]]: 

1043 """Rename/move the items, whereas the last item is considered the destination of 

1044 the move operation. 

1045 

1046 If the destination is a file, the first item (of two) must be a file as well. 

1047 

1048 If the destination is a directory, it may be preceded by one or more directories 

1049 or files. 

1050 

1051 The working tree will be affected in non-bare repositories. 

1052 

1053 :param items: 

1054 Multiple types of items are supported, please see the :meth:`remove` method 

1055 for reference. 

1056 

1057 :param skip_errors: 

1058 If ``True``, errors such as ones resulting from missing source files will be 

1059 skipped. 

1060 

1061 :param kwargs: 

1062 Additional arguments you would like to pass to :manpage:`git-mv(1)`, such as 

1063 ``dry_run`` or ``force``. 

1064 

1065 :return: 

1066 List(tuple(source_path_string, destination_path_string), ...) 

1067 

1068 A list of pairs, containing the source file moved as well as its actual 

1069 destination. Relative to the repository root. 

1070 

1071 :raise ValueError: 

1072 If only one item was given. 

1073 

1074 :raise git.exc.GitCommandError: 

1075 If git could not handle your request. 

1076 """ 

1077 args = [] 

1078 if skip_errors: 

1079 args.append("-k") 

1080 

1081 paths = self._items_to_rela_paths(items) 

1082 if len(paths) < 2: 

1083 raise ValueError("Please provide at least one source and one destination of the move operation") 

1084 

1085 was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None)) 

1086 kwargs["dry_run"] = True 

1087 

1088 # First execute rename in dry run so the command tells us what it actually does 

1089 # (for later output). 

1090 out = [] 

1091 mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines() 

1092 

1093 # Parse result - first 0:n/2 lines are 'checking ', the remaining ones are the 

1094 # 'renaming' ones which we parse. 

1095 for ln in range(int(len(mvlines) / 2), len(mvlines)): 

1096 tokens = mvlines[ln].split(" to ") 

1097 assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln] 

1098 

1099 # [0] = Renaming x 

1100 # [1] = y 

1101 out.append((tokens[0][9:], tokens[1])) 

1102 # END for each line to parse 

1103 

1104 # Either prepare for the real run, or output the dry-run result. 

1105 if was_dry_run: 

1106 return out 

1107 # END handle dry run 

1108 

1109 # Now apply the actual operation. 

1110 kwargs.pop("dry_run") 

1111 self.repo.git.mv(args, paths, **kwargs) 

1112 

1113 return out 

1114 

1115 def commit( 

1116 self, 

1117 message: str, 

1118 parent_commits: Union[List[Commit], None] = None, 

1119 head: bool = True, 

1120 author: Union[None, "Actor"] = None, 

1121 committer: Union[None, "Actor"] = None, 

1122 author_date: Union[datetime.datetime, str, None] = None, 

1123 commit_date: Union[datetime.datetime, str, None] = None, 

1124 skip_hooks: bool = False, 

1125 ) -> Commit: 

1126 """Commit the current default index file, creating a 

1127 :class:`~git.objects.commit.Commit` object. 

1128 

1129 For more information on the arguments, see 

1130 :meth:`Commit.create_from_tree <git.objects.commit.Commit.create_from_tree>`. 

1131 

1132 :note: 

1133 If you have manually altered the :attr:`entries` member of this instance, 

1134 don't forget to :meth:`write` your changes to disk beforehand. 

1135 

1136 :note: 

1137 Passing ``skip_hooks=True`` is the equivalent of using ``-n`` or 

1138 ``--no-verify`` on the command line. 

1139 

1140 :return: 

1141 :class:`~git.objects.commit.Commit` object representing the new commit 

1142 """ 

1143 if not skip_hooks: 

1144 run_commit_hook("pre-commit", self) 

1145 

1146 self._write_commit_editmsg(message) 

1147 run_commit_hook("commit-msg", self, self._commit_editmsg_filepath()) 

1148 message = self._read_commit_editmsg() 

1149 self._remove_commit_editmsg() 

1150 tree = self.write_tree() 

1151 rval = Commit.create_from_tree( 

1152 self.repo, 

1153 tree, 

1154 message, 

1155 parent_commits, 

1156 head, 

1157 author=author, 

1158 committer=committer, 

1159 author_date=author_date, 

1160 commit_date=commit_date, 

1161 ) 

1162 if not skip_hooks: 

1163 run_commit_hook("post-commit", self) 

1164 return rval 

1165 

1166 def _write_commit_editmsg(self, message: str) -> None: 

1167 with open(self._commit_editmsg_filepath(), "wb") as commit_editmsg_file: 

1168 commit_editmsg_file.write(message.encode(defenc)) 

1169 

1170 def _remove_commit_editmsg(self) -> None: 

1171 os.remove(self._commit_editmsg_filepath()) 

1172 

1173 def _read_commit_editmsg(self) -> str: 

1174 with open(self._commit_editmsg_filepath(), "rb") as commit_editmsg_file: 

1175 return commit_editmsg_file.read().decode(defenc) 

1176 

1177 def _commit_editmsg_filepath(self) -> str: 

1178 return osp.join(self.repo.common_dir, "COMMIT_EDITMSG") 

1179 

1180 def _flush_stdin_and_wait(cls, proc: "Popen[bytes]", ignore_stdout: bool = False) -> bytes: 

1181 stdin_IO = proc.stdin 

1182 if stdin_IO: 

1183 stdin_IO.flush() 

1184 stdin_IO.close() 

1185 

1186 stdout = b"" 

1187 if not ignore_stdout and proc.stdout: 

1188 stdout = proc.stdout.read() 

1189 

1190 if proc.stdout: 

1191 proc.stdout.close() 

1192 proc.wait() 

1193 return stdout 

1194 

1195 @default_index 

1196 def checkout( 

1197 self, 

1198 paths: Union[None, Iterable[PathLike]] = None, 

1199 force: bool = False, 

1200 fprogress: Callable = lambda *args: None, 

1201 **kwargs: Any, 

1202 ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]: 

1203 """Check out the given paths or all files from the version known to the index 

1204 into the working tree. 

1205 

1206 :note: 

1207 Be sure you have written pending changes using the :meth:`write` method in 

1208 case you have altered the entries dictionary directly. 

1209 

1210 :param paths: 

1211 If ``None``, all paths in the index will be checked out. 

1212 Otherwise an iterable of relative or absolute paths or a single path 

1213 pointing to files or directories in the index is expected. 

1214 

1215 :param force: 

1216 If ``True``, existing files will be overwritten even if they contain local 

1217 modifications. 

1218 If ``False``, these will trigger a :exc:`~git.exc.CheckoutError`. 

1219 

1220 :param fprogress: 

1221 See :meth:`IndexFile.add` for signature and explanation. 

1222 

1223 The provided progress information will contain ``None`` as path and item if 

1224 no explicit paths are given. Otherwise progress information will be send 

1225 prior and after a file has been checked out. 

1226 

1227 :param kwargs: 

1228 Additional arguments to be passed to :manpage:`git-checkout-index(1)`. 

1229 

1230 :return: 

1231 Iterable yielding paths to files which have been checked out and are 

1232 guaranteed to match the version stored in the index. 

1233 

1234 :raise git.exc.CheckoutError: 

1235 * If at least one file failed to be checked out. This is a summary, hence it 

1236 will checkout as many files as it can anyway. 

1237 * If one of files or directories do not exist in the index (as opposed to 

1238 the original git command, which ignores them). 

1239 

1240 :raise git.exc.GitCommandError: 

1241 If error lines could not be parsed - this truly is an exceptional state. 

1242 

1243 :note: 

1244 The checkout is limited to checking out the files in the index. Files which 

1245 are not in the index anymore and exist in the working tree will not be 

1246 deleted. This behaviour is fundamentally different to ``head.checkout``, 

1247 i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use 

1248 ``head.checkout`` instead of ``index.checkout``. 

1249 """ 

1250 args = ["--index"] 

1251 if force: 

1252 args.append("--force") 

1253 

1254 failed_files = [] 

1255 failed_reasons = [] 

1256 unknown_lines = [] 

1257 

1258 def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLike]) -> None: 

1259 stderr_IO = proc.stderr 

1260 if not stderr_IO: 

1261 return # Return early if stderr empty. 

1262 

1263 stderr_bytes = stderr_IO.read() 

1264 # line contents: 

1265 stderr = stderr_bytes.decode(defenc) 

1266 # git-checkout-index: this already exists 

1267 endings = ( 

1268 " already exists", 

1269 " is not in the cache", 

1270 " does not exist at stage", 

1271 " is unmerged", 

1272 ) 

1273 for line in stderr.splitlines(): 

1274 if not line.startswith("git checkout-index: ") and not line.startswith("git-checkout-index: "): 

1275 is_a_dir = " is a directory" 

1276 unlink_issue = "unable to unlink old '" 

1277 already_exists_issue = " already exists, no checkout" # created by entry.c:checkout_entry(...) 

1278 if line.endswith(is_a_dir): 

1279 failed_files.append(line[: -len(is_a_dir)]) 

1280 failed_reasons.append(is_a_dir) 

1281 elif line.startswith(unlink_issue): 

1282 failed_files.append(line[len(unlink_issue) : line.rfind("'")]) 

1283 failed_reasons.append(unlink_issue) 

1284 elif line.endswith(already_exists_issue): 

1285 failed_files.append(line[: -len(already_exists_issue)]) 

1286 failed_reasons.append(already_exists_issue) 

1287 else: 

1288 unknown_lines.append(line) 

1289 continue 

1290 # END special lines parsing 

1291 

1292 for e in endings: 

1293 if line.endswith(e): 

1294 failed_files.append(line[20 : -len(e)]) 

1295 failed_reasons.append(e) 

1296 break 

1297 # END if ending matches 

1298 # END for each possible ending 

1299 # END for each line 

1300 if unknown_lines: 

1301 raise GitCommandError(("git-checkout-index",), 128, stderr) 

1302 if failed_files: 

1303 valid_files = list(set(iter_checked_out_files) - set(failed_files)) 

1304 raise CheckoutError( 

1305 "Some files could not be checked out from the index due to local modifications", 

1306 failed_files, 

1307 valid_files, 

1308 failed_reasons, 

1309 ) 

1310 

1311 # END stderr handler 

1312 

1313 if paths is None: 

1314 args.append("--all") 

1315 kwargs["as_process"] = 1 

1316 fprogress(None, False, None) 

1317 proc = self.repo.git.checkout_index(*args, **kwargs) 

1318 proc.wait() 

1319 fprogress(None, True, None) 

1320 rval_iter = (e.path for e in self.entries.values()) 

1321 handle_stderr(proc, rval_iter) 

1322 return rval_iter 

1323 else: 

1324 if isinstance(paths, str): 

1325 paths = [paths] 

1326 

1327 # Make sure we have our entries loaded before we start checkout_index, which 

1328 # will hold a lock on it. We try to get the lock as well during our entries 

1329 # initialization. 

1330 self.entries # noqa: B018 

1331 

1332 args.append("--stdin") 

1333 kwargs["as_process"] = True 

1334 kwargs["istream"] = subprocess.PIPE 

1335 proc = self.repo.git.checkout_index(args, **kwargs) 

1336 # FIXME: Reading from GIL! 

1337 make_exc = lambda: GitCommandError(("git-checkout-index",) + tuple(args), 128, proc.stderr.read()) 

1338 checked_out_files: List[PathLike] = [] 

1339 

1340 for path in paths: 

1341 co_path = to_native_path_linux(self._to_relative_path(path)) 

1342 # If the item is not in the index, it could be a directory. 

1343 path_is_directory = False 

1344 

1345 try: 

1346 self.entries[(co_path, 0)] 

1347 except KeyError: 

1348 folder = str(co_path) 

1349 if not folder.endswith("/"): 

1350 folder += "/" 

1351 for entry in self.entries.values(): 

1352 if str(entry.path).startswith(folder): 

1353 p = entry.path 

1354 self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False) 

1355 checked_out_files.append(p) 

1356 path_is_directory = True 

1357 # END if entry is in directory 

1358 # END for each entry 

1359 # END path exception handlnig 

1360 

1361 if not path_is_directory: 

1362 self._write_path_to_stdin(proc, co_path, path, make_exc, fprogress, read_from_stdout=False) 

1363 checked_out_files.append(co_path) 

1364 # END path is a file 

1365 # END for each path 

1366 try: 

1367 self._flush_stdin_and_wait(proc, ignore_stdout=True) 

1368 except GitCommandError: 

1369 # Without parsing stdout we don't know what failed. 

1370 raise CheckoutError( # noqa: B904 

1371 "Some files could not be checked out from the index, probably because they didn't exist.", 

1372 failed_files, 

1373 [], 

1374 failed_reasons, 

1375 ) 

1376 

1377 handle_stderr(proc, checked_out_files) 

1378 return checked_out_files 

1379 # END paths handling 

1380 

1381 @default_index 

1382 def reset( 

1383 self, 

1384 commit: Union[Commit, "Reference", str] = "HEAD", 

1385 working_tree: bool = False, 

1386 paths: Union[None, Iterable[PathLike]] = None, 

1387 head: bool = False, 

1388 **kwargs: Any, 

1389 ) -> "IndexFile": 

1390 """Reset the index to reflect the tree at the given commit. This will not adjust 

1391 our HEAD reference by default, as opposed to 

1392 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`. 

1393 

1394 :param commit: 

1395 Revision, :class:`~git.refs.reference.Reference` or 

1396 :class:`~git.objects.commit.Commit` specifying the commit we should 

1397 represent. 

1398 

1399 If you want to specify a tree only, use :meth:`IndexFile.from_tree` and 

1400 overwrite the default index. 

1401 

1402 :param working_tree: 

1403 If ``True``, the files in the working tree will reflect the changed index. 

1404 If ``False``, the working tree will not be touched. 

1405 Please note that changes to the working copy will be discarded without 

1406 warning! 

1407 

1408 :param head: 

1409 If ``True``, the head will be set to the given commit. This is ``False`` by 

1410 default, but if ``True``, this method behaves like 

1411 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`. 

1412 

1413 :param paths: 

1414 If given as an iterable of absolute or repository-relative paths, only these 

1415 will be reset to their state at the given commit-ish. 

1416 The paths need to exist at the commit, otherwise an exception will be 

1417 raised. 

1418 

1419 :param kwargs: 

1420 Additional keyword arguments passed to :manpage:`git-reset(1)`. 

1421 

1422 :note: 

1423 :meth:`IndexFile.reset`, as opposed to 

1424 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`, will not delete any files in 

1425 order to maintain a consistent working tree. Instead, it will just check out 

1426 the files according to their state in the index. 

1427 If you want :manpage:`git-reset(1)`-like behaviour, use 

1428 :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead. 

1429 

1430 :return: 

1431 self 

1432 """ 

1433 # What we actually want to do is to merge the tree into our existing index, 

1434 # which is what git-read-tree does. 

1435 new_inst = type(self).from_tree(self.repo, commit) 

1436 if not paths: 

1437 self.entries = new_inst.entries 

1438 else: 

1439 nie = new_inst.entries 

1440 for path in paths: 

1441 path = self._to_relative_path(path) 

1442 try: 

1443 key = entry_key(path, 0) 

1444 self.entries[key] = nie[key] 

1445 except KeyError: 

1446 # If key is not in theirs, it musn't be in ours. 

1447 try: 

1448 del self.entries[key] 

1449 except KeyError: 

1450 pass 

1451 # END handle deletion keyerror 

1452 # END handle keyerror 

1453 # END for each path 

1454 # END handle paths 

1455 self.write() 

1456 

1457 if working_tree: 

1458 self.checkout(paths=paths, force=True) 

1459 # END handle working tree 

1460 

1461 if head: 

1462 self.repo.head.set_commit(self.repo.commit(commit), logmsg="%s: Updating HEAD" % commit) 

1463 # END handle head change 

1464 

1465 return self 

1466 

1467 # FIXME: This is documented to accept the same parameters as Diffable.diff, but this 

1468 # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.) 

1469 def diff( 

1470 self, 

1471 other: Union[ # type: ignore[override] 

1472 Literal[git_diff.DiffConstants.INDEX], 

1473 "Tree", 

1474 "Commit", 

1475 str, 

1476 None, 

1477 ] = git_diff.INDEX, 

1478 paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None, 

1479 create_patch: bool = False, 

1480 **kwargs: Any, 

1481 ) -> git_diff.DiffIndex: 

1482 """Diff this index against the working copy or a :class:`~git.objects.tree.Tree` 

1483 or :class:`~git.objects.commit.Commit` object. 

1484 

1485 For documentation of the parameters and return values, see 

1486 :meth:`Diffable.diff <git.diff.Diffable.diff>`. 

1487 

1488 :note: 

1489 Will only work with indices that represent the default git index as they 

1490 have not been initialized with a stream. 

1491 """ 

1492 # Only run if we are the default repository index. 

1493 if self._file_path != self._index_path(): 

1494 raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff()) 

1495 # Index against index is always empty. 

1496 if other is self.INDEX: 

1497 return git_diff.DiffIndex() 

1498 

1499 # Index against anything but None is a reverse diff with the respective item. 

1500 # Handle existing -R flags properly. 

1501 # Transform strings to the object so that we can call diff on it. 

1502 if isinstance(other, str): 

1503 other = self.repo.rev_parse(other) 

1504 # END object conversion 

1505 

1506 if isinstance(other, Object): # For Tree or Commit. 

1507 # Invert the existing R flag. 

1508 cur_val = kwargs.get("R", False) 

1509 kwargs["R"] = not cur_val 

1510 return other.diff(self.INDEX, paths, create_patch, **kwargs) 

1511 # END diff against other item handling 

1512 

1513 # If other is not None here, something is wrong. 

1514 if other is not None: 

1515 raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other) 

1516 

1517 # Diff against working copy - can be handled by superclass natively. 

1518 return super().diff(other, paths, create_patch, **kwargs)