|
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) |
|
0 commit comments