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

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

509 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 Actor, 

32 LazyMixin, 

33 LockedFD, 

34 join_path_native, 

35 file_contents_ro, 

36 to_native_path_linux, 

37 unbare_repo, 

38 to_bin_sha, 

39) 

40 

41from .fun import ( 

42 S_IFGITLINK, 

43 aggressive_tree_merge, 

44 entry_key, 

45 read_cache, 

46 run_commit_hook, 

47 stat_mode_to_index_mode, 

48 write_cache, 

49 write_tree_from_cache, 

50) 

51from .typ import BaseIndexEntry, IndexEntry, StageType 

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

53 

54# typing ----------------------------------------------------------------------------- 

55 

56from typing import ( 

57 Any, 

58 BinaryIO, 

59 Callable, 

60 Dict, 

61 Generator, 

62 IO, 

63 Iterable, 

64 Iterator, 

65 List, 

66 NoReturn, 

67 Sequence, 

68 TYPE_CHECKING, 

69 Tuple, 

70 Union, 

71) 

72 

73from git.types import Literal, PathLike 

74 

75if TYPE_CHECKING: 

76 from subprocess import Popen 

77 

78 from git.refs.reference import Reference 

79 from git.repo import Repo 

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 :class:`~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 

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

535 return t[0] != 0 

536 

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

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

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

540 # END for each unmerged blob 

541 for line in path_map.values(): 

542 line.sort() 

543 

544 return path_map 

545 

546 @classmethod 

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

548 return entry_key(*entry) 

549 

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

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

552 

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

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

555 

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

557 raised claiming the path is already at stage 0. 

558 

559 :raise ValueError: 

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

561 

562 :return: 

563 self 

564 

565 :note: 

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

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

568 """ 

569 for blob in iter_blobs: 

570 stage_null_key = (blob.path, 0) 

571 if stage_null_key in self.entries: 

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

573 # END assert blob is not stage 0 already 

574 

575 # Delete all possible stages. 

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

577 try: 

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

579 except KeyError: 

580 pass 

581 # END ignore key errors 

582 # END for each possible stage 

583 

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

585 # END for each blob 

586 

587 return self 

588 

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

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

591 we might have. 

592 

593 :note: 

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

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

596 

597 :return: 

598 self 

599 """ 

600 self._delete_entries_cache() 

601 # Allows to lazily reread on demand. 

602 return self 

603 

604 def write_tree(self) -> Tree: 

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

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

607 

608 :return: 

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

610 

611 :note: 

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

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

614 to the index directly. 

615 

616 :raise ValueError: 

617 If there are no entries in the cache. 

618 

619 :raise git.exc.UnmergedEntriesError: 

620 """ 

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

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

623 mdb = MemoryDB() 

624 entries = self._entries_sorted() 

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

626 

627 # Copy changed trees only. 

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

629 

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

631 # return sorted tree entries. 

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

633 root_tree._cache = tree_items 

634 return root_tree 

635 

636 def _process_diff_args( 

637 self, 

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

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

640 try: 

641 args.pop(args.index(self)) 

642 except IndexError: 

643 pass 

644 # END remove self 

645 return args 

646 

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

648 """ 

649 :return: 

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

651 it is not within our git directory. 

652 

653 :raise ValueError: 

654 """ 

655 if not osp.isabs(path): 

656 return path 

657 if self.repo.bare: 

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

659 if not osp.normpath(str(path)).startswith(str(self.repo.working_tree_dir)): 

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

661 result = os.path.relpath(path, self.repo.working_tree_dir) 

662 if str(path).endswith(os.sep) and not result.endswith(os.sep): 

663 result += os.sep 

664 return result 

665 

666 def _preprocess_add_items( 

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

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

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

670 paths = [] 

671 entries = [] 

672 # if it is a string put in list 

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

674 items = [items] 

675 

676 for item in items: 

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

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

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

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

681 elif isinstance(item, BaseIndexEntry): 

682 entries.append(item) 

683 else: 

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

685 # END for each item 

686 return paths, entries 

687 

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

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

690 

691 :note: 

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

693 This must be ensured in the calling code. 

694 """ 

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

696 

697 if S_ISLNK(st.st_mode): 

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

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

700 def open_stream() -> BinaryIO: 

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

702 else: 

703 

704 def open_stream() -> BinaryIO: 

705 return open(filepath, "rb") 

706 

707 with open_stream() as stream: 

708 fprogress(filepath, False, filepath) 

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

710 fprogress(filepath, True, filepath) 

711 return BaseIndexEntry( 

712 ( 

713 stat_mode_to_index_mode(st.st_mode), 

714 istream.binsha, 

715 0, 

716 to_native_path_linux(filepath), 

717 ) 

718 ) 

719 

720 @unbare_repo 

721 @git_working_dir 

722 def _entries_for_paths( 

723 self, 

724 paths: List[str], 

725 path_rewriter: Union[Callable, None], 

726 fprogress: Callable, 

727 entries: List[BaseIndexEntry], 

728 ) -> List[BaseIndexEntry]: 

729 entries_added: List[BaseIndexEntry] = [] 

730 if path_rewriter: 

731 for path in paths: 

732 if osp.isabs(path): 

733 abspath = path 

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

735 else: 

736 gitrelative_path = path 

737 if self.repo.working_tree_dir: 

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

739 # END obtain relative and absolute paths 

740 

741 blob = Blob( 

742 self.repo, 

743 Blob.NULL_BIN_SHA, 

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

745 to_native_path_linux(gitrelative_path), 

746 ) 

747 # TODO: variable undefined 

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

749 # END for each path 

750 del paths[:] 

751 # END rewrite paths 

752 

753 # HANDLE PATHS 

754 assert len(entries_added) == 0 

755 for filepath in self._iter_expand_paths(paths): 

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

757 # END for each filepath 

758 # END path handling 

759 return entries_added 

760 

761 def add( 

762 self, 

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

764 force: bool = True, 

765 fprogress: Callable = lambda *args: None, 

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

767 write: bool = True, 

768 write_extension_data: bool = False, 

769 ) -> List[BaseIndexEntry]: 

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

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

772 

773 :param items: 

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

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

776 relative or absolute. 

777 

778 - path string 

779 

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

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

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

783 

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

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

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

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

788 

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

790 into the object database. 

791 

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

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

794 directory and subdirectories. 

795 

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

797 

798 They are added at stage 0. 

799 

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

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

802 

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

804 

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

806 be a path relative to our repository. 

807 

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

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

810 data at the path. 

811 

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

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

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

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

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

817 created on filesystems not supporting it as well. 

818 

819 Please note that globs or directories are not allowed in 

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

821 

822 They are added at stage 0. 

823 

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

825 

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

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

828 binary sha's. 

829 

830 :param force: 

831 **CURRENTLY INEFFECTIVE** 

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

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

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

835 excluded. 

836 

837 :param fprogress: 

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

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

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

841 

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

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

844 

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

846 index already as the index is currently being processed. 

847 

848 :param path_rewriter: 

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

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

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

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

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

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

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

856 repository. 

857 

858 :param write: 

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

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

861 

862 :param write_extension_data: 

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

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

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

866 one representing the now changed index. 

867 

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

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

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

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

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

873 handled manually at all. 

874 

875 All current built-in extensions are listed here: 

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

877 

878 :return: 

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

880 just actually added. 

881 

882 :raise OSError: 

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

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

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

886 """ 

887 # Sort the entries into strings and Entries. 

888 # Blobs are converted to entries automatically. 

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

890 paths, entries = self._preprocess_add_items(items) 

891 entries_added: List[BaseIndexEntry] = [] 

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

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

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

895 if paths: 

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

897 

898 # HANDLE ENTRIES 

899 if entries: 

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

901 if null_mode_entries: 

902 raise ValueError( 

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

904 ) 

905 # END null mode should be remove 

906 

907 # HANDLE ENTRY OBJECT CREATION 

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

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

910 if null_entries_indices: 

911 

912 @git_working_dir 

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

914 for ei in null_entries_indices: 

915 null_entry = entries[ei] 

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

917 

918 # Update null entry. 

919 entries[ei] = BaseIndexEntry( 

920 ( 

921 null_entry.mode, 

922 new_entry.binsha, 

923 null_entry.stage, 

924 null_entry.path, 

925 ) 

926 ) 

927 # END for each entry index 

928 

929 # END closure 

930 

931 handle_null_entries(self) 

932 # END null_entry handling 

933 

934 # REWRITE PATHS 

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

936 # object sha's. 

937 if path_rewriter: 

938 for i, e in enumerate(entries): 

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

940 # END for each entry 

941 # END handle path rewriting 

942 

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

944 for i, entry in enumerate(entries): 

945 progress_sent = i in null_entries_indices 

946 if not progress_sent: 

947 fprogress(entry.path, False, entry) 

948 fprogress(entry.path, True, entry) 

949 # END handle progress 

950 # END for each entry 

951 entries_added.extend(entries) 

952 # END if there are base entries 

953 

954 # FINALIZE 

955 # Add the new entries to this instance. 

956 for entry in entries_added: 

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

958 

959 if write: 

960 self.write(ignore_extension_data=not write_extension_data) 

961 # END handle write 

962 

963 return entries_added 

964 

965 def _items_to_rela_paths( 

966 self, 

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

968 ) -> List[PathLike]: 

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

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

971 paths = [] 

972 # If string, put in list. 

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

974 items = [items] 

975 

976 for item in items: 

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

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

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

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

981 else: 

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

983 # END for each item 

984 return paths 

985 

986 @post_clear_cache 

987 @default_index 

988 def remove( 

989 self, 

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

991 working_tree: bool = False, 

992 **kwargs: Any, 

993 ) -> List[str]: 

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

995 as well. 

996 

997 :param items: 

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

999 

1000 - path string 

1001 

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

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

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

1005 relative to the git repository directory containing the working tree 

1006 

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

1008 

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

1010 

1011 Only the path portion is used in this case. 

1012 

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

1014 

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

1016 

1017 :param working_tree: 

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

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

1020 uncommitted changes in it. 

1021 

1022 :param kwargs: 

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

1024 ``r`` to allow recursive removal. 

1025 

1026 :return: 

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

1028 removed effectively. 

1029 

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

1031 Paths are relative to the repository. 

1032 """ 

1033 args = [] 

1034 if not working_tree: 

1035 args.append("--cached") 

1036 args.append("--") 

1037 

1038 # Preprocess paths. 

1039 paths = self._items_to_rela_paths(items) 

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

1041 

1042 # Process output to gain proper paths. 

1043 # rm 'path' 

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

1045 

1046 @post_clear_cache 

1047 @default_index 

1048 def move( 

1049 self, 

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

1051 skip_errors: bool = False, 

1052 **kwargs: Any, 

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

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

1055 the move operation. 

1056 

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

1058 

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

1060 or files. 

1061 

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

1063 

1064 :param items: 

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

1066 for reference. 

1067 

1068 :param skip_errors: 

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

1070 skipped. 

1071 

1072 :param kwargs: 

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

1074 ``dry_run`` or ``force``. 

1075 

1076 :return: 

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

1078 

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

1080 destination. Relative to the repository root. 

1081 

1082 :raise ValueError: 

1083 If only one item was given. 

1084 

1085 :raise git.exc.GitCommandError: 

1086 If git could not handle your request. 

1087 """ 

1088 args = [] 

1089 if skip_errors: 

1090 args.append("-k") 

1091 

1092 paths = self._items_to_rela_paths(items) 

1093 if len(paths) < 2: 

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

1095 

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

1097 kwargs["dry_run"] = True 

1098 

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

1100 # (for later output). 

1101 out = [] 

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

1103 

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

1105 # 'renaming' ones which we parse. 

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

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

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

1109 

1110 # [0] = Renaming x 

1111 # [1] = y 

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

1113 # END for each line to parse 

1114 

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

1116 if was_dry_run: 

1117 return out 

1118 # END handle dry run 

1119 

1120 # Now apply the actual operation. 

1121 kwargs.pop("dry_run") 

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

1123 

1124 return out 

1125 

1126 def commit( 

1127 self, 

1128 message: str, 

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

1130 head: bool = True, 

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

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

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

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

1135 skip_hooks: bool = False, 

1136 ) -> Commit: 

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

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

1139 

1140 For more information on the arguments, see 

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

1142 

1143 :note: 

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

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

1146 

1147 :note: 

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

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

1150 

1151 :return: 

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

1153 """ 

1154 if not skip_hooks: 

1155 run_commit_hook("pre-commit", self) 

1156 

1157 self._write_commit_editmsg(message) 

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

1159 message = self._read_commit_editmsg() 

1160 self._remove_commit_editmsg() 

1161 tree = self.write_tree() 

1162 rval = Commit.create_from_tree( 

1163 self.repo, 

1164 tree, 

1165 message, 

1166 parent_commits, 

1167 head, 

1168 author=author, 

1169 committer=committer, 

1170 author_date=author_date, 

1171 commit_date=commit_date, 

1172 ) 

1173 if not skip_hooks: 

1174 run_commit_hook("post-commit", self) 

1175 return rval 

1176 

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

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

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

1180 

1181 def _remove_commit_editmsg(self) -> None: 

1182 os.remove(self._commit_editmsg_filepath()) 

1183 

1184 def _read_commit_editmsg(self) -> str: 

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

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

1187 

1188 def _commit_editmsg_filepath(self) -> str: 

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

1190 

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

1192 stdin_IO = proc.stdin 

1193 if stdin_IO: 

1194 stdin_IO.flush() 

1195 stdin_IO.close() 

1196 

1197 stdout = b"" 

1198 if not ignore_stdout and proc.stdout: 

1199 stdout = proc.stdout.read() 

1200 

1201 if proc.stdout: 

1202 proc.stdout.close() 

1203 proc.wait() 

1204 return stdout 

1205 

1206 @default_index 

1207 def checkout( 

1208 self, 

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

1210 force: bool = False, 

1211 fprogress: Callable = lambda *args: None, 

1212 **kwargs: Any, 

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

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

1215 into the working tree. 

1216 

1217 :note: 

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

1219 case you have altered the entries dictionary directly. 

1220 

1221 :param paths: 

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

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

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

1225 

1226 :param force: 

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

1228 modifications. 

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

1230 

1231 :param fprogress: 

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

1233 

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

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

1236 prior and after a file has been checked out. 

1237 

1238 :param kwargs: 

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

1240 

1241 :return: 

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

1243 guaranteed to match the version stored in the index. 

1244 

1245 :raise git.exc.CheckoutError: 

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

1247 will checkout as many files as it can anyway. 

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

1249 the original git command, which ignores them). 

1250 

1251 :raise git.exc.GitCommandError: 

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

1253 

1254 :note: 

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

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

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

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

1259 ``head.checkout`` instead of ``index.checkout``. 

1260 """ 

1261 args = ["--index"] 

1262 if force: 

1263 args.append("--force") 

1264 

1265 failed_files = [] 

1266 failed_reasons = [] 

1267 unknown_lines = [] 

1268 

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

1270 stderr_IO = proc.stderr 

1271 if not stderr_IO: 

1272 return # Return early if stderr empty. 

1273 

1274 stderr_bytes = stderr_IO.read() 

1275 # line contents: 

1276 stderr = stderr_bytes.decode(defenc) 

1277 # git-checkout-index: this already exists 

1278 endings = ( 

1279 " already exists", 

1280 " is not in the cache", 

1281 " does not exist at stage", 

1282 " is unmerged", 

1283 ) 

1284 for line in stderr.splitlines(): 

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

1286 is_a_dir = " is a directory" 

1287 unlink_issue = "unable to unlink old '" 

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

1289 if line.endswith(is_a_dir): 

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

1291 failed_reasons.append(is_a_dir) 

1292 elif line.startswith(unlink_issue): 

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

1294 failed_reasons.append(unlink_issue) 

1295 elif line.endswith(already_exists_issue): 

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

1297 failed_reasons.append(already_exists_issue) 

1298 else: 

1299 unknown_lines.append(line) 

1300 continue 

1301 # END special lines parsing 

1302 

1303 for e in endings: 

1304 if line.endswith(e): 

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

1306 failed_reasons.append(e) 

1307 break 

1308 # END if ending matches 

1309 # END for each possible ending 

1310 # END for each line 

1311 if unknown_lines: 

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

1313 if failed_files: 

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

1315 raise CheckoutError( 

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

1317 failed_files, 

1318 valid_files, 

1319 failed_reasons, 

1320 ) 

1321 

1322 # END stderr handler 

1323 

1324 if paths is None: 

1325 args.append("--all") 

1326 kwargs["as_process"] = 1 

1327 fprogress(None, False, None) 

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

1329 proc.wait() 

1330 fprogress(None, True, None) 

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

1332 handle_stderr(proc, rval_iter) 

1333 return rval_iter 

1334 else: 

1335 if isinstance(paths, str): 

1336 paths = [paths] 

1337 

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

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

1340 # initialization. 

1341 self.entries # noqa: B018 

1342 

1343 args.append("--stdin") 

1344 kwargs["as_process"] = True 

1345 kwargs["istream"] = subprocess.PIPE 

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

1347 

1348 # FIXME: Reading from GIL! 

1349 def make_exc() -> GitCommandError: 

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

1351 

1352 checked_out_files: List[PathLike] = [] 

1353 

1354 for path in paths: 

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

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

1357 path_is_directory = False 

1358 

1359 try: 

1360 self.entries[(co_path, 0)] 

1361 except KeyError: 

1362 folder = str(co_path) 

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

1364 folder += "/" 

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

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

1367 p = entry.path 

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

1369 checked_out_files.append(p) 

1370 path_is_directory = True 

1371 # END if entry is in directory 

1372 # END for each entry 

1373 # END path exception handlnig 

1374 

1375 if not path_is_directory: 

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

1377 checked_out_files.append(co_path) 

1378 # END path is a file 

1379 # END for each path 

1380 try: 

1381 self._flush_stdin_and_wait(proc, ignore_stdout=True) 

1382 except GitCommandError: 

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

1384 raise CheckoutError( # noqa: B904 

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

1386 failed_files, 

1387 [], 

1388 failed_reasons, 

1389 ) 

1390 

1391 handle_stderr(proc, checked_out_files) 

1392 return checked_out_files 

1393 # END paths handling 

1394 

1395 @default_index 

1396 def reset( 

1397 self, 

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

1399 working_tree: bool = False, 

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

1401 head: bool = False, 

1402 **kwargs: Any, 

1403 ) -> "IndexFile": 

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

1405 our HEAD reference by default, as opposed to 

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

1407 

1408 :param commit: 

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

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

1411 represent. 

1412 

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

1414 overwrite the default index. 

1415 

1416 :param working_tree: 

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

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

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

1420 warning! 

1421 

1422 :param head: 

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

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

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

1426 

1427 :param paths: 

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

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

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

1431 raised. 

1432 

1433 :param kwargs: 

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

1435 

1436 :note: 

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

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

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

1440 the files according to their state in the index. 

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

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

1443 

1444 :return: 

1445 self 

1446 """ 

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

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

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

1450 if not paths: 

1451 self.entries = new_inst.entries 

1452 else: 

1453 nie = new_inst.entries 

1454 for path in paths: 

1455 path = self._to_relative_path(path) 

1456 try: 

1457 key = entry_key(path, 0) 

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

1459 except KeyError: 

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

1461 try: 

1462 del self.entries[key] 

1463 except KeyError: 

1464 pass 

1465 # END handle deletion keyerror 

1466 # END handle keyerror 

1467 # END for each path 

1468 # END handle paths 

1469 self.write() 

1470 

1471 if working_tree: 

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

1473 # END handle working tree 

1474 

1475 if head: 

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

1477 # END handle head change 

1478 

1479 return self 

1480 

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

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

1483 def diff( 

1484 self, 

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

1486 Literal[git_diff.DiffConstants.INDEX], 

1487 "Tree", 

1488 "Commit", 

1489 str, 

1490 None, 

1491 ] = git_diff.INDEX, 

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

1493 create_patch: bool = False, 

1494 **kwargs: Any, 

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

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

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

1498 

1499 For documentation of the parameters and return values, see 

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

1501 

1502 :note: 

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

1504 have not been initialized with a stream. 

1505 """ 

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

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

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

1509 # Index against index is always empty. 

1510 if other is self.INDEX: 

1511 return git_diff.DiffIndex() 

1512 

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

1514 # Handle existing -R flags properly. 

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

1516 if isinstance(other, str): 

1517 other = self.repo.rev_parse(other) 

1518 # END object conversion 

1519 

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

1521 # Invert the existing R flag. 

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

1523 kwargs["R"] = not cur_val 

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

1525 # END diff against other item handling 

1526 

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

1528 if other is not None: 

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

1530 

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

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