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