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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

583 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, S_ISREG 

18import subprocess 

19import sys 

20import tempfile 

21 

22from gitdb.base import IStream 

23from gitdb.db import MemoryDB 

24 

25from git.compat import defenc, force_bytes 

26from git.cmd import Git 

27import git.diff as git_diff 

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

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

30from git.objects.util import Serializable 

31from git.util import ( 

32 Actor, 

33 LazyMixin, 

34 LockedFD, 

35 join_path_native, 

36 file_contents_ro, 

37 _is_path_rooted, 

38 _to_relative_path, 

39 _validate_repo_path, 

40 to_native_path_linux, 

41 unbare_repo, 

42 to_bin_sha, 

43) 

44 

45from .fun import ( 

46 S_IFGITLINK, 

47 aggressive_tree_merge, 

48 entry_key, 

49 read_cache, 

50 run_commit_hook, 

51 stat_mode_to_index_mode, 

52 write_cache, 

53 write_tree_from_cache, 

54) 

55from .typ import BaseIndexEntry, IndexEntry, StageType 

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

57 

58# typing ----------------------------------------------------------------------------- 

59 

60from typing import ( 

61 Any, 

62 BinaryIO, 

63 Callable, 

64 cast, 

65 Dict, 

66 Generator, 

67 IO, 

68 Iterable, 

69 Iterator, 

70 List, 

71 NoReturn, 

72 Sequence, 

73 TYPE_CHECKING, 

74 Tuple, 

75 Union, 

76) 

77 

78from git.types import Literal, PathLike 

79 

80if TYPE_CHECKING: 

81 from subprocess import Popen 

82 

83 from git.refs.reference import Reference 

84 from git.repo import Repo 

85 

86 

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

88 

89# ------------------------------------------------------------------------------------ 

90 

91 

92@contextlib.contextmanager 

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

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

95 

96 :param directory: 

97 The directory in which the file is created. 

98 

99 :return: 

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

101 and deletes it on exit. 

102 """ 

103 if sys.platform == "win32": 

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

105 os.close(fd) 

106 try: 

107 yield name 

108 finally: 

109 os.remove(name) 

110 else: 

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

112 yield ctx.name 

113 

114 

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

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

117 git command function calls wherever possible. 

118 

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

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

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

122 case of partial working trees. 

123 

124 Entries: 

125 

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

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

128 

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

130 

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

132 

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

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

135 """ 

136 

137 unsafe_git_checkout_index_options = ["--prefix"] 

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

139 

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

141 

142 _VERSION = 2 

143 """The latest version we support.""" 

144 

145 S_IFGITLINK = S_IFGITLINK 

146 """Flags for a submodule.""" 

147 

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

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

150 

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

152 

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

154 repository's index on demand. 

155 """ 

156 self.repo = repo 

157 self.version = self._VERSION 

158 self._extension_data = b"" 

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

160 

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

162 if attr == "entries": 

163 try: 

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

165 except OSError: 

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

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

168 return 

169 # END exception handling 

170 

171 try: 

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

173 finally: 

174 os.close(fd) 

175 

176 self._deserialize(stream) 

177 else: 

178 super()._set_cache_(attr) 

179 

180 def _index_path(self) -> PathLike: 

181 if self.repo.git_dir: 

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

183 else: 

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

185 

186 @property 

187 def path(self) -> PathLike: 

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

189 return self._file_path 

190 

191 def _delete_entries_cache(self) -> None: 

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

193 try: 

194 del self.entries 

195 except AttributeError: 

196 # It failed in Python 2.6.5 with AttributeError. 

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

198 pass 

199 # END exception handling 

200 

201 # { Serializable Interface 

202 

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

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

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

206 return self 

207 

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

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

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

211 

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

213 entries = self._entries_sorted() 

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

215 if ignore_extension_data: 

216 extension_data = None 

217 write_cache(entries, stream, extension_data) 

218 return self 

219 

220 # } END serializable interface 

221 

222 def write( 

223 self, 

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

225 ignore_extension_data: bool = False, 

226 ) -> None: 

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

228 

229 :param file_path: 

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

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

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

233 

234 :param ignore_extension_data: 

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

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

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

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

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

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

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

242 """ 

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

244 # Otherwise it would be done when streaming. 

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

246 self.entries # noqa: B018 

247 lfd = LockedFD(file_path or self._file_path) 

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

249 

250 try: 

251 self._serialize(stream, ignore_extension_data) 

252 except BaseException: 

253 lfd.rollback() 

