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

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

533 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 to_native_path_linux, 

38 unbare_repo, 

39 to_bin_sha, 

40) 

41 

42from .fun import ( 

43 S_IFGITLINK, 

44 aggressive_tree_merge, 

45 entry_key, 

46 read_cache, 

47 run_commit_hook, 

48 stat_mode_to_index_mode, 

49 write_cache, 

50 write_tree_from_cache, 

51) 

52from .typ import BaseIndexEntry, IndexEntry, StageType 

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

54 

55# typing ----------------------------------------------------------------------------- 

56 

57from typing import ( 

58 Any, 

59 BinaryIO, 

60 Callable, 

61 Dict, 

62 Generator, 

63 IO, 

64 Iterable, 

65 Iterator, 

66 List, 

67 NoReturn, 

68 Sequence, 

69 TYPE_CHECKING, 

70 Tuple, 

71 Union, 

72) 

73 

74from git.types import Literal, PathLike 

75 

76if TYPE_CHECKING: 

77 from subprocess import Popen 

78 

79 from git.refs.reference import Reference 

80 from git.repo import Repo 

81 

82 

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

84 

85# ------------------------------------------------------------------------------------ 

86 

87 

88@contextlib.contextmanager 

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

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

91 

92 :param directory: 

93 The directory in which the file is created. 

94 

95 :return: 

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

97 and deletes it on exit. 

98 """ 

99 if sys.platform == "win32": 

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

101 os.close(fd) 

102 try: 

103 yield name 

104 finally: 

105 os.remove(name) 

106 else: 

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

108 yield ctx.name 

109 

110 

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

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

113 git command function calls wherever possible. 

114 

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

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

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

118 case of partial working trees. 

119 

120 Entries: 

121 

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

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

124 

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

126 

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

128 

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

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

131 """ 

132 

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

134 

135 _VERSION = 2 

136 """The latest version we support.""" 

137 

138 S_IFGITLINK = S_IFGITLINK 

139 """Flags for a submodule.""" 

140 

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

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

143 

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

145 

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

147 repository's index on demand. 

148 """ 

149 self.repo = repo 

150 self.version = self._VERSION 

151 self._extension_data = b"" 

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

153 

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

155 if attr == "entries": 

156 try: 

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

158 except OSError: 

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

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

161 return 

162 # END exception handling 

163 

164 try: 

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

166 finally: 

167 os.close(fd) 

168 

169 self._deserialize(stream) 

170 else: 

171 super()._set_cache_(attr) 

172 

173 def _index_path(self) -> PathLike: 

174 if self.repo.git_dir: 

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

176 else: 

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

178 

179 @property 

180 def path(self) -> PathLike: 

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

182 return self._file_path 

183 

184 def _delete_entries_cache(self) -> None: 

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

186 try: 

187 del self.entries 

188 except AttributeError: 

189 # It failed in Python 2.6.5 with AttributeError. 

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

191 pass 

192 # END exception handling 

193 

194 # { Serializable Interface 

195 

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

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

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

199 return self 

200 

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

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

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

204 

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

206 entries = self._entries_sorted() 

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

208 if ignore_extension_data: 

209 extension_data = None 

210 write_cache(entries, stream, extension_data) 

211 return self 

212 

213 # } END serializable interface 

214 

215 def write( 

216 self, 

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

218 ignore_extension_data: bool = False, 

219 ) -> None: 

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

221 

222 :param file_path: 

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

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

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

226 

227 :param ignore_extension_data: 

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

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

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

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

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

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

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

235 """ 

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

237 # Otherwise it would be done when streaming. 

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

239 self.entries # noqa: B018 

240 lfd = LockedFD(file_path or self._file_path) 

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

242 

243 try: 

244 self._serialize(stream, ignore_extension_data) 

245 except BaseException: 

246 lfd.rollback() 

247 raise 

248 

249 lfd.commit() 

250 

251 # Make sure we represent what we have written. 

252 if file_path is not None: 

253 self._file_path = file_path 

254 

255 @post_clear_cache 

256 @default_index 

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

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

259 a common base treeish into account. 

260 

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

262 existing tree as the left side of the merge. 

263 

264 :param rhs: 

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

266 

267 :param base: 

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

269 index which equals lhs. 

270 

271 :return: 

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

273 conflicts) 

274 

275 :raise git.exc.GitCommandError: 

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

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

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

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

280 """ 

281 # -i : ignore working tree status 

282 # --aggressive : handle more merge cases 

283 # -m : do an actual merge 

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

285 if base is not None: 

286 args.append(base) 

287 args.append(rhs) 

288 

289 self.repo.git.read_tree(args) 

290 return self 

291 

292 @classmethod 

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

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

295 

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

297 

298 :param repo: 

299 The repository treeish are located in. 

300 

301 :param tree_sha: 

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

303 

304 :return: 

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

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

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

308 """ 

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

310 base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes) 

311 

312 inst = cls(repo) 

313 # Convert to entries dict. 

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

315 zip( 

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

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

318 ) 

319 ) 

320 

321 inst.entries = entries 

322 return inst 

323 

324 @classmethod 

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

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

327 The original index will remain unaltered. 

328 

329 :param repo: 

330 The repository treeish are located in. 

331 

332 :param treeish: 

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

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

335 

336 The result changes according to the amount of trees: 

337 

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

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

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

341 one. It behaves like a fast-forward. 

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

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

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

345 

346 :param kwargs: 

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

348 

349 :return: 

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

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

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

353 

354 :note: 

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

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

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

358 

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

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

361 unexpected interference. 

362 """ 

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

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

365 

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

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

368 if len(treeish) > 1: 

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

370 arg_list.append("--reset") 

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

372 arg_list.append("--aggressive") 

373 # END merge handling 

374 

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

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

377 with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index: 

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

379 arg_list.extend(treeish) 

380 

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

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

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

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

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

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

387 index = cls(repo, tmp_index) 

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

389 return index 

390 # END index merge handling 

391 

392 # UTILITIES 

393 

394 @unbare_repo 

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

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

397 accordingly. 

398 

399 :note: 

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

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

402 and do not prune. 

403 """ 

404 

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

406 raise e 

407 

408 r = str(self.repo.working_tree_dir) 

409 rs = r + os.sep 

410 for path in paths: 

411 abs_path = os.fspath(path) 

412 if not osp.isabs(abs_path): 

413 abs_path = osp.join(r, path) 

414 # END make absolute path 

415 

416 try: 

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

418 except OSError: 

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

420 pass 

421 else: 

422 if S_ISLNK(st.st_mode): 

423 yield abs_path.replace(rs, "") 

424 continue 

425 # END check symlink 

426 

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

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

429 resolved_paths = glob.glob(abs_path) 

430 # not abs_path in resolved_paths: 

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

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

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

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

435 # characters. 

436 if abs_path not in resolved_paths: 

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

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

439 continue 

440 # END glob handling 

441 try: 

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

443 for rela_file in files: 

444 # Add relative paths only. 

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

446 # END for each file in subdir 

447 # END for each subdirectory 

448 except OSError: 

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

450 yield abs_path.replace(rs, "") 

451 # END path exception handling 

452 # END for each path 

453 

454 def _write_path_to_stdin( 

455 self, 

456 proc: "Popen", 

457 filepath: PathLike, 

458 item: PathLike, 

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

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

461 read_from_stdout: bool = True, 

462 ) -> Union[None, str]: 

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

464 progress. 

465 

466 :return: 

467 stdout string 

468 

469 :param read_from_stdout: 

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

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

472 

473 :note: 

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

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

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

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

478 

479 :note: 

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

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

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

483 stdin to break the pipe. 

484 """ 

485 fprogress(filepath, False, item) 

486 rval: Union[None, str] = None 

487 

488 if proc.stdin is not None: 

489 try: 

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

491 except IOError as e: 

492 # Pipe broke, usually because some error happened. 

493 raise fmakeexc() from e 

494 # END write exception handling 

495 proc.stdin.flush() 

496 

497 if read_from_stdout and proc.stdout is not None: 

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

499 fprogress(filepath, True, item) 

500 return rval 

501 

502 def iter_blobs( 

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

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

505 """ 

506 :return: 

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

508 stages, tuple(stage, Blob). 

509 

510 :param predicate: 

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

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

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

514 """ 

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

516 blob = entry.to_blob(self.repo) 

517 blob.size = entry.size 

518 output = (entry.stage, blob) 

519 if predicate(output): 

520 yield output 

521 # END for each entry 

522 

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

524 """ 

525 :return: 

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

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

528 

529 :note: 

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

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

532 stage 3 will not have a stage 3 entry. 

533 """ 

534 

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

536 return t[0] != 0 

537 

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

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

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

541 # END for each unmerged blob 

542 for line in path_map.values(): 

543 line.sort() 

544 

545 return path_map 

546 

547 @classmethod 

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

549 return entry_key(*entry) 

550 

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

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

553 

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

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

556 

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

558 raised claiming the path is already at stage 0. 

559 

560 :raise ValueError: 

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

562 

563 :return: 

564 self 

565 

566 :note: 

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

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

569 """ 

570 for blob in iter_blobs: 

571 stage_null_key = (blob.path, 0) 

572 if stage_null_key in self.entries: 

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

574 # END assert blob is not stage 0 already 

575 

576 # Delete all possible stages. 

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

578 try: 

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

580 except KeyError: 

581 pass 

582 # END ignore key errors 

583 # END for each possible stage 

584 

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

586 # END for each blob 

587 

588 return self 

589 

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

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

592 we might have. 

593 

594 :note: 

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

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

597 

598 :return: 

599 self 

600 """ 

601 self._delete_entries_cache() 

602 # Allows to lazily reread on demand. 

603 return self 

604 

605 def write_tree(self) -> Tree: 

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

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

608 

609 :return: 

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

611 

612 :note: 

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

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

615 to the index directly. 

616 

617 :raise ValueError: 

618 If there are no entries in the cache. 

619 

620 :raise git.exc.UnmergedEntriesError: 

621 """ 

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

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

624 mdb = MemoryDB() 

625 entries = self._entries_sorted() 

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

627 

628 # Copy changed trees only. 

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

630 

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

632 # return sorted tree entries. 

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

634 root_tree._cache = tree_items 

635 return root_tree 

636 

