| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent d72ffb3 commit a467e99
1 file changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -28,7 +28,7 @@ | |||
| 28 | 28 | from git.objects.submodule.base import Submodule | |
| 29 | 29 | from git.objects.submodule.root import RootModule, RootUpdateProgress | |
| 30 | 30 | from git.repo.fun import find_submodule_git_dir, touch | |
| 31 | - from git.util import HIDE_WINDOWS_KNOWN_ERRORS, cwd, join_path_native, to_native_path_linux | ||
| 31 | + from git.util import HIDE_WINDOWS_KNOWN_ERRORS, cwd, join_path_native, to_native_path_linux, rmtree | ||
| 32 | 32 | ||
| 33 | 33 | from test.lib import TestBase, with_rw_directory, with_rw_repo, PathLikeMock | |
| 34 | 34 | ||
@@ -163,6 +163,162 @@ def test_movable_submodule_copies_are_isolated(movable_submodule, movable_submod | |||
| 163 | 163 | assert "copy-only" not in original.heads | |
| 164 | 164 | ||
| 165 | 165 | ||
| 166 | + def _copy_rejection_layout(source, destination): | ||
| 167 | + """Copy files normally, then recreate directory links without symlink metadata.""" | ||
| 168 | + links = {} | ||
| 169 | + | ||
| 170 | + def ignore_links(directory, names): | ||
| 171 | + ignored = [] | ||
| 172 | + for name in names: | ||
| 173 | + path = Path(directory, name) | ||
| 174 | + if path.is_symlink(): | ||
| 175 | + links[path.relative_to(source)] = os.readlink(path) | ||
| 176 | + ignored.append(name) | ||
| 177 | + return ignored | ||
| 178 | + | ||
| 179 | + shutil.copytree(source, destination, ignore=ignore_links) | ||
| 180 | + for relative, target in links.items(): | ||
| 181 | + Path(destination, relative).symlink_to(target, target_is_directory=True) | ||
| 182 | + | ||
| 183 | + | ||
| 184 | + @pytest.fixture(scope="module") | ||
| 185 | + def prepared_rejection_layout(tmp_path_factory, movable_submodule_baseline): | ||
| 186 | + """Restore private copies at a stable path, preserving absolute Git links.""" | ||
| 187 | + layouts = {} | ||
| 188 | + | ||
| 189 | + @contextlib.contextmanager | ||
| 190 | + def restore(key, prepare): | ||
| 191 | + if key not in layouts: | ||
| 192 | + directory = tmp_path_factory.mktemp("rejection-layout") | ||
| 193 | + root = directory / "active" | ||
| 194 | + shutil.copytree(movable_submodule_baseline("logical-name"), root / "parent", symlinks=True) | ||
| 195 | + with git.Repo(root / "parent") as parent: | ||
| 196 | + prepare(root, parent.submodules[0]) | ||
| 197 | + baseline = directory / "baseline" | ||
| 198 | + _copy_rejection_layout(root, baseline) | ||
| 199 | + rmtree(root) | ||
| 200 | + layouts[key] = root, baseline | ||
| 201 | + root, baseline = layouts[key] | ||
| 202 | + _copy_rejection_layout(baseline, root) | ||
| 203 | + try: | ||
| 204 | + yield root | ||
| 205 | + finally: | ||
| 206 | + rmtree(root) | ||
| 207 | + | ||
| 208 | + return restore | ||
| 209 | + | ||
| 210 | + | ||
| 211 | + def test_prepared_rejection_layout_restores_private_state(prepared_rejection_layout, monkeypatch): | ||
| 212 | + copystat = shutil.copystat | ||
| 213 | + | ||
| 214 | + def reject_symlink_metadata(source, destination, **kwargs): | ||
| 215 | + if Path(source).is_symlink(): | ||
| 216 | + raise PermissionError("Cygwin cannot copy this symlink's metadata") | ||
| 217 | + return copystat(source, destination, **kwargs) | ||
| 218 | + | ||
| 219 | + monkeypatch.setattr(shutil, "copystat", reject_symlink_metadata) | ||
| 220 | + | ||
| 221 | + def prepare(root, sm): | ||
| 222 | + (root / "link").symlink_to(root / "parent/module", target_is_directory=True) | ||
| 223 | + | ||
| 224 | + with prepared_rejection_layout("restore-check", prepare) as root: | ||
| 225 | + (root / "parent/module/file").write_text("changed", encoding="utf-8") | ||
| 226 | + (root / "extra").touch() | ||
| 227 | + assert not root.exists() | ||
| 228 | + with prepared_rejection_layout("restore-check", prepare) as restored: | ||
| 229 | + assert restored == root | ||
| 230 | + assert (root / "link/file").read_text(encoding="utf-8") == "content" | ||
| 231 | + assert not (root / "extra").exists() | ||
| 232 | + | ||
| 233 | + | ||
| 234 | + @pytest.fixture | ||
| 235 | + def nested_metadata_submodule(prepared_rejection_layout): | ||
| 236 | + def prepare(root, sm): | ||
| 237 | + sm.repo.create_submodule("other", "other", sm.url).module().close() | ||
| 238 | + | ||
| 239 | + with prepared_rejection_layout("nested", prepare) as root, git.Repo(root / "parent") as parent: | ||
| 240 | + yield parent.submodules[0], parent.submodules[1] | ||
| 241 | + | ||
| 242 | + | ||
| 243 | + @pytest.fixture | ||
| 244 | + def existing_nested_metadata(prepared_rejection_layout, state): | ||
| 245 | + def prepare(root, sm): | ||
| 246 | + checkout = Path(sm.repo.working_tree_dir, "new") | ||
| 247 | + with git.Repo.clone_from( | ||
| 248 | + sm.url, | ||
| 249 | + checkout, | ||
| 250 | + separate_git_dir=str(Path(sm.repo.git_dir, "modules", sm.name, "child")), | ||
| 251 | + allow_unsafe_options=True, | ||
| 252 | + ): | ||
| 253 | + pass | ||
| 254 | + if state == "retained": | ||
| 255 | + shutil.rmtree(checkout) | ||
| 256 | + | ||
| 257 | + with prepared_rejection_layout(("existing-nested", state), prepare) as root, git.Repo(root / "parent") as parent: | ||
| 258 | + yield parent.submodules[0] | ||
| 259 | + | ||
| 260 | + | ||
| 261 | + @pytest.fixture | ||
| 262 | + def separate_metadata_submodule(prepared_rejection_layout): | ||
| 263 | + def prepare(root, sm): | ||
| 264 | + checkout = root / "separate" | ||
| 265 | + with git.Repo.init(checkout, separate_git_dir=str(checkout / "metadata"), allow_unsafe_options=True) as parent: | ||
| 266 | + module = parent.create_submodule("module", "module", sm.url) | ||
| 267 | + parent.index.commit("Add submodule") | ||
| 268 | + module.module().close() | ||
| 269 | + | ||
| 270 | + with prepared_rejection_layout("separate", prepare) as root, git.Repo(root / "separate") as parent: | ||
| 271 | + yield parent.submodules[0] | ||
| 272 | + | ||
| 273 | + | ||
| 274 | + @pytest.fixture | ||
| 275 | + def intermediate_symlink_submodule(prepared_rejection_layout, target_kind): | ||
| 276 | + def prepare(root, sm): | ||
| 277 | + checkout = Path(sm.repo.working_tree_dir) | ||
| 278 | + target = checkout / "target" if target_kind == "internal" else root / "outside" | ||
| 279 | + if target_kind != "dangling": | ||
| 280 | + target.mkdir() | ||
| 281 | + (checkout / "nested").mkdir() | ||
| 282 | + link = checkout / "nested/link" | ||
| 283 | + link.symlink_to( | ||
| 284 | + target if target_kind == "absolute" else os.path.relpath(target, link.parent), target_is_directory=True | ||
| 285 | + ) | ||
| 286 | + sm.repo.index.add(["nested/link"]) | ||
| 287 | + sm.repo.index.commit("Record layout") | ||
| 288 | + | ||
| 289 | + with prepared_rejection_layout(("intermediate-link", target_kind), prepare) as root, git.Repo( | ||
| 290 | + root / "parent" | ||
| 291 | + ) as parent: | ||
| 292 | + target = root / "parent/target" if target_kind == "internal" else root / "outside" | ||
| 293 | + yield parent.submodules[0], target | ||
| 294 | + | ||
| 295 | + | ||
| 296 | + @pytest.fixture | ||
| 297 | + def leaf_metadata_submodule(prepared_rejection_layout): | ||
| 298 | + def prepare(root, sm): | ||
| 299 | + sm.repo.git.worktree("add", "--detach", str(root / "worktree")) | ||
| 300 | + with git.Repo(root / "worktree") as parent: | ||
| 301 | + module = parent.submodules[0] | ||
| 302 | + module.update(init=True) | ||
| 303 | + module.module().close() | ||
| 304 | + | ||
| 305 | + with prepared_rejection_layout("leaf-metadata", prepare) as root, git.Repo(root / "worktree") as parent: | ||
| 306 | + yield parent.submodules[0] | ||
| 307 | + | ||
| 308 | + | ||
| 309 | + @pytest.fixture | ||
| 310 | + def retained_metadata_submodule(prepared_rejection_layout): | ||
| 311 | + def prepare(root, sm): | ||
| 312 | + sm.remove(force=True) | ||
| 313 | + | ||
| 314 | + with prepared_rejection_layout("retained", prepare) as root, git.Repo(root / "parent") as parent: | ||
| 315 | + # The submodule is no longer in the index; retain its original identity. | ||
| 316 | + with GitConfigParser(Path(parent.git_dir, "modules/logical-name/config")) as reader: | ||
| 317 | + url = reader.get_value('remote "origin"', "url") | ||
| 318 | + sm = parent.head.commit.tree["module"] | ||
| 319 | + yield Submodule(parent, sm.binsha, name="logical-name", path="module", url=url) | ||
| 320 | + | ||
| 321 | + | ||
| 166 | 322 | def _move_snapshot(submodule): | |
| 167 | 323 | """Capture index, configuration, and path state to detect side effects of rejected moves.""" | |
| 168 | 324 | parent = submodule.repo | |
@@ -237,35 +393,32 @@ def test_add_rejects_metadata_checkout_without_writing_files(movable_submodule): | |||
| 237 | 393 | @pytest.mark.parametrize("metadata_name", ["metadata", "MeTaDaTa"]) | |
| 238 | 394 | @pytest.mark.parametrize("operation", ["add", "clone", "update", "move"]) | |
| 239 | 395 | def test_submodule_rejects_checkout_in_separate_metadata( | |
| 240 | - movable_submodule, tmp_path, absolute_path, metadata_name, operation | ||
| 396 | + separate_metadata_submodule, absolute_path, metadata_name, operation | ||
| 241 | 397 | ): | |
| 242 | - root = tmp_path / "separate" | ||
| 243 | - with git.Repo.init(root, separate_git_dir=str(root / "metadata"), allow_unsafe_options=True) as parent: | ||
| 244 | - if not (root / metadata_name).is_dir(): | ||
| 245 | - pytest.skip("Requires a case-insensitive filesystem") | ||
| 246 | - sm = parent.create_submodule("module", "module", movable_submodule.url) | ||
| 247 | - parent.index.commit("Add submodule") | ||
| 248 | - before = _move_snapshot(sm) | ||
| 249 | - paths = set(root.rglob("*")) | ||
| 250 | - path = root / metadata_name / "new" if absolute_path else f"{metadata_name}/new" | ||
| 251 | - with pytest.raises(ValueError, match="Git metadata"): | ||
| 252 | - if operation == "add": | ||
| 253 | - parent.create_submodule("new", path, sm.url) | ||
| 254 | - elif operation == "clone": | ||
| 255 | - Submodule._clone_repo(parent, sm.url, path, "new") | ||
| 256 | - elif operation == "update": | ||
| 257 | - Submodule(parent, sm.binsha, name="new", path=path, url=sm.url).update(init=True) | ||
| 258 | - else: | ||
| 259 | - sm.move(path) | ||
| 260 | - assert _move_snapshot(sm) == before | ||
| 261 | - assert set(root.rglob("*")) == paths | ||
| 398 | + sm = separate_metadata_submodule | ||
| 399 | + parent = sm.repo | ||
| 400 | + root = Path(parent.working_tree_dir) | ||
| 401 | + if not (root / metadata_name).is_dir(): | ||
| 402 | + pytest.skip("Requires a case-insensitive filesystem") | ||
| 403 | + before = _move_snapshot(sm) | ||
| 404 | + paths = set(root.rglob("*")) | ||
| 405 | + path = root / metadata_name / "new" if absolute_path else f"{metadata_name}/new" | ||
| 406 | + with pytest.raises(ValueError, match="Git metadata"): | ||
| 407 | + if operation == "add": | ||
| 408 | + parent.create_submodule("new", path, sm.url) | ||
| 409 | + elif operation == "clone": | ||
| 410 | + Submodule._clone_repo(parent, sm.url, path, "new") | ||
| 411 | + elif operation == "update": | ||
| 412 | + Submodule(parent, sm.binsha, name="new", path=path, url=sm.url).update(init=True) | ||
| 413 | + else: | ||
| 414 | + sm.move(path) | ||
| 415 | + assert _move_snapshot(sm) == before | ||
| 416 | + assert set(root.rglob("*")) == paths | ||
| 262 | 417 | ||
| 263 | 418 | ||
| 264 | 419 | @pytest.mark.parametrize("operation", ["add", "clone", "update"]) | |
| 265 | - def test_submodule_rejects_nested_metadata_before_mutation(movable_submodule, operation): | ||
| 266 | - sm = movable_submodule | ||
| 267 | - other = sm.repo.create_submodule("other", "other", sm.url) | ||
| 268 | - other.module().close() | ||
| 420 | + def test_submodule_rejects_nested_metadata_before_mutation(nested_metadata_submodule, operation): | ||
| 421 | + sm, other = nested_metadata_submodule | ||
| 269 | 422 | root = Path(sm.repo.working_tree_dir) | |
| 270 | 423 | before = _move_snapshot(sm), _move_snapshot(other) | |
| 271 | 424 | paths = set(root.rglob("*")) | |
@@ -282,16 +435,9 @@ def test_submodule_rejects_nested_metadata_before_mutation(movable_submodule, op | |||
| 282 | 435 | ||
| 283 | 436 | ||
| 284 | 437 | @pytest.mark.parametrize("state", ["retained", "checked-out"]) | |
| 285 | - def test_update_rejects_existing_nested_metadata(movable_submodule, state): | ||
| 286 | - sm = movable_submodule | ||
| 438 | + def test_update_rejects_existing_nested_metadata(existing_nested_metadata, state): | ||
| 439 | + sm = existing_nested_metadata | ||
| 287 | 440 | name = f"{sm.name}/child" | |
| 288 | - checkout = Path(sm.repo.working_tree_dir, "new") | ||
| 289 | - with git.Repo.clone_from( | ||
| 290 | - sm.url, checkout, separate_git_dir=str(Path(sm.repo.git_dir, "modules", name)), allow_unsafe_options=True | ||
| 291 | - ): | ||
| 292 | - pass | ||
| 293 | - if state == "retained": | ||
| 294 | - shutil.rmtree(checkout) | ||
| 295 | 441 | before = _move_snapshot(sm) | |
| 296 | 442 | paths = set(Path(sm.repo.working_tree_dir).rglob("*")) | |
| 297 | 443 | with pytest.raises(ValueError, match="inside.*Git directory"): | |
@@ -400,10 +546,9 @@ def test_readd_retained_submodule_without_checkout(movable_submodule, ref_format | |||
| 400 | 546 | ||
| 401 | 547 | ||
| 402 | 548 | @pytest.mark.parametrize("change", ["url", "clone-options", "branch", "depth"]) | |
| 403 | - def test_readd_retained_submodule_rejects_incompatible_options(movable_submodule, change): | ||
| 404 | - sm = movable_submodule | ||
| 549 | + def test_readd_retained_submodule_rejects_incompatible_options(retained_metadata_submodule, change): | ||
| 550 | + sm = retained_metadata_submodule | ||
| 405 | 551 | name, path, url = sm.name, sm.path, sm.url | |
| 406 | - sm.remove(force=True) | ||
| 407 | 552 | root = Path(sm.repo.working_tree_dir) | |
| 408 | 553 | ||
| 409 | 554 | def snapshot(): | |
@@ -429,26 +574,16 @@ def snapshot(): | |||
| 429 | 574 | @pytest.mark.parametrize("configuration,module", [(True, True), (False, True), (True, False)]) | |
| 430 | 575 | @pytest.mark.parametrize("absolute_path", [False, True]) | |
| 431 | 576 | def test_move_rejects_intermediate_symlink( | |
| 432 | - movable_submodule, tmp_path, target_kind, configuration, module, absolute_path | ||
| 577 | + intermediate_symlink_submodule, target_kind, configuration, module, absolute_path | ||
| 433 | 578 | ): | |
| 434 | 579 | """Reject intermediate symlinks before changing repository state or their targets. | |
| 435 | 580 | ||
| 436 | 581 | Cover relative and absolute destinations in every move mode, including links | |
| 437 | 582 | within the repository and dangling links, which must also be rejected. | |
| 438 | 583 | """ | |
| 439 | - submodule = movable_submodule | ||
| 584 | + submodule, target = intermediate_symlink_submodule | ||
| 440 | 585 | parent = submodule.repo | |
| 441 | 586 | root = Path(parent.working_tree_dir) | |
| 442 | - target = root / "target" if target_kind == "internal" else tmp_path / "outside" | ||
| 443 | - if target_kind != "dangling": | ||
| 444 | - target.mkdir() | ||
| 445 | - (root / "nested").mkdir() | ||
| 446 | - link = root / "nested" / "link" | ||
| 447 | - link.symlink_to( | ||
| 448 | - target if target_kind == "absolute" else os.path.relpath(target, link.parent), target_is_directory=True | ||
| 449 | - ) | ||
| 450 | - parent.index.add(["nested/link"]) | ||
| 451 | - parent.index.commit("Record layout") | ||
| 452 | 587 | tree = parent.git.write_tree() | |
| 453 | 588 | before = _move_snapshot(submodule) | |
| 454 | 589 | destination = root / "nested/link/new/moved" if absolute_path else "nested/link/new/moved" | |
@@ -482,25 +617,23 @@ def test_move_normal_destination(movable_submodule, absolute_path): | |||
| 482 | 617 | ||
| 483 | 618 | ||
| 484 | 619 | @pytest.mark.parametrize("metadata_dir", ["git_dir", "common_dir"]) | |
| 485 | - def test_move_rejects_leaf_symlink_to_metadata(movable_submodule, tmp_path, metadata_dir): | ||
| 486 | - root = tmp_path / "worktree" | ||
| 487 | - movable_submodule.repo.git.worktree("add", "--detach", str(root)) | ||
| 488 | - with git.Repo(root) as parent: | ||
| 489 | - assert not osp.samefile(parent.git_dir, parent.common_dir) | ||
| 490 | - submodule = parent.submodules[0] | ||
| 491 | - submodule.update(init=True) | ||
| 492 | - target = Path(getattr(parent, metadata_dir)) | ||
| 493 | - destination = root / "destination" | ||
| 494 | - destination.symlink_to(target, target_is_directory=True) | ||
| 495 | - before = _move_snapshot(submodule) | ||
| 496 | - | ||
| 497 | - with pytest.raises(ValueError, match="Git metadata"): | ||
| 498 | - submodule.move("destination", module=False) | ||
| 620 | + def test_move_rejects_leaf_symlink_to_metadata(leaf_metadata_submodule, metadata_dir): | ||
| 621 | + submodule = leaf_metadata_submodule | ||
| 622 | + parent = submodule.repo | ||
| 623 | + root = Path(parent.working_tree_dir) | ||
| 624 | + assert not osp.samefile(parent.git_dir, parent.common_dir) | ||
| 625 | + target = Path(getattr(parent, metadata_dir)) | ||
| 626 | + destination = root / "destination" | ||
| 627 | + destination.symlink_to(target, target_is_directory=True) | ||
| 628 | + before = _move_snapshot(submodule) | ||
| 499 | 629 | ||
| 500 | - assert _move_snapshot(submodule) == before | ||
| 501 | - assert destination.is_symlink() | ||
| 502 | - assert destination.samefile(target) | ||
| 503 | - assert Path(submodule.abspath, "file").read_text(encoding="utf-8") == "content" | ||
| 630 | + with pytest.raises(ValueError, match="Git metadata"): | ||
| 631 | + submodule.move("destination", module=False) | ||
| 632 | + | ||
| 633 | + assert _move_snapshot(submodule) == before | ||
| 634 | + assert destination.is_symlink() | ||
| 635 | + assert destination.samefile(target) | ||
| 636 | + assert Path(submodule.abspath, "file").read_text(encoding="utf-8") == "content" | ||
| 504 | 637 | ||
| 505 | 638 | ||
| 506 | 639 | @pytest.mark.parametrize("kind", ["empty", "nonempty", "file", "dangling"]) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments