| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,29 +9,28 @@ | |||
| 9 | 9 | import re | |
| 10 | 10 | import warnings | |
| 11 | 11 | ||
| 12 | - from git.cmd import Git, handle_process_output | ||
| 13 | - from git.compat import defenc | ||
| 14 | - from git.objects.base import IndexObject | ||
| 15 | - from git.objects.blob import Blob | ||
| 16 | - from git.objects.util import mode_str_to_int | ||
| 17 | - from git.util import finalize_process, hex_to_bin | ||
| 18 | - | ||
| 19 | 12 | # typing ------------------------------------------------------------------ | |
| 20 | - | ||
| 21 | 13 | from typing import ( | |
| 14 | + TYPE_CHECKING, | ||
| 22 | 15 | Any, | |
| 23 | 16 | Iterator, | |
| 24 | 17 | List, | |
| 25 | 18 | Match, | |
| 26 | 19 | Optional, | |
| 27 | 20 | Sequence, | |
| 28 | 21 | Tuple, | |
| 29 | - TYPE_CHECKING, | ||
| 30 | 22 | TypeVar, | |
| 31 | 23 | Union, | |
| 32 | 24 | cast, | |
| 33 | 25 | ) | |
| 34 | - from git.types import PathLike, Literal | ||
| 26 | + | ||
| 27 | + from git.cmd import Git, handle_process_output | ||
| 28 | + from git.compat import defenc | ||
| 29 | + from git.objects.base import IndexObject | ||
| 30 | + from git.objects.blob import Blob | ||
| 31 | + from git.objects.util import mode_str_to_int | ||
| 32 | + from git.types import Literal, PathLike | ||
| 33 | + from git.util import finalize_process, hex_to_bin | ||
| 35 | 34 | ||
| 36 | 35 | if TYPE_CHECKING: | |
| 37 | 36 | from subprocess import Popen | |
@@ -410,7 +409,8 @@ class Diff: | |||
| 410 | 409 | re_header = re.compile( | |
| 411 | 410 | rb""" | |
| 412 | 411 | ^diff[ ]--git | |
| 413 | - [ ](?P<a_path_fallback>"?[ab]/.+?"?)[ ](?P<b_path_fallback>"?[ab]/.+?"?)\n | ||
| 412 | + [ ](?P<header_names>(?P<a_path_fallback>"?[ab]/.+?"?) | ||
| 413 | + [ ](?P<b_path_fallback>"?[ab]/.+?"?))\n | ||
| 414 | 414 | (?:^old[ ]mode[ ](?P<old_mode>\d+)\n | |
| 415 | 415 | ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? | |
| 416 | 416 | (?:^similarity[ ]index[ ]\d+%\n | |
@@ -419,7 +419,7 @@ class Diff: | |||
| 419 | 419 | (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? | |
| 420 | 420 | (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? | |
| 421 | 421 | (?:^similarity[ ]index[ ]\d+%\n | |
| 422 | - ^copy[ ]from[ ].*\n | ||
| 422 | + ^copy[ ]from[ ](?P<copied_from_name>.*)\n | ||
| 423 | 423 | ^copy[ ]to[ ](?P<copied_file_name>.*)(?:\n|$))? | |
| 424 | 424 | (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) | |
| 425 | 425 | \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? | |
@@ -626,6 +626,42 @@ def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_m | |||
| 626 | 626 | ||
| 627 | 627 | return None | |
| 628 | 628 | ||
| 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. | ||
| 632 | + | ||
| 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. | ||
| 639 | + | ||
| 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 | ||
| 664 | + | ||
| 629 | 665 | @classmethod | |
| 630 | 666 | def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex["Diff"]: | |
| 631 | 667 | """Create a new :class:`DiffIndex` from the given process output which must be | |
@@ -662,6 +698,7 @@ def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> N | |||
| 662 | 698 | a_mode, b_mode = None, None # For mypy. | |
| 663 | 699 | for _header in cls.re_header.finditer(text): | |
| 664 | 700 | ( | |
| 701 | + header_names, | ||
| 665 | 702 | a_path_fallback, | |
| 666 | 703 | b_path_fallback, | |
| 667 | 704 | old_mode, | |
@@ -670,6 +707,7 @@ def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> N | |||
| 670 | 707 | rename_to, | |
| 671 | 708 | new_file_mode, | |
| 672 | 709 | deleted_file_mode, | |
| 710 | + copied_from_name, | ||
| 673 | 711 | copied_file_name, | |
| 674 | 712 | a_blob_id, | |
| 675 | 713 | b_blob_id, | |
@@ -684,8 +722,15 @@ def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> N | |||
| 684 | 722 | bool(copied_file_name), | |
| 685 | 723 | ) | |
| 686 | 724 | ||
| 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) | ||
| 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 | ||
| 689 | 734 | ||
| 690 | 735 | # Our only means to find the actual text is to see what has not been matched | |
| 691 | 736 | # by our regex, and then retro-actively assign it to our index. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,23 +4,21 @@ | |||
| 4 | 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ | |
| 5 | 5 | ||
| 6 | 6 | import gc | |
| 7 | - from io import BytesIO | ||
| 8 | 7 | import os.path as osp | |
| 9 | 8 | import shutil | |
| 10 | 9 | import sys | |
| 11 | 10 | import tempfile | |
| 11 | + from io import BytesIO | ||
| 12 | 12 | ||
| 13 | 13 | import ddt | |
| 14 | 14 | import pytest | |
| 15 | - | ||
| 16 | 15 | from gitdb.base import IStream | |
| 17 | 16 | ||
| 18 | - from git import NULL_TREE, Blob, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule | ||
| 17 | + from git import NULL_TREE, Blob, Diff, Diffable, DiffIndex, GitCommandError, Repo, Submodule | ||
| 19 | 18 | from git.cmd import Git | |
| 20 | 19 | from git.diff import decode_path | |
| 21 | 20 | from git.exc import UnsafeOptionError | |
| 22 | 21 | from git.index.typ import BaseIndexEntry | |
| 23 | - | ||
| 24 | 22 | from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory | |
| 25 | 23 | ||
| 26 | 24 | ||
@@ -270,6 +268,92 @@ def test_diff_raw_paths_with_colon_or_line_feed(self, rw_dir): | |||
| 270 | 268 | self.assertEqual([d.b_path for d in diffs], sorted(paths)) | |
| 271 | 269 | self.assertEqual({d.change_type for d in diffs}, {"A"}) | |
| 272 | 270 | ||
| 271 | + @with_rw_directory | ||
| 272 | + def test_diff_patch_paths_named_only_by_the_header_line(self, rw_dir): | ||
| 273 | + # Binary files, empty files, mode changes and copies have no `---` and `+++` | ||
| 274 | + # lines, so their paths come from the `diff --git a/<path> b/<path>` line, where | ||
| 275 | + # git does not quote a space. Each path contains " a/" or " b/" so that the line | ||
| 276 | + # alone does not show where the first name ends. | ||
| 277 | + repo = Repo.init(rw_dir) | ||
| 278 | + binary = repo.odb.store(IStream(Blob.type, 1, BytesIO(b"\0"))).binsha | ||
| 279 | + empty = repo.odb.store(IStream(Blob.type, 0, BytesIO(b""))).binsha | ||
| 280 | + paths = ["assets b/logo.png", "docs a/empty", "quoted\t a/tab"] | ||
| 281 | + repo.index.add( | ||
| 282 | + [ | ||
| 283 | + BaseIndexEntry((0o100644, binary, 0, paths[0])), | ||
| 284 | + BaseIndexEntry((0o100644, empty, 0, paths[1])), | ||
| 285 | + BaseIndexEntry((0o100644, binary, 0, paths[2])), | ||
| 286 | + ] | ||
| 287 | + ) | ||
| 288 | + commit = repo.index.commit("paths") | ||
| 289 | + | ||
| 290 | + diffs = commit.diff(NULL_TREE, create_patch=True) | ||
| 291 | + self.assertEqual({(d.a_path, d.b_path) for d in diffs}, {(path, path) for path in paths}) | ||
| 292 | + | ||
| 293 | + repo.index.add( | ||
| 294 | + [ | ||
| 295 | + BaseIndexEntry((0o100755, binary, 0, paths[0])), | ||
| 296 | + BaseIndexEntry((0o100644, binary, 0, "copy b/logo.png")), | ||
| 297 | + ] | ||
| 298 | + ) | ||
| 299 | + changed = repo.index.commit("mode change and copy") | ||
| 300 | + | ||
| 301 | + diffs = commit.diff(changed, create_patch=True, C=True, find_copies_harder=True) | ||
| 302 | + self.assertEqual( | ||
| 303 | + {(d.a_path, d.b_path, d.copied_file) for d in diffs}, | ||
| 304 | + {(paths[0], paths[0], False), (paths[0], "copy b/logo.png", True)}, | ||
| 305 | + ) | ||
| 306 | + | ||
| 307 | + @ddt.data( | ||
| 308 | + (b"a/x b/x", b"x"), | ||
| 309 | + (b"a/x b/x ", b"x "), | ||
| 310 | + (b"a/x b/y", None), | ||
| 311 | + (b"a/x b/xx", None), | ||
| 312 | + (b"a/x-b/x", None), | ||
| 313 | + (b"", None), | ||
| 314 | + (b'"a/x\\t" "b/x\\t"', b"x\t"), | ||
| 315 | + (b'"a/x\\t" "b/x\\011"', b"x\t"), | ||
| 316 | + (b'"a/x\\t" "b/y\\t"', None), | ||
| 317 | + (b'"a/x\\t" "b/x\\t', None), | ||
| 318 | + ) | ||
| 319 | + def test_same_path_in_header(self, case): | ||
| 320 | + names, expected = case | ||
| 321 | + self.assertEqual(Diff._same_path_in_header(names), expected) | ||
| 322 | + | ||
| 323 | + def test_same_path_in_header_copies_linear_bytes(self): | ||
| 324 | + class SliceCountingBytes(bytes): | ||
| 325 | + sliced_bytes = 0 | ||
| 326 | + | ||
| 327 | + def __getitem__(self, key): | ||
| 328 | + result = super().__getitem__(key) | ||
| 329 | + if isinstance(key, slice): | ||
| 330 | + self.sliced_bytes += len(result) | ||
| 331 | + return result | ||
| 332 | + | ||
| 333 | + path = b"x " * 1000 | ||
| 334 | + names = SliceCountingBytes(b"a/" + path + b" b/" + path) | ||
| 335 | + self.assertEqual(Diff._same_path_in_header(names), path) | ||
| 336 | + # Count copied bytes rather than asserting a machine-dependent time limit. | ||
| 337 | + self.assertLessEqual(names.sliced_bytes, 2 * len(names)) | ||
| 338 | + | ||
| 339 | + @with_rw_directory | ||
| 340 | + def test_diff_patch_mode_only_with_long_space_heavy_path(self, rw_dir): | ||
| 341 | + repo = Repo.init(rw_dir) | ||
| 342 | + path = b"x " * 250000 | ||
| 343 | + blob = repo.odb.store(IStream(Blob.type, 0, BytesIO(b""))).binsha | ||
| 344 | + trees = [] | ||
| 345 | + # Store trees directly: Git accepts paths longer than filesystem or index limits. | ||
| 346 | + for mode in (b"100644", b"100755"): | ||
| 347 | + data = mode + b" " + path + b"\0" + blob | ||
| 348 | + tree = repo.odb.store(IStream(b"tree", len(data), BytesIO(data))) | ||
| 349 | + trees.append(repo.tree(tree.binsha.hex())) | ||
| 350 | + | ||
| 351 | + diffs = trees[0].diff(trees[1], create_patch=True) | ||
| 352 | + self.assertEqual(len(diffs), 1) | ||
| 353 | + self.assertEqual((diffs[0].a_rawpath, diffs[0].b_rawpath), (path, path)) | ||
| 354 | + self.assertEqual((diffs[0].a_mode, diffs[0].b_mode), (0o100644, 0o100755)) | ||
| 355 | + self.assertEqual(diffs[0].diff, b"") | ||
| 356 | + | ||
| 273 | 357 | def test_empty_diff(self): | |
| 274 | 358 | res = [] | |
| 275 | 359 | Diff._handle_diff_line(b"", None, res) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments