Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/git/index/base.py: 39%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
2#
3# This module is part of GitPython and is released under the
4# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
6"""Module containing :class:`IndexFile`, an Index implementation facilitating all kinds
7of index manipulations such as querying and merging."""
9__all__ = ["IndexFile", "CheckoutError", "StageType"]
11import contextlib
12import datetime
13import glob
14from io import BytesIO
15import os
16import os.path as osp
17from stat import S_ISLNK
18import subprocess
19import sys
20import tempfile
22from gitdb.base import IStream
23from gitdb.db import MemoryDB
25from git.compat import defenc, force_bytes
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 LazyMixin,
32 LockedFD,
33 join_path_native,
34 file_contents_ro,
35 to_native_path_linux,
36 unbare_repo,
37 to_bin_sha,
38)
40from .fun import (
41 S_IFGITLINK,
42 aggressive_tree_merge,
43 entry_key,
44 read_cache,
45 run_commit_hook,
46 stat_mode_to_index_mode,
47 write_cache,
48 write_tree_from_cache,
49)
50from .typ import BaseIndexEntry, IndexEntry, StageType
51from .util import TemporaryFileSwap, post_clear_cache, default_index, git_working_dir
53# typing -----------------------------------------------------------------------------
55from typing import (
56 Any,
57 BinaryIO,
58 Callable,
59 Dict,
60 Generator,
61 IO,
62 Iterable,
63 Iterator,
64 List,
65 NoReturn,
66 Sequence,
67 TYPE_CHECKING,
68 Tuple,
69 Union,
70)
72from git.types import Literal, PathLike
74if TYPE_CHECKING:
75 from subprocess import Popen
77 from git.refs.reference import Reference
78 from git.repo import Repo
79 from git.util import Actor
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 `~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 """
533 is_unmerged_blob = lambda t: t[0] != 0
534 path_map: Dict[PathLike, List[Tuple[StageType, Blob]]] = {}
535 for stage, blob in self.iter_blobs(is_unmerged_blob):
536 path_map.setdefault(blob.path, []).append((stage, blob))
537 # END for each unmerged blob
538 for line in path_map.values():
539 line.sort()
541 return path_map
543 @classmethod
544 def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[PathLike, StageType]:
545 return entry_key(*entry)
547 def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile":
548 """Resolve the blobs given in blob iterator.
550 This will effectively remove the index entries of the respective path at all
551 non-null stages and add the given blob as new stage null blob.
553 For each path there may only be one blob, otherwise a :exc:`ValueError` will be
554 raised claiming the path is already at stage 0.
556 :raise ValueError:
557 If one of the blobs already existed at stage 0.
559 :return:
560 self
562 :note:
563 You will have to write the index manually once you are done, i.e.
564 ``index.resolve_blobs(blobs).write()``.
565 """
566 for blob in iter_blobs:
567 stage_null_key = (blob.path, 0)
568 if stage_null_key in self.entries:
569 raise ValueError("Path %r already exists at stage 0" % str(blob.path))
570 # END assert blob is not stage 0 already
572 # Delete all possible stages.
573 for stage in (1, 2, 3):
574 try:
575 del self.entries[(blob.path, stage)]
576 except KeyError:
577 pass
578 # END ignore key errors
579 # END for each possible stage
581 self.entries[stage_null_key] = IndexEntry.from_blob(blob)
582 # END for each blob
584 return self
586 def update(self) -> "IndexFile":
587 """Reread the contents of our index file, discarding all cached information
588 we might have.
590 :note:
591 This is a possibly dangerous operations as it will discard your changes to
592 :attr:`index.entries <entries>`.
594 :return:
595 self
596 """
597 self._delete_entries_cache()
598 # Allows to lazily reread on demand.
599 return self
601 def write_tree(self) -> Tree:
602 """Write this index to a corresponding :class:`~git.objects.tree.Tree` object
603 into the repository's object database and return it.
605 :return:
606 :class:`~git.objects.tree.Tree` object representing this index.
608 :note:
609 The tree will be written even if one or more objects the tree refers to does
610 not yet exist in the object database. This could happen if you added entries
611 to the index directly.
613 :raise ValueError:
614 If there are no entries in the cache.
616 :raise git.exc.UnmergedEntriesError:
617 """
618 # We obtain no lock as we just flush our contents to disk as tree.
619 # If we are a new index, the entries access will load our data accordingly.
620 mdb = MemoryDB()
621 entries = self._entries_sorted()
622 binsha, tree_items = write_tree_from_cache(entries, mdb, slice(0, len(entries)))
624 # Copy changed trees only.
625 mdb.stream_copy(mdb.sha_iter(), self.repo.odb)
627 # Note: Additional deserialization could be saved if write_tree_from_cache would
628 # return sorted tree entries.
629 root_tree = Tree(self.repo, binsha, path="")
630 root_tree._cache = tree_items
631 return root_tree
633 def _process_diff_args(
634 self,
635 args: List[Union[PathLike, "git_diff.Diffable"]],
636 ) -> List[Union[PathLike, "git_diff.Diffable"]]:
637 try:
638 args.pop(args.index(self))
639 except IndexError:
640 pass
641 # END remove self
642 return args
644 def _to_relative_path(self, path: PathLike) -> PathLike:
645 """
646 :return:
647 Version of path relative to our git directory or raise :exc:`ValueError` if
648 it is not within our git directory.
650 :raise ValueError:
651 """
652 if not osp.isabs(path):
653 return path
654 if self.repo.bare:
655 raise InvalidGitRepositoryError("require non-bare repository")
656 if not str(path).startswith(str(self.repo.working_tree_dir)):
657 raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
658 return os.path.relpath(path, self.repo.working_tree_dir)
660 def _preprocess_add_items(
661 self, items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]
662 ) -> Tuple[List[PathLike], List[BaseIndexEntry]]:
663 """Split the items into two lists of path strings and BaseEntries."""
664 paths = []
665 entries = []
666 # if it is a string put in list
667 if isinstance(items, (str, os.PathLike)):
668 items = [items]
670 for item in items:
671 if isinstance(item, (str, os.PathLike)):
672 paths.append(self._to_relative_path(item))
673 elif isinstance(item, (Blob, Submodule)):
674 entries.append(BaseIndexEntry.from_blob(item))
675 elif isinstance(item, BaseIndexEntry):
676 entries.append(item)
677 else:
678 raise TypeError("Invalid Type: %r" % item)
679 # END for each item
680 return paths, entries
682 def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry:
683 """Store file at filepath in the database and return the base index entry.
685 :note:
686 This needs the :func:`~git.index.util.git_working_dir` decorator active!
687 This must be ensured in the calling code.
688 """
689 st = os.lstat(filepath) # Handles non-symlinks as well.
690 if S_ISLNK(st.st_mode):
691 # In PY3, readlink is a string, but we need bytes.
692 # In PY2, it was just OS encoded bytes, we assumed UTF-8.
693 open_stream: Callable[[], BinaryIO] = lambda: BytesIO(force_bytes(os.readlink(filepath), encoding=defenc))
694 else:
695 open_stream = lambda: open(filepath, "rb")
696 with open_stream() as stream:
697 fprogress(filepath, False, filepath)
698 istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream))
699 fprogress(filepath, True, filepath)
700 return BaseIndexEntry(
701 (
702 stat_mode_to_index_mode(st.st_mode),
703 istream.binsha,
704 0,
705 to_native_path_linux(filepath),
706 )
707 )
709 @unbare_repo
710 @git_working_dir
711 def _entries_for_paths(
712 self,
713 paths: List[str],
714 path_rewriter: Union[Callable, None],
715 fprogress: Callable,
716 entries: List[BaseIndexEntry],
717 ) -> List[BaseIndexEntry]:
718 entries_added: List[BaseIndexEntry] = []
719 if path_rewriter:
720 for path in paths:
721 if osp.isabs(path):
722 abspath = path
723 gitrelative_path = path[len(str(self.repo.working_tree_dir)) + 1 :]
724 else:
725 gitrelative_path = path
726 if self.repo.working_tree_dir:
727 abspath = osp.join(self.repo.working_tree_dir, gitrelative_path)
728 # END obtain relative and absolute paths
730 blob = Blob(
731 self.repo,
732 Blob.NULL_BIN_SHA,
733 stat_mode_to_index_mode(os.stat(abspath).st_mode),
734 to_native_path_linux(gitrelative_path),
735 )
736 # TODO: variable undefined
737 entries.append(BaseIndexEntry.from_blob(blob))
738 # END for each path
739 del paths[:]
740 # END rewrite paths
742 # HANDLE PATHS
743 assert len(entries_added) == 0
744 for filepath in self._iter_expand_paths(paths):
745 entries_added.append(self._store_path(filepath, fprogress))
746 # END for each filepath
747 # END path handling
748 return entries_added
750 def add(
751 self,
752 items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
753 force: bool = True,
754 fprogress: Callable = lambda *args: None,
755 path_rewriter: Union[Callable[..., PathLike], None] = None,
756 write: bool = True,
757 write_extension_data: bool = False,
758 ) -> List[BaseIndexEntry]:
759 R"""Add files from the working tree, specific blobs, or
760 :class:`~git.index.typ.BaseIndexEntry`\s to the index.
762 :param items:
763 Multiple types of items are supported, types can be mixed within one call.
764 Different types imply a different handling. File paths may generally be
765 relative or absolute.
767 - path string
769 Strings denote a relative or absolute path into the repository pointing
770 to an existing file, e.g., ``CHANGES``, `lib/myfile.ext``,
771 ``/home/gitrepo/lib/myfile.ext``.
773 Absolute paths must start with working tree directory of this index's
774 repository to be considered valid. For example, if it was initialized
775 with a non-normalized path, like ``/root/repo/../repo``, absolute paths
776 to be added must start with ``/root/repo/../repo``.
778 Paths provided like this must exist. When added, they will be written
779 into the object database.
781 PathStrings may contain globs, such as ``lib/__init__*``. Or they can be
782 directories like ``lib``, which will add all the files within the
783 directory and subdirectories.
785 This equals a straight :manpage:`git-add(1)`.
787 They are added at stage 0.
789 - :class:~`git.objects.blob.Blob` or
790 :class:`~git.objects.submodule.base.Submodule` object
792 Blobs are added as they are assuming a valid mode is set.
794 The file they refer to may or may not exist in the file system, but must
795 be a path relative to our repository.
797 If their sha is null (40*0), their path must exist in the file system
798 relative to the git repository as an object will be created from the
799 data at the path.
801 The handling now very much equals the way string paths are processed,
802 except that the mode you have set will be kept. This allows you to
803 create symlinks by settings the mode respectively and writing the target
804 of the symlink directly into the file. This equals a default Linux
805 symlink which is not dereferenced automatically, except that it can be
806 created on filesystems not supporting it as well.
808 Please note that globs or directories are not allowed in
809 :class:`~git.objects.blob.Blob` objects.
811 They are added at stage 0.
813 - :class:`~git.index.typ.BaseIndexEntry` or type
815 Handling equals the one of :class:~`git.objects.blob.Blob` objects, but
816 the stage may be explicitly set. Please note that Index Entries require
817 binary sha's.
819 :param force:
820 **CURRENTLY INEFFECTIVE**
821 If ``True``, otherwise ignored or excluded files will be added anyway. As
822 opposed to the :manpage:`git-add(1)` command, we enable this flag by default
823 as the API user usually wants the item to be added even though they might be
824 excluded.
826 :param fprogress:
827 Function with signature ``f(path, done=False, item=item)`` called for each
828 path to be added, one time once it is about to be added where ``done=False``
829 and once after it was added where ``done=True``.
831 ``item`` is set to the actual item we handle, either a path or a
832 :class:`~git.index.typ.BaseIndexEntry`.
834 Please note that the processed path is not guaranteed to be present in the
835 index already as the index is currently being processed.
837 :param path_rewriter:
838 Function, with signature ``(string) func(BaseIndexEntry)``, returning a path
839 for each passed entry which is the path to be actually recorded for the
840 object created from :attr:`entry.path <git.index.typ.BaseIndexEntry.path>`.
841 This allows you to write an index which is not identical to the layout of
842 the actual files on your hard-disk. If not ``None`` and `items` contain
843 plain paths, these paths will be converted to Entries beforehand and passed
844 to the path_rewriter. Please note that ``entry.path`` is relative to the git
845 repository.
847 :param write:
848 If ``True``, the index will be written once it was altered. Otherwise the
849 changes only exist in memory and are not available to git commands.
851 :param write_extension_data:
852 If ``True``, extension data will be written back to the index. This can lead
853 to issues in case it is containing the 'TREE' extension, which will cause
854 the :manpage:`git-commit(1)` command to write an old tree, instead of a new
855 one representing the now changed index.
857 This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the
858 'TREE' extension altogether. You should set it to ``True`` if you intend to
859 use :meth:`IndexFile.commit` exclusively while maintaining support for
860 third-party extensions. Besides that, you can usually safely ignore the
861 built-in extensions when using GitPython on repositories that are not
862 handled manually at all.
864 All current built-in extensions are listed here:
865 https://git-scm.com/docs/index-format
867 :return:
868 List of :class:`~git.index.typ.BaseIndexEntry`\s representing the entries
869 just actually added.
871 :raise OSError:
872 If a supplied path did not exist. Please note that
873 :class:`~git.index.typ.BaseIndexEntry` objects that do not have a null sha
874 will be added even if their paths do not exist.
875 """
876 # Sort the entries into strings and Entries.
877 # Blobs are converted to entries automatically.
878 # Paths can be git-added. For everything else we use git-update-index.
879 paths, entries = self._preprocess_add_items(items)
880 entries_added: List[BaseIndexEntry] = []
881 # This code needs a working tree, so we try not to run it unless required.
882 # That way, we are OK on a bare repository as well.
883 # If there are no paths, the rewriter has nothing to do either.
884 if paths:
885 entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries))
887 # HANDLE ENTRIES
888 if entries:
889 null_mode_entries = [e for e in entries if e.mode == 0]
890 if null_mode_entries:
891 raise ValueError(
892 "At least one Entry has a null-mode - please use index.remove to remove files for clarity"
893 )
894 # END null mode should be remove
896 # HANDLE ENTRY OBJECT CREATION
897 # Create objects if required, otherwise go with the existing shas.
898 null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA]
899 if null_entries_indices:
901 @git_working_dir
902 def handle_null_entries(self: "IndexFile") -> None:
903 for ei in null_entries_indices:
904 null_entry = entries[ei]
905 new_entry = self._store_path(null_entry.path, fprogress)
907 # Update null entry.
908 entries[ei] = BaseIndexEntry(
909 (
910 null_entry.mode,
911 new_entry.binsha,
912 null_entry.stage,
913 null_entry.path,
914 )
915 )
916 # END for each entry index
918 # END closure
920 handle_null_entries(self)
921 # END null_entry handling
923 # REWRITE PATHS
924 # If we have to rewrite the entries, do so now, after we have generated all
925 # object sha's.
926 if path_rewriter:
927 for i, e in enumerate(entries):
928 entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e)))
929 # END for each entry
930 # END handle path rewriting
932 # Just go through the remaining entries and provide progress info.
933 for i, entry in enumerate(entries):
934 progress_sent = i in null_entries_indices
935 if not progress_sent:
936 fprogress(entry.path, False, entry)
937 fprogress(entry.path, True, entry)
938 # END handle progress
939 # END for each entry
940 entries_added.extend(entries)
941 # END if there are base entries
943 # FINALIZE
944 # Add the new entries to this instance.
945 for entry in entries_added:
946 self.entries[(entry.path, 0)] = IndexEntry.from_base(entry)
948 if write:
949 self.write(ignore_extension_data=not write_extension_data)
950 # END handle write
952 return entries_added
954 def _items_to_rela_paths(
955 self,
956 items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]],
957 ) -> List[PathLike]:
958 """Returns a list of repo-relative paths from the given items which
959 may be absolute or relative paths, entries or blobs."""
960 paths = []
961 # If string, put in list.
962 if isinstance(items, (str, os.PathLike)):
963 items = [items]
965 for item in items:
966 if isinstance(item, (BaseIndexEntry, (Blob, Submodule))):
967 paths.append(self._to_relative_path(item.path))
968 elif isinstance(item, (str, os.PathLike)):
969 paths.append(self._to_relative_path(item))
970 else:
971 raise TypeError("Invalid item type: %r" % item)
972 # END for each item
973 return paths
975 @post_clear_cache
976 @default_index
977 def remove(
978 self,
979 items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
980 working_tree: bool = False,
981 **kwargs: Any,
982 ) -> List[str]:
983 R"""Remove the given items from the index and optionally from the working tree
984 as well.
986 :param items:
987 Multiple types of items are supported which may be be freely mixed.
989 - path string
991 Remove the given path at all stages. If it is a directory, you must
992 specify the ``r=True`` keyword argument to remove all file entries below
993 it. If absolute paths are given, they will be converted to a path
994 relative to the git repository directory containing the working tree
996 The path string may include globs, such as ``*.c``.
998 - :class:~`git.objects.blob.Blob` object
1000 Only the path portion is used in this case.
1002 - :class:`~git.index.typ.BaseIndexEntry` or compatible type
1004 The only relevant information here is the path. The stage is ignored.
1006 :param working_tree:
1007 If ``True``, the entry will also be removed from the working tree,
1008 physically removing the respective file. This may fail if there are
1009 uncommitted changes in it.
1011 :param kwargs:
1012 Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as
1013 ``r`` to allow recursive removal.
1015 :return:
1016 List(path_string, ...) list of repository relative paths that have been
1017 removed effectively.
1019 This is interesting to know in case you have provided a directory or globs.
1020 Paths are relative to the repository.
1021 """
1022 args = []
1023 if not working_tree:
1024 args.append("--cached")
1025 args.append("--")
1027 # Preprocess paths.
1028 paths = self._items_to_rela_paths(items)
1029 removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
1031 # Process output to gain proper paths.
1032 # rm 'path'
1033 return [p[4:-1] for p in removed_paths]
1035 @post_clear_cache
1036 @default_index
1037 def move(
1038 self,
1039 items: Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]],
1040 skip_errors: bool = False,
1041 **kwargs: Any,
1042 ) -> List[Tuple[str, str]]:
1043 """Rename/move the items, whereas the last item is considered the destination of
1044 the move operation.
1046 If the destination is a file, the first item (of two) must be a file as well.
1048 If the destination is a directory, it may be preceded by one or more directories
1049 or files.
1051 The working tree will be affected in non-bare repositories.
1053 :param items:
1054 Multiple types of items are supported, please see the :meth:`remove` method
1055 for reference.
1057 :param skip_errors:
1058 If ``True``, errors such as ones resulting from missing source files will be
1059 skipped.
1061 :param kwargs:
1062 Additional arguments you would like to pass to :manpage:`git-mv(1)`, such as
1063 ``dry_run`` or ``force``.
1065 :return:
1066 List(tuple(source_path_string, destination_path_string), ...)
1068 A list of pairs, containing the source file moved as well as its actual
1069 destination. Relative to the repository root.
1071 :raise ValueError:
1072 If only one item was given.
1074 :raise git.exc.GitCommandError:
1075 If git could not handle your request.
1076 """
1077 args = []
1078 if skip_errors:
1079 args.append("-k")
1081 paths = self._items_to_rela_paths(items)
1082 if len(paths) < 2:
1083 raise ValueError("Please provide at least one source and one destination of the move operation")
1085 was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None))
1086 kwargs["dry_run"] = True
1088 # First execute rename in dry run so the command tells us what it actually does
1089 # (for later output).
1090 out = []
1091 mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
1093 # Parse result - first 0:n/2 lines are 'checking ', the remaining ones are the
1094 # 'renaming' ones which we parse.
1095 for ln in range(int(len(mvlines) / 2), len(mvlines)):
1096 tokens = mvlines[ln].split(" to ")
1097 assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
1099 # [0] = Renaming x
1100 # [1] = y
1101 out.append((tokens[0][9:], tokens[1]))
1102 # END for each line to parse
1104 # Either prepare for the real run, or output the dry-run result.
1105 if was_dry_run:
1106 return out
1107 # END handle dry run
1109 # Now apply the actual operation.
1110 kwargs.pop("dry_run")
1111 self.repo.git.mv(args, paths, **kwargs)
1113 return out
1115 def commit(
1116 self,
1117 message: str,
1118 parent_commits: Union[List[Commit], None] = None,
1119 head: bool = True,
1120 author: Union[None, "Actor"] = None,
1121 committer: Union[None, "Actor"] = None,
1122 author_date: Union[datetime.datetime, str, None] = None,
1123 commit_date: Union[datetime.datetime, str, None] = None,
1124 skip_hooks: bool = False,
1125 ) -> Commit:
1126 """Commit the current default index file, creating a
1127 :class:`~git.objects.commit.Commit` object.
1129 For more information on the arguments, see
1130 :meth:`Commit.create_from_tree <git.objects.commit.Commit.create_from_tree>`.
1132 :note:
1133 If you have manually altered the :attr:`entries` member of this instance,
1134 don't forget to :meth:`write` your changes to disk beforehand.
1136 :note:
1137 Passing ``skip_hooks=True`` is the equivalent of using ``-n`` or
1138 ``--no-verify`` on the command line.
1140 :return:
1141 :class:`~git.objects.commit.Commit` object representing the new commit
1142 """
1143 if not skip_hooks:
1144 run_commit_hook("pre-commit", self)
1146 self._write_commit_editmsg(message)
1147 run_commit_hook("commit-msg", self, self._commit_editmsg_filepath())
1148 message = self._read_commit_editmsg()
1149 self._remove_commit_editmsg()
1150 tree = self.write_tree()
1151 rval = Commit.create_from_tree(
1152 self.repo,
1153 tree,
1154 message,
1155 parent_commits,
1156 head,
1157 author=author,
1158 committer=committer,
1159 author_date=author_date,
1160 commit_date=commit_date,
1161 )
1162 if not skip_hooks:
1163 run_commit_hook("post-commit", self)
1164 return rval
1166 def _write_commit_editmsg(self, message: str) -> None:
1167 with open(self._commit_editmsg_filepath(), "wb") as commit_editmsg_file:
1168 commit_editmsg_file.write(message.encode(defenc))
1170 def _remove_commit_editmsg(self) -> None:
1171 os.remove(self._commit_editmsg_filepath())
1173 def _read_commit_editmsg(self) -> str:
1174 with open(self._commit_editmsg_filepath(), "rb") as commit_editmsg_file:
1175 return commit_editmsg_file.read().decode(defenc)
1177 def _commit_editmsg_filepath(self) -> str:
1178 return osp.join(self.repo.common_dir, "COMMIT_EDITMSG")
1180 def _flush_stdin_and_wait(cls, proc: "Popen[bytes]", ignore_stdout: bool = False) -> bytes:
1181 stdin_IO = proc.stdin
1182 if stdin_IO:
1183 stdin_IO.flush()
1184 stdin_IO.close()
1186 stdout = b""
1187 if not ignore_stdout and proc.stdout:
1188 stdout = proc.stdout.read()
1190 if proc.stdout:
1191 proc.stdout.close()
1192 proc.wait()
1193 return stdout
1195 @default_index
1196 def checkout(
1197 self,
1198 paths: Union[None, Iterable[PathLike]] = None,
1199 force: bool = False,
1200 fprogress: Callable = lambda *args: None,
1201 **kwargs: Any,
1202 ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]:
1203 """Check out the given paths or all files from the version known to the index
1204 into the working tree.
1206 :note:
1207 Be sure you have written pending changes using the :meth:`write` method in
1208 case you have altered the entries dictionary directly.
1210 :param paths:
1211 If ``None``, all paths in the index will be checked out.
1212 Otherwise an iterable of relative or absolute paths or a single path
1213 pointing to files or directories in the index is expected.
1215 :param force:
1216 If ``True``, existing files will be overwritten even if they contain local
1217 modifications.
1218 If ``False``, these will trigger a :exc:`~git.exc.CheckoutError`.
1220 :param fprogress:
1221 See :meth:`IndexFile.add` for signature and explanation.
1223 The provided progress information will contain ``None`` as path and item if
1224 no explicit paths are given. Otherwise progress information will be send
1225 prior and after a file has been checked out.
1227 :param kwargs:
1228 Additional arguments to be passed to :manpage:`git-checkout-index(1)`.
1230 :return:
1231 Iterable yielding paths to files which have been checked out and are
1232 guaranteed to match the version stored in the index.
1234 :raise git.exc.CheckoutError:
1235 * If at least one file failed to be checked out. This is a summary, hence it
1236 will checkout as many files as it can anyway.
1237 * If one of files or directories do not exist in the index (as opposed to
1238 the original git command, which ignores them).
1240 :raise git.exc.GitCommandError:
1241 If error lines could not be parsed - this truly is an exceptional state.
1243 :note:
1244 The checkout is limited to checking out the files in the index. Files which
1245 are not in the index anymore and exist in the working tree will not be
1246 deleted. This behaviour is fundamentally different to ``head.checkout``,
1247 i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use
1248 ``head.checkout`` instead of ``index.checkout``.
1249 """
1250 args = ["--index"]
1251 if force:
1252 args.append("--force")
1254 failed_files = []
1255 failed_reasons = []
1256 unknown_lines = []
1258 def handle_stderr(proc: "Popen[bytes]", iter_checked_out_files: Iterable[PathLike]) -> None:
1259 stderr_IO = proc.stderr
1260 if not stderr_IO:
1261 return # Return early if stderr empty.
1263 stderr_bytes = stderr_IO.read()
1264 # line contents:
1265 stderr = stderr_bytes.decode(defenc)
1266 # git-checkout-index: this already exists
1267 endings = (
1268 " already exists",
1269 " is not in the cache",
1270 " does not exist at stage",
1271 " is unmerged",
1272 )
1273 for line in stderr.splitlines():
1274 if not line.startswith("git checkout-index: ") and not line.startswith("git-checkout-index: "):
1275 is_a_dir = " is a directory"
1276 unlink_issue = "unable to unlink old '"
1277 already_exists_issue = " already exists, no checkout" # created by entry.c:checkout_entry(...)
1278 if line.endswith(is_a_dir):
1279 failed_files.append(line[: -len(is_a_dir)])
1280 failed_reasons.append(is_a_dir)
1281 elif line.startswith(unlink_issue):
1282 failed_files.append(line[len(unlink_issue) : line.rfind("'")])
1283 failed_reasons.append(unlink_issue)
1284 elif line.endswith(already_exists_issue):
1285 failed_files.append(line[: -len(already_exists_issue)])
1286 failed_reasons.append(already_exists_issue)
1287 else:
1288 unknown_lines.append(line)
1289 continue
1290 # END special lines parsing
1292 for e in endings:
1293 if line.endswith(e):
1294 failed_files.append(line[20 : -len(e)])
1295 failed_reasons.append(e)
1296 break
1297 # END if ending matches
1298 # END for each possible ending
1299 # END for each line
1300 if unknown_lines:
1301 raise GitCommandError(("git-checkout-index",), 128, stderr)
1302 if failed_files:
1303 valid_files = list(set(iter_checked_out_files) - set(failed_files))
1304 raise CheckoutError(
1305 "Some files could not be checked out from the index due to local modifications",
1306 failed_files,
1307 valid_files,
1308 failed_reasons,
1309 )
1311 # END stderr handler
1313 if paths is None:
1314 args.append("--all")
1315 kwargs["as_process"] = 1
1316 fprogress(None, False, None)
1317 proc = self.repo.git.checkout_index(*args, **kwargs)
1318 proc.wait()
1319 fprogress(None, True, None)
1320 rval_iter = (e.path for e in self.entries.values())
1321 handle_stderr(proc, rval_iter)
1322 return rval_iter
1323 else:
1324 if isinstance(paths, str):
1325 paths = [paths]
1327 # Make sure we have our entries loaded before we start checkout_index, which
1328 # will hold a lock on it. We try to get the lock as well during our entries
1329 # initialization.
1330 self.entries # noqa: B018
1332 args.append("--stdin")
1333 kwargs["as_process"] = True
1334 kwargs["istream"] = subprocess.PIPE
1335 proc = self.repo.git.checkout_index(args, **kwargs)
1336 # FIXME: Reading from GIL!
1337 make_exc = lambda: GitCommandError(("git-checkout-index",) + tuple(args), 128, proc.stderr.read())
1338 checked_out_files: List[PathLike] = []
1340 for path in paths:
1341 co_path = to_native_path_linux(self._to_relative_path(path))
1342 # If the item is not in the index, it could be a directory.
1343 path_is_directory = False
1345 try:
1346 self.entries[(co_path, 0)]
1347 except KeyError:
1348 folder = str(co_path)
1349 if not folder.endswith("/"):
1350 folder += "/"
1351 for entry in self.entries.values():
1352 if str(entry.path).startswith(folder):
1353 p = entry.path
1354 self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
1355 checked_out_files.append(p)
1356 path_is_directory = True
1357 # END if entry is in directory
1358 # END for each entry
1359 # END path exception handlnig
1361 if not path_is_directory:
1362 self._write_path_to_stdin(proc, co_path, path, make_exc, fprogress, read_from_stdout=False)
1363 checked_out_files.append(co_path)
1364 # END path is a file
1365 # END for each path
1366 try:
1367 self._flush_stdin_and_wait(proc, ignore_stdout=True)
1368 except GitCommandError:
1369 # Without parsing stdout we don't know what failed.
1370 raise CheckoutError( # noqa: B904
1371 "Some files could not be checked out from the index, probably because they didn't exist.",
1372 failed_files,
1373 [],
1374 failed_reasons,
1375 )
1377 handle_stderr(proc, checked_out_files)
1378 return checked_out_files
1379 # END paths handling
1381 @default_index
1382 def reset(
1383 self,
1384 commit: Union[Commit, "Reference", str] = "HEAD",
1385 working_tree: bool = False,
1386 paths: Union[None, Iterable[PathLike]] = None,
1387 head: bool = False,
1388 **kwargs: Any,
1389 ) -> "IndexFile":
1390 """Reset the index to reflect the tree at the given commit. This will not adjust
1391 our HEAD reference by default, as opposed to
1392 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1394 :param commit:
1395 Revision, :class:`~git.refs.reference.Reference` or
1396 :class:`~git.objects.commit.Commit` specifying the commit we should
1397 represent.
1399 If you want to specify a tree only, use :meth:`IndexFile.from_tree` and
1400 overwrite the default index.
1402 :param working_tree:
1403 If ``True``, the files in the working tree will reflect the changed index.
1404 If ``False``, the working tree will not be touched.
1405 Please note that changes to the working copy will be discarded without
1406 warning!
1408 :param head:
1409 If ``True``, the head will be set to the given commit. This is ``False`` by
1410 default, but if ``True``, this method behaves like
1411 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`.
1413 :param paths:
1414 If given as an iterable of absolute or repository-relative paths, only these
1415 will be reset to their state at the given commit-ish.
1416 The paths need to exist at the commit, otherwise an exception will be
1417 raised.
1419 :param kwargs:
1420 Additional keyword arguments passed to :manpage:`git-reset(1)`.
1422 :note:
1423 :meth:`IndexFile.reset`, as opposed to
1424 :meth:`HEAD.reset <git.refs.head.HEAD.reset>`, will not delete any files in
1425 order to maintain a consistent working tree. Instead, it will just check out
1426 the files according to their state in the index.
1427 If you want :manpage:`git-reset(1)`-like behaviour, use
1428 :meth:`HEAD.reset <git.refs.head.HEAD.reset>` instead.
1430 :return:
1431 self
1432 """
1433 # What we actually want to do is to merge the tree into our existing index,
1434 # which is what git-read-tree does.
1435 new_inst = type(self).from_tree(self.repo, commit)
1436 if not paths:
1437 self.entries = new_inst.entries
1438 else:
1439 nie = new_inst.entries
1440 for path in paths:
1441 path = self._to_relative_path(path)
1442 try:
1443 key = entry_key(path, 0)
1444 self.entries[key] = nie[key]
1445 except KeyError:
1446 # If key is not in theirs, it musn't be in ours.
1447 try:
1448 del self.entries[key]
1449 except KeyError:
1450 pass
1451 # END handle deletion keyerror
1452 # END handle keyerror
1453 # END for each path
1454 # END handle paths
1455 self.write()
1457 if working_tree:
1458 self.checkout(paths=paths, force=True)
1459 # END handle working tree
1461 if head:
1462 self.repo.head.set_commit(self.repo.commit(commit), logmsg="%s: Updating HEAD" % commit)
1463 # END handle head change
1465 return self
1467 # FIXME: This is documented to accept the same parameters as Diffable.diff, but this
1468 # does not handle NULL_TREE for `other`. (The suppressed mypy error is about this.)
1469 def diff(
1470 self,
1471 other: Union[ # type: ignore[override]
1472 Literal[git_diff.DiffConstants.INDEX],
1473 "Tree",
1474 "Commit",
1475 str,
1476 None,
1477 ] = git_diff.INDEX,
1478 paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
1479 create_patch: bool = False,
1480 **kwargs: Any,
1481 ) -> git_diff.DiffIndex:
1482 """Diff this index against the working copy or a :class:`~git.objects.tree.Tree`
1483 or :class:`~git.objects.commit.Commit` object.
1485 For documentation of the parameters and return values, see
1486 :meth:`Diffable.diff <git.diff.Diffable.diff>`.
1488 :note:
1489 Will only work with indices that represent the default git index as they
1490 have not been initialized with a stream.
1491 """
1492 # Only run if we are the default repository index.
1493 if self._file_path != self._index_path():
1494 raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff())
1495 # Index against index is always empty.
1496 if other is self.INDEX:
1497 return git_diff.DiffIndex()
1499 # Index against anything but None is a reverse diff with the respective item.
1500 # Handle existing -R flags properly.
1501 # Transform strings to the object so that we can call diff on it.
1502 if isinstance(other, str):
1503 other = self.repo.rev_parse(other)
1504 # END object conversion
1506 if isinstance(other, Object): # For Tree or Commit.
1507 # Invert the existing R flag.
1508 cur_val = kwargs.get("R", False)
1509 kwargs["R"] = not cur_val
1510 return other.diff(self.INDEX, paths, create_patch, **kwargs)
1511 # END diff against other item handling
1513 # If other is not None here, something is wrong.
1514 if other is not None:
1515 raise ValueError("other must be None, Diffable.INDEX, a Tree or Commit, was %r" % other)
1517 # Diff against working copy - can be handled by superclass natively.
1518 return super().diff(other, paths, create_patch, **kwargs)