254 raise 

255 

256 lfd.commit() 

257 

258 # Make sure we represent what we have written. 

259 if file_path is not None: 

260 self._file_path = file_path 

261 

262 @post_clear_cache 

263 @default_index 

264 def merge_tree( 

265 self, 

266 rhs: Treeish, 

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

268 allow_unsafe_options: bool = False, 

269 ) -> "IndexFile": 

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

271 a common base treeish into account. 

272 

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

274 existing tree as the left side of the merge. 

275 

276 :param rhs: 

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

278 

279 :param base: 

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

281 index which equals lhs. 

282 

283 :param allow_unsafe_options: 

284 Allow options that may write to arbitrary paths. 

285 

286 :return: 

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

288 conflicts) 

289 

290 :raise git.exc.GitCommandError: 

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

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

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

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

295 """ 

296 if not allow_unsafe_options: 

297 Git.check_unsafe_options( 

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

299 unsafe_options=self.unsafe_git_read_tree_options, 

300 ) 

301 

302 # -i : ignore working tree status 

303 # --aggressive : handle more merge cases 

304 # -m : do an actual merge 

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

306 if base is not None: 

307 args.append(base) 

308 args.append(rhs) 

309 

310 self.repo.git.read_tree(args) 

311 return self 

312 

313 @classmethod 

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

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

316 

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

318 

319 :param repo: 

320 The repository treeish are located in. 

321 

322 :param tree_sha: 

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

324 

325 :return: 

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

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

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

329 """ 

330 tree_sha_bytes: List[bytes] = [ 

331 to_bin_sha(t if isinstance(t, bytes) else str(t).encode("ascii")) for t in tree_sha 

332 ] 

333 base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes) 

334 

335 inst = cls(repo) 

336 # Convert to entries dict. 

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

338 zip( 

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

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

341 ) 

342 ) 

343 

344 inst.entries = entries 

345 return inst 

346 

347 @classmethod 

348 def from_tree( 

349 cls, 

350 repo: "Repo", 

351 *treeish: Treeish, 

352 allow_unsafe_options: bool = False, 

353 **kwargs: Any, 

354 ) -> "IndexFile": 

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

356 The original index will remain unaltered. 

357 

358 :param repo: 

359 The repository treeish are located in. 

360 

361 :param treeish: 

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

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

364 

365 The result changes according to the amount of trees: 

366 

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

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

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

370 one. It behaves like a fast-forward. 

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

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

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

374 

375 :param kwargs: 

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

377 

378 :param allow_unsafe_options: 

379 Allow options that may write to arbitrary paths. 

380 

381 :return: 

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

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

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

385 

386 :note: 

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

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

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

390 

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

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

393 unexpected interference. 

394 """ 

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

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

397 

398 if not allow_unsafe_options: 

399 Git.check_unsafe_options( 

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

401 unsafe_options=cls.unsafe_git_read_tree_options, 

402 ) 

403 

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

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

406 if len(treeish) > 1: 

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

408 arg_list.append("--reset") 

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

410 arg_list.append("--aggressive") 

411 # END merge handling 

412 

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

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

415 with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index: 

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

417 arg_list.extend(treeish) 

418 

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

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

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

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

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

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

425 index = cls(repo, tmp_index) 

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

427 return index 

428 # END index merge handling 

429 

430 # UTILITIES 

431 

432 @unbare_repo 

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

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

435 accordingly. 

436 

437 :note: 

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

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

440 and do not prune. 

441 """ 

442 

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

444 raise e 

445 

446 r = str(self.repo.working_tree_dir) 

447 rs = r + os.sep 

448 for path in paths: 

449 abs_path = os.fspath(path) 

450 if not osp.isabs(abs_path): 

451 abs_path = osp.join(r, path) 

452 # END make absolute path 

453 

454 try: 

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

456 except OSError: 

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

458 pass 

459 else: 

460 if S_ISLNK(st.st_mode): 

461 yield abs_path.replace(rs, "") 

462 continue 

463 # END check symlink 

464 

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

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

467 resolved_paths = glob.glob(abs_path) 

468 # not abs_path in resolved_paths: 

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

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

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

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

473 # characters. 

474 if abs_path not in resolved_paths: 

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

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

477 continue 

478 # END glob handling 

479 try: 

480 for root, dirs, files in os.walk(abs_path, onerror=raise_exc): 

481 for dirname in dirs[:]: 

482 directory = osp.join(root, dirname) 

483 try: 

484 _validate_repo_path(to_native_path_linux(osp.relpath(directory, r))) 

