Skip to content

Commit

Permalink
perf(updating): avoid creating subproject copy
Browse files Browse the repository at this point in the history
  • Loading branch information
sisp committed Aug 28, 2024
1 parent c685192 commit 0927918
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 31 deletions.
56 changes: 25 additions & 31 deletions copier/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from functools import cached_property, partial
from itertools import chain
from pathlib import Path
from shutil import copytree, ignore_patterns, rmtree
from shutil import rmtree
from tempfile import TemporaryDirectory
from types import TracebackType
from typing import (
Expand Down Expand Up @@ -54,6 +54,7 @@
normalize_git_path,
printf,
readlink,
set_git_alternates,
)
from .types import (
MISSING,
Expand Down Expand Up @@ -923,9 +924,7 @@ def _apply_update(self) -> None: # noqa: C901
prefix=f"{__name__}.old_copy.",
) as old_copy, TemporaryDirectory(
prefix=f"{__name__}.new_copy.",
) as new_copy, TemporaryDirectory(
prefix=f"{__name__}.dst_copy.",
) as dst_copy:
) as new_copy:
# Copy old template into a temporary destination
with replace(
self,
Expand All @@ -941,10 +940,14 @@ def _apply_update(self) -> None: # noqa: C901
self._execute_tasks(
self.template.migration_tasks("before", self.subproject.template) # type: ignore[arg-type]
)
# Create a Git tree object from the current (possibly dirty) index
# and keep the object reference.
with local.cwd(subproject_top):
subproject_head = git("write-tree").strip()
with local.cwd(old_copy):
self._git_initialize_repo()
git("remote", "add", "real_dst", "file://" + str(subproject_top))
git("fetch", "--depth=1", "real_dst", "HEAD")
# Configure borrowing Git objects from the real destination.
set_git_alternates(subproject_top)
# 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.
Expand All @@ -954,7 +957,8 @@ def _apply_update(self) -> None: # noqa: C901
"-r",
"--diff-filter=D",
"--name-only",
"HEAD...FETCH_HEAD",
"HEAD",
subproject_head,
).splitlines()
exclude_plus_removed = list(
set(self.exclude).union(
Expand All @@ -971,19 +975,6 @@ def _apply_update(self) -> None: # noqa: C901
)
)
)
# Create a copy of the real destination after applying migrations
# but before performing any further update for extracting the diff
# between the temporary destination of the old template and the
# real destination later.
with local.cwd(dst_copy):
copytree(
subproject_top,
".",
symlinks=True,
ignore=ignore_patterns("/.git"),
dirs_exist_ok=True,
)
self._git_initialize_repo()
# Clear last answers cache to load possible answers migration, if skip_answered flag is not set
if self.skip_answered is False:
self.answers = AnswersMap()
Expand Down Expand Up @@ -1015,14 +1006,14 @@ def _apply_update(self) -> None: # noqa: C901
new_worker.run_copy()
with local.cwd(new_copy):
self._git_initialize_repo()
# Extract diff between temporary destination and (copy from above)
# real destination with some special handling of newly added files
# in both the poject and the template.
new_copy_head = git("rev-parse", "HEAD").strip()
# Extract diff between temporary destination and real destination
# with some special handling of newly added files in both the poject
# and the template.
with local.cwd(old_copy):
git("remote", "add", "dst_copy", "file://" + str(dst_copy))
git("fetch", "--depth=1", "dst_copy", "HEAD:dst_copy")
git("remote", "add", "new_copy", "file://" + str(new_copy))
git("fetch", "--depth=1", "new_copy", "HEAD:new_copy")
# Configure borrowing Git objects from the real destination and
# temporary destination of the new template.
set_git_alternates(subproject_top, Path(new_copy))
# Create an empty file in the temporary destination when the
# same file was added in *both* the project and the temporary
# destination of the new template. With this minor change, the
Expand All @@ -1034,17 +1025,20 @@ def _apply_update(self) -> None: # noqa: C901
"diff-tree", "-r", "--diff-filter=A", "--name-only"
]
for filename in (
set(diff_added_cmd("HEAD...dst_copy").splitlines())
) & set(diff_added_cmd("HEAD...new_copy").splitlines()):
set(diff_added_cmd("HEAD", subproject_head).splitlines())
) & set(diff_added_cmd("HEAD", new_copy_head).splitlines()):
f = Path(filename)
f.parent.mkdir(parents=True, exist_ok=True)
f.touch(Path(dst_copy, filename).stat().st_mode)
f.touch((subproject_top / filename).stat().st_mode)
git("add", filename)
self._git_commit("add new empty files")
# Extract diff between temporary destination and real
# destination
diff_cmd = git[
"diff-tree", f"--unified={self.context_lines}", "HEAD...dst_copy"
"diff-tree",
f"--unified={self.context_lines}",
"HEAD",
subproject_head,
]
try:
diff = diff_cmd("--inter-hunk-context=-1")
Expand Down
36 changes: 36 additions & 0 deletions copier/tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Some utility functions."""

from __future__ import annotations

import errno
Expand Down Expand Up @@ -232,3 +233,38 @@ def escape_git_path(path: str) -> str:
lambda match: "".join(f"\\{whitespace}" for whitespace in match.group()),
path,
)


def get_git_objects_dir(path: Path) -> Path:
"""Get the absolute path of a Git repository's objects directory."""
# FIXME: A lazy import is currently necessary to avoid circular imports with
# `errors.py`.
from .vcs import get_git

git = get_git()
return Path(
git(
"-C",
path,
"rev-parse",
"--path-format=absolute",
"--git-path",
"objects",
).strip()
)


def set_git_alternates(*repos: Path, path: Path = Path(".")) -> None:
"""Set Git alternates to borrow Git objects from other repositories.
Alternates are paths of other repositories' object directories written to
`$GIT_DIR/objects/info/alternates` and delimited by the newline character.
Args:
*repos: The paths of repositories from which to borrow Git objects.
path: The path of the repository where to set Git alternates. Defaults
to the current working directory.
"""
alternates_file = get_git_objects_dir(path) / "info" / "alternates"
alternates_file.parent.mkdir(parents=True, exist_ok=True)
alternates_file.write_bytes(b"\n".join(map(bytes, map(get_git_objects_dir, repos))))
40 changes: 40 additions & 0 deletions tests/test_updatediff.py
Original file line number Diff line number Diff line change
Expand Up @@ -1478,3 +1478,43 @@ def test_update_with_new_file_in_template_and_project_via_migration(
>>>>>>> after updating
"""
)


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

with local.cwd(src):
build_file_tree(
{
"version.txt": "v1",
"{{ _copier_conf.answers_file }}.jinja": "{{ _copier_answers|to_nice_yaml }}",
}
)
git("init")
git("add", ".")
git("commit", "-m1")
git("tag", "v1")

run_copy(str(src), dst, overwrite=True)
assert "_commit: v1" in (dst / ".copier-answers.yml").read_text()

with local.cwd(dst):
git("init", "--separate-git-dir", dst_git_dir)
# Add a file to make sure the subproject's tree object is different from
# that of the fresh copy from the old template version; otherwise, we
# cannot test the linking of local (temporary) repositories for
# borrowing Git objects.
build_file_tree({"foo.txt": "bar"})
git("add", ".")
git("commit", "-m1")

with local.cwd(src):
build_file_tree({"version.txt": "v2"})
git("add", ".")
git("commit", "-m2")
git("tag", "v2")

run_update(dst, overwrite=True)
assert "_commit: v2" in (dst / ".copier-answers.yml").read_text()

0 comments on commit 0927918

Please sign in to comment.