-
-
Notifications
You must be signed in to change notification settings - Fork 186
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
Improve conflict resolution #627
Closed
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
"""Main functions and classes, used to generate or update projects.""" | ||
|
||
import os | ||
import platform | ||
import subprocess | ||
import sys | ||
|
@@ -8,15 +9,14 @@ | |
from functools import partial | ||
from itertools import chain | ||
from pathlib import Path | ||
from shutil import rmtree | ||
from typing import Callable, Iterable, List, Mapping, Optional, Sequence | ||
from shutil import copyfile, rmtree | ||
from typing import Callable, Iterable, List, Mapping, Optional, Sequence, Set | ||
from unicodedata import normalize | ||
|
||
import pathspec | ||
from jinja2.loaders import FileSystemLoader | ||
from jinja2.sandbox import SandboxedEnvironment | ||
from plumbum import ProcessExecutionError, colors | ||
from plumbum.cli.terminal import ask | ||
from plumbum import colors | ||
from plumbum.cmd import git | ||
from plumbum.machines import local | ||
from pydantic.dataclasses import dataclass | ||
|
@@ -40,9 +40,23 @@ | |
# HACK https://github.com/python/mypy/issues/8520#issuecomment-772081075 | ||
if sys.version_info >= (3, 8): | ||
from functools import cached_property | ||
|
||
else: | ||
from backports.cached_property import cached_property | ||
|
||
# Backport of `shutil.copytree` for python 3.7 to accept `dirs_exist_ok` argument | ||
if sys.version_info >= (3, 8): | ||
from shutil import copytree | ||
else: | ||
from distutils.dir_util import copy_tree | ||
|
||
def copytree(src: Path, dst: Path, dirs_exist_ok: bool = False): | ||
"""Backport of `shutil.copytree` with `dirs_exist_ok` argument. | ||
|
||
Can be remove once python 3.7 dropped. | ||
""" | ||
copy_tree(str(src), str(dst)) | ||
|
||
|
||
@dataclass | ||
class Worker: | ||
|
@@ -218,10 +232,7 @@ def _path_matcher(self, patterns: Iterable[str]) -> Callable[[Path], bool]: | |
return spec.match_file | ||
|
||
def _solve_render_conflict(self, dst_relpath: Path): | ||
"""Properly solve render conflicts. | ||
|
||
It can ask the user if running in interactive mode. | ||
""" | ||
"""Properly solve render conflicts.""" | ||
assert not dst_relpath.is_absolute() | ||
printf( | ||
"conflict", | ||
|
@@ -239,16 +250,8 @@ def _solve_render_conflict(self, dst_relpath: Path): | |
file_=sys.stderr, | ||
) | ||
return False | ||
if self.overwrite or dst_relpath == self.answers_relpath: | ||
printf( | ||
"overwrite", | ||
dst_relpath, | ||
style=Style.WARNING, | ||
quiet=self.quiet, | ||
file_=sys.stderr, | ||
) | ||
return True | ||
return bool(ask(f" Overwrite {dst_relpath}?", default=True)) | ||
|
||
return True | ||
|
||
def _render_allowed( | ||
self, | ||
|
@@ -657,26 +660,31 @@ def run_update(self) -> None: | |
print( | ||
f"Updating to template version {self.template.version}", file=sys.stderr | ||
) | ||
|
||
# Copy old template into a temporary destination | ||
with TemporaryDirectory(prefix=f"{__name__}.update_diff.") as dst_temp: | ||
old_worker = replace( | ||
with TemporaryDirectory( | ||
prefix=f"{__name__}.update_diff.reference." | ||
) as reference_dst_temp, TemporaryDirectory( | ||
prefix=f"{__name__}.update_diff.original." | ||
) as original_dst_temp, TemporaryDirectory( | ||
prefix=f"{__name__}.update_diff.merge." | ||
) as merge_dst_temp: | ||
# Copy reference to be used as base by merge-file | ||
copytree(self.dst_path, reference_dst_temp, dirs_exist_ok=True) | ||
|
||
# Compute modification from the original template to be used as other by merge-file | ||
original_worker = replace( | ||
self, | ||
dst_path=dst_temp, | ||
dst_path=original_dst_temp, | ||
data=self.subproject.last_answers, | ||
defaults=True, | ||
quiet=True, | ||
src_path=self.subproject.template.url, | ||
vcs_ref=self.subproject.template.commit, | ||
) | ||
old_worker.run_copy() | ||
# Extract diff between temporary destination and real destination | ||
with local.cwd(dst_temp): | ||
subproject_top = git( | ||
"-C", | ||
self.subproject.local_abspath.absolute(), | ||
"rev-parse", | ||
"--show-toplevel", | ||
).strip() | ||
original_worker.run_copy() | ||
# Apply pre-commit hooks if provided by .git | ||
with local.cwd(original_dst_temp): | ||
git("init", retcode=None) | ||
git("add", ".") | ||
git("config", "user.name", "Copier") | ||
|
@@ -686,37 +694,57 @@ def run_update(self) -> None: | |
git("commit", "--allow-empty", "-am", "dumb commit 2") | ||
git("config", "--unset", "user.name") | ||
git("config", "--unset", "user.email") | ||
git("remote", "add", "real_dst", "file://" + subproject_top) | ||
git("fetch", "--depth=1", "real_dst", "HEAD") | ||
diff_cmd = git["diff-tree", "--unified=1", "HEAD...FETCH_HEAD"] | ||
try: | ||
diff = diff_cmd("--inter-hunk-context=-1") | ||
except ProcessExecutionError: | ||
print( | ||
colors.warn | ||
| "Make sure Git >= 2.24 is installed to improve updates.", | ||
file=sys.stderr, | ||
) | ||
diff = diff_cmd("--inter-hunk-context=0") | ||
# Run pre-migration tasks | ||
self._execute_tasks( | ||
self.template.migration_tasks("before", self.subproject.template) | ||
) | ||
# Clear last answers cache to load possible answers migration | ||
with suppress(AttributeError): | ||
del self.answers | ||
with suppress(AttributeError): | ||
del self.subproject.last_answers | ||
# Do a normal update in final destination | ||
self.run_copy() | ||
# Try to apply cached diff into final destination | ||
with local.cwd(self.subproject.local_abspath): | ||
apply_cmd = git["apply", "--reject", "--exclude", self.answers_relpath] | ||
for skip_pattern in chain( | ||
self.skip_if_exists, self.template.skip_if_exists | ||
): | ||
apply_cmd = apply_cmd["--exclude", skip_pattern] | ||
(apply_cmd << diff)(retcode=None) | ||
|
||
# Run pre-migration tasks | ||
self._execute_tasks( | ||
self.template.migration_tasks("before", self.subproject.template) | ||
) | ||
# Clear last answers cache to load possible answers migration | ||
with suppress(AttributeError): | ||
del self.answers | ||
with suppress(AttributeError): | ||
del self.subproject.last_answers | ||
# Do a normal update in final destination | ||
self.run_copy() | ||
|
||
# Extract the list of files to merge | ||
participating_files: Set[Path] = set() | ||
for src_dir in (original_dst_temp, reference_dst_temp): | ||
for root, dirs, files in os.walk(src_dir, topdown=True): | ||
if root == src_dir and ".git" in dirs: | ||
dirs.remove(".git") | ||
root = Path(root).relative_to(src_dir) | ||
participating_files.update(Path(root, f) for f in files) | ||
|
||
# Merging files | ||
for basename in sorted(participating_files): | ||
subfile_names = [] | ||
for subfile_kind, src_dir in [ | ||
("modified", reference_dst_temp), | ||
("old upstream", original_dst_temp), | ||
("new upstream", self.dst_path), | ||
]: | ||
path = Path(src_dir, basename) | ||
if path.is_file(): | ||
copyfile(path, Path(merge_dst_temp, subfile_kind)) | ||
else: | ||
subfile_kind = os.devnull | ||
subfile_names.append(subfile_kind) | ||
|
||
with local.cwd(merge_dst_temp): | ||
output = git("merge-file", "-p", *subfile_names, retcode=None) | ||
|
||
dest_path = Path(self.dst_path, basename) | ||
# Remove the file if it was already removed in the project | ||
if not output and "modified" not in subfile_names: | ||
try: | ||
dest_path.unlink() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you fixing #461 too? 😍 |
||
except FileNotFoundError: | ||
pass | ||
else: | ||
dest_path.parent.mkdir(parents=True, exist_ok=True) | ||
dest_path.write_text(output) | ||
|
||
# Run post-migration tasks | ||
self._execute_tasks( | ||
self.template.migration_tasks("after", self.subproject.template) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,25 +22,18 @@ other git ref you want. | |
|
||
When updating, Copier will do its best to respect your project evolution by using the | ||
answers you provided when copied last time. However, sometimes it's impossible for | ||
Copier to know what to do with a diff code hunk. In those cases, you will find `*.rej` | ||
files that contain the unresolved diffs. _You should review those manually_ before | ||
committing. | ||
|
||
You probably don't want `*.rej` files in your git history, but if you add them to | ||
`.gitignore`, some important changes could pass unnoticed to you. That's why the | ||
recommended way to deal with them is to _not_ add them to add a | ||
[pre-commit](https://pre-commit.com/) (or equivalent) hook that forbids them, just like | ||
this: | ||
Copier to know what to do with a diff code hunk. In those cases, each conflicted files | ||
will contain the unresolved diffs. _Those conflicts must be reviewed before committing_. | ||
That's why we recommand to add them to add a [pre-commit](https://pre-commit.com/) (or | ||
equivalent) hook that detects them, just like this: | ||
|
||
```yaml title=".pre-commit-config.yaml" | ||
repos: | ||
- repo: local | ||
- repo: https://github.com/pre-commit/pre-commit-hooks | ||
rev: v4.0.1 | ||
hooks: | ||
- id: forbidden-files | ||
name: forbidden files | ||
entry: found copier update rejection files; review them and remove them | ||
language: fail | ||
files: "\\.rej$" | ||
- id: check-merge-conflict | ||
args: [--assume-in-merge] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These lines have wrong indentation and are not valid yaml. Can you fix that please? |
||
``` | ||
|
||
## Never change the answers file manually | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is the perfect spot to check if the file has changed and show a brief diff to the user (
difflib.unified_diff(old_upstream, new_upstream)
)