Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/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

544 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 

26from git.cmd import Git 

27import git.diff as git_diff 

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

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

30from git.objects.util import Serializable 

31from git.util import ( 

32 Actor, 

33 LazyMixin, 

34 LockedFD, 

35 join_path_native, 

36 file_contents_ro, 

37 _is_path_rooted, 

38 _to_relative_path, 

39 to_native_path_linux, 

40 unbare_repo, 

41 to_bin_sha, 

42) 

43 

44from .fun import ( 

45 S_IFGITLINK, 

46 aggressive_tree_merge, 

47 entry_key, 

48 read_cache, 

49 run_commit_hook, 

50 stat_mode_to_index_mode, 

51 write_cache, 

52 write_tree_from_cache, 

53) 

54from .typ import BaseIndexEntry, IndexEntry, StageType 

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

56 

57# typing ----------------------------------------------------------------------------- 

58 

59from typing import ( 

60 Any, 

61 BinaryIO, 

62 Callable, 

63 cast, 

64 Dict, 

65 Generator, 

66 IO, 

67 Iterable, 

68 Iterator, 

69 List, 

70 NoReturn, 

71 Sequence, 

72 TYPE_CHECKING, 

73 Tuple, 

74 Union, 

75) 

76 

77from git.types import Literal, PathLike 

78 

79if TYPE_CHECKING: 

80 from subprocess import Popen 

81 

82 from git.refs.reference import Reference 

83 from git.repo import Repo 

84 

85 

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

87 

88# ------------------------------------------------------------------------------------ 

89 

90 

91@contextlib.contextmanager 

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

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

94 

95 :param directory: 

96 The directory in which the file is created. 

97 

98 :return: 

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

100 and deletes it on exit. 

101 """ 

102 if sys.platform == "win32": 

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

104 os.close(fd) 

105 try: 

106 yield name 

107 finally: 

108 os.remove(name) 

109 else: 

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

111 yield ctx.name 

112 

113 

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

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

116 git command function calls wherever possible. 

117 

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

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

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

121 case of partial working trees. 

122 

123 Entries: 

124 

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

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

127 

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

129 

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

131 

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

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

134 """ 

135 

136 unsafe_git_checkout_index_options = ["--prefix"] 

137 unsafe_git_read_tree_options = ["--index-output"] 

138 

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

140 

141 _VERSION = 2 

142 """The latest version we support.""" 

143 

144 S_IFGITLINK = S_IFGITLINK 

145 """Flags for a submodule.""" 

146 

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

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

149 

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

151 

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

153 repository's index on demand. 

154 """ 

155 self.repo = repo 

156 self.version = self._VERSION 

157 self._extension_data = b"" 

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

159 

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

161 if attr == "entries": 

162 try: 

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

164 except OSError: 

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

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

167 return 

168 # END exception handling 

169 

170 try: 

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

172 finally: 

173 os.close(fd) 

174 

175 self._deserialize(stream) 

176 else: 

177 super()._set_cache_(attr) 

178 

179 def _index_path(self) -> PathLike: 

180 if self.repo.git_dir: 

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

182 else: 

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

184 

185 @property 

186 def path(self) -> PathLike: 

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

188 return self._file_path 

189 

190 def _delete_entries_cache(self) -> None: 

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

192 try: 

193 del self.entries 

194 except AttributeError: 

195 # It failed in Python 2.6.5 with AttributeError. 

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

197 pass 

198 # END exception handling 

199 

200 # { Serializable Interface 

201 

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

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

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

205 return self 

206 

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

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

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

210 

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

212 entries = self._entries_sorted() 

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

214 if ignore_extension_data: 

215 extension_data = None 

216 write_cache(entries, stream, extension_data) 

217 return self 

218 

219 # } END serializable interface 

220 

221 def write( 

222 self, 

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

224 ignore_extension_data: bool = False, 

225 ) -> None: 

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

227 

228 :param file_path: 

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

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

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

232 

233 :param ignore_extension_data: 

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

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

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

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

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

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

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

241 """ 

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

243 # Otherwise it would be done when streaming. 

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

245 self.entries # noqa: B018 

246 lfd = LockedFD(file_path or self._file_path) 

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

248 

249 try: 

250 self._serialize(stream, ignore_extension_data) 

251 except BaseException: 

252 lfd.rollback() 

253 raise 

254 

255 lfd.commit() 

