Skip to content

Commit

Permalink
feat: include local dirty changes by default (#520)
Browse files Browse the repository at this point in the history
- if a template is vcs-tracked and dirty, the changes are committed automatically with a "wip" commit and propagated to the subproject
- user is warned in this scenario
- tests for copy and update on dirty templates

fixes #184
  • Loading branch information
sabard authored Jan 11, 2022
1 parent c085ca6 commit 6cc94c6
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Summary:
- Raise a CopierAnswersInterrupt instead of a bare KeyboardInterrupt to provide
callers with additional context - such as the partially completed AnswersMap.
- Support for `user_defaults`, which take precedence over template defaults.
- Copy dirty changes from a git-tracked template to the project by default, to make
testing easier.

### Changed

Expand Down
4 changes: 4 additions & 0 deletions copier/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,7 @@ class UnknownCopierVersionWarning(UserWarning, CopierWarning):

class OldTemplateWarning(UserWarning, CopierWarning):
"""Template was designed for an older Copier version."""


class DirtyLocalWarning(UserWarning, CopierWarning):
"""Changes and untracked files present in template."""
4 changes: 2 additions & 2 deletions copier/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,8 @@ def templates_suffix(self) -> str:
def local_abspath(self) -> Path:
"""Get the absolute path to the template on disk.
This may clone it if `url` points to a
VCS-tracked template.
This may clone it if `url` points to a VCS-tracked template.
Dirty changes for local VCS-tracked templates will be copied.
"""
result = Path(self.url)
if self.vcs == "git":
Expand Down
30 changes: 30 additions & 0 deletions copier/vcs.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Utilities related to VCS."""
import os
import re
import sys
import tempfile
from pathlib import Path
from warnings import warn

from packaging import version
from packaging.version import Version
from plumbum import TF, ProcessExecutionError, colors, local
from plumbum.cmd import git

from .errors import DirtyLocalWarning
from .tools import TemporaryDirectory
from .types import OptBool, OptStr, StrOrPath

Expand Down Expand Up @@ -111,20 +114,47 @@ def checkout_latest_tag(local_repo: StrOrPath, use_prereleases: OptBool = False)
def clone(url: str, ref: OptStr = None) -> str:
"""Clone repo into some temporary destination.
Includes dirty changes for local templates by copying into a temp
directory and applying a wip commit there.
Args:
url:
Git-parseable URL of the repo. As returned by
[get_repo][copier.vcs.get_repo].
ref:
Reference to checkout. For Git repos, defaults to `HEAD`.
"""

location = tempfile.mkdtemp(prefix=f"{__name__}.clone.")
_clone = git["clone", "--no-checkout", url, location]
# Faster clones if possible
if GIT_VERSION >= Version("2.27"):
_clone = _clone["--filter=blob:none"]
_clone()

if not ref and os.path.exists(url) and Path(url).is_dir():
is_dirty = False
with local.cwd(url):
is_dirty = bool(git("status", "--porcelain").strip())
if is_dirty:
url_abspath = Path(url).absolute()
with local.cwd(location):
git("--git-dir=.git", f"--work-tree={url_abspath}", "add", "-A")
git(
"--git-dir=.git",
f"--work-tree={url_abspath}",
"commit",
"-m",
"Copier automated commit for draft changes",
"--no-verify",
)
warn(
"Dirty template changes included automatically.",
DirtyLocalWarning,
)

with local.cwd(location):
git("checkout", ref or "HEAD")
git("submodule", "update", "--checkout", "--init", "--recursive", "--force")

return location
115 changes: 115 additions & 0 deletions tests/test_dirty_local.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import os
from pathlib import Path
from shutil import copy2, copytree

import pytest
from plumbum import local
from plumbum.cmd import git

import copier
from copier.errors import DirtyLocalWarning
from copier.main import run_copy, run_update

from .helpers import DATA, PROJECT_TEMPLATE, build_file_tree


def test_copy(tmp_path_factory):
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))

# dirs_exist_ok not available in Python 3.7
for item in os.listdir(PROJECT_TEMPLATE):
item_src_path = os.path.join(PROJECT_TEMPLATE, item)
item_dst_path = os.path.join(src, item)
if os.path.isdir(item_src_path):
copytree(item_src_path, item_dst_path)
else:
copy2(item_src_path, item_dst_path)

with local.cwd(src):
git("init")

with pytest.warns(DirtyLocalWarning):
copier.copy(str(src), str(dst), data=DATA, quiet=True)

generated = (dst / "pyproject.toml").read_text()
control = (Path(__file__).parent / "reference_files" / "pyproject.toml").read_text()
assert generated == control

# assert template still dirty
with local.cwd(src):
assert bool(git("status", "--porcelain").strip())


def test_update(tmp_path_factory):
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))

build_file_tree(
{
src
/ ".copier-answers.yml.jinja": """\
# Changes here will be overwritten by Copier
{{ _copier_answers|to_nice_yaml }}
""",
src
/ "copier.yml": """\
_envops:
"keep_trailing_newline": True
""",
src
/ "aaaa.txt": """
Lorem ipsum
""",
src
/ "to_delete.txt": """
delete me.
""",
}
)

with local.cwd(src):
git("init")
git("add", "-A")
git("commit", "-m", "first commit on src")

run_copy(str(src), dst, defaults=True, overwrite=True)

with local.cwd(src):
# test adding a file
with open("test_file.txt", "w") as f:
f.write("Test content")

# test updating a file
with open("aaaa.txt", "a") as f:
f.write("dolor sit amet")

# test removing a file
os.remove("to_delete.txt")

# dst must be vcs-tracked to use run_update
with local.cwd(dst):
git("init")
git("add", "-A")
git("commit", "-m", "first commit on dst")

# make sure changes have not yet propagated
assert not os.path.exists(dst / "test_file.txt")

p1 = src / "aaaa.txt"
p2 = dst / "aaaa.txt"
assert p1.read_text() != p2.read_text()

assert os.path.exists(dst / "to_delete.txt")

with pytest.warns(DirtyLocalWarning):
run_update(dst, defaults=True, overwrite=True)

# make sure changes propagate after update
assert os.path.exists(dst / "test_file.txt")

p1 = src / "aaaa.txt"
p2 = dst / "aaaa.txt"
assert p1.read_text() == p2.read_text()

# HACK https://github.com/copier-org/copier/issues/461
# TODO test file deletion on update
# assert not os.path.exists(dst / "to_delete.txt")

0 comments on commit 6cc94c6

Please sign in to comment.