Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/diff.py: 27%
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.blob import Blob
15from git.objects.util import mode_str_to_int
16from git.util import finalize_process, hex_to_bin
18# typing ------------------------------------------------------------------
20from typing import (
21 Any,
22 Iterator,
23 List,
24 Match,
25 Optional,
26 Sequence,
27 Tuple,
28 TYPE_CHECKING,
29 TypeVar,
30 Union,
31 cast,
32)
33from git.types import PathLike, Literal
35if TYPE_CHECKING:
36 from subprocess import Popen
38 from git.objects.base import IndexObject
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 There are a few cases where ``None`` has to be expected as member variable value:
383 New File::
385 a_mode is None
386 a_blob is None
387 a_path is None
389 Deleted File::
391 b_mode is None
392 b_blob is None
393 b_path is None
395 Working Tree Blobs:
397 When comparing to working trees, the working tree blob will have a null hexsha
398 as a corresponding object does not yet exist. The mode will be null as well. The
399 path will be available, though.
401 If it is listed in a diff, the working tree version of the file must differ from
402 the version in the index or tree, and hence has been modified.
403 """
405 # Precompiled regex.
406 re_header = re.compile(
407 rb"""
408 ^diff[ ]--git
409 [ ](?P<a_path_fallback>"?[ab]/.+?"?)[ ](?P<b_path_fallback>"?[ab]/.+?"?)\n
410 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
411 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
412 (?:^similarity[ ]index[ ]\d+%\n
413 ^rename[ ]from[ ](?P<rename_from>.*)\n
414 ^rename[ ]to[ ](?P<rename_to>.*)(?:\n|$))?
415 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
416 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
417 (?:^similarity[ ]index[ ]\d+%\n
418 ^copy[ ]from[ ].*\n
419 ^copy[ ]to[ ](?P<copied_file_name>.*)(?:\n|$))?
420 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
421 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
422 (?:^---[ ](?P<a_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
423 (?:^\+\+\+[ ](?P<b_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))?
424 """,
425 re.VERBOSE | re.MULTILINE,
426 )
428 # These can be used for comparisons.
429 NULL_HEX_SHA = "0" * 40
430 NULL_BIN_SHA = b"\0" * 20
432 __slots__ = (
433 "a_blob",
434 "b_blob",
435 "a_mode",
436 "b_mode",
437 "a_rawpath",
438 "b_rawpath",
439 "new_file",
440 "deleted_file",
441 "copied_file",
442 "raw_rename_from",
443 "raw_rename_to",
444 "diff",
445 "change_type",
446 "score",
447 )
449 def __init__(
450 self,
451 repo: "Repo",
452 a_rawpath: Optional[bytes],
453 b_rawpath: Optional[bytes],
454 a_blob_id: Union[str, bytes, None],
455 b_blob_id: Union[str, bytes, None],
456 a_mode: Union[bytes, str, None],
457 b_mode: Union[bytes, str, None],
458 new_file: bool,
459 deleted_file: bool,
460 copied_file: bool,
461 raw_rename_from: Optional[bytes],
462 raw_rename_to: Optional[bytes],
463 diff: Union[str, bytes, None],
464 change_type: Optional[Lit_change_type],
465 score: Optional[int],
466 ) -> None:
467 assert a_rawpath is None or isinstance(a_rawpath, bytes)
468 assert b_rawpath is None or isinstance(b_rawpath, bytes)
469 self.a_rawpath = a_rawpath
470 self.b_rawpath = b_rawpath
472 self.a_mode = mode_str_to_int(a_mode) if a_mode else None
473 self.b_mode = mode_str_to_int(b_mode) if b_mode else None
475 # Determine whether this diff references a submodule. If it does then
476 # we need to overwrite "repo" to the corresponding submodule's repo instead.
477 if repo and a_rawpath:
478 for submodule in repo.submodules:
479 if submodule.path == a_rawpath.decode(defenc, "replace"):
480 if submodule.module_exists():
481 repo = submodule.module()
482 break
484 self.a_blob: Union["IndexObject", None]
485 if a_blob_id is None or a_blob_id == self.NULL_HEX_SHA:
486 self.a_blob = None
487 else:
488 self.a_blob = Blob(repo, hex_to_bin(a_blob_id), mode=self.a_mode, path=self.a_path)
490 self.b_blob: Union["IndexObject", None]
491 if b_blob_id is None or b_blob_id == self.NULL_HEX_SHA:
492 self.b_blob = None
493 else:
494 self.b_blob = Blob(repo, hex_to_bin(b_blob_id), mode=self.b_mode, path=self.b_path)
496 self.new_file: bool = new_file
497 self.deleted_file: bool = deleted_file
498 self.copied_file: bool = copied_file
500 # Be clear and use None instead of empty strings.
501 assert raw_rename_from is None or isinstance(raw_rename_from, bytes)
502 assert raw_rename_to is None or isinstance(raw_rename_to, bytes)
503 self.raw_rename_from = raw_rename_from or None
504 self.raw_rename_to = raw_rename_to or None
506 self.diff = diff
507 self.change_type: Union[Lit_change_type, None] = change_type
508 self.score = score
510 def __eq__(self, other: object) -> bool:
511 for name in self.__slots__:
512 if getattr(self, name) != getattr(other, name):
513 return False
514 # END for each name
515 return True
517 def __ne__(self, other: object) -> bool:
518 return not (self == other)
520 def __hash__(self) -> int:
521 return hash(tuple(getattr(self, n) for n in self.__slots__))
523 def __str__(self) -> str:
524 h = "%s"
525 if self.a_blob:
526 h %= self.a_blob.path
527 elif self.b_blob:
528 h %= self.b_blob.path
530 msg = ""
531 line = None
532 line_length = 0
533 for b, n in zip((self.a_blob, self.b_blob), ("lhs", "rhs")):
534 if b:
535 line = "\n%s: %o | %s" % (n, b.mode, b.hexsha)
536 else:
537 line = "\n%s: None" % n
538 # END if blob is not None
539 line_length = max(len(line), line_length)
540 msg += line
541 # END for each blob
543 # Add headline.
544 h += "\n" + "=" * line_length
546 if self.deleted_file:
547 msg += "\nfile deleted in rhs"
548 if self.new_file:
549 msg += "\nfile added in rhs"
550 if self.copied_file:
551 msg += "\nfile %r copied from %r" % (self.b_path, self.a_path)
552 if self.rename_from:
553 msg += "\nfile renamed from %r" % self.rename_from
554 if self.rename_to:
555 msg += "\nfile renamed to %r" % self.rename_to
556 if self.diff:
557 msg += "\n---"
558 try:
559 msg += self.diff.decode(defenc) if isinstance(self.diff, bytes) else self.diff
560 except UnicodeDecodeError:
561 msg += "OMITTED BINARY DATA"
562 # END handle encoding
563 msg += "\n---"
564 # END diff info
566 return h + msg
568 @property
569 def a_path(self) -> Optional[str]:
570 return self.a_rawpath.decode(defenc, "replace") if self.a_rawpath else None
572 @property
573 def b_path(self) -> Optional[str]:
574 return self.b_rawpath.decode(defenc, "replace") if self.b_rawpath else None
576 @property
577 def rename_from(self) -> Optional[str]:
578 return self.raw_rename_from.decode(defenc, "replace") if self.raw_rename_from else None
580 @property
581 def rename_to(self) -> Optional[str]:
582 return self.raw_rename_to.decode(defenc, "replace") if self.raw_rename_to else None
584 @property
585 def renamed(self) -> bool:
586 """Deprecated, use :attr:`renamed_file` instead.
588 :return:
589 ``True`` if the blob of our diff has been renamed
591 :note:
592 This property is deprecated.
593 Please use the :attr:`renamed_file` property instead.
594 """
595 warnings.warn(
596 "Diff.renamed is deprecated, use Diff.renamed_file instead",
597 DeprecationWarning,
598 stacklevel=2,
599 )
600 return self.renamed_file
602 @property
603 def renamed_file(self) -> bool:
604 """:return: ``True`` if the blob of our diff has been renamed"""
605 return self.rename_from != self.rename_to
607 @classmethod
608 def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_match: bytes) -> Optional[bytes]:
609 if path_match:
610 return decode_path(path_match)
612 if rename_match:
613 return decode_path(rename_match, has_ab_prefix=False)
615 if path_fallback_match:
616 return decode_path(path_fallback_match)
618 return None
620 @classmethod
621 def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex["Diff"]:
622 """Create a new :class:`DiffIndex` from the given process output which must be
623 in patch format.
625 :param repo:
626 The repository we are operating on.
628 :param proc:
629 :manpage:`git-diff(1)` process to read from
630 (supports :class:`Git.AutoInterrupt <git.cmd.Git.AutoInterrupt>` wrapper).
632 :return:
633 :class:`DiffIndex`
634 """
636 # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
637 text_list: List[bytes] = []
638 stderr_list: List[bytes] = []
640 def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
641 finalize_process(proc, stderr=b"".join(stderr_list))
643 handle_process_output(
644 proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False
645 )
647 # For now, we have to bake the stream.
648 text = b"".join(text_list)
649 index: "DiffIndex" = DiffIndex()
650 previous_header: Union[Match[bytes], None] = None
651 header: Union[Match[bytes], None] = None
652 a_path, b_path = None, None # For mypy.
653 a_mode, b_mode = None, None # For mypy.
654 for _header in cls.re_header.finditer(text):
655 (
656 a_path_fallback,
657 b_path_fallback,
658 old_mode,
659 new_mode,
660 rename_from,
661 rename_to,
662 new_file_mode,
663 deleted_file_mode,
664 copied_file_name,
665 a_blob_id,
666 b_blob_id,
667 b_mode,
668 a_path,
669 b_path,
670 ) = _header.groups()
672 new_file, deleted_file, copied_file = (
673 bool(new_file_mode),
674 bool(deleted_file_mode),
675 bool(copied_file_name),
676 )
678 a_path = cls._pick_best_path(a_path, rename_from, a_path_fallback)
679 b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback)
681 # Our only means to find the actual text is to see what has not been matched
682 # by our regex, and then retro-actively assign it to our index.
683 if previous_header is not None:
684 index[-1].diff = text[previous_header.end() : _header.start()]
685 # END assign actual diff
687 # Make sure the mode is set if the path is set. Otherwise the resulting blob
688 # is invalid. We just use the one mode we should have parsed.
689 a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode))
690 b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode)
691 index.append(
692 Diff(
693 repo,
694 a_path,
695 b_path,
696 a_blob_id and a_blob_id.decode(defenc),
697 b_blob_id and b_blob_id.decode(defenc),
698 a_mode and a_mode.decode(defenc),
699 b_mode and b_mode.decode(defenc),
700 new_file,
701 deleted_file,
702 copied_file,
703 rename_from,
704 rename_to,
705 None,
706 None,
707 None,
708 )
709 )
711 previous_header = _header
712 header = _header
713 # END for each header we parse
714 if index and header:
715 index[-1].diff = text[header.end() :]
716 # END assign last diff
718 return index
720 @staticmethod
721 def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]) -> None:
722 lines = lines_bytes.decode(defenc)
724 # Discard everything before the first colon, and the colon itself.
725 _, _, lines = lines.partition(":")
727 for line in lines.split("\x00:"):
728 if not line:
729 # The line data is empty, skip.
730 continue
731 meta, _, path = line.partition("\x00")
732 path = path.rstrip("\x00")
733 a_blob_id: Optional[str]
734 b_blob_id: Optional[str]
735 old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4)
736 # Change type can be R100
737 # R: status letter
738 # 100: score (in case of copy and rename)
739 change_type: Lit_change_type = cast(Lit_change_type, _change_type[0])
740 score_str = "".join(_change_type[1:])
741 score = int(score_str) if score_str.isdigit() else None
742 path = path.strip("\n")
743 a_path = path.encode(defenc)
744 b_path = path.encode(defenc)
745 deleted_file = False
746 new_file = False
747 copied_file = False
748 rename_from = None
749 rename_to = None
751 # NOTE: We cannot conclude from the existence of a blob to change type,
752 # as diffs with the working do not have blobs yet.
753 if change_type == "D":
754 b_blob_id = None # Optional[str]
755 deleted_file = True
756 elif change_type == "A":
757 a_blob_id = None
758 new_file = True
759 elif change_type == "C":
760 copied_file = True
761 a_path_str, b_path_str = path.split("\x00", 1)
762 a_path = a_path_str.encode(defenc)
763 b_path = b_path_str.encode(defenc)
764 elif change_type == "R":
765 a_path_str, b_path_str = path.split("\x00", 1)
766 a_path = a_path_str.encode(defenc)
767 b_path = b_path_str.encode(defenc)
768 rename_from, rename_to = a_path, b_path
769 elif change_type == "T":
770 # Nothing to do.
771 pass
772 # END add/remove handling
774 diff = Diff(
775 repo,
776 a_path,
777 b_path,
778 a_blob_id,
779 b_blob_id,
780 old_mode,
781 new_mode,
782 new_file,
783 deleted_file,
784 copied_file,
785 rename_from,
786 rename_to,
787 "",
788 change_type,
789 score,
790 )
791 index.append(diff)
793 @classmethod
794 def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]":
795 """Create a new :class:`DiffIndex` from the given process output which must be
796 in raw format.
798 :param repo:
799 The repository we are operating on.
801 :param proc:
802 Process to read output from.
804 :return:
805 :class:`DiffIndex`
806 """
807 # handles
808 # :100644 100644 687099101... 37c5e30c8... M .gitignore
810 index: "DiffIndex" = DiffIndex()
811 stderr_list: List[bytes] = []
813 def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
814 finalize_process(proc, stderr=b"".join(stderr_list))
816 handle_process_output(
817 proc,
818 lambda byt: cls._handle_diff_line(byt, repo, index),
819 stderr_list.append,
820 finalize_process_with_stderr,
821 decode_streams=False,
822 )
824 return index