256 

257 # Make sure we represent what we have written. 

258 if file_path is not None: 

259 self._file_path = file_path 

260 

261 @post_clear_cache 

262 @default_index 

263 def merge_tree( 

264 self, 

265 rhs: Treeish, 

266 base: Union[None, Treeish] = None, 

267 allow_unsafe_options: bool = False, 

268 ) -> "IndexFile": 

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

270 a common base treeish into account. 

271 

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

273 existing tree as the left side of the merge. 

274 

275 :param rhs: 

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

277 

278 :param base: 

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

280 index which equals lhs. 

281 

282 :param allow_unsafe_options: 

283 Allow options that may write to arbitrary paths. 

284 

285 :return: 

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

287 conflicts) 

288 

289 :raise git.exc.GitCommandError: 

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

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

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

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

294 """ 

295 if not allow_unsafe_options: 

296 Git.check_unsafe_options( 

297 options=Git._option_candidates([base, rhs]), 

298 unsafe_options=self.unsafe_git_read_tree_options, 

299 ) 

300 

301 # -i : ignore working tree status 

302 # --aggressive : handle more merge cases 

303 # -m : do an actual merge 

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

305 if base is not None: 

306 args.append(base) 

307 args.append(rhs) 

308 

309 self.repo.git.read_tree(args) 

310 return self 

311 

312 @classmethod 

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

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

315 

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

317 

318 :param repo: 

319 The repository treeish are located in. 

320 

321 :param tree_sha: 

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

323 

324 :return: 

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

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

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

328 """ 

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

330 base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes) 

331 

332 inst = cls(repo) 

333 # Convert to entries dict. 

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

335 zip( 

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

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

338 ) 

339 ) 

340 

341 inst.entries = entries 

342 return inst 

343 

344 @classmethod 

345 def from_tree( 

346 cls, 

347 repo: "Repo", 

348 *treeish: Treeish, 

349 allow_unsafe_options: bool = False, 

350 **kwargs: Any, 

351 ) -> "IndexFile": 

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

353 The original index will remain unaltered. 

354 

355 :param repo: 

356 The repository treeish are located in. 

357 

358 :param treeish: 

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

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

361 

362 The result changes according to the amount of trees: 

363 

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

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

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

367 one. It behaves like a fast-forward. 

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

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

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

371 

372 :param kwargs: 

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

374 

375 :param allow_unsafe_options: 

376 Allow options that may write to arbitrary paths. 

377 

378 :return: 

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

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

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

382 

383 :note: 

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

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

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

387 

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

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

390 unexpected interference. 

391 """ 

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

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

394 

395 if not allow_unsafe_options: 

396 Git.check_unsafe_options( 

397 options=Git._option_candidates(treeish, kwargs), 

398 unsafe_options=cls.unsafe_git_read_tree_options, 

399 ) 

400 

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

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

403 if len(treeish) > 1: 

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

405 arg_list.append("--reset") 

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

407 arg_list.append("--aggressive") 

408 # END merge handling 

409 

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

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

412 with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index: 

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

414 arg_list.extend(treeish) 

415 

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

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

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

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

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

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

422 index = cls(repo, tmp_index) 

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

424 return index 

425 # END index merge handling 

426 

427 # UTILITIES 

428 

429 @unbare_repo 

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

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

432 accordingly. 

433 

434 :note: 

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

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

437 and do not prune. 

438 """ 

439 

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

441 raise e 

442 

443 r = str(self.repo.working_tree_dir) 

444 rs = r + os.sep 

445 for path in paths: 

446 abs_path = os.fspath(path) 

447 if not osp.isabs(abs_path): 

448 abs_path = osp.join(r, path) 

449 # END make absolute path 

450 

451 try: 

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

453 except OSError: 

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

455 pass 

456 else: 

457 if S_ISLNK(st.st_mode): 

458 yield abs_path.replace(rs, "") 

459 continue 

460 # END check symlink 

461 

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

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

464 resolved_paths = glob.glob(abs_path) 

465 # not abs_path in resolved_paths: 

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

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

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

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

470 # characters. 

471 if abs_path not in resolved_paths: 

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

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

474 continue 

475 # END glob handling 

476 try: 

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

478 for rela_file in files: 

479 # Add relative paths only. 

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

481 # END for each file in subdir 

482 # END for each subdirectory 

483 except OSError: 

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

