Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/index/base.py: 42%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
26import git.diff as git_diff
27from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError
28from git.objects import Blob, Commit, Object, Submodule, Tree
29from git.objects.util import Serializable
30from git.util import (
31 Actor,
32 LazyMixin,
33 LockedFD,
34 join_path_native,
35 file_contents_ro,
36 to_native_path_linux,
37 unbare_repo,
38 to_bin_sha,
39)
41from .fun import (
42 S_IFGITLINK,
43 aggressive_tree_merge,
44 entry_key,
45 read_cache,
46 run_commit_hook,
47 stat_mode_to_index_mode,
48 write_cache,
49 write_tree_from_cache,
50)
51from .typ import BaseIndexEntry, IndexEntry, StageType
52from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir
54# typing -----------------------------------------------------------------------------
56from typing import (
57 Any,
58 BinaryIO,
59 Callable,
60 Dict,
61 Generator,
62 IO,
63 Iterable,
64 Iterator,
65 List,
66 NoReturn,
67 Sequence,
68 TYPE_CHECKING,
69 Tuple,
70 Union,
71)
73from git.types import Literal, PathLike
75if TYPE_CHECKING:
76 from subprocess import Popen
78 from git.refs.reference import Reference
79 from git.repo import Repo
82Treeish = Union[Tree, Commit, str, bytes]
84# ------------------------------------------------------------------------------------
87@contextlib.contextmanager
88def _named_temporary_file_for_subprocess(directory: PathLike) -> Generator[str, None, None]:
89 """Create a named temporary file git subprocesses can open, deleting it afterward.
91 :param directory:
92 The directory in which the file is created.
94 :return:
95 A context manager object that creates the file and provides its name on entry,
96 and deletes it on exit.
97 """
98 if sys.platform == "win32":
99 fd, name = tempfile.mkstemp(dir=directory)
100 os.close(fd)
101 try:
102 yield name
103 finally:
104 os.remove(name)
105 else:
106 with tempfile.NamedTemporaryFile(dir=directory) as ctx:
107 yield ctx.name
110class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
111 """An Index that can be manipulated using a native implementation in order to save
112 git command function calls wherever possible.
114 This provides custom merging facilities allowing to merge without actually changing
115 your index or your working tree. This way you can perform your own test merges based
116 on the index only without having to deal with the working copy. This is useful in
117 case of partial working trees.
119 Entries:
121 The index contains an entries dict whose keys are tuples of type
122 :class:`~git.index.typ.IndexEntry` to facilitate access.
124 You may read the entries dict or manipulate it using IndexEntry instance, i.e.::
126 index.entries[index.entry_key(index_entry_instance)] = index_entry_instance
128 Make sure you use :meth:`index.write() <write>` once you are done manipulating the
129 index directly before operating on it using the git command.
130 """
132 __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
134 _VERSION = 2
135 """The latest version we support."""
137 S_IFGITLINK = S_IFGITLINK
138 """Flags for a submodule."""
140 def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None:
141 """Initialize this Index instance, optionally from the given `file_path`.
143 If no `file_path` is given, we will be created from the current index file.
145 If a stream is not given, the stream will be initialized from the current
146 repository's index on demand.
147 """
148 self.repo = repo
149 self.version = self._VERSION
150 self._extension_data = b""
151 self._file_path: PathLike = file_path or self._index_path()
153 def _set_cache_(self, attr: str) -> None:
154 if attr == "entries":
155 try:
156 fd = os.open(self._file_path, os.O_RDONLY)
157 except OSError:
158 # In new repositories, there may be no index, which means we are empty.
159 self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {}
160 return
161 # END exception handling
163 try:
164 stream = file_contents_ro(fd, stream=True, allow_mmap=True)
165 finally:
166 os.close(fd)
168 self._deserialize(stream)
169 else:
170 super()._set_cache_(attr)
172 def _index_path(self) -> PathLike:
173 if self.repo.git_dir:
174 return join_path_native(self.repo.git_dir, "index")
175 else:
176 raise GitCommandError("No git directory given to join index path")
178 @property
179 def path(self) -> PathLike:
180 """:return: Path to the index file we are representing"""
181 return self._file_path
183 def _delete_entries_cache(self) -> None:
184 """Safely clear the entries cache so it can be recreated."""
185 try:
186 del self.entries
187 except AttributeError:
188 # It failed in Python 2.6.5 with AttributeError.
189 # FIXME: Look into whether we can just remove this except clause now.
190 pass
191 # END exception handling
193 # { Serializable Interface
195 def _deserialize(self, stream: IO) -> "IndexFile":
196 """Initialize this instance with index values read from the given stream."""
197 self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream)
198 return self
200 def _entries_sorted(self) -> List[IndexEntry]:
201 """:return: List of entries, in a sorted fashion, first by path, then by stage"""
202 return sorted(self.entries.values(), key=lambda e: (e.path, e.stage))
204 def _serialize(self, stream: IO, ignore_extension_data: bool = False) -> "IndexFile":
205 entries = self._entries_sorted()
206 extension_data = self._extension_data # type: Union[None, bytes]
207 if ignore_extension_data:
208 extension_data = None
209 write_cache(entries, stream, extension_data)
210 return self
212 # } END serializable interface
214 def write(
215 self,
216 file_path: Union[None, PathLike] = None,
217 ignore_extension_data: bool = False,
218 ) -> None:
219 """Write the current state to our file path or to the given one.
221 :param file_path:
222 If ``None``, we will write to our stored file path from which we have been
223 initialized. Otherwise we write to the given file path. Please note that
224 this will change the `file_path` of this index to the one you gave.
226 :param ignore_extension_data:
227 If ``True``, the TREE type extension data read in the index will not be
228 written to disk. NOTE that no extension data is actually written. Use this
229 if you have altered the index and would like to use
230 :manpage:`git-write-tree(1)` afterwards to create a tree representing your
231 written changes. If this data is present in the written index,
232 :manpage:`git-write-tree(1)` will instead write the stored/cached tree.
233 Alternatively, use :meth:`write_tree` to handle this case automatically.
234 """
235 # Make sure we have our entries read before getting a write lock.
236 # Otherwise it would be done when streaming.
237 # This can happen if one doesn't change the index, but writes it right away.
238 self.entries # noqa: B018
239 lfd = LockedFD(file_path or self._file_path)
240 stream = lfd.open(write=True, stream=True)
242 try:
243 self._serialize(stream, ignore_extension_data)
244 except BaseException:
245 lfd.rollback()
246 raise
248 lfd.commit()
250 # Make sure we represent what we have written.
251 if file_path is not None:
252 self._file_path = file_path
254 @post_clear_cache
255 @default_index
256 def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexFile":
257 """Merge the given `rhs` treeish into the current index, possibly taking
258 a common base treeish into account.
260 As opposed to the :func:`from_tree` method, this allows you to use an already
261 existing tree as the left side of the merge.
263 :param rhs:
264 Treeish reference pointing to the 'other' side of the merge.
266 :param base:
267 Optional treeish reference pointing to the common base of `rhs` and this
268 index which equals lhs.
270 :return:
271 self (containing the merge and possibly unmerged entries in case of
272 conflicts)
274 :raise git.exc.GitCommandError:
275 If there is a merge conflict. The error will be raised at the first
276 conflicting path. If you want to have proper merge resolution to be done by
277 yourself, you have to commit the changed index (or make a valid tree from
278 it) and retry with a three-way :meth:`index.from_tree <from_tree>` call.
279 """
280 # -i : ignore working tree status
281 # --aggressive : handle more merge cases
282 # -m : do an actual merge
283 args: List[Union[Treeish, str]] = ["--aggressive", "-i", "-m"]
284 if base is not None:
285 args.append(base)
286 args.append(rhs)
288 self.repo.git.read_tree(args)
289 return self
291 @classmethod
292 def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile":
293 """Merge the given treeish revisions into a new index which is returned.
295 This method behaves like ``git-read-tree --aggressive`` when doing the merge.
297 :param repo:
298 The repository treeish are located in.
300 :param tree_sha:
301 20 byte or 40 byte tree sha or tree objects.
303 :return:
304 New :class:`IndexFile` instance. Its path will be undefined.
305 If you intend to write such a merged Index, supply an alternate
306 ``file_path`` to its :meth:`write` method.
307 """
308 tree_sha_bytes: List[bytes] = [to_bin_sha(str(t)) for t in tree_sha]
309 base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes)
311 inst = cls(repo)
312 # Convert to entries dict.
313 entries: Dict[Tuple[PathLike, int], IndexEntry] = dict(
314 zip(
315 ((e.path, e.stage) for e in base_entries),
316 (IndexEntry.from_base(e) for e in base_entries),
317 )
318 )
320 inst.entries = entries
321 return inst
323 @classmethod
324 def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile":
325 R"""Merge the given treeish revisions into a new index which is returned.
326 The original index will remain unaltered.
328 :param repo:
329 The repository treeish are located in.
331 :param treeish:
332 One, two or three :class:`~git.objects.tree.Tree` objects,
333 :class:`~git.objects.commit.Commit`\s or 40 byte hexshas.
335 The result changes according to the amount of trees:
337 1. If 1 Tree is given, it will just be read into a new index.
338 2. If 2 Trees are given, they will be merged into a new index using a two
339 way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other'
340 one. It behaves like a fast-forward.
341 3. If 3 Trees are given, a 3-way merge will be performed with the first tree
342 being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current'
343 tree, tree 3 is the 'other' one.
345 :param kwargs:
346 Additional arguments passed to :manpage:`git-read-tree(1)`.
348 :return:
349 New :class:`IndexFile` instance. It will point to a temporary index location
350 which does not exist anymore. If you intend to write such a merged Index,
351 supply an alternate ``file_path`` to its :meth:`write` method.
353 :note:
354 In the three-way merge case, ``--aggressive`` will be specified to
355 automatically resolve more cases in a commonly correct manner. Specify
356 ``trivial=True`` as a keyword argument to override that.
358 As the underlying :manpage:`git-read-tree(1)` command takes into account the
359 current index, it will be temporarily moved out of the way to prevent any
360 unexpected interference.
361 """
362 if len(treeish) == 0 or len(treeish) > 3:
363 raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
365 arg_list: List[Union[Treeish, str]] = []
366 # Ignore that the working tree and index possibly are out of date.
367 if len(treeish) > 1:
368 # Drop unmerged entries when reading our index and merging.
369 arg_list.append("--reset")
370 # Handle non-trivial cases the way a real merge does.
371 arg_list.append("--aggressive")
372 # END merge handling
374 # Create the temporary file in the .git directory to be sure renaming
375 # works - /tmp/ directories could be on another device.
376 with _named_temporary_file_for_subprocess(repo.git_dir) as tmp_index:
377 arg_list.append("--index-output=%s" % tmp_index)
378 arg_list.extend(treeish)
380 # Move the current index out of the way - otherwise the merge may fail as it
381 # considers existing entries. Moving it essentially clears the index.
382 # Unfortunately there is no 'soft' way to do it.
383 # The TemporaryFileSwap ensures the original file gets put back.
384 with TemporaryFileSwap(join_path_native(repo.git_dir, "index")):
385 repo.git.read_tree(*arg_list, **kwargs)
386 index = cls(repo, tmp_index)
387 index.entries # noqa: B018 # Force it to read the file as we will delete the temp-file.
388 return index
389 # END index merge handling
391 # UTILITIES
393 @unbare_repo
394 def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]:
395 """Expand the directories in list of paths to the corresponding paths
396 accordingly.
398 :note:
399 git will add items multiple times even if a glob overlapped with manually
400 specified paths or if paths where specified multiple times - we respect that
401 and do not prune.
402 """
404 def raise_exc(e: Exception) -> NoReturn:
405 raise e
407 r = str(self.repo.working_tree_dir)
408 rs = r + os.sep
409 for path in paths:
410 abs_path = str(path)
411 if not osp.isabs(abs_path):
412 abs_path = osp.join(r, path)
413 # END make absolute path
415 try:
416 st = os.lstat(abs_path) # Handles non-symlinks as well.
417 except OSError:
418 # The lstat call may fail as the path may contain globs as well.
419 pass
420 else:
421 if S_ISLNK(st.st_mode):
422 yield abs_path.replace(rs, "")
423 continue
424 # END check symlink
426 # If the path is not already pointing to an existing file, resolve globs if possible.
427 if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path):
428 resolved_paths = glob.glob(abs_path)
429 # not abs_path in resolved_paths:
430 # A glob() resolving to the same path we are feeding it with is a
431 # glob() that failed to resolve. If we continued calling ourselves
432 # we'd endlessly recurse. If the condition below evaluates to true
433 # then we are likely dealing with a file whose name contains wildcard
434 # characters.
435 if abs_path not in resolved_paths:
436 for f in self._iter_expand_paths(glob.glob(abs_path)):
437 yield str(f).replace(rs, "")
438 continue
439 # END glob handling
440 try:
441 for root, _dirs, files in os.walk(abs_path, onerror=raise_exc):
442 for rela_file in files:
443 # Add relative paths only.
444 yield osp.join(root.replace(rs, ""), rela_file)
445 # END for each file in subdir
446 # END for each subdirectory
447 except OSError:
448 # It was a file or something that could not be iterated.
449 yield abs_path.replace(rs, "")
450 # END path exception handling
451 # END for each path
453 def _write_path_to_stdin(
454 self,
455 proc: "Popen",
456 filepath: PathLike,
457 item: PathLike,
458 fmakeexc: Callable[..., GitError],
459 fprogress: Callable[[PathLike, bool, PathLike], None],
460 read_from_stdout: bool = True,
461 ) -> Union[None, str]:
462 """Write path to ``proc.stdin`` and make sure it processes the item, including
463 progress.
465 :return:
466 stdout string
468 :param read_from_stdout:
469 If ``True``, ``proc.stdout`` will be read after the item was sent to stdin.
470 In that case, it will return ``None``.
472 :note:
473 There is a bug in :manpage:`git-update-index(1)` that prevents it from
474 sending reports just in time. This is why we have a version that tries to
475 read stdout and one which doesn't. In fact, the stdout is not important as
476 the piped-in files are processed anyway and just in time.
478 :note:
479 Newlines are essential here, git's behaviour is somewhat inconsistent on
480 this depending on the version, hence we try our best to deal with newlines
481 carefully. Usually the last newline will not be sent, instead we will close
482 stdin to break the pipe.
483 """
484 fprogress(filepath, False, item)
485 rval: Union[None, str] = None
487 if proc.stdin is not None:
488 try:
489 proc.stdin.write(("%s\n" % filepath).encode(defenc))
490 except IOError as e:
491 # Pipe broke, usually because some error happened.
492 raise fmakeexc() from e
493 # END write exception handling
494 proc.stdin.flush()
496 if read_from_stdout and proc.stdout is not None:
497 rval = proc.stdout.readline().strip()
498 fprogress(filepath, True, item)
499 return rval
501 def iter_blobs(
502 self, predicate: Callable[[Tuple[StageType, Blob]], bool] = lambda t: True
503 ) -> Iterator[Tuple[StageType, Blob]]:
504 """
505 :return:
506 Iterator yielding tuples of :class:`~git.objects.blob.Blob` objects and
507 stages, tuple(stage, Blob).
509 :param predicate:
510 Function(t) returning ``True`` if tuple(stage, Blob) should be yielded by
511 the iterator. A default filter, the :class:`~git.index.typ.BlobFilter`, allows you
512 to yield blobs only if they match a given list of paths.
513 """
514 for entry in self.entries.values():
515 blob = entry.to_blob(self.repo)
516 blob.size = entry.size
517 output = (entry.stage, blob)
518 if predicate(output):
519 yield output
520 # END for each entry
522 def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]:
523 """
524 :return:
525 Dict(path : list(tuple(stage, Blob, ...))), being a dictionary associating a
526 path in the index with a list containing sorted stage/blob pairs.
528 :note:
529 Blobs that have been removed in one side simply do not exist in the given
530 stage. That is, a file removed on the 'other' branch whose entries are at
531 stage 3 will not have a stage 3 entry.
532 """
534 def is_unmerged_blob(t: Tuple[StageType, Blob]) -> bool:
535 return t[0] != 0
537 path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {}
538 for stage, blob in self.iter_blobs(is_unmerged_blob):
539 path_map.setdefault(blob.path, []).append((stage, blob))
540 # END for each unmerged blob
541 for line in path_map.values():
542 line.sort()
544 return path_map
546 @classmethod
547 def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[PathLike, StageType]:
548 return entry_key(*entry)
550 def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
551 """Resolve the blobs given in blob iterator.
553 This will effectively remove the index entries of the respective path at all
554 non-null stages and add the given blob as new stage null blob.
556 For each path there may only be one blob, otherwise a :exc:`ValueError` will be
557 raised claiming the path is already at stage 0.
559 :raise ValueError:
560 If one of the blobs already existed at stage 0.
562 :return:
563 self
565 :note:
566 You will have to write the index manually once you are done, i.e.
567 ``index.resolve_blobs(blobs).write()``.
568 """
569 for blob in iter_blobs:
570 stage_null_key = (blob.path, 0)
571 if stage_null_key in self.entries:
572 raise ValueError("Path %r already exists at stage 0" % str(blob.path))
573 # END assert blob is not stage 0 already
575 # Delete all possible stages.
576 for stage in (1, 2, 3):
577 try:
578 del self.entries[(blob.path, stage)]
579 except KeyError:
580 pass
581 # END ignore key errors
582 # END for each possible stage
584 self.entries[stage_null_key] = IndexEntry.from_blob(blob)
585 # END for each blob
587 return self
589 def update(self) -> "IndexFile":
590 """Reread the contents of our index file, discarding all cached information
591 we might have.
593 :note:
594 This is a possibly dangerous operations as it will discard your changes to
595 :attr:`index.entries <entries>`.
597 :return:
598 self
599 """
600 self._delete_entries_cache()
601 # Allows to lazily reread on demand.
602 return self
604 def write_tree(self) -> Tree:
605 """Write this index to a corresponding :class:`~git.objects.tree.Tree` object
606 into the repository's object database and return it.
608 :return:
609 :class:`~git.objects.tree.Tree` object representing this index.
611 :note:
612 The tree will be written even if one or more objects the tree refers to does
613 not yet exist in the object database. This could happen if you added entries
614 to the index directly.
616 :raise ValueError:
617 If there are no entries in the cache.
619 :raise git.exc.UnmergedEntriesError:
620 """
621 # We obtain no lock as we just flush our contents to disk as tree.
622 # If we are a new index, the entries access will load our data accordingly.
623 mdb = MemoryDB()
624 entries = self._entries_sorted()
625 binsha, tree_items = write_tree_from_cache(entries, mdb, slice(0, len(entries)))
627 # Copy changed trees only.
628 mdb.stream_copy(mdb.sha_iter(), self.repo.odb)
630 # Note: Additional deserialization could be saved if write_tree_from_cache would
631 # return sorted tree entries.
632 root_tree = Tree(self.repo, binsha, path="")
633 root_tree._cache = tree_items
634 return root_tree
636 def _process_diff_args(
637 self,
638 args: List[Union[PathLike, "git_diff.Diffable"]],
639 ) -> List[Union[PathLike, "git_diff.Diffable"]]:
640 try:
641 args.pop(args.index(self))
642 except IndexError:
643 pass
644 # END remove self
645 return args
647 def _to_relative_path(self, path: PathLike) -> PathLike:
648 """
649 :return:
650 Version of path relative to our git directory or raise :exc:`ValueError` if
651 it is not within our git directory.
653 :raise ValueError:
654 """
655 if not osp.isabs(path):
656 return path
657 if self.repo.bare:
658 raise InvalidGitRepositoryError("require non-bare repository")
659 if not osp.normpath(str(path)).startswith(str(self.repo.working_tree_dir)):
660 raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
661 result = os.path.relpath(path, self.repo.working_tree_dir)
662 if str(path).endswith(os.sep) and not result.endswith(os.sep):
663 result += os.sep
664 return result
666 def _preprocess_add_items(
667 self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]]
668 ) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
669 """Split the items into two lists of path strings and BaseEntries."""
670 paths = []
671 entries = []
672 # if it is a string put in list
673 if isinstance(items, (str, os.PathLike)):
674 items = [items]
676 for item in items:
677 if isinstance(item, (str, os.PathLike)):
678 paths.append(self._to_relative_path(item))
679 elif isinstance(item, (Blob, Submodule)):
680 entries.append(BaseIndexEntry.from_blob(item))
681 elif isinstance(item, BaseIndexEntry):
682 entries.append(item)
683 else:
684 raise TypeError("Invalid Type: %r" % item)
685 # END for each item
686 return paths, entries
688 def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
689 """Store file at filepath in the database and return the base index entry.
691 :note:
692 This needs the :func:`~git.index.util.git_working_dir` decorator active!
693 This must be ensured in the calling code.
694 """
695 st = os.lstat(filepath) # Handles non-symlinks as well.
697 if S_ISLNK(st.st_mode):
698 # In PY3, readlink is a string, but we need bytes.
699 # In PY2, it was just OS encoded bytes, we assumed UTF-8.
700 def open_stream() -> BinaryIO:
701 return BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
702 else:
704 def open_stream() -> BinaryIO:
705 return open(filepath, "rb")
707 with open_stream() as stream:
708 fprogress(filepath, False, filepath)
709 istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream))
710 fprogress(filepath, True, filepath)
711 return BaseIndexEntry(
712 (
713 stat_mode_to_index_mode(st.st_mode),
714 istream.binsha,
715 0,
716 to_native_path_linux(filepath),
717 )
718 )
720 @unbare_repo
721 @git_working_dir
722 def _entries_for_paths(
723 self,
724 paths: List[str],
725 path_rewriter: Union[Callable, None],
726 fprogress: Callable,
727 entries: List[BaseIndexEntry],
728 ) -> List[BaseIndexEntry]:
729 entries_added: List[BaseIndexEntry] = []
730 if path_rewriter:
731 for path in paths:
732 if osp.isabs(path):
733 abspath = path
734 gitrelative_path = path[len(str(self.repo.working_tree_dir)) + 1 :]
735 else:
736 gitrelative_path = path
737 if self.repo.working_tree_dir:
738 abspath = osp.join(self.repo.working_tree_dir, gitrelative_path)
739 # END obtain relative and absolute paths
741 blob = Blob(
742 self.repo,
743 Blob.NULL_BIN_SHA,
744 stat_mode_to_index_mode(os.stat(abspath).st_mode),
745 to_native_path_linux(gitrelative_path),
746 )
747 # TODO: variable undefined
748 entries.append(BaseIndexEntry.from_blob(blob))
749 # END for each path
750 del paths[:]
751 # END rewrite paths
753 # HANDLE PATHS
754 assert len(entries_added) == 0
755 for filepath in self._iter_expand_paths(paths):
756 entries_added.append(self._store_path(filepath, fprogress))
757 # END for each filepath
758 # END path handling
759 return entries_added
761 def add(
762 self,
763 items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
764 force: bool = True,
765 fprogress: Callable = lambda *args: None,
766 path_rewriter: Union[Callable[..., PathLike], None] = None,
767 write: bool = True,
768 write_extension_data: bool = False,
769 ) -> List[BaseIndexEntry]:
770 R"""Add files from the working tree, specific blobs, or
771 :class:`~git.index.typ.BaseIndexEntry`\s to the index.
773 :param items:
774 Multiple types of items are supported, types can be mixed within one call.
775 Different types imply a different handling. File paths may generally be
776 relative or absolute.
778 - path string
780 Strings denote a relative or absolute path into the repository pointing
781 to an existing file, e.g., ``CHANGES``, ``lib/myfile.ext``,
782 ``/home/gitrepo/lib/myfile.ext``.
784 Absolute paths must start with working tree directory of this index's
785 repository to be considered valid. For example, if it was initialized
786 with a non-normalized path, like ``/root/repo/../repo``, absolute paths
787 to be added must start with ``/root/repo/../repo``.
789 Paths provided like this must exist. When added, they will be written
790 into the object database.
792 PathStrings may contain globs, such as ``lib/__init__*``. Or they can be
793 directories like ``lib``, which will add all the files within the
794 directory and subdirectories.
796 This equals a straight :manpage:`git-add(1)`.
798 They are added at stage 0.
800 - :class:`~git.objects.blob.Blob` or
801 :class:`~git.objects.submodule.base.Submodule` object
803 Blobs are added as they are assuming a valid mode is set.
805 The file they refer to may or may not exist in the file system, but must
806 be a path relative to our repository.
808 If their sha is null (40*0), their path must exist in the file system
809 relative to the git repository as an object will be created from the
810 data at the path.
812 The handling now very much equals the way string paths are processed,
813 except that the mode you have set will be kept. This allows you to
814 create symlinks by settings the mode respectively and writing the target
815 of the symlink directly into the file. This equals a default Linux
816 symlink which is not dereferenced automatically, except that it can be
817 created on filesystems not supporting it as well.
819 Please note that globs or directories are not allowed in
820 :class:`~git.objects.blob.Blob` objects.
822 They are added at stage 0.
824 - :class:`~git.index.typ.BaseIndexEntry` or type
826 Handling equals the one of :class:`~git.objects.blob.Blob` objects, but
827 the stage may be explicitly set. Please note that Index Entries require
828 binary sha's.
830 :param force:
831 **CURRENTLY INEFFECTIVE**
832 If ``True``, otherwise ignored or excluded files will be added anyway. As
833 opposed to the :manpage:`git-add(1)` command, we enable this flag by default
834 as the API user usually wants the item to be added even though they might be
835 excluded.
837 :param fprogress:
838 Function with signature ``f(path, done=False, item=item)`` called for each
839 path to be added, one time once it is about to be added where ``done=False``
840 and once after it was added where ``done=True``.
842 ``item`` is set to the actual item we handle, either a path or a
843 :class:`~git.index.typ.BaseIndexEntry`.
845 Please note that the processed path is not guaranteed to be present in the
846 index already as the index is currently being processed.
848 :param path_rewriter:
849 Function, with signature ``(string) func(BaseIndexEntry)``, returning a path
850 for each passed entry which is the path to be actually recorded for the
851 object created from :attr:`entry.path <git.index.typ.BaseIndexEntry.path>`.
852 This allows you to write an index which is not identical to the layout of
853 the actual files on your hard-disk. If not ``None`` and `items` contain
854 plain paths, these paths will be converted to Entries beforehand and passed
855 to the path_rewriter. Please note that ``entry.path`` is relative to the git
856 repository.
858 :param write:
859 If ``True``, the index will be written once it was altered. Otherwise the
860 changes only exist in memory and are not available to git commands.
862 :param write_extension_data:
863 If ``True``, extension data will be written back to the index. This can lead
864 to issues in case it is containing the 'TREE' extension, which will cause
865 the :manpage:`git-commit(1)` command to write an old tree, instead of a new
866 one representing the now changed index.
868 This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
869 'TREE' extension altogether. You should set it to ``True`` if you intend to
870 use :meth:`IndexFile.commit` exclusively while maintaining support for
871 third-party extensions. Besides that, you can usually safely ignore the
872 built-in extensions when using GitPython on repositories that are not
873 handled manually at all.
875 All current built-in extensions are listed here:
876 https://git-scm.com/docs/index-format
878 :return:
879 List of :class:`~git.index.typ.BaseIndexEntry`\s representing the entries
880 just actually added.
882 :raise OSError:
883 If a supplied path did not exist. Please note that
884 :class:`~git.index.typ.BaseIndexEntry` objects that do not have a null sha
885 will be added even if their paths do not exist.
886 """
887 # Sort the entries into strings and Entries.
888 # Blobs are converted to entries automatically.
889 # Paths can be git-added. For everything else we use git-update-index.
890 paths, entries = self._preprocess_add_items(items)
891 entries_added: List[BaseIndexEntry] = []
892 # This code needs a working tree, so we try not to run it unless required.
893 # That way, we are OK on a bare repository as well.
894 # If there are no paths, the rewriter has nothing to do either.
895 if paths:
896 entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
898 # HANDLE ENTRIES
899 if entries:
900 null_mode_entries = [e for e in entries if e.mode == 0]
901 if null_mode_entries:
902 raise ValueError(
903 "At least one Entry has a null-mode - please use index.remove to remove files for clarity"
904 )
905 # END null mode should be remove
907 # HANDLE ENTRY OBJECT CREATION
908 # Create objects if required, otherwise go with the existing shas.
909 null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
910 if null_entries_indices:
912 @git_working_dir
913 def handle_null_entries(self: "IndexFile") -> None:
914 for ei in null_entries_indices:
915 null_entry = entries[ei]
916 new_entry = self._store_path(null_entry.path, fprogress)
918 # Update null entry.
919 entries[ei] = BaseIndexEntry(
920 (
921 null_entry.mode,
922 new_entry.binsha,
923 null_entry.stage,
924 null_entry.path,
925 )
926 )
927 # END for each entry index
929 # END closure
931 handle_null_entries(self)
932 # END null_entry handling
934 # REWRITE PATHS
935 # If we have to rewrite the entries, do so now, after we have generated all
936 # object sha's.
937 if path_rewriter:
938 for i, e in enumerate(entries):
939 entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
940 # END for each entry
941 # END handle path rewriting
943 # Just go through the remaining entries and provide progress info.
944 for i, entry in enumerate(entries):
945 progress_sent = i in null_entries_indices
946 if not progress_sent:
947 fprogress(entry.path, False, entry)
948 fprogress(entry.path, True, entry)
949 # END handle progress
950 # END for each entry
951 entries_added.extend(entries)
952 # END if there are base entries
954 # FINALIZE
955 # Add the new entries to this instance.
956 for entry in entries_added:
957 self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
959 if write:
960 self.write(ignore_extension_data=not write_extension_data)
961 # END handle write
963 return entries_added
965 def _items_to_rela_paths(
966 self,
967 items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]],
968 ) -> List[PathLike]:
969 """Returns a list of repo-relative paths from the given items which
970 may be absolute or relative paths, entries or blobs."""
971 paths = []
972 # If string, put in list.
973 if isinstance(items, (str, os.PathLike)):
974 items = [items]
976 for item in items:
977 if isinstance(item, (BaseIndexEntry, (Blob, Submodule))):
978 paths.append(self._to_relative_path(item.path))
979 elif isinstance(item, (str, os.PathLike)):
980 paths.append(self._to_relative_path(item))
981 else:
982 raise TypeError("Invalid item type: %r" % item)
983 # END for each item
984 return paths
986 @post_clear_cache
987 @default_index
988 def remove(
989 self,
990 items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
991 working_tree: bool = False,
992 **kwargs: Any,
993 ) -> List[str]:
994 R"""Remove the given items from the index and optionally from the working tree
995 as well.
997 :param items:
998 Multiple types of items are supported which may be be freely mixed.
1000 - path string
1002 Remove the given path at all stages. If it is a directory, you must
1003 specify the ``r=True`` keyword argument to remove all file entries below
1004 it. If absolute paths are given, they will be converted to a path
1005 relative to the git repository directory containing the working tree
1007 The path string may include globs, such as ``*.c``.
1009 - :class:`~git.objects.blob.Blob` object
1011 Only the path portion is used in this case.
1013 - :class:`~git.index.typ.BaseIndexEntry` or compatible type
1015 The only relevant information here is the path. The stage is ignored.
1017 :param working_tree:
1018 If ``True``, the entry will also be removed from the working tree,
1019 physically removing the respective file. This may fail if there are
1020 uncommitted changes in it.
1022 :param kwargs:
1023 Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as
1024 ``r`` to allow recursive removal.
1026 :return:
1027 List(path_string, ...) list of repository relative paths that have been
1028 removed effectively.
1030 This is interesting to know in case you have provided a directory or globs.
1031 Paths are relative to the repository.
1032 """
1033 args = []
1034 if not working_tree:
1035 args.append("--cached")
1036 args.append("--")
1038 # Preprocess paths.
1039 paths = self._items_to_rela_paths(items)
1040 removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
1042 # Process output to gain proper paths.
1043 # rm 'path'
1044 return [p[4:-1] for p in removed_paths]
1046 @post_clear_cache
1047 @default_index
1048 def move(
1049 self,
1050 items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
1051 skip_errors: bool = False,
1052 **kwargs: Any,
1053 ) -> List[Tuple[str, str]]:
1054 """Rename/move the items, whereas the last item is considered the destination of
1055 the move operation.
1057 If the destination is a file, the first item (of two) must be a file as well.
1059 If the destination is a directory, it may be preceded by one or more directories
1060 or files.
1062 The working tree will be affected in non-bare repositories.
1064 :param items:
1065 Multiple types of items are supported, please see the :meth:`remove` method
1066 for reference.
1068 :param skip_errors:
1069 If ``True``, errors such as ones resulting from missing source files will be
1070 skipped.
1072 :param kwargs:
1073 Additional arguments you would like to pass to :manpage:`git-mv(1)`, such as
1074 ``dry_run`` or ``force``.
1076 :return:
1077 List(tuple(source_path_string, destination_path_string), ...)
1079 A list of pairs, containing the source file moved as well as its actual
1080 destination. Relative to the repository root.
1082 :raise ValueError:
1083 If only one item was given.
1085 :raise git.exc.GitCommandError:
1086 If git could not handle your request.
1087 """
1088 args = []
1089 if skip_errors:
1090 args.append("-k")
1092 paths = self._items_to_rela_paths(items)
1093 if len(paths) < 2:
1094 raise ValueError("Please provide at least one source and one destination of the move operation")
1096 was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None))
1097 kwargs["dry_run"] = True
1099 # First execute rename in dry run so the command tells us what it actually does
1100 # (for later output).
1101 out = []
1102 mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
1104 # Parse result - first 0:n/2 lines are 'checking ', the remaining ones are the
1105 # 'renaming' ones which we parse.
1106 for ln in range(int(len(mvlines) / 2), len(mvlines)):
1107 tokens = mvlines[ln].split(" to ")
1108 assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
1110 # [0] = Renaming x
1111 # [1] = y
1112 out.append((tokens[0][9:], tokens[1]))
1113 # END for each line to parse
1115 # Either prepare for the real run, or output the dry-run result.
1116 if was_dry_run:
1117 return out
1118 # END handle dry run
1120 # Now apply the actual operation.
1121 kwargs.pop("dry_run")
1122 self.repo.git.mv(args, paths, **kwargs)
1124 return out
1126 def commit(
1127 self,
1128 message: str,
1129 parent_commits: Union[List[Commit], None] = None,
1130 head: bool = True,
1131 author: Union[None, Actor] = None,
1132 committer: Union[None, Actor] = None,
1133 author_date: Union[datetime.datetime, str, None] = None,
1134 commit_date: Union[datetime.datetime, str, None] = None,
1135 skip_hooks: bool = False,
1136 ) -> Commit:
1137 """Commit the current default index file, creating a
1138 :class:`~git.objects.commit.Commit` object.
1140 For more information on the arguments, see
1141 :meth:`Commit.create_from_tree <git.objects.commit.Commit.create_from_tree>`.
1143 :note:
1144 If you have manually altered the :attr:`entries` member of this instance,
1145 don't forget to :meth:`write` your changes to disk beforehand.
1147 :note:
1148 Passing ``skip_hooks=True`` is the equivalent of using ``-n`` or
1149 ``--no-verify`` on the command line.
1151 :return:
1152 :class:`~git.objects.commit.Commit` object representing the new commit
1153 """
1154 if not skip_hooks:
1155 run_commit_hook("pre-commit", self)
1157 self._write_commit_editmsg(message)
1158 run_commit_hook("commit-msg", self, self._commit_editmsg_filepath())
1159 message = self._read_commit_editmsg()
1160 self._remove_commit_editmsg()
1161 tree = self.write_tree()
1162 rval = Commit.create_from_tree(
1163 self.repo,
1164 tree,
1165 message,
1166 parent_commits,
1167 head,
1168 author=author,
1169 committer=committer,
1170 author_date=author_date,
1171 commit_date=commit_date,
1172 )
1173 if not skip_hooks:
1174 run_commit_hook("post-commit", self)
1175 return rval
1177 def _write_commit_editmsg(self, message: str) -> None:
1178 with open(self._commit_editmsg_filepath(), "wb") as commit_editmsg_file:
1179 commit_editmsg_file.write(message.encode(defenc))
1181 def _remove_commit_editmsg(self) -> None:
1182 os.remove(self._commit_editmsg_filepath())
1184 def _read_commit_editmsg(self) -> str:
1185 with open(self._commit_editmsg_filepath(), "rb") as commit_editmsg_file:
1186 return commit_editmsg_file.read().decode(defenc)
1188 def _commit_editmsg_filepath(self) -> str:
1189 return osp.join(self.repo.common_dir, "COMMIT_EDITMSG")
1191 def _flush_stdin_and_wait(cls, proc: "Popen[bytes]", ignore_stdout: bool = False) -> bytes:
1192 stdin_IO = proc.stdin
1193 if stdin_IO:
1194 stdin_IO.flush()
1195 stdin_IO.close()
1197 stdout = b""
1198 if not ignore_stdout and proc.stdout:
1199 stdout = proc.stdout.read()
1201 if proc.stdout:
1202 proc.stdout.close()
1203 proc.wait()
1204 return stdout
1206 @default_index
1207 def checkout(
1208 self,
1209 paths: Union[None, Iterable[PathLike]] = None,
1210 force: bool = False,
1211 fprogress: Callable = lambda *args: None,
1212 **kwargs: Any,
1213 ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]:
1214 """Check out the given paths or all files from the version known to the index
1215 into the working tree.
1217 :note:
1218 Be sure you have written pending changes using the :meth:`write` method in
1219 case you have altered the entries dictionary directly.
1221 :param paths:
1222 If ``None``, all paths in the index will be checked out.
1223 Otherwise an iterable of relative or absolute paths or a single path
1224 pointing to files or directories in the index is expected.
1226 :param force:
1227 If ``True``, existing files will be overwritten even if they contain local
1228 modifications.
1229 If ``False``, these will trigger a :exc:`~git.exc.CheckoutError`.
1231 :param fprogress:
1232 See :meth:`IndexFile.add` for signature and explanation.
1234 The provided progress information will contain ``None`` as path and item if
1235 no explicit paths are given. Otherwise progress information will be send
1236 prior and after a file has been checked out.
1238 :param kwargs:
1239 Additional arguments to be passed to :manpage:`git-checkout-index(1)`.
1241 :return:
1242 Iterable yielding paths to files which have been checked out and are
1243 guaranteed to match the version stored in the index.
1245 :raise git.exc.CheckoutError:
1246 * If at least one file failed to be checked out. This is a summary, hence it
1247 will checkout as many files as it can anyway.
1248 * If one of files or directories do not exist in the index (as opposed to
1249 the original git command, which ignores them).
1251 :raise git.exc.GitCommandError:
1252 If error lines could not be parsed - this truly is an exceptional state.
1254 :note:
1255 The checkout is limited to checking out the files in the index. Files which
1256 are not in the index anymore and exist in the working tree will not be
1257 deleted. This behaviour is fundamentally different to ``head.checkout``,
1258 i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use
1259 ``head.checkout`` instead of ``index.checkout``.
1260 """
1261 args = ["--index"]
1262 if force:
1263 args.append("--force")
1265 failed_files = []
1266 failed_reasons = []
1267 unknown_lines = []
1269 def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLike]) -> None:
1270 stderr_IO = proc.stderr
1271 if not stderr_IO:
1272 return # Return early if stderr empty.
1274 stderr_bytes = stderr_IO.read()
1275 # line contents:
1276 stderr = stderr_bytes.decode(defenc)
1277 # git-checkout-index: this already exists
1278 endings = (
1279 " already exists",
1280 " is not in the cache",
1281 " does not exist at stage",
1282 " is unmerged",
1283 )
1284 for line in stderr.splitlines():
1285 if not line.startswith("git checkout-index: ") and not line.startswith("git-checkout-index: "):
1286 is_a_dir = " is a directory"
1287 unlink_issue = "unable to unlink old '"
1288 already_exists_issue = " already exists, no checkout" # created by entry.c:checkout_entry(...)
1289 if line.endswith(is_a_dir):
1290 failed_files.append(line[: -len(is_a_dir)])
1291 failed_reasons.append(is_a_dir)
1292 elif line.startswith(unlink_issue):
1293 failed_files.append(line[len(unlink_issue) : line.rfind("'")])
1294 failed_reasons.append(unlink_issue)
1295 elif line.endswith(already_exists_issue):
1296 failed_files.append(line[: -len(already_exists_issue)])
1297 failed_reasons.append(already_exists_issue)
1298 else:
1299 unknown_lines.append(line)
1300 continue
1301 # END special lines parsing
1303 for e in endings:
1304 if line.endswith(e):
1305 failed_files.append(line[20 : -len(e)])
1306 failed_reasons.append(e)
1307 break
1308 # END if ending matches
1309 # END for each possible ending
1310 # END for each line
1311 if unknown_lines:
1312 raise GitCommandError(("git-checkout-index",), 128, stderr)
1313 if failed_files:
1314 valid_files = list(set(iter_checked_out_files) - set(failed_files))
1315 raise CheckoutError(
1316 "Some files could not be checked out from the index due to local modifications",
1317 failed_files,
1318 valid_files,
1319 failed_reasons,
1320 )
1322 # END stderr handler
1324 if paths is None:
1325 args.append("--all")
1326 kwargs["as_process"] = 1
1327 fprogress(None, False, None)
1328 proc = self.repo.git.checkout_index(*args, **kwargs)
1329 proc.wait()
1330 fprogress(None, True, None)
1331 rval_iter = (e.path for e in self.entries.values())
1332 handle_stderr(proc, rval_iter)
1333 return rval_iter
1334 else:
1335 if isinstance(paths, str):
1336 paths = [paths]
1338 # Make sure we have our entries loaded before we start checkout_index, which
1339 # will hold a lock on it. We try to get the lock as well during our entries
1340 # initialization.
1341 self.entries # noqa: B018
1343 args.append("--stdin")
1344 kwargs["as_process"] = True
1345 kwargs["istream"] = subprocess.PIPE
1346 proc = self.repo.git.checkout_index(args, **kwargs)
1348 # FIXME: Reading from GIL!
1349 def make_exc() -> GitCommandError:
1350 return GitCommandError(("git-checkout-index", *args), 128, proc.stderr.read())
1352 checked_out_files: List[PathLike] = []
1354 for path in paths:
1355 co_path = to_native_path_linux(self._to_relative_path(path))
1356 # If the item is not in the index, it could be a directory.
1357 path_is_directory = False
1359 try:
1360 self.entries[(co_path, 0)]
1361 except KeyError:
1362 folder = str(co_path)
1363 if not folder.endswith("/"):
1364 folder += "/"
1365 for entry in self.entries.values():
1366 if str(entry.path).startswith(folder):
1367 p = entry.path
1368 self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
1369 checked_out_files.append(p)
1370 path_is_directory = True
1371 # END if entry is in directory
1372 # END for each entry
1373 # END path exception handlnig
1375 if not path_is_directory:
1376 self._write_path_to_stdin(proc, co_path, path, make_exc, fprogress, read_from_stdout=False)
1377 checked_out_files.append(co_path)
1378 # END path is a file
1379 # END for each path
1380 try:
1381 self._flush_stdin_and_wait(proc, ignore_stdout=True)
1382 except GitCommandError:
1383 # Without parsing stdout we don't know what failed.
1384 raise CheckoutError( # noqa: B904
1385 "Some files could not be checked out from the index, probably because they didn't exist.",
1386 failed_files,
1387 [],
1388 failed_reasons,
1389 )
1391 handle_stderr(proc, checked_out_files)
1392 return checked_out_files
1393 # END paths handling
1395 @default_index
1396 def reset(
1397 self,
1398 commit: Union[Commit, "Reference", str] = "HEAD",
1399 working_tree: bool = False,
1400 paths: Union[None, Iterable[PathLike]] = None,
1401 head: bool = False,
1402 **kwargs: Any,
1403 ) -> "IndexFile":
1404 """Reset the index to reflect the tree at the given commit. This will not adjust
1405 our HEAD reference by default, as opposed to
1406 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1408 :param commit:
1409 Revision, :class:`~git.refs.reference.Reference` or
1410 :class:`~git.objects.commit.Commit` specifying the commit we should
1411 represent.
1413 If you want to specify a tree only, use :meth:`IndexFile.from_tree` and
1414 overwrite the default index.
1416 :param working_tree:
1417 If ``True``, the files in the working tree will reflect the changed index.
1418 If ``False``, the working tree will not be touched.
1419 Please note that changes to the working copy will be discarded without
1420 warning!
1422 :param head:
1423 If ``True``, the head will be set to the given commit. This is ``False`` by
1424 default, but if ``True``, this method behaves like
1425 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1427 :param paths:
1428 If given as an iterable of absolute or repository-relative paths, only these
1429 will be reset to their state at the given commit-ish.
1430 The paths need to exist at the commit, otherwise an exception will be
1431 raised.
1433 :param kwargs:
1434 Additional keyword arguments passed to :manpage:`git-reset(1)`.
1436 :note:
1437 :meth:`IndexFile.reset`, as opposed to
1438 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`, will not delete any files in
1439 order to maintain a consistent working tree. Instead, it will just check out
1440 the files according to their state in the index.
1441 If you want :manpage:`git-reset(1)`-like behaviour, use
1442 :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead.
1444 :return:
1445 self
1446 """
1447 # What we actually want to do is to merge the tree into our existing index,
1448 # which is what git-read-tree does.
1449 new_inst = type(self).from_tree(self.repo, commit)
1450 if not paths:
1451 self.entries = new_inst.entries
1452 else:
1453 nie = new_inst.entries
1454 for path in paths:
1455 path = self._to_relative_path(path)
1456 try:
1457 key = entry_key(path, 0)
1458 self.entries[key] = nie[key]
1459 except KeyError:
1460 # If key is not in theirs, it mustn't be in ours.
1461 try:
1462 del self.entries[key]
1463 except KeyError:
1464 pass
1465 # END handle deletion keyerror
1466 # END handle keyerror
1467 # END for each path
1468 # END handle paths
1469 self.write()
1471 if working_tree:
1472 self.checkout(paths=paths, force=True)
1473 # END handle working tree
1475 if head:
1476 self.repo.head.set_commit(self.repo.commit(commit), logmsg="%s: Updating HEAD" % commit)
1477 # END handle head change
1479 return self
1481 # FIXME: This is documented to accept the same parameters as Diffable.diff, but this
1482 # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
1483 def diff(
1484 self,
1485 other: Union[ # type: ignore[override]
1486 Literal[git_diff.DiffConstants.INDEX],
1487 "Tree",
1488 "Commit",
1489 str,
1490 None,
1491 ] = git_diff.INDEX,
1492 paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
1493 create_patch: bool = False,
1494 **kwargs: Any,
1495 ) -> git_diff.DiffIndex[git_diff.Diff]:
1496 """Diff this index against the working copy or a :class:`~git.objects.tree.Tree`
1497 or :class:`~git.objects.commit.Commit` object.
1499 For documentation of the parameters and return values, see
1500 :meth:`Diffable.diff <git.diff.Diffable.diff>`.
1502 :note:
1503 Will only work with indices that represent the default git index as they
1504 have not been initialized with a stream.
1505 """
1506 # Only run if we are the default repository index.
1507 if self._file_path != self._index_path():
1508 raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
1509 # Index against index is always empty.
1510 if other is self.INDEX:
1511 return git_diff.DiffIndex()
1513 # Index against anything but None is a reverse diff with the respective item.
1514 # Handle existing -R flags properly.
1515 # Transform strings to the object so that we can call diff on it.
1516 if isinstance(other, str):
1517 other = self.repo.rev_parse(other)
1518 # END object conversion
1520 if isinstance(other, Object): # For Tree or Commit.
1521 # Invert the existing R flag.
1522 cur_val = kwargs.get("R", False)
1523 kwargs["R"] = not cur_val
1524 return other.diff(self.INDEX, paths, create_patch, **kwargs)
1525 # END diff against other item handling
1527 # If other is not None here, something is wrong.
1528 if other is not None:
1529 raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other)
1531 # Diff against working copy - can be handled by superclass natively.
1532 return super().diff(other, paths, create_patch, **kwargs)