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
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
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/
6"""Module containing :class:`IndexFile`, an Index implementation facilitating all kinds
7of index manipulations such as querying and merging."""
9__all__ = ["IndexFile", "CheckoutError", "StageType"]
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
22from gitdb.base import IStream
23from gitdb.db import MemoryDB
25from git.compat import defenc, force_bytes
26from git.cmd import Git
27import git.diff as git_diff
28from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError
29from git.objects import Blob, Commit, Object, Submodule, Tree
30from git.objects.util import Serializable
31from git.util import (
32 Actor,
33 LazyMixin,
34 LockedFD,
35 join_path_native,
36 file_contents_ro,
37 _is_path_rooted,
38 _to_relative_path,
39 to_native_path_linux,
40 unbare_repo,
41 to_bin_sha,
42)
44from .fun import (
45 S_IFGITLINK,
46 aggressive_tree_merge,
47 entry_key,
48 read_cache,
49 run_commit_hook,
50 stat_mode_to_index_mode,
51 write_cache,
52 write_tree_from_cache,
53)
54from .typ import BaseIndexEntry, IndexEntry, StageType
55from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir
57# typing -----------------------------------------------------------------------------
59from typing import (
60 Any,
61 BinaryIO,
62 Callable,
63 cast,
64 Dict,
65 Generator,
66 IO,
67 Iterable,
68 Iterator,
69 List,
70 NoReturn,
71 Sequence,
72 TYPE_CHECKING,
73 Tuple,
74 Union,
75)
77from git.types import Literal, PathLike
79if TYPE_CHECKING:
80 from subprocess import Popen
82 from git.refs.reference import Reference
83 from git.repo import Repo
86Treeish = Union[Tree, Commit, str, bytes]
88# ------------------------------------------------------------------------------------
91@contextlib.contextmanager
92def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, None, None]:
93 """Create a named temporary file git subprocesses can open, deleting it afterward.
95 :param directory:
96 The directory in which the file is created.
98 :return:
99 A context manager object that creates the file and provides its name on entry,
100 and deletes it on exit.
101 """
102 if sys.platform == "win32":
103 fd, name = tempfile.mkstemp(dir=directory)
104 os.close(fd)
105 try:
106 yield name
107 finally:
108 os.remove(name)
109 else:
110 with tempfile.NamedTemporaryFile(dir=directory) as ctx:
111 yield ctx.name
114class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
115 """An Index that can be manipulated using a native implementation in order to save
116 git command function calls wherever possible.
118 This provides custom merging facilities allowing to merge without actually changing
119 your index or your working tree. This way you can perform your own test merges based
120 on the index only without having to deal with the working copy. This is useful in
121 case of partial working trees.
123 Entries:
125 The index contains an entries dict whose keys are tuples of type
126 :class:`~git.index.typ.IndexEntry` to facilitate access.
128 You may read the entries dict or manipulate it using IndexEntry instance, i.e.::
130 index.entries[index.entry_key(index_entry_instance)] = index_entry_instance
132 Make sure you use :meth:`index.write() <write>` once you are done manipulating the
133 index directly before operating on it using the git command.
134 """
136 unsafe_git_checkout_index_options = ["--prefix"]
137 unsafe_git_read_tree_options = ["--index-output"]
139 __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
141 _VERSION = 2
142 """The latest version we support."""
144 S_IFGITLINK = S_IFGITLINK
145 """Flags for a submodule."""
147 def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None:
148 """Initialize this Index instance, optionally from the given `file_path`.
150 If no `file_path` is given, we will be created from the current index file.
152 If a stream is not given, the stream will be initialized from the current
153 repository's index on demand.
154 """
155 self.repo = repo
156 self.version = self._VERSION
157 self._extension_data = b""
158 self._file_path: PathLike = file_path or self._index_path()
160 def _set_cache_(self, attr: str) -> None:
161 if attr == "entries":
162 try:
163 fd = os.open(self._file_path, os.O_RDONLY)
164 except OSError:
165 # In new repositories, there may be no index, which means we are empty.
166 self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {}
167 return
168 # END exception handling
170 try:
171 stream = file_contents_ro(fd, stream=True, allow_mmap=True)
172 finally:
173 os.close(fd)
175 self._deserialize(stream)
176 else:
177 super()._set_cache_(attr)
179 def _index_path(self) -> PathLike:
180 if self.repo.git_dir:
181 return join_path_native(self.repo.git_dir, "index")
182 else:
183 raise GitCommandError("No git directory given to join index path")
185 @property
186 def path(self) -> PathLike:
187 """:return: Path to the index file we are representing"""
188 return self._file_path
190 def _delete_entries_cache(self) -> None:
191 """Safely clear the entries cache so it can be recreated."""
192 try:
193 del self.entries
194 except AttributeError:
195 # It failed in Python 2.6.5 with AttributeError.
196 # FIXME: Look into whether we can just remove this except clause now.
197 pass
198 # END exception handling
200 # { Serializable Interface
202 def _deserialize(self, stream: IO) -> "IndexFile":
203 """Initialize this instance with index values read from the given stream."""
204 self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream)
205 return self
207 def _entries_sorted(self) -> List[IndexEntry]:
208 """:return: List of entries, in a sorted fashion, first by path, then by stage"""
209 return sorted(self.entries.values(), key=lambda e: (e.path, e.stage))
211 def _serialize(self, stream: IO, ignore_extension_data: bool = False) -> "IndexFile":
212 entries = self._entries_sorted()
213 extension_data = self._extension_data # type: Union[None, bytes]
214 if ignore_extension_data:
215 extension_data = None
216 write_cache(entries, stream, extension_data)
217 return self
219 # } END serializable interface
221 def write(
222 self,
223 file_path: Union[None, PathLike] = None,
224 ignore_extension_data: bool = False,
225 ) -> None:
226 """Write the current state to our file path or to the given one.
228 :param file_path:
229 If ``None``, we will write to our stored file path from which we have been
230 initialized. Otherwise we write to the given file path. Please note that
231 this will change the `file_path` of this index to the one you gave.
233 :param ignore_extension_data:
234 If ``True``, the TREE type extension data read in the index will not be
235 written to disk. NOTE that no extension data is actually written. Use this
236 if you have altered the index and would like to use
237 :manpage:`git-write-tree(1)` afterwards to create a tree representing your
238 written changes. If this data is present in the written index,
239 :manpage:`git-write-tree(1)` will instead write the stored/cached tree.
240 Alternatively, use :meth:`write_tree` to handle this case automatically.
241 """
242 # Make sure we have our entries read before getting a write lock.
243 # Otherwise it would be done when streaming.
244 # This can happen if one doesn't change the index, but writes it right away.
245 self.entries # noqa: B018
246 lfd = LockedFD(file_path or self._file_path)
247 stream = lfd.open(write=True, stream=True)
249 try:
250 self._serialize(stream, ignore_extension_data)
251 except BaseException:
252 lfd.rollback()
253 raise
255 lfd.commit()
257 # Make sure we represent what we have written.
258 if file_path is not None:
259 self._file_path = file_path
261 @post_clear_cache
262 @default_index
263 def merge_tree(
264 self,
265 rhs: Treeish,
266 base: Union[None, Treeish] = None,
267 allow_unsafe_options: bool = False,
268 ) -> "IndexFile":
269 """Merge the given `rhs` treeish into the current index, possibly taking
270 a common base treeish into account.
272 As opposed to the :func:`from_tree` method, this allows you to use an already
273 existing tree as the left side of the merge.
275 :param rhs:
276 Treeish reference pointing to the 'other' side of the merge.
278 :param base:
279 Optional treeish reference pointing to the common base of `rhs` and this
280 index which equals lhs.
282 :param allow_unsafe_options:
283 Allow options that may write to arbitrary paths.
285 :return:
286 self (containing the merge and possibly unmerged entries in case of
287 conflicts)
289 :raise git.exc.GitCommandError:
290 If there is a merge conflict. The error will be raised at the first
291 conflicting path. If you want to have proper merge resolution to be done by
292 yourself, you have to commit the changed index (or make a valid tree from
293 it) and retry with a three-way :meth:`index.from_tree <from_tree>` call.
294 """
295 if not allow_unsafe_options:
296 Git.check_unsafe_options(
297 options=Git._option_candidates([base, rhs]),
298 unsafe_options=self.unsafe_git_read_tree_options,
299 )
301 # -i : ignore working tree status
302 # --aggressive : handle more merge cases
303 # -m : do an actual merge
304 args: List[Union[Treeish, str]] = ["--aggressive", "-i", "-m"]
305 if base is not None:
306 args.append(base)
307 args.append(rhs)
309 self.repo.git.read_tree(args)
310 return self
312 @classmethod
313 def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile":
314 """Merge the given treeish revisions into a new index which is returned.
316 This method behaves like ``git-read-tree --aggressive`` when doing the merge.
318 :param repo:
319 The repository treeish are located in.
321 :param tree_sha:
322 20 byte or 40 byte tree sha or tree objects.
324 :return:
325 New :class:`IndexFile` instance. Its path will be undefined.
326 If you intend to write such a merged Index, supply an alternate
327 ``file_path`` to its :meth:`write` method.
328 """
329 tree_sha_bytes: List[bytes] = [to_bin_sha(str(t)) for t in tree_sha]
330 base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes)
332 inst = cls(repo)
333 # Convert to entries dict.
334 entries: Dict[Tuple[PathLike, int], IndexEntry] = dict(
335 zip(
336 ((e.path, e.stage) for e in base_entries),
337 (IndexEntry.from_base(e) for e in base_entries),
338 )
339 )
341 inst.entries = entries
342 return inst
344 @classmethod
345 def from_tree(
346 cls,
347 repo: "Repo",
348 *treeish: Treeish,
349 allow_unsafe_options: bool = False,
350 **kwargs: Any,
351 ) -> "IndexFile":
352 R"""Merge the given treeish revisions into a new index which is returned.
353 The original index will remain unaltered.
355 :param repo:
356 The repository treeish are located in.
358 :param treeish:
359 One, two or three :class:`~git.objects.tree.Tree` objects,
360 :class:`~git.objects.commit.Commit`\s or 40 byte hexshas.
362 The result changes according to the amount of trees:
364 1. If 1 Tree is given, it will just be read into a new index.
365 2. If 2 Trees are given, they will be merged into a new index using a two
366 way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other'
367 one. It behaves like a fast-forward.
368 3. If 3 Trees are given, a 3-way merge will be performed with the first tree
369 being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current'
370 tree, tree 3 is the 'other' one.
372 :param kwargs:
373 Additional arguments passed to :manpage:`git-read-tree(1)`.
375 :param allow_unsafe_options:
376 Allow options that may write to arbitrary paths.
378 :return:
379 New :class:`IndexFile` instance. It will point to a temporary index location
380 which does not exist anymore. If you intend to write such a merged Index,
381 supply an alternate ``file_path`` to its :meth:`write` method.
383 :note:
384 In the three-way merge case, ``--aggressive`` will be specified to
385 automatically resolve more cases in a commonly correct manner. Specify
386 ``trivial=True`` as a keyword argument to override that.
388 As the underlying :manpage:`git-read-tree(1)` command takes into account the
389 current index, it will be temporarily moved out of the way to prevent any
390 unexpected interference.
391 """
392 if len(treeish) == 0 or len(treeish) > 3:
393 raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
395 if not allow_unsafe_options:
396 Git.check_unsafe_options(
397 options=Git._option_candidates(treeish, kwargs),
398 unsafe_options=cls.unsafe_git_read_tree_options,
399 )
401 arg_list: List[Union[Treeish, str]] = []
402 # Ignore that the working tree and index possibly are out of date.
403 if len(treeish) > 1:
404 # Drop unmerged entries when reading our index and merging.
405 arg_list.append("--reset")
406 # Handle non-trivial cases the way a real merge does.
407 arg_list.append("--aggressive")
408 # END merge handling
410 # Create the temporary file in the .git directory to be sure renaming
411 # works - /tmp/ directories could be on another device.
412 with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index:
413 arg_list.append("--index-output=%s" % tmp_index)
414 arg_list.extend(treeish)
416 # Move the current index out of the way - otherwise the merge may fail as it
417 # considers existing entries. Moving it essentially clears the index.
418 # Unfortunately there is no 'soft' way to do it.
419 # The TemporaryFileSwap ensures the original file gets put back.
420 with TemporaryFileSwap(join_path_native(repo.git_dir, "index")):
421 repo.git.read_tree(*arg_list, **kwargs)
422 index = cls(repo, tmp_index)
423 index.entries # noqa: B018 # Force it to read the file as we will delete the temp-file.
424 return index
425 # END index merge handling
427 # UTILITIES
429 @unbare_repo
430 def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]:
431 """Expand the directories in list of paths to the corresponding paths
432 accordingly.
434 :note:
435 git will add items multiple times even if a glob overlapped with manually
436 specified paths or if paths where specified multiple times - we respect that
437 and do not prune.
438 """
440 def raise_exc(e: Exception) -> NoReturn:
441 raise e
443 r = str(self.repo.working_tree_dir)
444 rs = r + os.sep
445 for path in paths:
446 abs_path = os.fspath(path)
447 if not osp.isabs(abs_path):
448 abs_path = osp.join(r, path)
449 # END make absolute path
451 try:
452 st = os.lstat(abs_path) # Handles non-symlinks as well.
453 except OSError:
454 # The lstat call may fail as the path may contain globs as well.
455 pass
456 else:
457 if S_ISLNK(st.st_mode):
458 yield abs_path.replace(rs, "")
459 continue
460 # END check symlink
462 # If the path is not already pointing to an existing file, resolve globs if possible.
463 if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path):
464 resolved_paths = glob.glob(abs_path)
465 # not abs_path in resolved_paths:
466 # A glob() resolving to the same path we are feeding it with is a
467 # glob() that failed to resolve. If we continued calling ourselves
468 # we'd endlessly recurse. If the condition below evaluates to true
469 # then we are likely dealing with a file whose name contains wildcard
470 # characters.
471 if abs_path not in resolved_paths:
472 for f in self._iter_expand_paths(glob.glob(abs_path)):
473 yield str(f).replace(rs, "")
474 continue
475 # END glob handling
476 try:
477 for root, _dirs, files in os.walk(abs_path, onerror=raise_exc):
478 for rela_file in files:
479 # Add relative paths only.
480 yield osp.join(root.replace(rs, ""), rela_file)
481 # END for each file in subdir
482 # END for each subdirectory
483 except OSError:
484 # It was a file or something that could not be iterated.
485 yield abs_path.replace(rs, "")
486 # END path exception handling
487 # END for each path
489 def _write_path_to_stdin(
490 self,
491 proc: "Popen",
492 filepath: PathLike,
493 item: PathLike,
494 fmakeexc: Callable[..., GitError],
495 fprogress: Callable[[PathLike, bool, PathLike], None],
496 read_from_stdout: bool = True,
497 ) -> Union[None, str]:
498 """Write path to ``proc.stdin`` and make sure it processes the item, including
499 progress.
501 :return:
502 stdout string
504 :param read_from_stdout:
505 If ``True``, ``proc.stdout`` will be read after the item was sent to stdin.
506 In that case, it will return ``None``.
508 :note:
509 There is a bug in :manpage:`git-update-index(1)` that prevents it from
510 sending reports just in time. This is why we have a version that tries to
511 read stdout and one which doesn't. In fact, the stdout is not important as
512 the piped-in files are processed anyway and just in time.
514 :note:
515 Newlines are essential here, git's behaviour is somewhat inconsistent on
516 this depending on the version, hence we try our best to deal with newlines
517 carefully. Usually the last newline will not be sent, instead we will close
518 stdin to break the pipe.
519 """
520 fprogress(filepath, False, item)
521 rval: Union[None, str] = None
523 if proc.stdin is not None:
524 try:
525 proc.stdin.write(("%s\n" % filepath).encode(defenc))
526 except IOError as e:
527 # Pipe broke, usually because some error happened.
528 raise fmakeexc() from e
529 # END write exception handling
530 proc.stdin.flush()
532 if read_from_stdout and proc.stdout is not None:
533 rval = proc.stdout.readline().strip()
534 fprogress(filepath, True, item)
535 return rval
537 def iter_blobs(
538 self, predicate: Callable[[Tuple[StageType, Blob]], bool] = lambda t: True
539 ) -> Iterator[Tuple[StageType, Blob]]:
540 """
541 :return:
542 Iterator yielding tuples of :class:`~git.objects.blob.Blob` objects and
543 stages, tuple(stage, Blob).
545 :param predicate:
546 Function(t) returning ``True`` if tuple(stage, Blob) should be yielded by
547 the iterator. A default filter, the :class:`~git.index.typ.BlobFilter`, allows you
548 to yield blobs only if they match a given list of paths.
549 """
550 for entry in self.entries.values():
551 blob = entry.to_blob(self.repo)
552 blob.size = entry.size
553 output = (entry.stage, blob)
554 if predicate(output):
555 yield output
556 # END for each entry
558 def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]:
559 """
560 :return:
561 Dict(path : list(tuple(stage, Blob, ...))), being a dictionary associating a
562 path in the index with a list containing sorted stage/blob pairs.
564 :note:
565 Blobs that have been removed in one side simply do not exist in the given
566 stage. That is, a file removed on the 'other' branch whose entries are at
567 stage 3 will not have a stage 3 entry.
568 """
570 def is_unmerged_blob(t: Tuple[StageType, Blob]) -> bool:
571 return t[0] != 0
573 path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {}
574 for stage, blob in self.iter_blobs(is_unmerged_blob):
575 path_map.setdefault(blob.path, []).append((stage, blob))
576 # END for each unmerged blob
577 for line in path_map.values():
578 line.sort()
580 return path_map
582 @classmethod
583 def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[PathLike, StageType]:
584 return entry_key(*entry)
586 def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
587 """Resolve the blobs given in blob iterator.
589 This will effectively remove the index entries of the respective path at all
590 non-null stages and add the given blob as new stage null blob.
592 For each path there may only be one blob, otherwise a :exc:`ValueError` will be
593 raised claiming the path is already at stage 0.
595 :raise ValueError:
596 If one of the blobs already existed at stage 0.
598 :return:
599 self
601 :note:
602 You will have to write the index manually once you are done, i.e.
603 ``index.resolve_blobs(blobs).write()``.
604 """
605 for blob in iter_blobs:
606 stage_null_key = (blob.path, 0)
607 if stage_null_key in self.entries:
608 raise ValueError("Path %r already exists at stage 0" % str(blob.path))
609 # END assert blob is not stage 0 already
611 # Delete all possible stages.
612 for stage in (1, 2, 3):
613 try:
614 del self.entries[(blob.path, stage)]
615 except KeyError:
616 pass
617 # END ignore key errors
618 # END for each possible stage
620 self.entries[stage_null_key] = IndexEntry.from_blob(blob)
621 # END for each blob
623 return self
625 def update(self) -> "IndexFile":
626 """Reread the contents of our index file, discarding all cached information
627 we might have.
629 :note:
630 This is a possibly dangerous operations as it will discard your changes to
631 :attr:`index.entries <entries>`.
633 :return:
634 self
635 """
636 self._delete_entries_cache()
637 # Allows to lazily reread on demand.
638 return self
640 def write_tree(self) -> Tree:
641 """Write this index to a corresponding :class:`~git.objects.tree.Tree` object
642 into the repository's object database and return it.
644 :return:
645 :class:`~git.objects.tree.Tree` object representing this index.
647 :note:
648 The tree will be written even if one or more objects the tree refers to does
649 not yet exist in the object database. This could happen if you added entries
650 to the index directly.
652 :raise ValueError:
653 If there are no entries in the cache.
655 :raise git.exc.UnmergedEntriesError:
656 """
657 # We obtain no lock as we just flush our contents to disk as tree.
658 # If we are a new index, the entries access will load our data accordingly.
659 mdb = MemoryDB()
660 entries = self._entries_sorted()
661 binsha, tree_items = write_tree_from_cache(entries, mdb, slice(0, len(entries)))
663 # Copy changed trees only.
664 mdb.stream_copy(mdb.sha_iter(), self.repo.odb)
666 # Note: Additional deserialization could be saved if write_tree_from_cache would
667 # return sorted tree entries.
668 root_tree = Tree(self.repo, binsha, path="")
669 root_tree._cache = tree_items
670 return root_tree
672 def _process_diff_args(
673 self,
674 args: List[Union[PathLike, "git_diff.Diffable"]],
675 ) -> List[Union[PathLike, "git_diff.Diffable"]]:
676 try:
677 args.pop(args.index(self))
678 except IndexError:
679 pass
680 # END remove self
681 return args
683 def _to_relative_path(self, path: PathLike) -> PathLike:
684 """
685 :return:
686 Version of path relative to our git directory or raise :exc:`ValueError` if
687 it is not within our git directory.
689 :raise ValueError:
690 """
691 if self.repo.bare:
692 drive, _tail = osp.splitdrive(os.fspath(path))
693 if drive or _is_path_rooted(path):
694 raise InvalidGitRepositoryError("paths with a drive or root require a non-bare repository")
695 return path
696 return _to_relative_path(cast(PathLike, self.repo.working_tree_dir), path)
698 def _preprocess_add_items(
699 self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]]
700 ) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
701 """Split the items into two lists of path strings and BaseEntries."""
702 paths = []
703 entries = []
704 # if it is a string put in list
705 if isinstance(items, (str, os.PathLike)):
706 items = [items]
708 for item in items:
709 if isinstance(item, (str, os.PathLike)):
710 paths.append(self._to_relative_path(item))
711 elif isinstance(item, (Blob, Submodule)):
712 entries.append(BaseIndexEntry.from_blob(item))
713 elif isinstance(item, BaseIndexEntry):
714 entries.append(item)
715 else:
716 raise TypeError("Invalid Type: %r" % item)
717 # END for each item
718 return paths, entries
720 def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
721 """Store file at filepath in the database and return the base index entry.
723 :note:
724 This needs the :func:`~git.index.util.git_working_dir` decorator active!
725 This must be ensured in the calling code.
726 """
727 st = os.lstat(filepath) # Handles non-symlinks as well.
729 if S_ISLNK(st.st_mode):
730 # In PY3, readlink is a string, but we need bytes.
731 # In PY2, it was just OS encoded bytes, we assumed UTF-8.
732 def open_stream() -> BinaryIO:
733 return BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
734 else:
736 def open_stream() -> BinaryIO:
737 return open(filepath, "rb")
739 with open_stream() as stream:
740 fprogress(filepath, False, filepath)
741 istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream))
742 fprogress(filepath, True, filepath)
743 return BaseIndexEntry(
744 (
745 stat_mode_to_index_mode(st.st_mode),
746 istream.binsha,
747 0,
748 to_native_path_linux(filepath),
749 )
750 )
752 @unbare_repo
753 @git_working_dir
754 def _entries_for_paths(
755 self,
756 paths: List[str],
757 path_rewriter: Union[Callable, None],
758 fprogress: Callable,
759 entries: List[BaseIndexEntry],
760 ) -> List[BaseIndexEntry]:
761 entries_added: List[BaseIndexEntry] = []
762 if path_rewriter:
763 working_tree_dir = self.repo.working_tree_dir
764 if working_tree_dir is None:
765 raise InvalidGitRepositoryError("Cannot rewrite paths without a working tree")
766 working_tree_dir = str(working_tree_dir)
767 for path in paths:
768 if osp.isabs(path):
769 abspath = path
770 gitrelative_path = path[len(working_tree_dir) + 1 :]
771 else:
772 gitrelative_path = path
773 abspath = osp.join(working_tree_dir, gitrelative_path)
774 # END obtain relative and absolute paths
776 blob = Blob(
777 self.repo,
778 Blob.NULL_BIN_SHA,
779 stat_mode_to_index_mode(os.stat(abspath).st_mode),
780 to_native_path_linux(gitrelative_path),
781 )
782 # TODO: variable undefined
783 entries.append(BaseIndexEntry.from_blob(blob))
784 # END for each path
785 del paths[:]
786 # END rewrite paths
788 # HANDLE PATHS
789 assert len(entries_added) == 0
790 for filepath in self._iter_expand_paths(paths):
791 entries_added.append(self._store_path(filepath, fprogress))
792 # END for each filepath
793 # END path handling
794 return entries_added
796 def add(
797 self,
798 items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
799 force: bool = True,
800 fprogress: Callable = lambda *args: None,
801 path_rewriter: Union[Callable[..., PathLike], None] = None,
802 write: bool = True,
803 write_extension_data: bool = False,
804 ) -> List[BaseIndexEntry]:
805 R"""Add files from the working tree, specific blobs, or
806 :class:`~git.index.typ.BaseIndexEntry`\s to the index.
808 :param items:
809 Multiple types of items are supported, types can be mixed within one call.
810 Different types imply a different handling. File paths may generally be
811 relative or absolute.
813 - path string
815 Strings denote a relative or absolute path into the repository pointing
816 to an existing file, e.g., ``CHANGES``, ``lib/myfile.ext``,
817 ``/home/gitrepo/lib/myfile.ext``.
819 Absolute paths must start with working tree directory of this index's
820 repository to be considered valid. For example, if it was initialized
821 with a non-normalized path, like ``/root/repo/../repo``, absolute paths
822 to be added must start with ``/root/repo/../repo``.
824 Paths provided like this must exist. When added, they will be written
825 into the object database.
827 PathStrings may contain globs, such as ``lib/__init__*``. Or they can be
828 directories like ``lib``, which will add all the files within the
829 directory and subdirectories.
831 This equals a straight :manpage:`git-add(1)`.
833 They are added at stage 0.
835 - :class:`~git.objects.blob.Blob` or
836 :class:`~git.objects.submodule.base.Submodule` object
838 Blobs are added as they are assuming a valid mode is set.
840 The file they refer to may or may not exist in the file system, but must
841 be a path relative to our repository.
843 If their sha is null (40*0), their path must exist in the file system
844 relative to the git repository as an object will be created from the
845 data at the path.
847 The handling now very much equals the way string paths are processed,
848 except that the mode you have set will be kept. This allows you to
849 create symlinks by settings the mode respectively and writing the target
850 of the symlink directly into the file. This equals a default Linux
851 symlink which is not dereferenced automatically, except that it can be
852 created on filesystems not supporting it as well.
854 Please note that globs or directories are not allowed in
855 :class:`~git.objects.blob.Blob` objects.
857 They are added at stage 0.
859 - :class:`~git.index.typ.BaseIndexEntry` or type
861 Handling equals the one of :class:`~git.objects.blob.Blob` objects, but
862 the stage may be explicitly set. Please note that Index Entries require
863 binary sha's.
865 :param force:
866 **CURRENTLY INEFFECTIVE**
867 If ``True``, otherwise ignored or excluded files will be added anyway. As
868 opposed to the :manpage:`git-add(1)` command, we enable this flag by default
869 as the API user usually wants the item to be added even though they might be
870 excluded.
872 :param fprogress:
873 Function with signature ``f(path, done=False, item=item)`` called for each
874 path to be added, one time once it is about to be added where ``done=False``
875 and once after it was added where ``done=True``.
877 ``item`` is set to the actual item we handle, either a path or a
878 :class:`~git.index.typ.BaseIndexEntry`.
880 Please note that the processed path is not guaranteed to be present in the
881 index already as the index is currently being processed.
883 :param path_rewriter:
884 Function, with signature ``(string) func(BaseIndexEntry)``, returning a path
885 for each passed entry which is the path to be actually recorded for the
886 object created from :attr:`entry.path <git.index.typ.BaseIndexEntry.path>`.
887 This allows you to write an index which is not identical to the layout of
888 the actual files on your hard-disk. If not ``None`` and `items` contain
889 plain paths, these paths will be converted to Entries beforehand and passed
890 to the path_rewriter. Please note that ``entry.path`` is relative to the git
891 repository.
893 :param write:
894 If ``True``, the index will be written once it was altered. Otherwise the
895 changes only exist in memory and are not available to git commands.
897 :param write_extension_data:
898 If ``True``, extension data will be written back to the index. This can lead
899 to issues in case it is containing the 'TREE' extension, which will cause
900 the :manpage:`git-commit(1)` command to write an old tree, instead of a new
901 one representing the now changed index.
903 This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
904 'TREE' extension altogether. You should set it to ``True`` if you intend to
905 use :meth:`IndexFile.commit` exclusively while maintaining support for
906 third-party extensions. Besides that, you can usually safely ignore the
907 built-in extensions when using GitPython on repositories that are not
908 handled manually at all.
910 All current built-in extensions are listed here:
911 https://git-scm.com/docs/index-format
913 :return:
914 List of :class:`~git.index.typ.BaseIndexEntry`\s representing the entries
915 just actually added.
917 :raise OSError:
918 If a supplied path did not exist. Please note that
919 :class:`~git.index.typ.BaseIndexEntry` objects that do not have a null sha
920 will be added even if their paths do not exist.
921 """
922 # Sort the entries into strings and Entries.
923 # Blobs are converted to entries automatically.
924 # Paths can be git-added. For everything else we use git-update-index.
925 paths, entries = self._preprocess_add_items(items)
926 entries_added: List[BaseIndexEntry] = []
927 # This code needs a working tree, so we try not to run it unless required.
928 # That way, we are OK on a bare repository as well.
929 # If there are no paths, the rewriter has nothing to do either.
930 if paths:
931 entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
933 # HANDLE ENTRIES
934 if entries:
935 null_mode_entries = [e for e in entries if e.mode == 0]
936 if null_mode_entries:
937 raise ValueError(
938 "At least one Entry has a null-mode - please use index.remove to remove files for clarity"
939 )
940 # END null mode should be remove
942 # HANDLE ENTRY OBJECT CREATION
943 # Create objects if required, otherwise go with the existing shas.
944 null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
945 if null_entries_indices:
947 @git_working_dir
948 def handle_null_entries(self: "IndexFile") -> None:
949 for ei in null_entries_indices:
950 null_entry = entries[ei]
951 new_entry = self._store_path(null_entry.path, fprogress)
953 # Update null entry.
954 entries[ei] = BaseIndexEntry(
955 (
956 null_entry.mode,
957 new_entry.binsha,
958 null_entry.stage,
959 null_entry.path,
960 )
961 )
962 # END for each entry index
964 # END closure
966 handle_null_entries(self)
967 # END null_entry handling
969 # REWRITE PATHS
970 # If we have to rewrite the entries, do so now, after we have generated all
971 # object sha's.
972 if path_rewriter:
973 for i, e in enumerate(entries):
974 entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
975 # END for each entry
976 # END handle path rewriting
978 # Just go through the remaining entries and provide progress info.
979 for i, entry in enumerate(entries):
980 progress_sent = i in null_entries_indices
981 if not progress_sent:
982 fprogress(entry.path, False, entry)
983 fprogress(entry.path, True, entry)
984 # END handle progress
985 # END for each entry
986 entries_added.extend(entries)
987 # END if there are base entries
989 # FINALIZE
990 # Add the new entries to this instance.
991 for entry in entries_added:
992 self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
994 if write:
995 self.write(ignore_extension_data=not write_extension_data)
996 # END handle write
998 return entries_added
1000 def _items_to_rela_paths(
1001 self,
1002 items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]],
1003 ) -> List[PathLike]:
1004 """Returns a list of repo-relative paths from the given items which
1005 may be absolute or relative paths, entries or blobs."""
1006 paths = []
1007 # If string, put in list.
1008 if isinstance(items, (str, os.PathLike)):
1009 items = [items]
1011 for item in items:
1012 if isinstance(item, (BaseIndexEntry, (Blob, Submodule))):
1013 paths.append(self._to_relative_path(item.path))
1014 elif isinstance(item, (str, os.PathLike)):
1015 paths.append(self._to_relative_path(item))
1016 else:
1017 raise TypeError("Invalid item type: %r" % item)
1018 # END for each item
1019 return paths
1021 @post_clear_cache
1022 @default_index
1023 def remove(
1024 self,
1025 items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
1026 working_tree: bool = False,
1027 allow_unsafe_options: bool = False,
1028 **kwargs: Any,
1029 ) -> List[str]:
1030 R"""Remove the given items from the index and optionally from the working tree
1031 as well.
1033 :param items:
1034 Multiple types of items are supported which may be be freely mixed.
1036 - path string
1038 Remove the given path at all stages. If it is a directory, you must
1039 specify the ``r=True`` keyword argument to remove all file entries below
1040 it. If absolute paths are given, they will be converted to a path
1041 relative to the git repository directory containing the working tree
1043 The path string may include globs, such as ``*.c``.
1045 - :class:`~git.objects.blob.Blob` object
1047 Only the path portion is used in this case.
1049 - :class:`~git.index.typ.BaseIndexEntry` or compatible type
1051 The only relevant information here is the path. The stage is ignored.
1053 :param working_tree:
1054 If ``True``, the entry will also be removed from the working tree,
1055 physically removing the respective file. This may fail if there are
1056 uncommitted changes in it.
1058 :param allow_unsafe_options:
1059 Allow unsafe options such as ``--pathspec-from-file`` to be passed to
1060 :manpage:`git-rm(1)`.
1062 :param kwargs:
1063 Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as
1064 ``r`` to allow recursive removal.
1066 :return:
1067 List(path_string, ...) list of repository relative paths that have been
1068 removed effectively.
1070 This is interesting to know in case you have provided a directory or globs.
1071 Paths are relative to the repository.
1072 """
1073 if not allow_unsafe_options:
1074 Git.check_unsafe_options(
1075 options=Git._option_candidates([], kwargs),
1076 unsafe_options=Git.unsafe_git_pathspec_from_file_options,
1077 )
1078 args = []
1079 if not working_tree:
1080 args.append("--cached")
1081 args.append("--")
1083 # Preprocess paths.
1084 paths = list(map(os.fspath, self._items_to_rela_paths(items))) # type: ignore[arg-type]
1085 removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
1087 # Process output to gain proper paths.
1088 # rm 'path'
1089 return [p[4:-1] for p in removed_paths]
1091 @post_clear_cache
1092 @default_index
1093 def move(
1094 self,
1095 items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
1096 skip_errors: bool = False,
1097 **kwargs: Any,
1098 ) -> List[Tuple[str, str]]:
1099 """Rename/move the items, whereas the last item is considered the destination of
1100 the move operation.
1102 If the destination is a file, the first item (of two) must be a file as well.
1104 If the destination is a directory, it may be preceded by one or more directories
1105 or files.
1107 The working tree will be affected in non-bare repositories.
1109 :param items:
1110 Multiple types of items are supported, please see the :meth:`remove` method
1111 for reference.
1113 :param skip_errors:
1114 If ``True``, errors such as ones resulting from missing source files will be
1115 skipped.
1117 :param kwargs:
1118 Additional arguments you would like to pass to :manpage:`git-mv(1)`, such as
1119 ``dry_run`` or ``force``.
1121 :return:
1122 List(tuple(source_path_string, destination_path_string), ...)
1124 A list of pairs, containing the source file moved as well as its actual
1125 destination. Relative to the repository root.
1127 :raise ValueError:
1128 If only one item was given.
1130 :raise git.exc.GitCommandError:
1131 If git could not handle your request.
1132 """
1133 args = []
1134 if skip_errors:
1135 args.append("-k")
1137 paths = self._items_to_rela_paths(items)
1138 if len(paths) < 2:
1139 raise ValueError("Please provide at least one source and one destination of the move operation")
1141 was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None))
1142 kwargs["dry_run"] = True
1144 # First execute rename in dry run so the command tells us what it actually does
1145 # (for later output).
1146 out = []
1147 mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
1149 # Parse result - first 0:n/2 lines are 'checking ', the remaining ones are the
1150 # 'renaming' ones which we parse.
1151 for ln in range(int(len(mvlines) / 2), len(mvlines)):
1152 tokens = mvlines[ln].split(" to ")
1153 assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
1155 # [0] = Renaming x
1156 # [1] = y
1157 out.append((tokens[0][9:], tokens[1]))
1158 # END for each line to parse
1160 # Either prepare for the real run, or output the dry-run result.
1161 if was_dry_run:
1162 return out
1163 # END handle dry run
1165 # Now apply the actual operation.
1166 kwargs.pop("dry_run")
1167 self.repo.git.mv(args, paths, **kwargs)
1169 return out
1171 def commit(
1172 self,
1173 message: str,
1174 parent_commits: Union[List[Commit], None] = None,
1175 head: bool = True,
1176 author: Union[None, Actor] = None,
1177 committer: Union[None, Actor] = None,
1178 author_date: Union[datetime.datetime, str, None] = None,
1179 commit_date: Union[datetime.datetime, str, None] = None,
1180 skip_hooks: bool = False,
1181 trailers: Union[None, "Dict[str, str]", "List[Tuple[str, str]]"] = None,
1182 ) -> Commit:
1183 """Commit the current default index file, creating a
1184 :class:`~git.objects.commit.Commit` object.
1186 For more information on the arguments, see
1187 :meth:`Commit.create_from_tree <git.objects.commit.Commit.create_from_tree>`.
1189 :note:
1190 If you have manually altered the :attr:`entries` member of this instance,
1191 don't forget to :meth:`write` your changes to disk beforehand.
1193 :note:
1194 Passing ``skip_hooks=True`` is the equivalent of using ``-n`` or
1195 ``--no-verify`` on the command line.
1197 :return:
1198 :class:`~git.objects.commit.Commit` object representing the new commit
1199 """
1200 if not skip_hooks:
1201 run_commit_hook("pre-commit", self)
1203 self._write_commit_editmsg(message)
1204 run_commit_hook("commit-msg", self, self._commit_editmsg_filepath())
1205 message = self._read_commit_editmsg()
1206 self._remove_commit_editmsg()
1207 tree = self.write_tree()
1208 rval = Commit.create_from_tree(
1209 self.repo,
1210 tree,
1211 message,
1212 parent_commits,
1213 head,
1214 author=author,
1215 committer=committer,
1216 author_date=author_date,
1217 commit_date=commit_date,
1218 trailers=trailers,
1219 )
1220 if not skip_hooks:
1221 run_commit_hook("post-commit", self)
1222 return rval
1224 def _write_commit_editmsg(self, message: str) -> None:
1225 with open(self._commit_editmsg_filepath(), "wb") as commit_editmsg_file:
1226 commit_editmsg_file.write(message.encode(defenc))
1228 def _remove_commit_editmsg(self) -> None:
1229 os.remove(self._commit_editmsg_filepath())
1231 def _read_commit_editmsg(self) -> str:
1232 with open(self._commit_editmsg_filepath(), "rb") as commit_editmsg_file:
1233 return commit_editmsg_file.read().decode(defenc)
1235 def _commit_editmsg_filepath(self) -> str:
1236 return osp.join(self.repo.common_dir, "COMMIT_EDITMSG")
1238 def _flush_stdin_and_wait(cls, proc: "Popen[bytes]", ignore_stdout: bool = False) -> bytes:
1239 stdin_IO = proc.stdin
1240 if stdin_IO:
1241 stdin_IO.flush()
1242 stdin_IO.close()
1244 stdout = b""
1245 if not ignore_stdout and proc.stdout:
1246 stdout = proc.stdout.read()
1248 if proc.stdout:
1249 proc.stdout.close()
1250 proc.wait()
1251 return stdout
1253 @default_index
1254 def checkout(
1255 self,
1256 paths: Union[None, Iterable[PathLike]] = None,
1257 force: bool = False,
1258 fprogress: Callable = lambda *args: None,
1259 allow_unsafe_options: bool = False,
1260 **kwargs: Any,
1261 ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]:
1262 """Check out the given paths or all files from the version known to the index
1263 into the working tree.
1265 :note:
1266 Be sure you have written pending changes using the :meth:`write` method in
1267 case you have altered the entries dictionary directly.
1269 :param paths:
1270 If ``None``, all paths in the index will be checked out.
1271 Otherwise an iterable of relative or absolute paths or a single path
1272 pointing to files or directories in the index is expected.
1274 :param force:
1275 If ``True``, existing files will be overwritten even if they contain local
1276 modifications.
1277 If ``False``, these will trigger a :exc:`~git.exc.CheckoutError`.
1279 :param fprogress:
1280 See :meth:`IndexFile.add` for signature and explanation.
1282 The provided progress information will contain ``None`` as path and item if
1283 no explicit paths are given. Otherwise progress information will be send
1284 prior and after a file has been checked out.
1286 :param allow_unsafe_options:
1287 Allow unsafe options, such as ``--prefix``.
1289 :param kwargs:
1290 Additional arguments to be passed to :manpage:`git-checkout-index(1)`.
1292 :return:
1293 Iterable yielding paths to files which have been checked out and are
1294 guaranteed to match the version stored in the index.
1296 :raise git.exc.CheckoutError:
1297 * If at least one file failed to be checked out. This is a summary, hence it
1298 will checkout as many files as it can anyway.
1299 * If one of files or directories do not exist in the index (as opposed to
1300 the original git command, which ignores them).
1302 :raise git.exc.GitCommandError:
1303 If error lines could not be parsed - this truly is an exceptional state.
1305 :note:
1306 The checkout is limited to checking out the files in the index. Files which
1307 are not in the index anymore and exist in the working tree will not be
1308 deleted. This behaviour is fundamentally different to ``head.checkout``,
1309 i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use
1310 ``head.checkout`` instead of ``index.checkout``.
1311 """
1312 if not allow_unsafe_options:
1313 Git.check_unsafe_options(
1314 options=Git._option_candidates([], kwargs),
1315 unsafe_options=self.unsafe_git_checkout_index_options,
1316 )
1318 args = ["--index"]
1319 if force:
1320 args.append("--force")
1322 failed_files = []
1323 failed_reasons = []
1324 unknown_lines = []
1326 def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLike]) -> None:
1327 stderr_IO = proc.stderr
1328 if not stderr_IO:
1329 return # Return early if stderr empty.
1331 stderr_bytes = stderr_IO.read()
1332 # line contents:
1333 stderr = stderr_bytes.decode(defenc)
1334 # git-checkout-index: this already exists
1335 endings = (
1336 " already exists",
1337 " is not in the cache",
1338 " does not exist at stage",
1339 " is unmerged",
1340 )
1341 for line in stderr.splitlines():
1342 if not line.startswith("git checkout-index: ") and not line.startswith("git-checkout-index: "):
1343 is_a_dir = " is a directory"
1344 unlink_issue = "unable to unlink old '"
1345 already_exists_issue = " already exists, no checkout" # created by entry.c:checkout_entry(...)
1346 if line.endswith(is_a_dir):
1347 failed_files.append(line[: -len(is_a_dir)])
1348 failed_reasons.append(is_a_dir)
1349 elif line.startswith(unlink_issue):
1350 failed_files.append(line[len(unlink_issue) : line.rfind("'")])
1351 failed_reasons.append(unlink_issue)
1352 elif line.endswith(already_exists_issue):
1353 failed_files.append(line[: -len(already_exists_issue)])
1354 failed_reasons.append(already_exists_issue)
1355 else:
1356 unknown_lines.append(line)
1357 continue
1358 # END special lines parsing
1360 for e in endings:
1361 if line.endswith(e):
1362 failed_files.append(line[20 : -len(e)])
1363 failed_reasons.append(e)
1364 break
1365 # END if ending matches
1366 # END for each possible ending
1367 # END for each line
1368 if unknown_lines:
1369 raise GitCommandError(("git-checkout-index",), 128, stderr)
1370 if failed_files:
1371 valid_files = list(set(iter_checked_out_files) - set(failed_files))
1372 raise CheckoutError(
1373 "Some files could not be checked out from the index due to local modifications",
1374 failed_files,
1375 valid_files,
1376 failed_reasons,
1377 )
1379 # END stderr handler
1381 if paths is None:
1382 args.append("--all")
1383 kwargs["as_process"] = 1
1384 fprogress(None, False, None)
1385 proc = self.repo.git.checkout_index(*args, **kwargs)
1386 proc.wait()
1387 fprogress(None, True, None)
1388 rval_iter = (e.path for e in self.entries.values())
1389 handle_stderr(proc, rval_iter)
1390 return rval_iter
1391 else:
1392 if isinstance(paths, str):
1393 paths = [paths]
1395 # Make sure we have our entries loaded before we start checkout_index, which
1396 # will hold a lock on it. We try to get the lock as well during our entries
1397 # initialization.
1398 self.entries # noqa: B018
1400 args.append("--stdin")
1401 kwargs["as_process"] = True
1402 kwargs["istream"] = subprocess.PIPE
1403 proc = self.repo.git.checkout_index(args, **kwargs)
1405 # FIXME: Reading from GIL!
1406 def make_exc() -> GitCommandError:
1407 return GitCommandError(("git-checkout-index", *args), 128, proc.stderr.read())
1409 checked_out_files: List[PathLike] = []
1411 for path in paths:
1412 co_path = to_native_path_linux(self._to_relative_path(path))
1413 # If the item is not in the index, it could be a directory.
1414 path_is_directory = False
1416 try:
1417 self.entries[(co_path, 0)]
1418 except KeyError:
1419 folder = co_path
1420 if not folder.endswith("/"):
1421 folder += "/"
1422 for entry in self.entries.values():
1423 if os.fspath(entry.path).startswith(folder):
1424 p = entry.path
1425 self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
1426 checked_out_files.append(p)
1427 path_is_directory = True
1428 # END if entry is in directory
1429 # END for each entry
1430 # END path exception handlnig
1432 if not path_is_directory:
1433 self._write_path_to_stdin(proc, co_path, path, make_exc, fprogress, read_from_stdout=False)
1434 checked_out_files.append(co_path)
1435 # END path is a file
1436 # END for each path
1437 try:
1438 self._flush_stdin_and_wait(proc, ignore_stdout=True)
1439 except GitCommandError:
1440 # Without parsing stdout we don't know what failed.
1441 raise CheckoutError( # noqa: B904
1442 "Some files could not be checked out from the index, probably because they didn't exist.",
1443 failed_files,
1444 [],
1445 failed_reasons,
1446 )
1448 handle_stderr(proc, checked_out_files)
1449 return checked_out_files
1450 # END paths handling
1452 @default_index
1453 def reset(
1454 self,
1455 commit: Union[Commit, "Reference", str] = "HEAD",
1456 working_tree: bool = False,
1457 paths: Union[None, Iterable[PathLike]] = None,
1458 head: bool = False,
1459 allow_unsafe_options: bool = False,
1460 **kwargs: Any,
1461 ) -> "IndexFile":
1462 """Reset the index to reflect the tree at the given commit. This will not adjust
1463 our HEAD reference by default, as opposed to
1464 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1466 :param commit:
1467 Revision, :class:`~git.refs.reference.Reference` or
1468 :class:`~git.objects.commit.Commit` specifying the commit we should
1469 represent.
1471 If you want to specify a tree only, use :meth:`IndexFile.from_tree` and
1472 overwrite the default index.
1474 :param working_tree:
1475 If ``True``, the files in the working tree will reflect the changed index.
1476 If ``False``, the working tree will not be touched.
1477 Please note that changes to the working copy will be discarded without
1478 warning!
1480 :param head:
1481 If ``True``, the head will be set to the given commit. This is ``False`` by
1482 default, but if ``True``, this method behaves like
1483 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1485 :param paths:
1486 If given as an iterable of absolute or repository-relative paths, only these
1487 will be reset to their state at the given commit-ish.
1488 The paths need to exist at the commit, otherwise an exception will be
1489 raised.
1491 :param allow_unsafe_options:
1492 Allow options that may write to arbitrary paths.
1494 :param kwargs:
1495 Additional keyword arguments passed to :manpage:`git-reset(1)`.
1497 :note:
1498 :meth:`IndexFile.reset`, as opposed to
1499 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`, will not delete any files in
1500 order to maintain a consistent working tree. Instead, it will just check out
1501 the files according to their state in the index.
1502 If you want :manpage:`git-reset(1)`-like behaviour, use
1503 :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead.
1505 :return:
1506 self
1507 """
1508 # What we actually want to do is to merge the tree into our existing index,
1509 # which is what git-read-tree does.
1510 new_inst = type(self).from_tree(self.repo, commit, allow_unsafe_options=allow_unsafe_options)
1511 if not paths:
1512 self.entries = new_inst.entries
1513 else:
1514 nie = new_inst.entries
1515 for path in paths:
1516 path = self._to_relative_path(path)
1517 key = entry_key(path, 0)
1518 try:
1519 self.entries[key] = nie[key]
1520 except KeyError:
1521 # If key is not in theirs, it mustn't be in ours.
1522 try:
1523 del self.entries[key]
1524 except KeyError:
1525 pass
1526 # END handle deletion keyerror
1527 # END handle keyerror
1528 # END for each path
1529 # END handle paths
1530 self.write()
1532 if working_tree:
1533 self.checkout(paths=paths, force=True)
1534 # END handle working tree
1536 if head:
1537 self.repo.head.set_commit(self.repo.commit(commit), logmsg="%s: Updating HEAD" % commit)
1538 # END handle head change
1540 return self
1542 def diff(
1543 self,
1544 other: Union[
1545 Literal[git_diff.DiffConstants.INDEX],
1546 Literal[git_diff.DiffConstants.NULL_TREE],
1547 "Tree",
1548 "Commit",
1549 str,
1550 None,
1551 ] = git_diff.INDEX,
1552 paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
1553 create_patch: bool = False,
1554 allow_unsafe_options: bool = False,
1555 **kwargs: Any,
1556 ) -> git_diff.DiffIndex[git_diff.Diff]:
1557 """Diff this index against the working copy or a :class:`~git.objects.tree.Tree`
1558 or :class:`~git.objects.commit.Commit` object.
1560 For documentation of the parameters and return values, see
1561 :meth:`Diffable.diff <git.diff.Diffable.diff>`.
1563 :note:
1564 Will only work with indices that represent the default git index as they
1565 have not been initialized with a stream.
1566 """
1567 if not allow_unsafe_options:
1568 Git.check_unsafe_options(
1569 options=Git._option_candidates([other], kwargs),
1570 unsafe_options=self.repo.unsafe_git_diff_options,
1571 clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW",
1572 )
1574 # Only run if we are the default repository index.
1575 if self._file_path != self._index_path():
1576 raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
1577 # Index against index is always empty.
1578 if other is self.INDEX:
1579 return git_diff.DiffIndex()
1581 if other == git_diff.NULL_TREE or other == git_diff.NULL_TREE_SHA:
1582 args: List[Union[PathLike, str]] = [
1583 "--cached",
1584 git_diff.NULL_TREE_SHA,
1585 "--abbrev=40",
1586 "--full-index",
1587 ]
1589 if not any(x in kwargs for x in ("find_renames", "no_renames", "M")):
1590 args.append("-M")
1592 if create_patch:
1593 args.append("-p")
1594 args.append("--no-ext-diff")
1595 else:
1596 args.append("--raw")
1597 args.append("-z")
1599 args.append("--no-color")
1601 if paths is not None and not isinstance(paths, (tuple, list)):
1602 paths = [paths]
1604 if paths:
1605 args.append("--")
1606 args.extend(paths)
1608 kwargs["as_process"] = True
1609 if create_patch:
1610 self.repo.git(c="diff.mnemonicPrefix=false")
1611 proc = self.repo.git.diff(*args, **kwargs)
1613 diff_method = (
1614 git_diff.Diff._index_from_patch_format if create_patch else git_diff.Diff._index_from_raw_format
1615 )
1616 index = diff_method(self.repo, proc)
1618 proc.wait()
1619 return index
1621 # Index against anything but None is a reverse diff with the respective item.
1622 # Handle existing -R flags properly.
1623 # Transform strings to the object so that we can call diff on it.
1624 if isinstance(other, str):
1625 other = self.repo.rev_parse(other)
1626 # END object conversion
1628 if isinstance(other, Object): # For Tree or Commit.
1629 # Invert the existing R flag.
1630 cur_val = kwargs.get("R", False)
1631 kwargs["R"] = not cur_val
1632 return other.diff(
1633 self.INDEX,
1634 paths,
1635 create_patch,
1636 allow_unsafe_options=allow_unsafe_options,
1637 **kwargs,
1638 )
1639 # END diff against other item handling
1641 # If other is not None here, something is wrong.
1642 if other is not None:
1643 raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other)
1645 # Diff against working copy - can be handled by superclass natively.
1646 return super().diff(other, paths, create_patch, allow_unsafe_options=allow_unsafe_options, **kwargs)