485 yield abs_path.replace(rs, "") 

486 # END path exception handling 

487 # END for each path 

488 

489 def _write_path_to_stdin( 

490 self, 

491 proc: "Popen", 

492 filepath: PathLike, 

493 item: PathLike, 

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

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

496 read_from_stdout: bool = True, 

497 ) -> Union[None, str]: 

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

499 progress. 

500 

501 :return: 

502 stdout string 

503 

504 :param read_from_stdout: 

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

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

507 

508 :note: 

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

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

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

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

513 

514 :note: 

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

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

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

518 stdin to break the pipe. 

519 """ 

520 fprogress(filepath, False, item) 

521 rval: Union[None, str] = None 

522 

523 if proc.stdin is not None: 

524 try: 

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

526 except IOError as e: 

527 # Pipe broke, usually because some error happened. 

528 raise fmakeexc() from e 

529 # END write exception handling 

530 proc.stdin.flush() 

531 

532 if read_from_stdout and proc.stdout is not None: 

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

534 fprogress(filepath, True, item) 

535 return rval 

536 

537 def iter_blobs( 

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

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

540 """ 

541 :return: 

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

543 stages, tuple(stage, Blob). 

544 

545 :param predicate: 

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

547 the iterator. A default filter, the :class:`~git.index.typ.BlobFilter`, allows you 

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

549 """ 

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

551 blob = entry.to_blob(self.repo) 

552 blob.size = entry.size 

553 output = (entry.stage, blob) 

554 if predicate(output): 

555 yield output 

556 # END for each entry 

557 

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

559 """ 

560 :return: 

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

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

563 

564 :note: 

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

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

567 stage 3 will not have a stage 3 entry. 

568 """ 

569 

570 def is_unmerged_blob(t: Tuple[StageType, Blob]) -> bool: 

571 return t[0] != 0 

572 

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

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

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

576 # END for each unmerged blob 

577 for line in path_map.values(): 

578 line.sort() 

579 

580 return path_map 

581 

582 @classmethod 

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

584 return entry_key(*entry) 

585 

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

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

588 

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

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

591 

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

593 raised claiming the path is already at stage 0. 

594 

595 :raise ValueError: 

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

597 

598 :return: 

599 self 

600 

601 :note: 

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

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

604 """ 

605 for blob in iter_blobs: 

606 stage_null_key = (blob.path, 0) 

607 if stage_null_key in self.entries: 

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

609 # END assert blob is not stage 0 already 

610 

611 # Delete all possible stages. 

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

613 try: 

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

615 except KeyError: 

616 pass 

617 # END ignore key errors 

618 # END for each possible stage 

619 

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

621 # END for each blob 

622 

623 return self 

624 

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

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

627 we might have. 

628 

629 :note: 

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

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

632 

633 :return: 

634 self 

635 """ 

636 self._delete_entries_cache() 

637 # Allows to lazily reread on demand. 

638 return self 

639 

640 def write_tree(self) -> Tree: 

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

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

643 

644 :return: 

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

646 

647 :note: 

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

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

650 to the index directly. 

651 

652 :raise ValueError: 

653 If there are no entries in the cache. 

654 

655 :raise git.exc.UnmergedEntriesError: 

656 """ 

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

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

659 mdb = MemoryDB() 

660 entries = self._entries_sorted() 

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

662 

663 # Copy changed trees only. 

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

665 

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

667 # return sorted tree entries. 

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

669 root_tree._cache = tree_items 

670 return root_tree 

671 

