Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(updating): exclude deleted paths on update #1719

Merged
merged 6 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Don't exclude deleted paths matched by skip_if_exists
  • Loading branch information
lkubb committed Aug 12, 2024
commit 30f2a77dc8d26f3cf8cc9cc70bb43c1120943a80
15 changes: 14 additions & 1 deletion copier/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,9 @@ def _apply_update(self) -> None: # noqa: C901
git("fetch", "--depth=1", "real_dst", "HEAD")
# Save a list of files that were intentionally removed in the generated
# project to avoid recreating them during the update.
# Files listed in `skip_if_exists` should only be skipped if they exist.
# They should even be recreated if deleted intentionally.
# This was discussed in issue #1718.
lkubb marked this conversation as resolved.
Show resolved Hide resolved
files_removed = git(
"diff-tree",
"-r",
Expand All @@ -956,7 +959,17 @@ def _apply_update(self) -> None: # noqa: C901
).splitlines()
exclude_plus_removed = list(
set(self.exclude).union(
map(escape_git_path, map(normalize_git_path, files_removed))
map(
escape_git_path,
map(
normalize_git_path,
(
path
for path in files_removed
if not self.match_skip(path)
),
),
)
)
)
# Create a copy of the real destination after applying migrations
Expand Down
14 changes: 11 additions & 3 deletions copier/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,18 @@ def normalize_git_path(path: str) -> str:


def escape_git_path(path: str) -> str:
"""Escape paths that will be used as literal gitwildmatch patterns.

If the path was returned by a Git command, it should be unescaped completely.
``normalize_git_path`` can be used for this purpose.

Args:
path: The Git path to escape.

Returns:
str: The escaped Git path.
"""
Escape paths that will be used as literal gitwildmatch patterns.
The paths should be unescaped completely, as done by ``normalize_git_path``."""
# GitwWildMatchPattern.escape does not escape backslashes
# GitWildMatchPattern.escape does not escape backslashes
# or trailing whitespace.
path = path.replace("\\", "\\\\")
path = GitWildMatchPattern.escape(path)
Expand Down
54 changes: 31 additions & 23 deletions tests/test_updatediff.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import pexpect
import pytest
import yaml
from pathspec.patterns.gitwildmatch import GitWildMatchPattern
from plumbum import local

from copier.cli import CopierApp
Expand Down Expand Up @@ -558,34 +557,41 @@ def test_skip_update(tmp_path_factory: pytest.TempPathFactory) -> None:
@pytest.mark.parametrize(
"file_name",
(
"normal_file",
"skip_normal_file",
pytest.param(
"unicode_âñ",
"skip_unicode_âñ",
marks=pytest.mark.xfail(
platform.system() in {"Darwin", "Windows"},
reason="OS without proper UTF-8 filesystem.",
),
),
"file with whitespace",
" leading_whitespace",
"trailing_whitespace ",
" multi_whitespace ",
"skip file with whitespace",
" skip_leading_whitespace",
"skip_trailing_whitespace ",
" skip_multi_whitespace ",
pytest.param(
"\tother_whitespace\t\\t",
"\tskip_other_whitespace\t\\t",
marks=pytest.mark.skipif(
platform.system() == "Windows",
reason="Disallowed characters in file name",
),
),
pytest.param(
"back\\space",
"\a\f\n\t\vskip_control\a\f\n\t\vcharacters\v\t\n\f\a",
marks=pytest.mark.skipif(
platform.system() == "Windows",
reason="Disallowed characters in file name",
),
),
pytest.param(
"!special",
"skip_back\\slash",
marks=pytest.mark.skipif(
platform.system() == "Windows",
reason="Disallowed characters in file name",
),
),
pytest.param(
"!skip_special",
marks=pytest.mark.skipif(
platform.system() == "Windows",
reason="Disallowed characters in file name",
Expand All @@ -597,15 +603,16 @@ def test_skip_update_deleted(
file_name: str, tmp_path_factory: pytest.TempPathFactory
) -> None:
"""
Ensure that ``skip_if_exists`` does not interfere with
deleted paths during updates (i.e. they stay deleted).
Issue #1718
Ensure that paths in ``skip_if_exists`` are always recreated
if they are absent before updating.
This was discussed in issue #1718.
lkubb marked this conversation as resolved.
Show resolved Hide resolved
"""
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))

with local.cwd(src):
build_file_tree(
{
"copier.yaml": f"_skip_if_exists: ['{GitWildMatchPattern.escape(file_name)}']",
"copier.yaml": "_skip_if_exists: ['*skip*']",
"{{ _copier_conf.answers_file }}.jinja": "{{ _copier_answers|to_yaml }}",
file_name: "1",
"another_file": "foobar",
Expand All @@ -627,7 +634,8 @@ def test_skip_update_deleted(
git("add", ".")
git("commit", "-m1")
run_update(dst, overwrite=True)
assert not skip_me.exists()
assert skip_me.exists()
assert skip_me.read_text() == "1"


@pytest.mark.parametrize(
Expand Down Expand Up @@ -669,7 +677,7 @@ def test_skip_update_deleted(
),
),
pytest.param(
"back\\space",
"back\\slash",
marks=pytest.mark.skipif(
platform.system() == "Windows",
reason="Disallowed characters in file name",
Expand All @@ -683,7 +691,7 @@ def test_skip_update_deleted(
),
),
pytest.param(
"not_wildmatch*",
"dont_wildmatch*",
marks=pytest.mark.skipif(
platform.system() == "Windows",
reason="Disallowed characters in file name",
Expand All @@ -706,7 +714,7 @@ def test_update_deleted_path(
"{{ _copier_conf.answers_file }}.jinja": "{{ _copier_answers|to_yaml }}",
file_name: "foo",
"another_file": "foobar",
"not_wildmatch": "bar",
"dont_wildmatch": "bar",
}
)
git("init")
Expand All @@ -715,10 +723,10 @@ def test_update_deleted_path(
git("tag", "1.0.0")
run_copy(str(src), dst, defaults=True, overwrite=True)
updated_file = dst / file_name
not_wildmatch = dst / "not_wildmatch"
dont_wildmatch = dst / "dont_wildmatch"
answers_file = dst / ".copier-answers.yml"
answers = yaml.safe_load(answers_file.read_text())
assert not_wildmatch.read_text() == "bar"
assert dont_wildmatch.read_text() == "bar"
assert updated_file.read_text() == "foo"
assert answers["_commit"] == "1.0.0"
updated_file.unlink()
Expand All @@ -727,12 +735,12 @@ def test_update_deleted_path(
git("add", ".")
git("commit", "-m1")
with local.cwd(src):
build_file_tree({file_name: "bar", "not_wildmatch": "baz"})
build_file_tree({file_name: "bar", "dont_wildmatch": "baz"})
git("commit", "-am2")
git("tag", "2.0.0")
run_update(dst, overwrite=True)
assert not_wildmatch.exists()
assert not_wildmatch.read_text() == "baz"
assert dont_wildmatch.exists()
assert dont_wildmatch.read_text() == "baz"
assert not updated_file.exists()


Expand Down