Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/diff.py: 23%
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
12# typing ------------------------------------------------------------------
13from typing import (
14 TYPE_CHECKING,
15 Any,
16 Iterator,
17 List,
18 Match,
19 Optional,
20 Sequence,
21 Tuple,
22 TypeVar,
23 Union,
24 cast,
25)
27from git.cmd import Git, handle_process_output
28from git.compat import defenc
29from git.objects.base import IndexObject
30from git.objects.blob import Blob
31from git.objects.util import mode_str_to_int
32from git.types import Literal, PathLike
33from git.util import finalize_process, hex_to_bin
35if TYPE_CHECKING:
36 from subprocess import Popen
38 from git.objects.commit import Commit
39 from git.objects.tree import Tree
40 from git.repo.base import Repo
42Lit_change_type = Literal["A", "D", "C", "M", "R", "T", "U"]
44# ------------------------------------------------------------------------
47@enum.unique
48class DiffConstants(enum.Enum):
49 """Special objects for :meth:`Diffable.diff`.
51 See the :meth:`Diffable.diff` method's ``other`` parameter, which accepts various
52 values including these.
54 :note:
55 These constants are also available as attributes of the :mod:`git.diff` module,
56 the :class:`Diffable` class and its subclasses and instances, and the top-level
57 :mod:`git` module.
58 """
60 NULL_TREE = enum.auto()
61 """Stand-in indicating you want to compare against the empty tree in diffs.
63 Also accessible as :const:`git.NULL_TREE`, :const:`git.diff.NULL_TREE`, and
64 :const:`Diffable.NULL_TREE`.
65 """
67 INDEX = enum.auto()
68 """Stand-in indicating you want to diff against the index.
70 Also accessible as :const:`git.INDEX`, :const:`git.diff.INDEX`, and
71 :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. The latter has been
72 kept for backward compatibility and made an alias of this, so it may still be used.
73 """
76NULL_TREE: Literal[DiffConstants.NULL_TREE] = DiffConstants.NULL_TREE
77"""Stand-in indicating you want to compare against the empty tree in diffs.
79See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
81This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed as
82:const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`.
83"""
85NULL_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
86"""SHA of Git's canonical empty tree object."""
88INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX
89"""Stand-in indicating you want to diff against the index.
91See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter.
93This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as
94:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
95"""
98def _unquote_path(path: bytes) -> bytes:
99 result = bytearray()
100 escapes = {
101 ord("a"): 7,
102 ord("b"): 8,
103 ord("f"): 12,
104 ord("n"): 10,
105 ord("r"): 13,
106 ord("t"): 9,
107 ord("v"): 11,
108 }
109 i = 0
110 while i < len(path):
111 if path[i] != ord("\\") or i + 1 == len(path):
112 result.append(path[i])
113 i += 1
114 continue
115 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]):
116 result.append(int(path[i + 1 : i + 4], 8))
117 i += 4
118 continue
119 escaped = path[i + 1]
120 if escaped in escapes or escaped in b'\\"':
121 result.append(escapes.get(escaped, escaped))
122 else:
123 result.extend(path[i : i + 2])
124 i += 2
125 return bytes(result)
128def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
129 if path == b"/dev/null":
130 return None
132 if path.startswith(b'"') and path.endswith(b'"'):
133 path = _unquote_path(path[1:-1])
135 if has_ab_prefix:
136 assert path.startswith(b"a/") or path.startswith(b"b/")
137 path = path[2:]
139 return path
142class Diffable:
143 """Common interface for all objects that can be diffed against another object of
144 compatible type.
146 :note:
147 Subclasses require a :attr:`repo` member, as it is the case for
148 :class:`~git.objects.base.Object` instances. For practical reasons we do not
149 derive from :class:`~git.objects.base.Object`.
150 """
152 __slots__ = ()
154 repo: "Repo"
155 """Repository to operate on. Must be provided by subclass or sibling class."""
157 NULL_TREE = NULL_TREE
158 """Stand-in indicating you want to compare against the empty tree in diffs.
160 See the :meth:`diff` method, which accepts this as a value of its ``other``
161 parameter.
163 This is the same as :const:`DiffConstants.NULL_TREE`, and may also be accessed as
164 :const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`.
165 """
167 INDEX = INDEX
168 """Stand-in indicating you want to diff against the index.
170 See the :meth:`diff` method, which accepts this as a value of its ``other``
171 parameter.
173 This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as
174 :const:`git.INDEX` and :const:`git.diff.INDEX`, as well as :class:`Diffable.INDEX`,
175 which is kept for backward compatibility (it is now defined an alias of this).
176 """
178 Index = INDEX
179 """Stand-in indicating you want to diff against the index
180 (same as :const:`~Diffable.INDEX`).
182 This is an alias of :const:`~Diffable.INDEX`, for backward compatibility. See
183 :const:`~Diffable.INDEX` and :meth:`diff` for details.
185 :note:
186 Although always meant for use as an opaque constant, this was formerly defined
187 as a class. Its usage is unchanged, but static type annotations that attempt
188 to permit only this object must be changed to avoid new mypy errors. This was
189 previously not possible to do, though ``Type[Diffable.Index]`` approximated it.
190 It is now possible to do precisely, using ``Literal[DiffConstants.INDEX]``.
191 """
193 def _process_diff_args(
194 self,
195 args: List[Union[PathLike, "Diffable"]],
196 ) -> List[Union[PathLike, "Diffable"]]:
197 """
198 :return:
199 Possibly altered version of the given args list.
200 This method is called right before git command execution.
201 Subclasses can use it to alter the behaviour of the superclass.
202 """
203 return args
205 def diff(
206 self,
207 other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX,
208 paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None,
209 create_patch: bool = False,
210 allow_unsafe_options: bool = False,
211 **kwargs: Any,
212 ) -> "DiffIndex[Diff]":
213 """Create diffs between two items being trees, trees and index or an index and
214 the working tree. Detects renames automatically.
216 :param other:
217 This the item to compare us with.
219 * If ``None``, we will be compared to the working tree.
221 * If a :class:`~git.types.Tree_ish` or string, it will be compared against
222 the respective tree.
224 * If :const:`INDEX`, it will be compared against the index.
226 * If :const:`NULL_TREE`, it will compare against the empty tree.
228 This parameter defaults to :const:`INDEX` (rather than ``None``) so that the
229 method will not by default fail on bare repositories.
231 :param paths:
232 This a list of paths or a single path to limit the diff to. It will only
233 include at least one of the given path or paths.
235 :param create_patch:
236 If ``True``, the returned :class:`Diff` contains a detailed patch that if
237 applied makes the self to other. Patches are somewhat costly as blobs have
238 to be read and diffed.
240 :param allow_unsafe_options:
241 If ``True``, allow options such as ``--output``, ``--no-index``, and ``-O``
242 that can write to or read from arbitrary filesystem paths.
244 :param kwargs:
245 Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to
246 swap both sides of the diff.
248 :return:
249 A :class:`DiffIndex` representing the computed diff.
251 :note:
252 On a bare repository, `other` needs to be provided as :const:`INDEX`, or as
253 an instance of :class:`~git.objects.tree.Tree` or
254 :class:`~git.objects.commit.Commit`, or a git command error will occur.
255 """
256 if not allow_unsafe_options:
257 Git.check_unsafe_options(
258 options=Git._option_candidates([other], kwargs),
259 unsafe_options=self.repo.unsafe_git_diff_options,
260 clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW",
261 )
263 args: List[Union[PathLike, Diffable]] = []
264 args.append("--abbrev=40") # We need full shas.
265 args.append("--full-index") # Get full index paths, not only filenames.
267 # Remove default '-M' arg (check for renames) if user is overriding it.
268 if not any(x in kwargs for x in ("find_renames", "no_renames", "M")):
269 args.append("-M")
271 if create_patch:
272 args.append("-p")
273 args.append("--no-ext-diff")
274 else:
275 args.append("--raw")
276 args.append("-z")
278 # Ensure we never see colored output.
279 # Fixes: https://github.com/gitpython-developers/GitPython/issues/172
280 args.append("--no-color")
282 if paths is not None and not isinstance(paths, (tuple, list)):
283 paths = [paths]
285 diff_cmd = self.repo.git.diff
286 if other is INDEX:
287 args.insert(0, "--cached")
288 elif other is NULL_TREE:
289 args.insert(0, "-r") # Recursive diff-tree.
290 args.insert(0, "--root")
291 diff_cmd = self.repo.git.diff_tree
292 elif other is not None:
293 args.insert(0, "-r") # Recursive diff-tree.
294 args.insert(0, other)
295 diff_cmd = self.repo.git.diff_tree
297 args.insert(0, self)
299 # paths is a list or tuple here, or None.
300 if paths:
301 args.append("--")
302 args.extend(paths)
303 # END paths handling
305 kwargs["as_process"] = True
306 args = self._process_diff_args(args)
307 if create_patch:
308 self.repo.git(c="diff.mnemonicPrefix=false")
309 proc = diff_cmd(*args, **kwargs)
311 diff_method = Diff._index_from_patch_format if create_patch else Diff._index_from_raw_format
312 index = diff_method(self.repo, proc)
314 proc.wait()
315 return index
318T_Diff = TypeVar("T_Diff", bound="Diff")
321class DiffIndex(List[T_Diff]):
322 R"""An index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff
323 properties.
325 The class improves the diff handling convenience.
326 """
328 change_type: Sequence[Literal["A", "C", "D", "R", "M", "T"]] = ("A", "C", "D", "R", "M", "T") # noqa: F821
329 """Change type invariant identifying possible ways a blob can have changed:
331 * ``A`` = Added
332 * ``D`` = Deleted
333 * ``R`` = Renamed
334 * ``M`` = Modified
335 * ``T`` = Changed in the type
336 """
338 def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]:
339 """
340 :return:
341 Iterator yielding :class:`Diff` instances that match the given `change_type`
343 :param change_type:
344 Member of :attr:`DiffIndex.change_type`, namely:
346 * 'A' for added paths
347 * 'D' for deleted paths
348 * 'R' for renamed paths
349 * 'M' for paths with modified data
350 * 'T' for changed in the type paths
351 """
352 if change_type not in self.change_type:
353 raise ValueError("Invalid change type: %s" % change_type)
355 for diffidx in self:
356 if diffidx.change_type == change_type:
357 yield diffidx
358 elif change_type == "A" and diffidx.new_file:
359 yield diffidx
360 elif change_type == "D" and diffidx.deleted_file:
361 yield diffidx
362 elif change_type == "C" and diffidx.copied_file:
363 yield diffidx
364 elif change_type == "R" and diffidx.renamed_file:
365 yield diffidx
366 elif change_type == "M" and diffidx.a_blob and diffidx.b_blob and diffidx.a_blob != diffidx.b_blob:
367 yield diffidx
368 # END for each diff
371class Diff:
372 """A Diff contains diff information between two Trees.
374 It contains two sides a and b of the diff. Members are prefixed with "a" and "b"
375 respectively to indicate that.
377 Diffs keep information about the changed blob objects, the file mode, renames,
378 deletions and new files.
380 For submodule changes, ``a_blob`` and ``b_blob`` are
381 :class:`~git.objects.base.IndexObject` instances whose SHAs refer to commits in
382 the submodule repository.
384 There are a few cases where ``None`` has to be expected as member variable value:
386 New File::
388 a_mode is None
389 a_blob is None
390 a_path is None
392 Deleted File::
394 b_mode is None
395 b_blob is None
396 b_path is None
398 Working Tree Blobs:
400 When comparing to working trees, the working tree blob will have a null hexsha
401 as a corresponding object does not yet exist. The mode will be null as well. The
402 path will be available, though.
404 If it is listed in a diff, the working tree version of the file must differ from
405 the version in the index or tree, and hence has been modified.
406 """
408 # Precompiled regex.
409 re_header = re.compile(
410 rb"""
411 ^diff[ ]--git
412 [ ](?P<header_names>(?P<a_path_fallback>"?[ab]/.+?"?)
413 [ ](?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[ ](?P<copied_from_name>.*)\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 @staticmethod
630 def _same_path_in_header(names: bytes) -> Optional[bytes]:
631 """Return the path that a ``diff --git a/<path> b/<path>`` line names twice.
633 git separates the two names with a space but does not quote a path for
634 containing one, so once the path contains ``" a/"`` or ``" b/"`` the line alone
635 does not show where the first name ends. When no ``---``, ``+++``, rename or
636 copy line names the file, both names are the same path, and the split is
637 accepted only where the two halves agree, as ``git_header_name`` in git's
638 ``apply.c`` does. A quoted first name ends at its closing quote.
640 :return:
641 The path without its ``a/`` prefix, or ``None`` if no split agrees.
642 """
643 if names.startswith(b'"'):
644 end = 1
645 while end < len(names) and names[end : end + 1] != b'"':
646 end += 2 if names[end : end + 1] == b"\\" else 1
647 split = end + 1
648 else:
649 # Equal unquoted paths have equal-length names, so only the midpoint
650 # can separate them. Trying every space would copy the header quadratically.
651 if len(names) % 2 == 0:
652 return None
653 split = len(names) // 2
654 if names[split : split + 1] != b" ":
655 return None
656 a_name, b_name = names[:split], names[split + 1 :]
657 if a_name.startswith(b'"'):
658 if not (a_name.endswith(b'"') and b_name.startswith(b'"') and b_name.endswith(b'"')):
659 return None
660 a_name, b_name = _unquote_path(a_name[1:-1]), _unquote_path(b_name[1:-1])
661 if a_name.startswith(b"a/") and b_name.startswith(b"b/") and a_name[2:] == b_name[2:]:
662 return a_name[2:]
663 return None
665 @classmethod
666 def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex["Diff"]:
667 """Create a new :class:`DiffIndex` from the given process output which must be
668 in patch format.
670 :param repo:
671 The repository we are operating on.
673 :param proc:
674 :manpage:`git-diff(1)` process to read from
675 (supports :class:`Git.AutoInterrupt <git.cmd.Git.AutoInterrupt>` wrapper).
677 :return:
678 :class:`DiffIndex`
679 """
681 # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
682 text_list: List[bytes] = []
683 stderr_list: List[bytes] = []
685 def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
686 finalize_process(proc, stderr=b"".join(stderr_list))
688 handle_process_output(
689 proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False
690 )
692 # For now, we have to bake the stream.
693 text = b"".join(text_list)
694 index: "DiffIndex" = DiffIndex()
695 previous_header: Union[Match[bytes], None] = None
696 header: Union[Match[bytes], None] = None
697 a_path, b_path = None, None # For mypy.
698 a_mode, b_mode = None, None # For mypy.
699 for _header in cls.re_header.finditer(text):
700 (
701 header_names,
702 a_path_fallback,
703 b_path_fallback,
704 old_mode,
705 new_mode,
706 rename_from,
707 rename_to,
708 new_file_mode,
709 deleted_file_mode,
710 copied_from_name,
711 copied_file_name,
712 a_blob_id,
713 b_blob_id,
714 b_mode,
715 a_path,
716 b_path,
717 ) = _header.groups()
719 new_file, deleted_file, copied_file = (
720 bool(new_file_mode),
721 bool(deleted_file_mode),
722 bool(copied_file_name),
723 )
725 # Mode changes, binary files and empty files have no `---` and `+++` lines,
726 # so only the `diff --git` line names them, and it names the same path twice.
727 named_elsewhere = any((a_path, b_path, rename_from, rename_to, copied_from_name, copied_file_name))
728 same_path = None if named_elsewhere else cls._same_path_in_header(header_names)
729 if same_path is None:
730 a_path = cls._pick_best_path(a_path, rename_from or copied_from_name, a_path_fallback)
731 b_path = cls._pick_best_path(b_path, rename_to or copied_file_name, b_path_fallback)
732 else:
733 a_path = b_path = same_path
735 # Our only means to find the actual text is to see what has not been matched
736 # by our regex, and then retro-actively assign it to our index.
737 if previous_header is not None:
738 index[-1].diff = text[previous_header.end() : _header.start()]
739 # END assign actual diff
741 # Make sure the mode is set if the path is set. Otherwise the resulting blob
742 # is invalid. We just use the one mode we should have parsed.
743 a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode))
744 b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode)
745 index.append(
746 Diff(
747 repo,
748 a_path,
749 b_path,
750 a_blob_id and a_blob_id.decode(defenc),
751 b_blob_id and b_blob_id.decode(defenc),
752 a_mode and a_mode.decode(defenc),
753 b_mode and b_mode.decode(defenc),
754 new_file,
755 deleted_file,
756 copied_file,
757 rename_from,
758 rename_to,
759 None,
760 None,
761 None,
762 )
763 )
765 previous_header = _header
766 header = _header
767 # END for each header we parse
768 if index and header:
769 index[-1].diff = text[header.end() :]
770 # END assign last diff
772 return index
774 @staticmethod
775 def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]) -> None:
776 lines = lines_bytes.decode(defenc)
778 # Discard everything before the first colon, and the colon itself.
779 _, _, lines = lines.partition(":")
781 # Each record is a metadata field starting with a colon, followed by one path,
782 # or two for copies and renames, each terminated by NUL. A path may itself
783 # start with a colon or contain a line feed, so read the fields in order
784 # instead of splitting at colons.
785 fields = iter(lines.rstrip("\n").split("\x00"))
786 for meta in fields:
787 if not meta:
788 # The line data is empty, skip.
789 continue
790 if meta.startswith(":"):
791 meta = meta[1:]
792 a_blob_id: Optional[str]
793 b_blob_id: Optional[str]
794 old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4)
795 # Change type can be R100
796 # R: status letter
797 # 100: score (in case of copy and rename)
798 change_type: Lit_change_type = cast(Lit_change_type, _change_type[0])
799 score_str = "".join(_change_type[1:])
800 score = int(score_str) if score_str.isdigit() else None
801 path = next(fields, "")
802 a_path = path.encode(defenc)
803 b_path = path.encode(defenc)
804 deleted_file = False
805 new_file = False
806 copied_file = False
807 rename_from = None
808 rename_to = None
810 # NOTE: We cannot conclude from the existence of a blob to change type,
811 # as diffs with the working do not have blobs yet.
812 if change_type == "D":
813 b_blob_id = None # Optional[str]
814 deleted_file = True
815 elif change_type == "A":
816 a_blob_id = None
817 new_file = True
818 elif change_type == "C":
819 copied_file = True
820 b_path = next(fields, "").encode(defenc)
821 elif change_type == "R":
822 b_path = next(fields, "").encode(defenc)
823 rename_from, rename_to = a_path, b_path
824 elif change_type == "T":
825 # Nothing to do.
826 pass
827 # END add/remove handling
829 diff = Diff(
830 repo,
831 a_path,
832 b_path,
833 a_blob_id,
834 b_blob_id,
835 old_mode,
836 new_mode,
837 new_file,
838 deleted_file,
839 copied_file,
840 rename_from,
841 rename_to,
842 "",
843 change_type,
844 score,
845 )
846 index.append(diff)
848 @classmethod
849 def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]":
850 """Create a new :class:`DiffIndex` from the given process output which must be
851 in raw format.
853 :param repo:
854 The repository we are operating on.
856 :param proc:
857 Process to read output from.
859 :return:
860 :class:`DiffIndex`
861 """
862 # handles
863 # :100644 100644 687099101... 37c5e30c8... M .gitignore
865 index: "DiffIndex" = DiffIndex()
866 text_list: List[bytes] = []
867 stderr_list: List[bytes] = []
869 def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None:
870 finalize_process(proc, stderr=b"".join(stderr_list))
872 # Output is read line by line, but paths may contain line feeds, so parse
873 # the records only once all of it has been read.
874 handle_process_output(
875 proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False
876 )
877 cls._handle_diff_line(b"".join(text_list), repo, index)
879 return index