485 except ValueError: 

486 dirs.remove(dirname) 

487 continue 

488 if osp.islink(directory): 

489 dirs.remove(dirname) 

490 yield osp.relpath(directory, r) 

491 for rela_file in files: 

492 # Add relative paths only. 

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

494 # END for each file in subdir 

495 # END for each subdirectory 

496 except OSError: 

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

498 yield abs_path.replace(rs, "") 

499 # END path exception handling 

500 # END for each path 

501 

502 def _write_path_to_stdin( 

503 self, 

504 proc: "Popen", 

505 filepath: PathLike, 

506 item: PathLike, 

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

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

509 read_from_stdout: bool = True, 

510 ) -> Union[None, str]: 

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

512 progress. 

513 

514 :return: 

515 stdout string 

516 

517 :param read_from_stdout: 

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

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

520 

521 :note: 

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

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

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

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

526 

527 :note: 

528 Paths are NUL-terminated, so the command has to run with ``-z``. A path can 

529 contain a line feed, and with line-feed separation git would read such a 

530 path as two paths and act on files that were never passed. git also unquotes 

531 a line-feed separated path that begins with a double quote. 

532 """ 

533 fprogress(filepath, False, item) 

534 rval: Union[None, str] = None 

535 

536 if proc.stdin is not None: 

537 try: 

538 proc.stdin.write(("%s\0" % filepath).encode(defenc)) 

539 except OSError as e: 

540 # Pipe broke, usually because some error happened. 

541 raise fmakeexc() from e 

542 # END write exception handling 

543 proc.stdin.flush() 

544 

545 if read_from_stdout and proc.stdout is not None: 

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

547 fprogress(filepath, True, item) 

548 return rval 

549 

550 def iter_blobs( 

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

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

553 """ 

554 :return: 

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

556 stages, tuple(stage, Blob). 

557 

558 :param predicate: 

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

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

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

562 """ 

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

564 blob = entry.to_blob(self.repo) 

565 blob.size = entry.size 

566 output = (entry.stage, blob) 

567 if predicate(output): 

568 yield output 

569 # END for each entry 

570 

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

572 """ 

573 :return: 

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

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

576 

577 :note: 

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

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

580 stage 3 will not have a stage 3 entry. 

581 """ 

582 

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

584 return t[0] != 0 

585 

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

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

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

589 # END for each unmerged blob 

590 for line in path_map.values(): 

591 line.sort() 

592 

593 return path_map 

594 

595 @classmethod 

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

597 return entry_key(*entry) 

598 

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

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

601 

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

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

604 

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

606 raised claiming the path is already at stage 0. 

607 

608 :raise ValueError: 

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

610 

611 :return: 

612 self 

613 

614 :note: 

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

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

617 """ 

618 for blob in iter_blobs: 

619 stage_null_key = (blob.path, 0) 

620 if stage_null_key in self.entries: 

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

622 # END assert blob is not stage 0 already 

623 

624 # Delete all possible stages. 

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

626 try: 

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

628 except KeyError: 

629 pass 

630 # END ignore key errors 

631 # END for each possible stage 

632 

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

634 # END for each blob 

635 

636 return self 

637 

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

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

640 we might have. 

641 

642 :note: 

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

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

645 

646 :return: 

647 self 

648 """ 

649 self._delete_entries_cache() 

650 # Allows to lazily reread on demand. 

651 return self 

652 

653 def write_tree(self) -> Tree: 

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

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

656 

657 :return: 

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

659 

660 :note: 

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

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

663 to the index directly. 

664 

665 :raise ValueError: 

666 If there are no entries in the cache. 

667 

668 :raise git.exc.UnmergedEntriesError: 

669 """ 

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

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

672 mdb = MemoryDB() 

673 entries = self._entries_sorted() 

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

675 

676 # Copy changed trees only. 

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

678 

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

680 # return sorted tree entries. 

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

682 root_tree._cache = tree_items 

683 return root_tree 

684 