672 def _process_diff_args( 

673 self, 

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

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

676 try: 

677 args.pop(args.index(self)) 

678 except IndexError: 

679 pass 

680 # END remove self 

681 return args 

682 

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

684 """ 

685 :return: 

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

687 it is not within our git directory. 

688 

689 :raise ValueError: 

690 """ 

691 if self.repo.bare: 

692 drive, _tail = osp.splitdrive(os.fspath(path)) 

693 if drive or _is_path_rooted(path): 

694 raise InvalidGitRepositoryError("paths with a drive or root require a non-bare repository") 

695 return path 

696 return _to_relative_path(cast(PathLike, self.repo.working_tree_dir), path) 

697 

698 def _preprocess_add_items( 

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

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

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

702 paths = [] 

703 entries = [] 

704 # if it is a string put in list 

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

706 items = [items] 

707 

708 for item in items: 

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

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

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

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

713 elif isinstance(item, BaseIndexEntry): 

714 entries.append(item) 

715 else: 

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

717 # END for each item 

718 return paths, entries 

719 

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

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

722 

723 :note: 

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

725 This must be ensured in the calling code. 

726 """ 

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

728 

729 if S_ISLNK(st.st_mode): 

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

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

732 def open_stream() -> BinaryIO: 

733 return BytesIO(force_bytes(os.readlink(filepath), encoding=defenc)) 

734 else: 

735 

736 def open_stream() -> BinaryIO: 

737 return open(filepath, "rb") 

738 

739 with open_stream() as stream: 

740 fprogress(filepath, False, filepath) 

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

742 fprogress(filepath, True, filepath) 

743 return BaseIndexEntry( 

744 ( 

745 stat_mode_to_index_mode(st.st_mode), 

746 istream.binsha, 

747 0, 

748 to_native_path_linux(filepath), 

749 ) 

750 ) 

751 

752 @unbare_repo 

753 @git_working_dir 

754 def _entries_for_paths( 

755 self, 

756 paths: List[str], 

757 path_rewriter: Union[Callable, None], 

758 fprogress: Callable, 

759 entries: List[BaseIndexEntry], 

760 ) -> List[BaseIndexEntry]: 

761 entries_added: List[BaseIndexEntry] = [] 

762 if path_rewriter: 

763 working_tree_dir = self.repo.working_tree_dir 

764 if working_tree_dir is None: 

765 raise InvalidGitRepositoryError("Cannot rewrite paths without a working tree") 

766 working_tree_dir = str(working_tree_dir) 

767 for path in paths: 

768 if osp.isabs(path): 

769 abspath = path 

770 gitrelative_path = path[len(working_tree_dir) + 1 :] 

771 else: 

772 gitrelative_path = path 

773 abspath = osp.join(working_tree_dir, gitrelative_path) 

774 # END obtain relative and absolute paths 

775 

776 blob = Blob( 

777 self.repo, 

778 Blob.NULL_BIN_SHA, 

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

780 to_native_path_linux(gitrelative_path), 

781 ) 

782 # TODO: variable undefined 

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

784 # END for each path 

785 del paths[:] 

786 # END rewrite paths 

787 

788 # HANDLE PATHS 

789 assert len(entries_added) == 0 

790 for filepath in self._iter_expand_paths(paths): 

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

792 # END for each filepath 

793 # END path handling 

794 return entries_added 

795 

796 def add( 

797 self, 

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

799 force: bool = True, 

800 fprogress: Callable = lambda *args: None, 

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

802 write: bool = True, 

803 write_extension_data: bool = False, 

804 ) -> List[BaseIndexEntry]: 

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

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

807 

808 :param items: 

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

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

811 relative or absolute. 

812 

813 - path string 

814 

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

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

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

818 

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

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

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

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

823 

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

825 into the object database. 

826 

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

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

829 directory and subdirectories. 

830 

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

832 

833 They are added at stage 0. 

834 

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

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

837 

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

839 

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

841 be a path relative to our repository. 

842 

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

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

845 data at the path. 

846 

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

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

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

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

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

852 created on filesystems not supporting it as well. 

853 

854 Please note that globs or directories are not allowed in 

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

856 

857 They are added at stage 0. 

858 

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

860 

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

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

863 binary sha's. 

864 

865 :param force: 

866 **CURRENTLY INEFFECTIVE** 

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

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

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

870 excluded. 

871 

872 :param fprogress: 

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

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

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

876 

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

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

879 

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

881 index already as the index is currently being processed. 

882 

883 :param path_rewriter: 

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

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

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

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

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

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

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

891 repository. 

892 

893 :param write: 

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

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

896 

897 :param write_extension_data: 

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

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

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

901 one representing the now changed index. 

902 

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

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

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

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

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

908 handled manually at all. 

909 

910 All current built-in extensions are listed here: 

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

912 

913 :return: 

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

915 just actually added. 

916 

917 :raise OSError: 

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

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

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

921 """ 

922 # Sort the entries into strings and Entries. 

923 # Blobs are converted to entries automatically. 

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

925 paths, entries = self._preprocess_add_items(items) 

926 entries_added: List[BaseIndexEntry] = [] 

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

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

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

930 if paths: 

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

932 

933 # HANDLE ENTRIES 

934 if entries: 

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

936 if null_mode_entries: 

937 raise ValueError( 

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

939 ) 

940 # END null mode should be remove 

941 

942 # HANDLE ENTRY OBJECT CREATION 

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

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

945 if null_entries_indices: 

946 

947 @git_working_dir 

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

949 for ei in null_entries_indices: 

950 null_entry = entries[ei] 

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

952 

953 # Update null entry. 

954 entries[ei] = BaseIndexEntry( 

955 ( 

956 null_entry.mode, 

957 new_entry.binsha, 

958 null_entry.stage, 

959 null_entry.path, 

960 ) 

961 ) 

962 # END for each entry index 

963 

964 # END closure 

965 

966 handle_null_entries(self) 

967 # END null_entry handling 

968 

969 # REWRITE PATHS 

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

971 # object sha's. 

972 if path_rewriter: 

973 for i, e in enumerate(entries): 

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

975 # END for each entry 

976 # END handle path rewriting 

977 

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

979 for i, entry in enumerate(entries): 

980 progress_sent = i in null_entries_indices 

981 if not progress_sent: 

982 fprogress(entry.path, False, entry) 

983 fprogress(entry.path, True, entry) 

984 # END handle progress 

985 # END for each entry 

986 entries_added.extend(entries) 

987 # END if there are base entries 

988 

989 # FINALIZE 

990 # Add the new entries to this instance. 

991 for entry in entries_added: 

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

993 

994 if write: 

995 self.write(ignore_extension_data=not write_extension_data) 

996 # END handle write 

997 

998 return entries_added 

999 

1000 def _items_to_rela_paths( 

1001 self, 

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

1003 ) -> List[PathLike]: 

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

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

1006 paths = [] 

1007 # If string, put in list. 

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

1009 items = [items] 

1010 

1011 for item in items: 

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

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

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

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

1016 else: 

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

1018 # END for each item 

1019 return paths 

1020 

1021 @post_clear_cache 

1022 @default_index 

1023 def remove( 

1024 self, 

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

1026 working_tree: bool = False, 

1027 allow_unsafe_options: bool = False, 

1028 **kwargs: Any, 

1029 ) -> List[str]: 

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

1031 as well. 

1032 

1033 :param items: 

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

1035 

1036 - path string 

1037 

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

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

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

1041 relative to the git repository directory containing the working tree 

1042 

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

1044 

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

1046 

1047 Only the path portion is used in this case. 

1048 

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

1050 

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

1052 

1053 :param working_tree: 

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

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

1056 uncommitted changes in it. 

1057 

1058 :param allow_unsafe_options: 

1059 Allow unsafe options such as ``--pathspec-from-file`` to be passed to 

1060 :manpage:`git-rm(1)`. 

1061 

1062 :param kwargs: 

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

1064 ``r`` to allow recursive removal. 

1065 

1066 :return: 

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

1068 removed effectively. 

1069 

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

1071 Paths are relative to the repository. 

1072 """ 

1073 if not allow_unsafe_options: 

1074 Git.check_unsafe_options( 

1075 options=Git._option_candidates([], kwargs), 

1076 unsafe_options=Git.unsafe_git_pathspec_from_file_options, 

1077 ) 

1078 args = [] 

1079 if not working_tree: 

1080 args.append("--cached") 

1081 args.append("--") 

1082 

1083 # Preprocess paths. 

1084 paths = list(map(os.fspath, self._items_to_rela_paths(items))) # type: ignore[arg-type] 

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

1086 

1087 # Process output to gain proper paths. 

1088 # rm 'path' 

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

1090 

1091 @post_clear_cache 

1092 @default_index 

1093 def move( 

1094 self, 

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

1096 skip_errors: bool = False, 

1097 **kwargs: Any, 

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

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

1100 the move operation. 

1101 

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

1103 

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

1105 or files. 

1106 

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

1108 

1109 :param items: 

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

1111 for reference. 

1112 

1113 :param skip_errors: 

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

1115 skipped. 

1116 

1117 :param kwargs: 

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

1119 ``dry_run`` or ``force``. 

1120 

1121 :return: 

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

1123 

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

1125 destination. Relative to the repository root. 

1126 

1127 :raise ValueError: 

1128 If only one item was given. 

1129 

1130 :raise git.exc.GitCommandError: 

1131 If git could not handle your request. 

1132 """ 

1133 args = [] 

1134 if skip_errors: 

1135 args.append("-k") 

1136 

1137 paths = self._items_to_rela_paths(items) 

1138 if len(paths) < 2: 

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

1140 

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

1142 kwargs["dry_run"] = True 

1143 

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

1145 # (for later output). 

1146 out = [] 

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

1148 

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

1150 # 'renaming' ones which we parse. 

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

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

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

1154 

1155 # [0] = Renaming x 

1156 # [1] = y 

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

1158 # END for each line to parse 

1159 

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

1161 if was_dry_run: 

1162 return out 

1163 # END handle dry run 

1164 

1165 # Now apply the actual operation. 

1166 kwargs.pop("dry_run") 

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

1168 

1169 return out 

1170 

1171 def commit( 

1172 self, 

1173 message: str, 

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

1175 head: bool = True, 

1176 author: Union[None, Actor] = None, 

1177 committer: Union[None, Actor] = None, 

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

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

1180 skip_hooks: bool = False, 

1181 trailers: Union[None, "Dict[str, str]", "List[Tuple[str, str]]"] = None, 

1182 ) -> Commit: 

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

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

1185 

1186 For more information on the arguments, see 

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

1188 

1189 :note: 

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

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

1192 

1193 :note: 

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

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

1196 

1197 :return: 

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

1199 """ 

1200 if not skip_hooks: 

1201 run_commit_hook("pre-commit", self) 

1202 

1203 self._write_commit_editmsg(message) 

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

1205 message = self._read_commit_editmsg() 

1206 self._remove_commit_editmsg() 

1207 tree = self.write_tree() 

1208 rval = Commit.create_from_tree( 

1209 self.repo, 

1210 tree, 

1211 message, 

1212 parent_commits, 

1213 head, 

1214 author=author, 

1215 committer=committer, 

1216 author_date=author_date, 

1217 commit_date=commit_date, 

1218 trailers=trailers, 

1219 ) 

1220 if not skip_hooks: 

1221 run_commit_hook("post-commit", self) 

1222 return rval 

1223 

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

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

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

1227 

1228 def _remove_commit_editmsg(self) -> None: 

1229 os.remove(self._commit_editmsg_filepath()) 

1230 

1231 def _read_commit_editmsg(self) -> str: 

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

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

1234 

1235 def _commit_editmsg_filepath(self) -> str: 

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

1237 

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

1239 stdin_IO = proc.stdin 

1240 if stdin_IO: 

1241 stdin_IO.flush() 

1242 stdin_IO.close() 

1243 

1244 stdout = b"" 

1245 if not ignore_stdout and proc.stdout: 

1246 stdout = proc.stdout.read() 

1247 

1248 if proc.stdout: 

1249 proc.stdout.close() 

1250 proc.wait() 

1251 return stdout 

1252 

1253 @default_index 

1254 def checkout( 

1255 self, 

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

1257 force: bool = False, 

1258 fprogress: Callable = lambda *args: None, 

1259 allow_unsafe_options: bool = False, 

1260 **kwargs: Any, 

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

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

1263 into the working tree. 

1264 

1265 :note: 

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

1267 case you have altered the entries dictionary directly. 

1268 

1269 :param paths: 

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

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

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

1273 

1274 :param force: 

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

1276 modifications. 

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

1278 

1279 :param fprogress: 

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

1281 

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

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

1284 prior and after a file has been checked out. 

1285 

1286 :param allow_unsafe_options: 

1287 Allow unsafe options, such as ``--prefix``. 

1288 

1289 :param kwargs: 

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

1291 

1292 :return: 

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

1294 guaranteed to match the version stored in the index. 

1295 

1296 :raise git.exc.CheckoutError: 

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

1298 will checkout as many files as it can anyway. 

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

1300 the original git command, which ignores them). 

1301 

1302 :raise git.exc.GitCommandError: 

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

1304 

1305 :note: 

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

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

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

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

1310 ``head.checkout`` instead of ``index.checkout``. 

1311 """ 

1312 if not allow_unsafe_options: 

1313 Git.check_unsafe_options( 

1314 options=Git._option_candidates([], kwargs), 

1315 unsafe_options=self.unsafe_git_checkout_index_options, 

1316 ) 

1317 

1318 args = ["--index"] 

1319 if force: 

1320 args.append("--force") 

1321 

1322 failed_files = [] 

1323 failed_reasons = [] 

1324 unknown_lines = [] 

1325 

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

1327 stderr_IO = proc.stderr 

1328 if not stderr_IO: 

1329 return # Return early if stderr empty. 

1330 

1331 stderr_bytes = stderr_IO.read() 

1332 # line contents: 

1333 stderr = stderr_bytes.decode(defenc) 

1334 # git-checkout-index: this already exists 

1335 endings = ( 

1336 " already exists", 

1337 " is not in the cache", 

1338 " does not exist at stage", 

1339 " is unmerged", 

1340 ) 

1341 for line in stderr.splitlines(): 

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

1343 is_a_dir = " is a directory" 

1344 unlink_issue = "unable to unlink old '" 

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

1346 if line.endswith(is_a_dir): 

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

1348 failed_reasons.append(is_a_dir) 

1349 elif line.startswith(unlink_issue): 

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

1351 failed_reasons.append(unlink_issue) 

1352 elif line.endswith(already_exists_issue): 

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

1354 failed_reasons.append(already_exists_issue) 

1355 else: 

1356 unknown_lines.append(line) 

1357 continue 

1358 # END special lines parsing 

1359 

1360 for e in endings: 

1361 if line.endswith(e): 

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

1363 failed_reasons.append(e) 

1364 break 

1365 # END if ending matches 

1366 # END for each possible ending 

1367 # END for each line 

1368 if unknown_lines: 

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

1370 if failed_files: 

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

1372 raise CheckoutError( 

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

1374 failed_files, 

1375 valid_files, 

1376 failed_reasons, 

1377 ) 

1378 

1379 # END stderr handler 

1380 

1381 if paths is None: 

1382 args.append("--all") 

1383 kwargs["as_process"] = 1 

1384 fprogress(None, False, None) 

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

1386 proc.wait() 

1387 fprogress(None, True, None) 

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

1389 handle_stderr(proc, rval_iter) 

1390 return rval_iter 

1391 else: 

1392 if isinstance(paths, str): 

1393 paths = [paths] 

1394 

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

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

1397 # initialization. 

1398 self.entries # noqa: B018 

1399 

1400 args.append("--stdin") 

1401 kwargs["as_process"] = True 

1402 kwargs["istream"] = subprocess.PIPE 

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

1404 

1405 # FIXME: Reading from GIL! 

1406 def make_exc() -> GitCommandError: 

1407 return GitCommandError(("git-checkout-index", *args), 128, proc.stderr.read()) 

1408 

1409 checked_out_files: List[PathLike] = [] 

1410 

1411 for path in paths: 

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

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

1414 path_is_directory = False 

1415 

1416 try: 

1417 self.entries[(co_path, 0)] 

1418 except KeyError: 

1419 folder = co_path 

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

1421 folder += "/" 

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

1423 if os.fspath(entry.path).startswith(folder): 

1424 p = entry.path 

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

1426 checked_out_files.append(p) 

1427 path_is_directory = True 

1428 # END if entry is in directory 

1429 # END for each entry 

1430 # END path exception handlnig 

1431 

1432 if not path_is_directory: 

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

1434 checked_out_files.append(co_path) 

1435 # END path is a file 

1436 # END for each path 

1437 try: 

1438 self._flush_stdin_and_wait(proc, ignore_stdout=True) 

1439 except GitCommandError: 

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

1441 raise CheckoutError( # noqa: B904 

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

1443 failed_files, 

1444 [], 

1445 failed_reasons, 

1446 ) 

1447 

1448 handle_stderr(proc, checked_out_files) 

1449 return checked_out_files 

1450 # END paths handling 

1451 

1452 @default_index 

1453 def reset( 

1454 self, 

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

1456 working_tree: bool = False, 

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

1458 head: bool = False, 

1459 allow_unsafe_options: bool = False, 

1460 **kwargs: Any, 

1461 ) -> "IndexFile": 

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

1463 our HEAD reference by default, as opposed to 

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

1465 

1466 :param commit: 

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

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

1469 represent. 

1470 

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

1472 overwrite the default index. 

1473 

1474 :param working_tree: 

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

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

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

1478 warning! 

1479 

1480 :param head: 

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

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

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

1484 

1485 :param paths: 

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

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

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

1489 raised. 

1490 

1491 :param allow_unsafe_options: 

1492 Allow options that may write to arbitrary paths. 

1493 

1494 :param kwargs: 

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

1496 

1497 :note: 

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

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

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

1501 the files according to their state in the index. 

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

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

1504 

1505 :return: 

1506 self 

1507 """ 

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

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

1510 new_inst = type(self).from_tree(self.repo, commit, allow_unsafe_options=allow_unsafe_options) 

1511 if not paths: 

1512 self.entries = new_inst.entries 

1513 else: 

1514 nie = new_inst.entries 

1515 for path in paths: 

1516 path = self._to_relative_path(path) 

1517 key = entry_key(path, 0) 

1518 try: 

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

1520 except KeyError: 

1521 # If key is not in theirs, it mustn't be in ours. 

1522 try: 

1523 del self.entries[key] 

1524 except KeyError: 

1525 pass 

1526 # END handle deletion keyerror 

1527 # END handle keyerror 

1528 # END for each path 

1529 # END handle paths 

1530 self.write() 

1531 

1532 if working_tree: 

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

1534 # END handle working tree 

1535 

1536 if head: 

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

1538 # END handle head change 

1539 

1540 return self 

1541 

1542 def diff( 

1543 self, 

1544 other: Union[ 

1545 Literal[git_diff.DiffConstants.INDEX], 

1546 Literal[git_diff.DiffConstants.NULL_TREE], 

1547 "Tree", 

1548 "Commit", 

1549 str, 

1550 None, 

1551 ] = git_diff.INDEX, 

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

1553 create_patch: bool = False, 

1554 allow_unsafe_options: bool = False, 

1555 **kwargs: Any, 

1556 ) -> git_diff.DiffIndex[git_diff.Diff]: 

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

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

1559 

1560 For documentation of the parameters and return values, see 

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

1562 

1563 :note: 

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

1565 have not been initialized with a stream. 

1566 """ 

1567 if not allow_unsafe_options: 

1568 Git.check_unsafe_options( 

1569 options=Git._option_candidates([other], kwargs), 

1570 unsafe_options=self.repo.unsafe_git_diff_options, 

1571 clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW", 

1572 ) 

1573 

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

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

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

1577 # Index against index is always empty. 

1578 if other is self.INDEX: 

1579 return git_diff.DiffIndex() 

1580 

1581 if other == git_diff.NULL_TREE or other == git_diff.NULL_TREE_SHA: 

1582 args: List[Union[PathLike, str]] = [ 

1583 "--cached", 

1584 git_diff.NULL_TREE_SHA, 

1585 "--abbrev=40", 

1586 "--full-index", 

1587 ] 

1588 

1589 if not any(x in kwargs for x in ("find_renames", "no_renames", "M")): 

1590 args.append("-M") 

1591 

1592 if create_patch: 

1593 args.append("-p") 

1594 args.append("--no-ext-diff") 

1595 else: 

1596 args.append("--raw") 

1597 args.append("-z") 

1598 

1599 args.append("--no-color") 

1600 

1601 if paths is not None and not isinstance(paths, (tuple, list)): 

1602 paths = [paths] 

1603 

1604 if paths: 

1605 args.append("--") 

1606 args.extend(paths) 

1607 

1608 kwargs["as_process"] = True 

1609 if create_patch: 

1610 self.repo.git(c="diff.mnemonicPrefix=false") 

1611 proc = self.repo.git.diff(*args, **kwargs) 

1612 

1613 diff_method = ( 

1614 git_diff.Diff._index_from_patch_format if create_patch else git_diff.Diff._index_from_raw_format 

1615 ) 

1616 index = diff_method(self.repo, proc) 

1617 

1618 proc.wait() 

1619 return index 

1620 

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

1622 # Handle existing -R flags properly. 

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

1624 if isinstance(other, str): 

1625 other = self.repo.rev_parse(other) 

1626 # END object conversion 

1627 

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

1629 # Invert the existing R flag. 

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

1631 kwargs["R"] = not cur_val 

1632 return other.diff( 

1633 self.INDEX, 

1634 paths, 

1635 create_patch, 

1636 allow_unsafe_options=allow_unsafe_options, 

1637 **kwargs, 

1638 ) 

1639 # END diff against other item handling 

1640 

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

1642 if other is not None: 

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

1644 

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

1646 return super().diff(other, paths, create_patch, allow_unsafe_options=allow_unsafe_options, **kwargs)