Skip to content

Commit

Permalink
fix: include dirty local changes when copying HEAD
Browse files Browse the repository at this point in the history
This was the intended and documented behavior, but at some point it got broken.

Well, now it's tested and it works.

Besides, there's a new FAQ to avoid further surprises.

Fixes #787
  • Loading branch information
yajo committed Apr 7, 2023
1 parent 065d6ba commit e34bcda
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 15 deletions.
9 changes: 4 additions & 5 deletions copier/vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ def is_in_git_repo(path: StrOrPath) -> bool:
def is_git_shallow_repo(path: StrOrPath) -> bool:
"""Indicate if a given path is a git shallow repo directory."""
try:
git("-C", path, "rev-parse", "--is-shallow-repository")
return True
return git("-C", path, "rev-parse", "--is-shallow-repository").strip() == "true"
except (OSError, ProcessExecutionError):
return False

Expand Down Expand Up @@ -160,8 +159,8 @@ def clone(url: str, ref: OptStr = None) -> str:
else:
_clone = _clone["--filter=blob:none"]
_clone()

if not ref and os.path.exists(url) and Path(url).is_dir():
# Include dirty changes if checking out a local HEAD
if ref in {None, "HEAD"} and os.path.exists(url) and Path(url).is_dir():
is_dirty = False
with local.cwd(url):
is_dirty = bool(git("status", "--porcelain").strip())
Expand All @@ -183,7 +182,7 @@ def clone(url: str, ref: OptStr = None) -> str:
)

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

return location
Expand Down
39 changes: 39 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,42 @@ names!
If the repository containing the template is a shallow clone, the git process called by
Copier might consume unusually high resources. To avoid that, use a fully-cloned
repository.

## While developing, why the template doesn't include dirty changes?

Copier follows [a specific algorithm](./configuring.md#templates-versions) to choose
what reference to use from the template. It also
[includes dirty changes in the `HEAD` ref while developing locally](./configuring.md#copying-dirty-changes).

However, did you make sure you are selecting the `HEAD` ref for copying?

Imagine this is the status of your dirty template in `./src`:

```shell
$ git -C ./src status --porcelain=v1
?? new-file.txt
$ git -C ./src tag
v1.0.0
v2.0.0
```

Now, if you copy that template into a folder like this:

```shell
$ copier copy ./src ./dst
```

... you'll notice there's no `new-file.txt`. Why?

Well, Copier indeed included that into the `HEAD` ref. However, it still selected
`v2.0.0` as the ref to copy, because that's what Copier does.

However, if you do this:

```shell
$ copier -r HEAD copy ./src ./dst
```

... then you'll notice `new-file.txt` does exist. You passed a specific ref to copy, so
Copier skips its autodetection and just goes for the `HEAD` you already chose.
16 changes: 7 additions & 9 deletions docs/generating.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ By default, Copier will copy from the last release found in template Git tags, s
[PEP 440](https://peps.python.org/pep-0440/), regardless of whether the template is from
a URL or a local clone of a Git repository.

The exception to this is if you use a local clone of a template repository that has had
any modifications made, in this case Copier will use this modified working copy of the
template to aid development of new template features.
### Copying dirty changes

If you use a local clone of a template repository that has had any uncommitted
modifications made, Copier will use this modified working copy of the template to aid
development of new template features.

If you would like to override the version of template being installed, the
[`--vcs-ref`](../configuring/#vcs_ref) argument can be used to specify a branch, tag or
Expand All @@ -55,17 +57,13 @@ For example to use the latest master branch from a public repository:
copier --vcs-ref master https://github.com/foo/copier-template.git ./path/to/destination
```

Or to work from the current checked out revision of a local template:
Or to work from the current checked out revision of a local template (including dirty
changes):

```shell
copier --vcs-ref HEAD path/to/project/template path/to/destination
```

!!! tip

If there are uncommited changes in the local template, they will be included in the `HEAD`
ref

## Regenerating a project

When you execute `copier copy $template $project` again over a preexisting `$project`,
Expand Down
19 changes: 18 additions & 1 deletion tests/test_dirty_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_copy(tmp_path_factory: pytest.TempPathFactory) -> None:
git("init")

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

generated = (dst / "pyproject.toml").read_text()
control = (Path(__file__).parent / "reference_files" / "pyproject.toml").read_text()
Expand All @@ -39,6 +39,23 @@ def test_copy(tmp_path_factory: pytest.TempPathFactory) -> None:
assert bool(git("status", "--porcelain").strip())


def test_copy_dirty_head(tmp_path_factory: pytest.TempPathFactory) -> None:
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
build_file_tree(
{
src / "tracked": "",
src / "untracked": "",
}
)
with local.cwd(src):
git("init")
git("add", "tracked")
git("commit", "-m1")
copier.run_copy(str(src), dst, vcs_ref="HEAD")
assert (dst / "tracked").exists()
assert (dst / "untracked").exists()


def test_update(tmp_path_factory: pytest.TempPathFactory) -> None:
src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))

Expand Down

0 comments on commit e34bcda

Please sign in to comment.