Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/diff.py: 24%
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__all__ = ["DiffConstants", "NULL_TREE", "NULL_TREE_SHA", "INDEX", "Diffable", "DiffIndex", "Diff"]
8import enum
9import re
10import warnings
12from git.cmd import Git, handle_process_output
13from git.compat import defenc
14from git.objects.base import IndexObject
15from git.objects.blob import Blob
16from git.objects.util import mode_str_to_int
17from git.util import finalize_process, hex_to_bin
19# typing ------------------------------------------------------------------
21from typing import (
22 Any,
23 Iterator,
24 List,
25 Match,
26 Optional,
27 Sequence,
28 Tuple,
29 TYPE_CHECKING,
30 TypeVar,
31 Union,
32 cast,
33)
34from git.types import PathLike, Literal
36if TYPE_CHECKING:
37 from subprocess import Popen
39 from git.objects.commit import Commit
40 from git.objects.tree import Tree
41 from git.repo.base import Repo
43Lit_change_type = Literal["A", "D", "C", "M", "R", "T", "U"]
45# ------------------------------------------------------------------------
48@enum.unique
49class DiffConstants(enum.Enum):
50 """Special objects for :meth:`Diffable.diff`.
52 See the :meth:`Diffable.diff` method's ``other`` parameter, which accepts various
53 values including these.
55 :note:
56 These constants are also available as attributes of the :mod:`git.diff` module,
57 the :class:`Diffable` class and its subclasses and instances, and the top-level
58 :mod:`git` module.
59 """
61 NULL_TREE = enum.auto()
62 """Stand-in indicating you want to compare against the empty tree in diffs.
64 Also accessible as :const:`git.NULL_TREE`, :const:`git.diff.NULL_TREE`, and
65 :const:`Diffable.NULL_TREE`.
66 """
68 INDEX = enum.auto()
69 """Stand-in indicating you want to diff against the index.
71 Also accessible as :const:`git.INDEX`, :const:`git.diff.INDEX`, and
72 :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. The latter has been
73 kept for backward compatibility and made an alias of this, so it may still be used.
74 """
77NULL_TREE: Literal[DiffConstants.NULL_TREE] = DiffConstants.NULL_TREE
78"""Stand-in indicating you want to compare against the empty tree in diffs.
80See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
82This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed as
83:const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`.
84"""
86NULL_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
87"""SHA of Git's canonical empty tree object."""
89INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX
90"""Stand-in indicating you want to diff against the index.
92See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
94This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as
95:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
96"""
99def _unquote_path(path: bytes) -> bytes:
100 result = bytearray()
101 escapes = {
102 ord("a"): 7,
103 ord("b"): 8,
104 ord("f"): 12,
105 ord("n"): 10,
106 ord("r"): 13,
107 ord("t"): 9,
108 ord("v"): 11,
109 }
110 i = 0
111 while i < len(path):
112 if path[i] != ord("\\") or i + 1 == len(path):
113 result.append(path[i])
114 i += 1
115 continue
116 if path[i + 1] in b"0123" and i + 3 < len(path) and all(c in b"01234567" for c in path[i + 2 : i + 4]):
117 result.append(int(path[i + 1 : i + 4], 8))
118 i += 4
119 continue
120 escaped = path[i + 1]
121 if escaped in escapes or escaped in b'\\"':
122 result.append(escapes.get(escaped, escaped))
123 else:
124 result.extend(path[i : i + 2])
125 i += 2
126 return bytes(result)
129def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
130 if path == b"/dev/null":
131 return None
133 if path.startswith(b'"') and path.endswith(b'"'):
134 path = _unquote_path(path[1:-1])
136 if has_ab_prefix:
137 assert path.startswith(b"a/") or path.startswith(b"b/")
138 path = path[2:]
140 return path
143class Diffable:
144 """Common interface for all objects that can be diffed against another object of
145 compatible type.
147 :note:
148 Subclasses require a :attr:`repo` member, as it is the case for
149 :class:`~git.objects.base.Object` instances. For practical reasons we do not
150 derive from :class:`~git.objects.base.Object`.
151 """
153 __slots__ = ()
155 repo: "Repo"
156 """Repository to operate on. Must be provided by subclass or sibling class."""
158 NULL_TREE = NULL_TREE
159 """Stand-in indicating you want to compare against the empty tree in diffs.
161 See the :meth:`diff` method, which accepts this as a value of its ``other``
162 parameter.
164 This is the same as :const:`DiffConstants.NULL_TREE`, and may also be accessed as
165 :const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`.
166 """
168 INDEX = INDEX
169 """Stand-in indicating you want to diff against the index.
171 See the :meth:`diff` method, which accepts this as a value of its ``other``
172 parameter.
174 This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as
175 :const:`git.INDEX` and :const:`git.diff.INDEX`, as well as :class:`Diffable.INDEX`,
176 which is kept for backward compatibility (it is now defined an alias of this).
177 """
179 Index = INDEX
180 """Stand-in indicating you want to diff against the index
181 (same as :const:`~Diffable.INDEX`).
183 This is an alias of :const:`~Diffable.INDEX`, for backward compatibility. See
184 :const:`~Diffable.INDEX` and :meth:`diff` for details.
186 :note:
187 Although always meant for use as an opaque constant, this was formerly defined
188 as a class. Its usage is unchanged, but static type annotations that attempt
189 to permit only this object must be changed to avoid new mypy errors. This was
190 previously not possible to do, though ``Type[Diffable.Index]`` approximated it.
191 It is now possible to do precisely, using ``Literal[DiffConstants.INDEX]``.
192 """
194 def _process_diff_args(
195 self,
196 args: List[Union[PathLike, "Diffable"]],
197 ) -> List[Union[PathLike, "Diffable"]]:
198 """
199 :return:
200 Possibly altered version of the given args list.
201 This method is called right before git command execution.
202 Subclasses can use it to alter the behaviour of the superclass.
203 """
204 return args
206 def diff(
207 self,
208 other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX,
209 paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
210 create_patch: bool = False,
211 allow_unsafe_options: bool = False,
212 **kwargs: Any,
213 ) -> "DiffIndex[Diff]":
214 """Create diffs between two items being trees, trees and index or an index and
215 the working tree. Detects renames automatically.
217 :param other:
218 This the item to compare us with.
220 * If ``None``, we will be compared to the working tree.
222 * If a :class:`~git.types.Tree_ish` or string, it will be compared against
223 the respective tree.
225 * If :const:`INDEX`, it will be compared against the index.
227 * If :const:`NULL_TREE`, it will compare against the empty tree.
229 This parameter defaults to :const:`INDEX` (rather than ``None``) so that the
230 method will not by default fail on bare repositories.
232 :param paths:
233 This a list of paths or a single path to limit the diff to. It will only
234 include at least one of the given path or paths.
236 :param create_patch:
237 If ``True``, the returned :class:`Diff` contains a detailed patch that if
238 applied makes the self to other. Patches are somewhat costly as blobs have
239 to be read and diffed.
241 :param allow_unsafe_options:
242 If ``True``, allow options such as ``--output``, ``--no-index``, and ``-O``
243 that can write to or read from arbitrary filesystem paths.
245 :param kwargs:
246 Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to
247 swap both sides of the diff.
249 :return:
250 A :class:`DiffIndex` representing the computed diff.
252 :note:
253 On a bare repository, `other` needs to be provided as :const:`INDEX`, or as
254 an instance of :class:`~git.objects.tree.Tree` or
255 :class:`~git.objects.commit.Commit`, or a git command error will occur.
256 """
257 if not allow_unsafe_options:
258 Git.check_unsafe_options(
259 options=Git._option_candidates([other], kwargs),
260 unsafe_options=self.repo.unsafe_git_diff_options,
261 clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW",
262 )
264 args: List[Union[PathLike, Diffable]] = []
265 args.append("--abbrev=40") # We need full shas.
266 args.append("--full-index") # Get full index paths, not only filenames.
268 # Remove default '-M' arg (check for renames) if user is overriding it.
269 if not any(x in kwargs for x in ("find_renames", "no_renames", "M")):
270 args.append("-M")
272 if create_patch:
273 args.append("-p")
274 args.append("--no-ext-diff")
275 else:
276 args.append("--raw")
277 args.append("-z")
279 # Ensure we never see colored output.
280 # Fixes: https://github.com/gitpython-developers/GitPython/issues/172
281 args.append("--no-color")
283 if paths is not None and not isinstance(paths, (tuple, list)):
284 paths = [paths]
286 diff_cmd = self.repo.git.diff
287 if other is INDEX:
288 args.insert(0, "--cached")
289 elif other is NULL_TREE:
290 args.insert(0, "-r") # Recursive diff-tree.
291 args.insert(0, "--root")
292 diff_cmd = self.repo.git.diff_tree
293 elif other is not None:
294 args.insert(0, "-r") # Recursive diff-tree.
295 args.insert(0, other)
296 diff_cmd = self.repo.git.diff_tree
298 args.insert(0, self)
300 # paths is a list or tuple here, or None.
301 if paths:
302 args.append("--")
303 args.extend(paths)
304 # END paths handling
306 kwargs["as_process"] = True
307 args = self._process_diff_args(args)
308 if create_patch:
309 self.repo.git(c="diff.mnemonicPrefix=false")
310 proc = diff_cmd(*args, **kwargs)
312 diff_method = Diff._index_from_patch_format if create_patch else Diff._index_from_raw_format
313 index = diff_method(self.repo, proc)
315 proc.wait()
316 return index
319T_Diff = TypeVar("T_Diff", bound="Diff")
322class DiffIndex(List[T_Diff]):
323 R"""An index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
324 properties.
326 The class improves the diff handling convenience.
327 """
329 change_type: Sequence[Literal["A", "C", "D", "R", "M", "T"]] = ("A", "C", "D", "R", "M", "T") # noqa: F821
330 """Change type invariant identifying possible ways a blob can have changed:
332 * ``A`` = Added
333 * ``D`` = Deleted
334 * ``R`` = Renamed
335 * ``M`` = Modified
336 * ``T`` = Changed in the type
337 """
339 def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
340 """
341 :return:
342 Iterator yielding :class:`Diff` instances that match the given `change_type`
344 :param change_type:
345 Member of :attr:`DiffIndex.change_type`, namely:
347 * 'A' for added paths
348 * 'D' for deleted paths
349 * 'R' for renamed paths
350 * 'M' for paths with modified data
351 * 'T' for changed in the type paths
352 """
353 if change_type not in self.change_type:
354 raise ValueError("Invalid change type: %s" % change_type)
356 for diffidx in self:
357 if diffidx.change_type == change_type:
358 yield diffidx
359 elif change_type == "A" and diffidx.new_file:
360 yield diffidx
361 elif change_type == "D" and diffidx.deleted_file:
362 yield diffidx
363 elif change_type == "C" and diffidx.copied_file:
364 yield diffidx
365 elif change_type == "R" and diffidx.renamed_file:
366 yield diffidx
367 elif change_type == "M" and diffidx.a_blob and diffidx.b_blob and diffidx.a_blob != diffidx.b_blob:
368 yield diffidx
369 # END for each diff
372class Diff:
373 """A Diff contains diff information between two Trees.
375 It contains two sides a and b of the diff. Members are prefixed with "a" and "b"
376 respectively to indicate that.
378 Diffs keep information about the changed blob objects, the file mode, renames,
379 deletions and new files.
381 For submodule changes, ``a_blob`` and ``b_blob`` are
382 :class:`~git.objects.base.IndexObject` instances whose SHAs refer to commits in
383 the submodule repository.
385 There are a few cases where ``None`` has to be expected as member variable value:
387 New File::
389 a_mode is None
390 a_blob is None
391 a_path is None
393 Deleted File::
395 b_mode is None
396 b_blob is None
397 b_path is None
399 Working Tree Blobs:
401 When comparing to working trees, the working tree blob will have a null hexsha
402 as a corresponding object does not yet exist. The mode will be null as well. The
403 path will be available, though.
405 If it is listed in a diff, the working tree version of the file must differ from
406 the version in the index or tree, and hence has been modified.
407 """
409 # Precompiled regex.
410 re_header = re.compile(
411 rb"""
412 ^diff[ ]--git
413 [ ](?P<a_path_fallback>"?[ab]/.+?"?)[ ](?P<b_path_fallback>"?[ab]/.+?"?)\n
414 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
415 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
416 (?:^similarity[ ]index[ ]\d+%\n
417 ^rename[ ]from[ ](?P<rename_from>.*)\n
418 ^rename[ ]to[ ](?P<rename_to>.*)(?:\n|$))?
419 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
420 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
421 (?:^similarity[ ]index[ ]\d+%\n
422 ^copy[ ]from[ ].*\n
423 ^copy[ ]to[ ](?P<copied_file_name>.*)(?:\n|$))?
424 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
425 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
426 (?:^---[ ](?P<a_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
427 (?:^\+\+\+[ ](?P<b_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
428 """,
429 re.VERBOSE | re.MULTILINE,
430 )
432 # These can be used for comparisons.
433 NULL_HEX_SHA = "0" * 40
434 NULL_BIN_SHA = b"\0" * 20
436 __slots__ = (
437 "a_blob",
438 "b_blob",
439 "a_mode",
440 "b_mode",
441 "a_rawpath",
442 "b_rawpath",
443 "new_file",
444 "deleted_file",
445 "copied_file",
446 "raw_rename_from",
447 "raw_rename_to",
448 "diff",
449 "change_type",
450 "score",
451 )
453 def __init__(
454 self,
455 repo: "Repo",
456 a_rawpath: Optional[bytes],
457 b_rawpath: Optional[bytes],
458 a_blob_id: Union[str, bytes, None],
459 b_blob_id: Union[str, bytes, None],
460 a_mode: Union[bytes, str, None],
461 b_mode: Union[bytes, str, None],
462 new_file: bool,
463 deleted_file: bool,
464 copied_file: bool,
465 raw_rename_from: Optional[bytes],
466 raw_rename_to: Optional[bytes],
467 diff: Union[str, bytes, None],
468 change_type: Optional[Lit_change_type],
469 score: Optional[int],
470 ) -> None:
471 assert a_rawpath is None or isinstance(a_rawpath, bytes)
472 assert b_rawpath is None or isinstance(b_rawpath, bytes)
473 self.a_rawpath = a_rawpath
474 self.b_rawpath = b_rawpath
476 self.a_mode = mode_str_to_int(a_mode) if a_mode else None
477 self.b_mode = mode_str_to_int(b_mode) if b_mode else None
479 # Determine whether this diff references a submodule. If it does then
480 # we need to overwrite "repo" to the corresponding submodule's repo instead.
481 if repo and a_rawpath:
482 for submodule in repo.submodules:
483 if submodule.path == a_rawpath.decode(defenc, "replace"):
484 if submodule.module_exists():
485 repo = submodule.module()
486 break
488 # Gitlinks reference commits; generic index objects preserve their path and mode.
489 self.a_blob: Union["IndexObject", None]
490 if a_blob_id is None or a_blob_id == self.NULL_HEX_SHA:
491 self.a_blob = None
492 else:
493 self.a_blob = (IndexObject if self.a_mode == 0o160000 else Blob)(
494 repo, hex_to_bin(a_blob_id), mode=self.a_mode, path=self.a_path
495 )
497 self.b_blob: Union["IndexObject", None]
498 if b_blob_id is None or b_blob_id == self.NULL_HEX_SHA:
499 self.b_blob = None
500 else:
501 self.b_blob = (IndexObject if self.b_mode == 0o160000 else Blob)(
502 repo, hex_to_bin(b_blob_id), mode=self.b_mode, path=self.b_path
503 )
505 self.new_file: bool = new_file
506 self.deleted_file: bool = deleted_file
507 self.copied_file: bool = copied_file
509 # Be clear and use None instead of empty strings.
510 assert raw_rename_from is None or isinstance(raw_rename_from, bytes)
511 assert raw_rename_to is None or isinstance(raw_rename_to, bytes)
512 self.raw_rename_from = raw_rename_from or None
513 self.raw_rename_to = raw_rename_to or None
515 self.diff = diff
516 self.change_type: Union[Lit_change_type, None] = change_type
517 self.score = score
519 def __eq__(self, other: object) -> bool:
520 for name in self.__slots__:
521 if getattr(self, name) != getattr(other, name):
522 return False
523 # END for each name
524 return True
526 def __ne__(self, other: object) -> bool:
527 return not (self == other)
529 def __hash__(self) -> int:
530 return hash(tuple(getattr(self, n) for n in self.__slots__))
532 def __str__(self) -> str:
533 h = "%s"
534 if self.a_blob:
535 h %= self.a_blob.path
536 elif self.b_blob:
537 h %= self.b_blob.path
539 msg = ""
540 line = None
541 line_length = 0
542 for b, n in zip((self.a_blob, self.b_blob), ("lhs", "rhs")):
543 if b:
544 line = "\n%s: %o | %s" % (n, b.mode, b.hexsha)
545 else:
546 line = "\n%s: None" % n
547 # END if blob is not None
548 line_length = max(len(line), line_length)
549 msg += line
550 # END for each blob
552 # Add headline.
553 h += "\n" + "=" * line_length
555 if self.deleted_file:
556 msg += "\nfile deleted in rhs"
557 if self.new_file:
558 msg += "\nfile added in rhs"
559 if self.copied_file:
560 msg += "\nfile %r copied from %r" % (self.b_path, self.a_path)
561 if self.rename_from:
562 msg += "\nfile renamed from %r" % self.rename_from
563 if self.rename_to:
564 msg += "\nfile renamed to %r" % self.rename_to
565 if self.diff:
566 msg += "\n---"
567 try:
568 msg += self.diff.decode(defenc) if isinstance(self.diff, bytes) else self.diff
569 except UnicodeDecodeError:
570 msg += "OMITTED BINARY DATA"
571 # END handle encoding
572 msg += "\n---"
573 # END diff info
575 return h + msg
577 @property
578 def a_path(self) -> Optional[str]:
579 return self.a_rawpath.decode(defenc, "replace") if self.a_rawpath else None
581 @property
582 def b_path(self) -> Optional[str]:
583 return self.b_rawpath.decode(defenc, "replace") if self.b_rawpath else None
585 @property
586 def rename_from(self) -> Optional[str]:
587 return self.raw_rename_from.decode(defenc, "replace") if self.raw_rename_from else None
589 @property
590 def rename_to(self) -> Optional[str]:
591 return self.raw_rename_to.decode(defenc, "replace") if self.raw_rename_to else None
593 @property
594 def renamed(self) -> bool:
595 """Deprecated, use :attr:`renamed_file` instead.
597 :return:
598 ``True`` if the blob of our diff has been renamed
600 :note:
601 This property is deprecated.
602 Please use the :attr:`renamed_file` property instead.
603 """
604 warnings.warn(
605 "Diff.renamed is deprecated, use Diff.renamed_file instead",
606 DeprecationWarning,
607 stacklevel=2,
608 )
609 return self.renamed_file
611 @property
612 def renamed_file(self) -> bool:
613 """:return: ``True`` if the blob of our diff has been renamed"""
614 return self.rename_from != self.rename_to
616 @classmethod
617 def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_match: bytes) -> Optional[bytes]:
618 if path_match:
619 return decode_path(path_match)
621 if rename_match:
622 return decode_path(rename_match, has_ab_prefix=False)
624 if path_fallback_match:
625 return decode_path(path_fallback_match)
627 return None
629 @classmethod
630 def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex["Diff"]:
631 """Create a new :class:`DiffIndex` from the given process output which must be
632 in patch format.
634 :param repo:
635 The repository we are operating on.
637 :param proc:
638 :manpage:`git-diff(1)` process to read from
639 (supports :class:`Git.AutoInterrupt <git.cmd.Git.AutoInterrupt>` wrapper).
641 :return:
642 :class:`DiffIndex`
643 """
645 # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
646 text_list: List[bytes] = []
647 stderr_list: List[bytes] = []
649 def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
650 finalize_process(proc, stderr=b"".join(stderr_list))
652 handle_process_output(
653 proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False
654 )
656 # For now, we have to bake the stream.
657 text = b"".join(text_list)
658 index: "DiffIndex" = DiffIndex()
659 previous_header: Union[Match[bytes], None] = None
660 header: Union[Match[bytes], None] = None
661 a_path, b_path = None, None # For mypy.
662 a_mode, b_mode = None, None # For mypy.
663 for _header in cls.re_header.finditer(text):
664 (
665 a_path_fallback,
666 b_path_fallback,
667 old_mode,
668 new_mode,
669 rename_from,
670 rename_to,
671 new_file_mode,
672 deleted_file_mode,
673 copied_file_name,
674 a_blob_id,
675 b_blob_id,
676 b_mode,
677 a_path,
678 b_path,
679 ) = _header.groups()
681 new_file, deleted_file, copied_file = (
682 bool(new_file_mode),
683 bool(deleted_file_mode),
684 bool(copied_file_name),
685 )
687 a_path = cls._pick_best_path(a_path, rename_from, a_path_fallback)
688 b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback)
690 # Our only means to find the actual text is to see what has not been matched
691 # by our regex, and then retro-actively assign it to our index.
692 if previous_header is not None:
693 index[-1].diff = text[previous_header.end() : _header.start()]
694 # END assign actual diff
696 # Make sure the mode is set if the path is set. Otherwise the resulting blob
697 # is invalid. We just use the one mode we should have parsed.
698 a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode))
699 b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode)
700 index.append(
701 Diff(
702 repo,
703 a_path,
704 b_path,
705 a_blob_id and a_blob_id.decode(defenc),
706 b_blob_id and b_blob_id.decode(defenc),
707 a_mode and a_mode.decode(defenc),
708 b_mode and b_mode.decode(defenc),
709 new_file,
710 deleted_file,
711 copied_file,
712 rename_from,
713 rename_to,
714 None,
715 None,
716 None,
717 )
718 )
720 previous_header = _header
721 header = _header
722 # END for each header we parse
723 if index and header:
724 index[-1].diff = text[header.end() :]
725 # END assign last diff
727 return index
729 @staticmethod
730 def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]) -> None:
731 lines = lines_bytes.decode(defenc)
733 # Discard everything before the first colon, and the colon itself.
734 _, _, lines = lines.partition(":")
736 for line in lines.split("\x00:"):
737 if not line:
738 # The line data is empty, skip.
739 continue
740 meta, _, path = line.partition("\x00")
741 path = path.rstrip("\x00")
742 a_blob_id: Optional[str]
743 b_blob_id: Optional[str]
744 old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4)
745 # Change type can be R100
746 # R: status letter
747 # 100: score (in case of copy and rename)
748 change_type: Lit_change_type = cast(Lit_change_type, _change_type[0])
749 score_str = "".join(_change_type[1:])
750 score = int(score_str) if score_str.isdigit() else None
751 path = path.strip("\n")
752 a_path = path.encode(defenc)
753 b_path = path.encode(defenc)
754 deleted_file = False
755 new_file = False
756 copied_file = False
757 rename_from = None
758 rename_to = None
760 # NOTE: We cannot conclude from the existence of a blob to change type,
761 # as diffs with the working do not have blobs yet.
762 if change_type == "D":
763 b_blob_id = None # Optional[str]
764 deleted_file = True
765 elif change_type == "A":
766 a_blob_id = None
767 new_file = True
768 elif change_type == "C":
769 copied_file = True
770 a_path_str, b_path_str = path.split("\x00", 1)
771 a_path = a_path_str.encode(defenc)
772 b_path = b_path_str.encode(defenc)
773 elif change_type == "R":
774 a_path_str, b_path_str = path.split("\x00", 1)
775 a_path = a_path_str.encode(defenc)
776 b_path = b_path_str.encode(defenc)
777 rename_from, rename_to = a_path, b_path
778 elif change_type == "T":
779 # Nothing to do.
780 pass
781 # END add/remove handling
783 diff = Diff(
784 repo,
785 a_path,
786 b_path,
787 a_blob_id,
788 b_blob_id,
789 old_mode,
790 new_mode,
791 new_file,
792 deleted_file,
793 copied_file,
794 rename_from,
795 rename_to,
796 "",
797 change_type,
798 score,
799 )
800 index.append(diff)
802 @classmethod
803 def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]":
804 """Create a new :class:`DiffIndex` from the given process output which must be
805 in raw format.
807 :param repo:
808 The repository we are operating on.
810 :param proc:
811 Process to read output from.
813 :return:
814 :class:`DiffIndex`
815 """
816 # handles
817 # :100644 100644 687099101... 37c5e30c8... M .gitignore
819 index: "DiffIndex" = DiffIndex()
820 stderr_list: List[bytes] = []
822 def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
823 finalize_process(proc, stderr=b"".join(stderr_list))
825 handle_process_output(
826 proc,
827 lambda byt: cls._handle_diff_line(byt, repo, index),
828 stderr_list.append,
829 finalize_process_with_stderr,
830 decode_streams=False,
831 )
833 return index