637 def _process_diff_args( 

638 self, 

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

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

641 try: 

642 args.pop(args.index(self)) 

643 except IndexError: 

644 pass 

645 # END remove self 

646 return args 

647 

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

649 """ 

650 :return: 

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

652 it is not within our git directory. 

653 

654 :raise ValueError: 

655 """ 

656 if not osp.isabs(path): 

657 return path 

658 if self.repo.bare: 

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

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

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

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

663 if os.fspath(path).endswith(os.sep) and not result.endswith(os.sep): 

664 result += os.sep 

665 return result 

666 

667 def _preprocess_add_items( 

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

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

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

671 paths = [] 

672 entries = [] 

673 # if it is a string put in list 

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

675 items = [items] 

676 

677 for item in items: 

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

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

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

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

682 elif isinstance(item, BaseIndexEntry): 

683 entries.append(item) 

684 else: 

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

686 # END for each item 

687 return paths, entries 

688 

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

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

691 

692 :note: 

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

694 This must be ensured in the calling code. 

695 """ 

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

697 

698 if S_ISLNK(st.st_mode): 

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

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

701 def open_stream() -> BinaryIO: 

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

703 else: 

704 

705 def open_stream() -> BinaryIO: 

706 return open(filepath, "rb") 

707 

708 with open_stream() as stream: 

709 fprogress(filepath, False, filepath) 

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

711 fprogress(filepath, True, filepath) 

712 return BaseIndexEntry( 

713 ( 

714 stat_mode_to_index_mode(st.st_mode), 

715 istream.binsha, 

716 0, 

717 to_native_path_linux(filepath), 

718 ) 

719 ) 

720 

721 @unbare_repo 

722 @git_working_dir 

723 def _entries_for_paths( 

724 self, 

725 paths: List[str], 

726 path_rewriter: Union[Callable, None], 

727 fprogress: Callable, 

728 entries: List[BaseIndexEntry], 

729 ) -> List[BaseIndexEntry]: 

730 entries_added: List[BaseIndexEntry] = [] 

731 if path_rewriter: 

732 for path in paths: 

733 if osp.isabs(path): 

734 abspath = path 

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

736 else: 

737 gitrelative_path = path 

738 if self.repo.working_tree_dir: 

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

740 # END obtain relative and absolute paths 

741 

742 blob = Blob( 

743 self.repo, 

744 Blob.NULL_BIN_SHA, 

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

746 to_native_path_linux(gitrelative_path), 

747 ) 

748 # TODO: variable undefined 

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

750 # END for each path 

751 del paths[:] 

752 # END rewrite paths 

753 

754 # HANDLE PATHS 

755 assert len(entries_added) == 0 

756 for filepath in self._iter_expand_paths(paths): 

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

758 # END for each filepath 

759 # END path handling 

760 return entries_added 

761 

762 def add( 

763 self, 

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

765 force: bool = True, 

766 fprogress: Callable = lambda *args: None, 

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

768 write: bool = True, 

769 write_extension_data: bool = False, 

770 ) -> List[BaseIndexEntry]: 

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

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

773 

774 :param items: 

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

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

777 relative or absolute. 

778 

779 - path string 

780 

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

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

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

784 

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

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

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

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

789 

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

791 into the object database. 

792 

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

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

795 directory and subdirectories. 

796 

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

798 

799 They are added at stage 0. 

800 

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

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

803 

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

805 

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

807 be a path relative to our repository. 

808 

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

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

811 data at the path. 

812 

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

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

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

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

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

818 created on filesystems not supporting it as well. 

819 

820 Please note that globs or directories are not allowed in 

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

822 

823 They are added at stage 0. 

824 

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

826 

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

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

829 binary sha's. 

830 

831 :param force: 

832 **CURRENTLY INEFFECTIVE** 

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

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

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

836 excluded. 

837 

838 :param fprogress: 

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

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

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

842 

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

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

845 

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

847 index already as the index is currently being processed. 

848 

849 :param path_rewriter: 

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

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

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

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

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

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

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

857 repository. 

858 

859 :param write: 

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

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

862 

863 :param write_extension_data: 

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

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

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

867 one representing the now changed index. 

868 

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

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

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

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

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

874 handled manually at all. 

875 

876 All current built-in extensions are listed here: 

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

878 

879 :return: 

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

881 just actually added. 

882 

883 :raise OSError: 

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

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

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

887 """ 

888 # Sort the entries into strings and Entries. 

889 # Blobs are converted to entries automatically. 

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

891 paths, entries = self._preprocess_add_items(items) 

892 entries_added: List[BaseIndexEntry] = [] 

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

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

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

896 if paths: 

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

898 

899 # HANDLE ENTRIES 

900 if entries: 

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

902 if null_mode_entries: 

903 raise ValueError( 

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

905 ) 

906 # END null mode should be remove 

907 

908 # HANDLE ENTRY OBJECT CREATION 

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

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

911 if null_entries_indices: 

912 

913 @git_working_dir 

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

915 for ei in null_entries_indices: 

916 null_entry = entries[ei] 

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

918 

919 # Update null entry. 

920 entries[ei] = BaseIndexEntry( 

921 ( 

922 null_entry.mode, 

923 new_entry.binsha, 

924 null_entry.stage, 

925 null_entry.path, 

926 ) 

927 ) 

928 # END for each entry index 

929 

930 # END closure 

931 

932 handle_null_entries(self) 

933 # END null_entry handling 

934 

935 # REWRITE PATHS 

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

937 # object sha's. 

938 if path_rewriter: 

939 for i, e in enumerate(entries): 

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

941 # END for each entry 

942 # END handle path rewriting 

943 

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

945 for i, entry in enumerate(entries): 

946 progress_sent = i in null_entries_indices 

947 if not progress_sent: 

948 fprogress(entry.path, False, entry) 

949 fprogress(entry.path, True, entry) 

950 # END handle progress 

951 # END for each entry 

952 entries_added.extend(entries) 

953 # END if there are base entries 

954 

955 # FINALIZE 

956 # Add the new entries to this instance. 

957 for entry in entries_added: 

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

959 

960 if write: 

961 self.write(ignore_extension_data=not write_extension_data) 

962 # END handle write 

963 

964 return entries_added 

965 

966 def _items_to_rela_paths( 

967 self, 

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

969 ) -> List[PathLike]: 

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

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

972 paths = [] 

973 # If string, put in list. 

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

975 items = [items] 

976 

977 for item in items: 

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

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

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

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

982 else: 

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

984 # END for each item 

985 return paths 

986 

987 @post_clear_cache 

988 @default_index 

989 def remove( 

990 self, 

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

992 working_tree: bool = False, 

993 **kwargs: Any, 

994 ) -> List[str]: 

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

996 as well. 

997 

998 :param items: 

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

1000 

1001 - path string 

1002 

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

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

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

1006 relative to the git repository directory containing the working tree 

1007 

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

1009 

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

1011 

1012 Only the path portion is used in this case. 

1013 

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

1015 

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

1017 

1018 :param working_tree: 

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

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

1021 uncommitted changes in it. 

1022 

1023 :param kwargs: 

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

1025 ``r`` to allow recursive removal. 

1026 

1027 :return: 

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

1029 removed effectively. 

1030 

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

1032 Paths are relative to the repository. 

1033 """ 

1034 args = [] 

1035 if not working_tree: 

1036 args.append("--cached") 

1037 args.append("--") 

1038 

1039 # Preprocess paths. 

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

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

1042 

1043 # Process output to gain proper paths. 

1044 # rm 'path' 

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

1046 

1047 @post_clear_cache 

1048 @default_index 

1049 def move( 

1050 self, 

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

1052 skip_errors: bool = False, 

1053 **kwargs: Any, 

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

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

1056 the move operation. 

1057 

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

1059 

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

1061 or files. 

1062 

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

1064 

1065 :param items: 

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

1067 for reference. 

1068 

1069 :param skip_errors: 

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

1071 skipped. 

1072 

1073 :param kwargs: 

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

1075 ``dry_run`` or ``force``. 

1076 

1077 :return: 

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

1079 

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

1081 destination. Relative to the repository root. 

1082 

1083 :raise ValueError: 

1084 If only one item was given. 

1085 

1086 :raise git.exc.GitCommandError: 

1087 If git could not handle your request. 

1088 """ 

1089 args = [] 

1090 if skip_errors: 

1091 args.append("-k") 

1092 

1093 paths = self._items_to_rela_paths(items) 

1094 if len(paths) < 2: 

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

1096 

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

1098 kwargs["dry_run"] = True 

1099 

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

1101 # (for later output). 

1102 out = [] 

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

1104 

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

1106 # 'renaming' ones which we parse. 

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

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

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

1110 

1111 # [0] = Renaming x 

1112 # [1] = y 

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

1114 # END for each line to parse 

1115 

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

1117 if was_dry_run: 

1118 return out 

1119 # END handle dry run 

1120 

1121 # Now apply the actual operation. 

1122 kwargs.pop("dry_run") 

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

1124 

1125 return out 

1126 

1127 def commit( 

1128 self, 

1129 message: str, 

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

1131 head: bool = True, 

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

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

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

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

1136 skip_hooks: bool = False, 

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

1138 ) -> Commit: 

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

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

1141 

1142 For more information on the arguments, see 

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

1144 

1145 :note: 

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

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

1148 

1149 :note: 

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

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

1152 

1153 :return: 

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

1155 """ 

1156 if not skip_hooks: 

1157 run_commit_hook("pre-commit", self) 

1158 

1159 self._write_commit_editmsg(message) 

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

1161 message = self._read_commit_editmsg() 

1162 self._remove_commit_editmsg() 

1163 tree = self.write_tree() 

1164 rval = Commit.create_from_tree( 

1165 self.repo, 

1166 tree, 

1167 message, 

1168 parent_commits, 

1169 head, 

1170 author=author, 

1171 committer=committer, 

1172 author_date=author_date, 

1173 commit_date=commit_date, 

1174 trailers=trailers, 

1175 ) 

1176 if not skip_hooks: 

1177 run_commit_hook("post-commit", self) 

1178 return rval 

1179 

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

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

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

1183 

1184 def _remove_commit_editmsg(self) -> None: 

1185 os.remove(self._commit_editmsg_filepath()) 

1186 

1187 def _read_commit_editmsg(self) -> str: 

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

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

1190 

1191 def _commit_editmsg_filepath(self) -> str: 

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

1193 

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

1195 stdin_IO = proc.stdin 

1196 if stdin_IO: 

1197 stdin_IO.flush() 

1198 stdin_IO.close() 

1199 

1200 stdout = b"" 

1201 if not ignore_stdout and proc.stdout: 

1202 stdout = proc.stdout.read() 

1203 

1204 if proc.stdout: 

1205 proc.stdout.close() 

1206 proc.wait() 

1207 return stdout 

1208 

1209 @default_index 

1210 def checkout( 

1211 self, 

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

1213 force: bool = False, 

1214 fprogress: Callable = lambda *args: None, 

1215 **kwargs: Any, 

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

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

1218 into the working tree. 

1219 

1220 :note: 

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

1222 case you have altered the entries dictionary directly. 

1223 

1224 :param paths: 

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

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

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

1228 

1229 :param force: 

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

1231 modifications. 

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

1233 

1234 :param fprogress: 

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

1236 

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

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

1239 prior and after a file has been checked out. 

1240 

1241 :param kwargs: 

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

1243 

1244 :return: 

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

1246 guaranteed to match the version stored in the index. 

1247 

1248 :raise git.exc.CheckoutError: 

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

1250 will checkout as many files as it can anyway. 

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

1252 the original git command, which ignores them). 

1253 

1254 :raise git.exc.GitCommandError: 

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

1256 

1257 :note: 

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

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

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

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

1262 ``head.checkout`` instead of ``index.checkout``. 

1263 """ 

1264 args = ["--index"] 

1265 if force: 

1266 args.append("--force") 

1267 

1268 failed_files = [] 

1269 failed_reasons = [] 

1270 unknown_lines = [] 

1271 

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

1273 stderr_IO = proc.stderr 

1274 if not stderr_IO: 

1275 return # Return early if stderr empty. 

1276 

1277 stderr_bytes = stderr_IO.read() 

1278 # line contents: 

1279 stderr = stderr_bytes.decode(defenc) 

1280 # git-checkout-index: this already exists 

1281 endings = ( 

1282 " already exists", 

1283 " is not in the cache", 

1284 " does not exist at stage", 

1285 " is unmerged", 

1286 ) 

1287 for line in stderr.splitlines(): 

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

1289 is_a_dir = " is a directory" 

1290 unlink_issue = "unable to unlink old '" 

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

1292 if line.endswith(is_a_dir): 

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

1294 failed_reasons.append(is_a_dir) 

1295 elif line.startswith(unlink_issue): 

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

1297 failed_reasons.append(unlink_issue) 

1298 elif line.endswith(already_exists_issue): 

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

1300 failed_reasons.append(already_exists_issue) 

1301 else: 

1302 unknown_lines.append(line) 

1303 continue 

1304 # END special lines parsing 

1305 

1306 for e in endings: 

1307 if line.endswith(e): 

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

1309 failed_reasons.append(e) 

1310 break 

1311 # END if ending matches 

1312 # END for each possible ending 

1313 # END for each line 

1314 if unknown_lines: 

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

1316 if failed_files: 

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

1318 raise CheckoutError( 

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

1320 failed_files, 

1321 valid_files, 

1322 failed_reasons, 

1323 ) 

1324 

1325 # END stderr handler 

1326 

1327 if paths is None: 

1328 args.append("--all") 

1329 kwargs["as_process"] = 1 

1330 fprogress(None, False, None) 

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

1332 proc.wait() 

1333 fprogress(None, True, None) 

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

1335 handle_stderr(proc, rval_iter) 

1336 return rval_iter 

1337 else: 

1338 if isinstance(paths, str): 

1339 paths = [paths] 

1340 

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

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

1343 # initialization. 

1344 self.entries # noqa: B018 

1345 

1346 args.append("--stdin") 

1347 kwargs["as_process"] = True 

1348 kwargs["istream"] = subprocess.PIPE 

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

1350 

1351 # FIXME: Reading from GIL! 

1352 def make_exc() -> GitCommandError: 

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

1354 

1355 checked_out_files: List[PathLike] = [] 

1356 

1357 for path in paths: 

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

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

1360 path_is_directory = False 

1361 

1362 try: 

1363 self.entries[(co_path, 0)] 

1364 except KeyError: 

1365 folder = co_path 

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

1367 folder += "/" 

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

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

1370 p = entry.path 

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

1372 checked_out_files.append(p) 

1373 path_is_directory = True 

1374 # END if entry is in directory 

1375 # END for each entry 

1376 # END path exception handlnig 

1377 

1378 if not path_is_directory: 

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

1380 checked_out_files.append(co_path) 

1381 # END path is a file 

1382 # END for each path 

1383 try: 

1384 self._flush_stdin_and_wait(proc, ignore_stdout=True) 

1385 except GitCommandError: 

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

1387 raise CheckoutError( # noqa: B904 

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

1389 failed_files, 

1390 [], 

1391 failed_reasons, 

1392 ) 

1393 

1394 handle_stderr(proc, checked_out_files) 

1395 return checked_out_files 

1396 # END paths handling 

1397 

1398 @default_index 

1399 def reset( 

1400 self, 

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

1402 working_tree: bool = False, 

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

1404 head: bool = False, 

1405 **kwargs: Any, 

1406 ) -> "IndexFile": 

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

1408 our HEAD reference by default, as opposed to 

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

1410 

1411 :param commit: 

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

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

1414 represent. 

1415 

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

1417 overwrite the default index. 

1418 

1419 :param working_tree: 

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

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

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

1423 warning! 

1424 

1425 :param head: 

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

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

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

1429 

1430 :param paths: 

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

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

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

1434 raised. 

1435 

1436 :param kwargs: 

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

1438 

1439 :note: 

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

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

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

1443 the files according to their state in the index. 

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

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

1446 

1447 :return: 

1448 self 

1449 """ 

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

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

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

1453 if not paths: 

1454 self.entries = new_inst.entries 

1455 else: 

1456 nie = new_inst.entries 

1457 for path in paths: 

1458 path = self._to_relative_path(path) 

1459 try: 

1460 key = entry_key(path, 0) 

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

1462 except KeyError: 

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

1464 try: 

1465 del self.entries[key] 

1466 except KeyError: 

1467 pass 

1468 # END handle deletion keyerror 

1469 # END handle keyerror 

1470 # END for each path 

1471 # END handle paths 

1472 self.write() 

1473 

1474 if working_tree: 

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

1476 # END handle working tree 

1477 

1478 if head: 

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

1480 # END handle head change 

1481 

1482 return self 

1483 

1484 def diff( 

1485 self, 

1486 other: Union[ 

1487 Literal[git_diff.DiffConstants.INDEX], 

1488 Literal[git_diff.DiffConstants.NULL_TREE], 

1489 "Tree", 

1490 "Commit", 

1491 str, 

1492 None, 

1493 ] = git_diff.INDEX, 

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

1495 create_patch: bool = False, 

1496 allow_unsafe_options: bool = False, 

1497 **kwargs: Any, 

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

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

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

1501 

1502 For documentation of the parameters and return values, see 

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

1504 

1505 :note: 

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

1507 have not been initialized with a stream. 

1508 """ 

1509 if not allow_unsafe_options: 

1510 Git.check_unsafe_options( 

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

1512 unsafe_options=self.repo.unsafe_git_revision_options, 

1513 ) 

1514 

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

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

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

1518 # Index against index is always empty. 

1519 if other is self.INDEX: 

1520 return git_diff.DiffIndex() 

1521 

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

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

1524 "--cached", 

1525 git_diff.NULL_TREE_SHA, 

1526 "--abbrev=40", 

1527 "--full-index", 

1528 ] 

1529 

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

1531 args.append("-M") 

1532 

1533 if create_patch: 

1534 args.append("-p") 

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

1536 else: 

1537 args.append("--raw") 

1538 args.append("-z") 

1539 

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

1541 

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

1543 paths = [paths] 

1544 

1545 if paths: 

1546 args.append("--") 

1547 args.extend(paths) 

1548 

1549 kwargs["as_process"] = True 

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

1551 

1552 diff_method = ( 

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

1554 ) 

1555 index = diff_method(self.repo, proc) 

1556 

1557 proc.wait() 

1558 return index 

1559 

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

1561 # Handle existing -R flags properly. 

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

1563 if isinstance(other, str): 

1564 other = self.repo.rev_parse(other) 

1565 # END object conversion 

1566 

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

1568 # Invert the existing R flag. 

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

1570 kwargs["R"] = not cur_val 

1571 return other.diff( 

1572 self.INDEX, 

1573 paths, 

1574 create_patch, 

1575 allow_unsafe_options=allow_unsafe_options, 

1576 **kwargs, 

1577 ) 

1578 # END diff against other item handling 

1579 

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

1581 if other is not None: 

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

1583 

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

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