685 def _process_diff_args( 

686 self, 

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

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

689 try: 

690 args.pop(args.index(self)) 

691 except IndexError: 

692 pass 

693 # END remove self 

694 return args 

695 

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

697 """ 

698 :return: 

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

700 it is not within our git directory. 

701 

702 :raise ValueError: 

703 """ 

704 if self.repo.bare: 

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

706 if drive or _is_path_rooted(path): 

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

708 return path 

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

710 

711 def _preprocess_add_items( 

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

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

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

715 paths = [] 

716 entries = [] 

717 # if it is a string put in list 

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

719 items = [items] 

720 

721 for item in items: 

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

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

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

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

726 elif isinstance(item, BaseIndexEntry): 

727 entries.append(item) 

728 else: 

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

730 # END for each item 

731 # Source paths must be safe to read, but their recorded names may be rewritten. 

732 # Apply mode-dependent restrictions to the final entries in add(). 

733 for entry in entries: 

734 _validate_repo_path(entry.path) 

735 return paths, entries 

736 

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

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

739 

740 :note: 

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

742 This must be ensured in the calling code. 

743 """ 

744 filepath = self._to_relative_path(filepath) 

745 _validate_repo_path(filepath) 

746 parent = osp.realpath(self.repo.working_dir) 

747 for component in os.fspath(filepath).split("/")[:-1]: 

748 parent = osp.join(parent, component) 

749 if osp.islink(parent) or osp.normcase(osp.realpath(parent)) != osp.normcase(osp.abspath(parent)): 

750 raise ValueError("Cannot stage a path beyond a symbolic link: %r" % filepath) 

751 st = os.lstat(filepath) 

752 if not S_ISLNK(st.st_mode) and not S_ISREG(st.st_mode): 

753 raise ValueError("Can only stage a regular file or symbolic link: %r" % filepath) 

754 

755 stream_size = st.st_size 

756 if S_ISLNK(st.st_mode): 

757 # readlink is a string, but we need bytes. 

758 target = force_bytes(os.readlink(filepath), encoding=defenc) 

759 stream_size = len(target) 

760 

761 def open_stream() -> BinaryIO: 

762 return BytesIO(target) 

763 else: 

764 

765 def open_stream() -> BinaryIO: 

766 # Do not follow a final symlink or block on a FIFO substituted 

767 # between lstat and open on platforms supporting these flags. 

768 def opener(path: str, flags: int) -> int: 

769 return os.open(path, flags | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)) 

770 

771 return open(filepath, "rb", opener=opener) 

772 

773 with open_stream() as stream: 

774 if not S_ISLNK(st.st_mode): 

775 st = os.fstat(stream.fileno()) 

776 if not S_ISREG(st.st_mode): 

777 raise ValueError("Can only stage a regular file: %r" % filepath) 

778 stream_size = st.st_size 

779 fprogress(filepath, False, filepath) 

780 istream = self.repo.odb.store(IStream(Blob.type, stream_size, stream)) 

781 fprogress(filepath, True, filepath) 

782 return BaseIndexEntry( 

783 ( 

784 stat_mode_to_index_mode(st.st_mode), 

785 istream.binsha, 

786 0, 

787 to_native_path_linux(filepath), 

788 ) 

789 ) 

790 

791 @unbare_repo 

792 @git_working_dir 

793 def _entries_for_paths( 

794 self, 

795 paths: List[str], 

796 path_rewriter: Union[Callable, None], 

797 fprogress: Callable, 

798 entries: List[BaseIndexEntry], 

799 ) -> List[BaseIndexEntry]: 

800 entries_added: List[BaseIndexEntry] = [] 

801 if path_rewriter: 

802 working_tree_dir = self.repo.working_tree_dir 

803 if working_tree_dir is None: 

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

805 working_tree_dir = str(working_tree_dir) 

806 for path in paths: 

807 if osp.isabs(path): 

808 abspath = path 

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

810 else: 

811 gitrelative_path = path 

812 abspath = osp.join(working_tree_dir, gitrelative_path) 

813 # END obtain relative and absolute paths 

814 

815 blob = Blob( 

816 self.repo, 

817 Blob.NULL_BIN_SHA, 

818 stat_mode_to_index_mode(os.lstat(abspath).st_mode), 

819 to_native_path_linux(gitrelative_path), 

820 ) 

821 # TODO: variable undefined 

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

823 # END for each path 

824 del paths[:] 

825 # END rewrite paths 

826 

827 # HANDLE PATHS 

828 assert len(entries_added) == 0 

829 for filepath in self._iter_expand_paths(paths): 

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

831 # END for each filepath 

832 # END path handling 

833 return entries_added 

834 

835 def add( 

836 self, 

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

838 force: bool = True, 

839 fprogress: Callable = lambda *args: None, 

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

841 write: bool = True, 

842 write_extension_data: bool = False, 

843 ) -> List[BaseIndexEntry]: 

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

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

846 

847 :param items: 

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

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

850 relative or absolute. 

851 

852 - path string 

853 

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

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

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

857 

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

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

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

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

862 

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

864 into the object database. 

865 

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

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

868 directory and subdirectories. 

869 

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

871 

872 They are added at stage 0. 

873 

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

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

876 

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

878 

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

880 be a path relative to our repository. 

881 

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

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

884 data at the path. 

885 

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

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

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

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

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

891 created on filesystems not supporting it as well. 

892 

893 Please note that globs or directories are not allowed in 

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

895 

896 They are added at stage 0. 

897 

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

899 

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

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

902 binary sha's. 

903 

904 :param force: 

905 **CURRENTLY INEFFECTIVE** 

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

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

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

909 excluded. 

910 

911 :param fprogress: 

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

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

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

915 

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

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

918 

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

920 index already as the index is currently being processed. 

921 

922 :param path_rewriter: 

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

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

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

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

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

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

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

930 repository. 

931 

932 :param write: 

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

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

935 

936 :param write_extension_data: 

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

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

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

940 one representing the now changed index. 

941 

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

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

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

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

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

947 handled manually at all. 

948 

949 All current built-in extensions are listed here: 

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

951 

952 :return: 

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

954 just actually added. 

955 

956 :raise OSError: 

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

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

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

960 """ 

961 # Sort the entries into strings and Entries. 

962 # Blobs are converted to entries automatically. 

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

964 paths, entries = self._preprocess_add_items(items) 

965 entries_added: List[BaseIndexEntry] = [] 

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

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

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

969 if paths: 

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

971 

972 # HANDLE ENTRIES 

973 if entries: 

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

975 if null_mode_entries: 

976 raise ValueError( 

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

978 ) 

979 # END null mode should be remove 

980 

981 # HANDLE ENTRY OBJECT CREATION 

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

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

984 if null_entries_indices: 

985 

986 @git_working_dir 

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

988 for ei in null_entries_indices: 

989 null_entry = entries[ei] 

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

991 

992 # Update null entry. 

993 entries[ei] = BaseIndexEntry( 

994 ( 

995 null_entry.mode, 

996 new_entry.binsha, 

997 null_entry.stage, 

998 null_entry.path, 

999 ) 

1000 ) 

1001 # END for each entry index 

1002 

1003 # END closure 

1004 

1005 handle_null_entries(self) 

1006 # END null_entry handling 

1007 

1008 # REWRITE PATHS 

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

1010 # object sha's. 

1011 if path_rewriter: 

1012 for i, e in enumerate(entries): 

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

1014 # END for each entry 

1015 # END handle path rewriting 

1016 

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

1018 for i, entry in enumerate(entries): 

1019 progress_sent = i in null_entries_indices 

1020 if not progress_sent: 

1021 fprogress(entry.path, False, entry) 

1022 fprogress(entry.path, True, entry) 

1023 # END handle progress 

1024 # END for each entry 

1025 entries_added.extend(entries) 

1026 # END if there are base entries 

1027 

1028 # FINALIZE 

1029 # Add the new entries to this instance. 

1030 for entry in entries_added: 

1031 _validate_repo_path(entry.path, entry.mode) 

1032 for entry in entries_added: 

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

1034 

1035 if write: 

1036 self.write(ignore_extension_data=not write_extension_data) 

1037 # END handle write 

1038 

1039 return entries_added 

1040 

1041 def _items_to_rela_paths( 

1042 self, 

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

1044 ) -> List[PathLike]: 

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

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

1047 paths = [] 

1048 # If string, put in list. 

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

1050 items = [items] 

1051 

1052 for item in items: 

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

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

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

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

1057 else: 

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

1059 # END for each item 

1060 return paths 

1061 

1062 @post_clear_cache 

1063 @default_index 

1064 def remove( 

1065 self, 

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

1067 working_tree: bool = False, 

1068 allow_unsafe_options: bool = False, 

1069 **kwargs: Any, 

1070 ) -> List[str]: 

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

1072 as well. 

1073 

1074 :param items: 

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

1076 

1077 - path string 

1078 

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

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

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

1082 relative to the git repository directory containing the working tree 

1083 

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

1085 

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

1087 

1088 Only the path portion is used in this case. 

1089 

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

1091 

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

1093 

1094 :param working_tree: 

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

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

1097 uncommitted changes in it. 

1098 

1099 :param allow_unsafe_options: 

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

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

1102 

1103 :param kwargs: 

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

1105 ``r`` to allow recursive removal. 

1106 

1107 :return: 

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

1109 removed effectively. 

1110 

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

1112 Paths are relative to the repository. 

1113 """ 

1114 if not allow_unsafe_options: 

1115 Git.check_unsafe_options( 

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

1117 unsafe_options=Git.unsafe_git_pathspec_from_file_options, 

1118 ) 

1119 args = [] 

1120 if not working_tree: 

1121 args.append("--cached") 

1122 args.append("--") 

1123 

1124 # Preprocess paths. 

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

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

1127 

1128 # Process output to gain proper paths. 

1129 # rm 'path' 

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

1131 

1132 @post_clear_cache 

1133 @default_index 

1134 def move( 

1135 self, 

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

1137 skip_errors: bool = False, 

1138 allow_unsafe_options: bool = False, 

1139 **kwargs: Any, 

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

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

1142 the move operation. 

1143 

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

1145 

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

1147 or files. 

1148 

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

1150 

1151 :param items: 

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

1153 for reference. 

1154 

1155 :param skip_errors: 

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

1157 skipped. 

1158 

1159 :param allow_unsafe_options: 

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

1161 :manpage:`git-mv(1)`. 

1162 

1163 :param kwargs: 

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

1165 ``dry_run`` or ``force``. 

1166 

1167 :return: 

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

1169 

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

1171 destination. Relative to the repository root. 

1172 

1173 :raise ValueError: 

1174 If only one item was given. 

1175 

1176 :raise git.exc.GitCommandError: 

1177 If git could not handle your request. 

1178 """ 

1179 if not allow_unsafe_options: 

1180 Git.check_unsafe_options( 

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

1182 unsafe_options=Git.unsafe_git_pathspec_from_file_options, 

1183 ) 

1184 args = [] 

1185 if skip_errors: 

1186 args.append("-k") 

1187 args.append("--") 

1188 

1189 paths = self._items_to_rela_paths(items) 

1190 if len(paths) < 2: 

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

1192 

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

1194 kwargs["dry_run"] = True 

1195 

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

1197 # (for later output). 

1198 out = [] 

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

1200 

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

1202 # 'renaming' ones which we parse. 

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

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

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

1206 

1207 # [0] = Renaming x 

1208 # [1] = y 

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

1210 # END for each line to parse 

1211 

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

1213 if was_dry_run: 

1214 return out 

1215 # END handle dry run 

1216 

1217 # Now apply the actual operation. 

1218 kwargs.pop("dry_run") 

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

1220 

1221 return out 

1222 

1223 def commit( 

1224 self, 

1225 message: str, 

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

1227 head: bool = True, 

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

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

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

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

1232 skip_hooks: bool = False, 

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

1234 ) -> Commit: 

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

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

1237 

1238 For more information on the arguments, see 

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

1240 

1241 :note: 

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

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

1244 

1245 :note: 

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

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

1248 

1249 :return: 

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

1251 """ 

1252 if not skip_hooks: 

1253 run_commit_hook("pre-commit", self) 

1254 

1255 self._write_commit_editmsg(message) 

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

1257 message = self._read_commit_editmsg() 

1258 self._remove_commit_editmsg() 

1259 tree = self.write_tree() 

1260 rval = Commit.create_from_tree( 

1261 self.repo, 

1262 tree, 

1263 message, 

1264 parent_commits, 

1265 head, 

1266 author=author, 

1267 committer=committer, 

1268 author_date=author_date, 

1269 commit_date=commit_date, 

1270 trailers=trailers, 

1271 ) 

1272 if not skip_hooks: 

1273 run_commit_hook("post-commit", self) 

1274 return rval 

1275 

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

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

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

1279 

1280 def _remove_commit_editmsg(self) -> None: 

1281 os.remove(self._commit_editmsg_filepath()) 

1282 

1283 def _read_commit_editmsg(self) -> str: 

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

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

1286 

1287 def _commit_editmsg_filepath(self) -> str: 

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

1289 

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

1291 stdin_IO = proc.stdin 

1292 if stdin_IO: 

1293 stdin_IO.flush() 

1294 stdin_IO.close() 

1295 

1296 stdout = b"" 

1297 if not ignore_stdout and proc.stdout: 

1298 stdout = proc.stdout.read() 

1299 

1300 if proc.stdout: 

1301 proc.stdout.close() 

1302 proc.wait() 

1303 return stdout 

1304 

1305 @default_index 

1306 def checkout( 

1307 self, 

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

1309 force: bool = False, 

1310 fprogress: Callable = lambda *args: None, 

1311 allow_unsafe_options: bool = False, 

1312 **kwargs: Any, 

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

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

1315 into the working tree. 

1316 

1317 :note: 

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

1319 case you have altered the entries dictionary directly. 

1320 

1321 :param paths: 

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

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

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

1325 

1326 :param force: 

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

1328 modifications. 

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

1330 

1331 :param fprogress: 

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

1333 

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

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

1336 prior and after a file has been checked out. 

1337 

1338 :param allow_unsafe_options: 

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

1340 

1341 :param kwargs: 

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

1343 

1344 :return: 

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

1346 guaranteed to match the version stored in the index. 

1347 

1348 :raise git.exc.CheckoutError: 

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

1350 will checkout as many files as it can anyway. 

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

1352 the original git command, which ignores them). 

1353 

1354 :raise git.exc.GitCommandError: 

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

1356 

1357 :note: 

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

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

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

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

1362 ``head.checkout`` instead of ``index.checkout``. 

1363 """ 

1364 if not allow_unsafe_options: 

1365 Git.check_unsafe_options( 

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

1367 unsafe_options=self.unsafe_git_checkout_index_options, 

1368 ) 

1369 

1370 args = ["--index"] 

1371 if force: 

1372 args.append("--force") 

1373 

1374 failed_files = [] 

1375 failed_reasons = [] 

1376 unknown_lines = [] 

1377 

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

1379 stderr_IO = proc.stderr 

1380 if not stderr_IO: 

1381 return # Return early if stderr empty. 

1382 

1383 stderr_bytes = stderr_IO.read() 

1384 # line contents: 

1385 stderr = stderr_bytes.decode(defenc) 

1386 # git-checkout-index: this already exists 

1387 endings = ( 

1388 " already exists", 

1389 " is not in the cache", 

1390 " does not exist at stage", 

1391 " is unmerged", 

1392 ) 

1393 for line in stderr.splitlines(): 

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

1395 is_a_dir = " is a directory" 

1396 unlink_issue = "unable to unlink old '" 

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

1398 if line.endswith(is_a_dir): 

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

1400 failed_reasons.append(is_a_dir) 

1401 elif line.startswith(unlink_issue): 

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

1403 failed_reasons.append(unlink_issue) 

1404 elif line.endswith(already_exists_issue): 

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

1406 failed_reasons.append(already_exists_issue) 

1407 else: 

1408 unknown_lines.append(line) 

1409 continue 

1410 # END special lines parsing 

1411 

1412 for e in endings: 

1413 if line.endswith(e): 

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

1415 failed_reasons.append(e) 

1416 break 

1417 # END if ending matches 

1418 # END for each possible ending 

1419 # END for each line 

1420 if unknown_lines: 

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

1422 if failed_files: 

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

1424 raise CheckoutError( 

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

1426 failed_files, 

1427 valid_files, 

1428 failed_reasons, 

1429 ) 

1430 

1431 # END stderr handler 

1432 

1433 # Read and validate the index before Git trusts its paths for checkout. 

1434 self._delete_entries_cache() 

1435 self.entries # noqa: B018 

1436 if paths is None: 

1437 args.append("--all") 

1438 kwargs["as_process"] = 1 

1439 fprogress(None, False, None) 

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

1441 proc.wait() 

1442 fprogress(None, True, None) 

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

1444 handle_stderr(proc, rval_iter) 

1445 return rval_iter 

1446 else: 

1447 if isinstance(paths, (str, os.PathLike)): 

1448 paths = [paths] 

1449 

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

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

1452 # initialization. 

1453 self.entries # noqa: B018 

1454 

1455 args.append("-z") 

1456 args.append("--stdin") 

1457 kwargs["as_process"] = True 

1458 kwargs["istream"] = subprocess.PIPE 

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

1460 

1461 # FIXME: Reading from GIL! 

1462 def make_exc() -> GitCommandError: 

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

1464 

1465 checked_out_files: List[PathLike] = [] 

1466 

1467 for path in paths: 

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

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

1470 path_is_directory = False 

1471 

1472 try: 

1473 self.entries[(co_path, 0)] 

1474 except KeyError: 

1475 folder = co_path 

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

1477 folder += "/" 

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

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

1480 p = entry.path 

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

1482 checked_out_files.append(p) 

1483 path_is_directory = True 

1484 # END if entry is in directory 

1485 # END for each entry 

1486 # END path exception handlnig 

1487 

1488 if not path_is_directory: 

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

1490 checked_out_files.append(co_path) 

1491 # END path is a file 

1492 # END for each path 

1493 try: 

1494 self._flush_stdin_and_wait(proc, ignore_stdout=True) 

1495 except GitCommandError: 

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

1497 raise CheckoutError( # noqa: B904 

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

1499 failed_files, 

1500 [], 

1501 failed_reasons, 

1502 ) 

1503 

1504 handle_stderr(proc, checked_out_files) 

1505 return checked_out_files 

1506 # END paths handling 

1507 

1508 @default_index 

1509 def reset( 

1510 self, 

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

1512 working_tree: bool = False, 

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

1514 head: bool = False, 

1515 allow_unsafe_options: bool = False, 

1516 **kwargs: Any, 

1517 ) -> "IndexFile": 

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

1519 our HEAD reference by default, as opposed to 

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

1521 

1522 :param commit: 

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

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

1525 represent. 

1526 

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

1528 overwrite the default index. 

1529 

1530 :param working_tree: 

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

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

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

1534 warning! 

1535 

1536 :param head: 

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

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

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

1540 

1541 :param paths: 

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

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

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

1545 raised. 

1546 

1547 :param allow_unsafe_options: 

1548 Allow options that may write to arbitrary paths. 

1549 

1550 :param kwargs: 

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

1552 

1553 :note: 

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

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

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

1557 the files according to their state in the index. 

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

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

1560 

1561 :return: 

1562 self 

1563 """ 

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

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

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

1567 if not paths: 

1568 self.entries = new_inst.entries 

1569 else: 

1570 nie = new_inst.entries 

1571 for path in paths: 

1572 path = self._to_relative_path(path) 

1573 key = entry_key(path, 0) 

1574 try: 

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

1576 except KeyError: 

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

1578 try: 

1579 del self.entries[key] 

1580 except KeyError: 

1581 pass 

1582 # END handle deletion keyerror 

1583 # END handle keyerror 

1584 # END for each path 

1585 # END handle paths 

1586 self.write() 

1587 

1588 if working_tree: 

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

1590 # END handle working tree 

1591 

1592 if head: 

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

1594 # END handle head change 

1595 

1596 return self 

1597 

1598 def diff( 

1599 self, 

1600 other: Union[ 

1601 Literal[git_diff.DiffConstants.INDEX], 

1602 Literal[git_diff.DiffConstants.NULL_TREE], 

1603 "Tree", 

1604 "Commit", 

1605 str, 

1606 None, 

1607 ] = git_diff.INDEX, 

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

1609 create_patch: bool = False, 

1610 allow_unsafe_options: bool = False, 

1611 **kwargs: Any, 

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

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

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

1615 

1616 For documentation of the parameters and return values, see 

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

1618 

1619 :note: 

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

1621 have not been initialized with a stream. 

1622 """ 

1623 if not allow_unsafe_options: 

1624 Git.check_unsafe_options( 

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

1626 unsafe_options=self.repo.unsafe_git_diff_options, 

1627 clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW", 

1628 ) 

1629 

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

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

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

1633 # Index against index is always empty. 

1634 if other is self.INDEX: 

1635 return git_diff.DiffIndex() 

1636 

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

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

1639 "--cached", 

1640 git_diff.NULL_TREE_SHA, 

1641 "--abbrev=40", 

1642 "--full-index", 

1643 ] 

1644 

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

1646 args.append("-M") 

1647 

1648 if create_patch: 

1649 args.append("-p") 

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

1651 else: 

1652 args.append("--raw") 

1653 args.append("-z") 

1654 

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

1656 

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

1658 paths = [paths] 

1659 

1660 if paths: 

1661 args.append("--") 

1662 args.extend(paths) 

1663 

1664 kwargs["as_process"] = True 

1665 if create_patch: 

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

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

1668 

1669 diff_method = ( 

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

1671 ) 

1672 index = diff_method(self.repo, proc) 

1673 

1674 proc.wait() 

1675 return index 

1676 

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

1678 # Handle existing -R flags properly. 

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

1680 if isinstance(other, str): 

1681 other = self.repo.rev_parse(other) 

1682 # END object conversion 

1683 

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

1685 # Invert the existing R flag. 

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

1687 kwargs["R"] = not cur_val 

1688 return other.diff( 

1689 self.INDEX, 

1690 paths, 

1691 create_patch, 

1692 allow_unsafe_options=allow_unsafe_options, 

1693 **kwargs, 

1694 ) 

1695 # END diff against other item handling 

1696 

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

1698 if other is not None: 

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

1700 

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

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