Skip to content

Commit c173f5c

Browse files
committed
refactor!: introduce new update algorithm based on git merge
1 parent de88305 commit c173f5c

8 files changed

Lines changed: 250 additions & 598 deletions

File tree

copier/_cli.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -375,23 +375,6 @@ class CopierUpdateSubApp(_Subcommand):
375375
"""
376376
)
377377

378-
conflict = cli.SwitchAttr(
379-
["-o", "--conflict"],
380-
cli.Set("rej", "inline"),
381-
default="inline",
382-
help=(
383-
"Behavior on conflict: Create .rej files, or add inline conflict markers."
384-
),
385-
)
386-
context_lines = cli.SwitchAttr(
387-
["-c", "--context-lines"],
388-
int,
389-
default=3,
390-
help=(
391-
"Lines of context to use for detecting conflicts. Increase for "
392-
"accuracy, decrease for resilience."
393-
),
394-
)
395378
defaults = cli.Flag(
396379
["-l", "-f", "--defaults"],
397380
help="Use default answers to questions, which might be null if not specified.",
@@ -417,8 +400,6 @@ def main(self, destination_path: cli.ExistingDirectory = ".") -> int:
417400
def inner() -> None:
418401
with self._worker(
419402
dst_path=destination_path,
420-
conflict=self.conflict,
421-
context_lines=self.context_lines,
422403
defaults=self.defaults,
423404
skip_answered=self.skip_answered,
424405
overwrite=True,

copier/_main.py

Lines changed: 160 additions & 304 deletions
Large diffs are not rendered by default.

docs/configuring.md

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -785,43 +785,6 @@ will delete that folder.
785785
Copier will never delete the folder if it didn't create it. For this reason, when
786786
running `copier update`, this setting has no effect.
787787

788-
!!! info
789-
790-
Not supported in `copier.yml`.
791-
792-
### `conflict`
793-
794-
- Format: `Literal["rej", "inline"]`
795-
- CLI flags: `-o`, `--conflict` (only available in `copier update` subcommand)
796-
- Default value: `inline`
797-
798-
When updating a project, sometimes Copier doesn't know what to do with a diff code hunk.
799-
This option controls the output format if this happens. Using `rej`, creates `*.rej`
800-
files that contain the unresolved diffs. The `inline` option (default) includes the diff
801-
code hunk in the file itself, similar to the behavior of `git merge`.
802-
803-
!!! info
804-
805-
Not supported in `copier.yml`.
806-
807-
### `context_lines`
808-
809-
- Format: `Int`
810-
- CLI flags: `-c`, `--context-lines` (only available in `copier update` subcommand)
811-
- Default value: `1`
812-
813-
During a project update, Copier needs to compare the template evolution with the
814-
subproject evolution. This way, it can detect what changed, where and how to merge those
815-
changes. [Refer here for more details on this process](updating.md).
816-
817-
The more lines you use, the more accurate Copier will be when detecting conflicts. But
818-
you will also have more conflicts to solve by yourself. FWIW, Git uses 3 lines by
819-
default.
820-
821-
The less lines you use, the less conflicts you will have. However, Copier will not be so
822-
accurate and could even move lines around if the file it's comparing has several similar
823-
code chunks.
824-
825788
!!! info
826789

827790
Not supported in `copier.yml`.

docs/creating.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,6 @@ Attributes:
108108
| ------------------ | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
109109
| `answers_file` | `PurePath` | The path for [the answers file][the-copier-answersyml-file] relative to `dst_path`.<br>See the [`answers_file`][] setting for related information. |
110110
| `cleanup_on_error` | `bool` | When `True`, delete `dst_path` if there's an error.<br>See the [`cleanup_on_error`][] setting for related information. |
111-
| `conflict` | `Literal["inline", "rej"]` | The output format of a diff code hunk when [updating][updating-a-project] a file yields conflicts.<br>See the [`conflict`][] setting for related information. |
112-
| `context_lines` | `PositiveInt` | Lines of context to consider when solving conflicts in updates.<br>See the [`context_lines`][] setting for related information. |
113111
| `data` | `dict[str, Any]` | Answers to the questionnaire, defined in the template, provided via CLI (`-d,--data`) or API (`data`).<br>See the [`data`][] setting for related information.<br>⚠️ May contain secret answers. |
114112
| `defaults` | `bool` | When `True`, use default answers to questions.<br>See the [`defaults`][] setting for related information. |
115113
| `dst_path` | `PurePath` | Destination path where to render the subproject.<br>⚠️ When [updating a project][updating-a-project], it may be a temporary directory, as Copier's update algorithm generates fresh copies using the old and new template versions in temporary locations. |

docs/updating.md

Lines changed: 33 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,11 @@ other Git ref you want.
3838

3939
When updating, Copier will do its best to respect your project evolution by using the
4040
answers you provided when copied last time. However, sometimes it's impossible for
41-
Copier to know what to do with a diff code hunk. In those cases, copier handles the
42-
conflict in one of two ways, controlled with the `--conflict` option:
43-
44-
- `--conflict rej`: Creates a separate `.rej` file for each file with conflicts. These
45-
files contain the unresolved diffs.
46-
- `--conflict inline` (default): Updates the file with conflict markers. This is quite
47-
similar to the conflict markers created when a `git merge` command encounters a
48-
conflict. For more information, see the "Checking Out Conflicts" section of the
49-
[`git` documentation](https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging).
41+
Copier to know what to merge the changes from the evolved template into the evolved
42+
project. In those cases, Copier updates a conflicting file with conflict markers in the
43+
same ways as a `git merge` command encounters conflicts; in fact, Copier uses
44+
`git merge` internally. For more information, see the "Checking Out Conflicts" section
45+
of the [`git` documentation](https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging).
5046

5147
If the update results in conflicts, _you should review those manually_ before
5248
committing.
@@ -56,13 +52,11 @@ Git history, but if you aren't careful, it's easy to make mistakes.
5652

5753
That's why the recommended way to prevent these mistakes is to add a
5854
[pre-commit](https://pre-commit.com/) (or equivalent) hook that forbids committing
59-
conflict files or markers. The recommended hook configuration depends on the `conflict`
60-
setting you use.
55+
conflict markers.
6156

6257
## Preventing Commit of Merge Conflicts
6358

64-
If you use `--conflict inline` (the default) then you need to check for conflicts
65-
markers in your files:
59+
You need to check for conflicts markers in your files:
6660

6761
```yaml title=".pre-commit-config.yaml"
6862
repos:
@@ -74,29 +68,6 @@ repos:
7468
args: [--assume-in-merge]
7569
```
7670
77-
If you use `--conflict rej` then you need to review and remove all generated `.rej`
78-
files:
79-
80-
```yaml title=".pre-commit-config.yaml"
81-
repos:
82-
- repo: local
83-
hooks:
84-
# Prevent committing .rej files
85-
- id: forbidden-files
86-
name: forbidden files
87-
entry:
88-
found Copier update rejection files; review and remove them before
89-
merging.
90-
language: fail
91-
files: "\\.rej$"
92-
```
93-
94-
!!! note
95-
96-
For projects that use both `rej` and `inline` depending on each user's preference,
97-
you can add both hooks to your `pre-commit-config.yaml` file, making sure that no
98-
unresolved merge conflicts are committed.
99-
10071
## Never change the answers file manually
10172
10273
!!! important
@@ -157,42 +128,40 @@ graph TD
157128
158129
%% nodes ----------------------------------------------------------
159130
template_repo("template repository")
160-
template_current("/tmp/template<br>(current tag)")
161-
template_latest("/tmp/template<br>(latest tag)")
131+
template_current("/tmp/template-old<br>(current tag)")
132+
template_latest("/tmp/template-new<br>(latest tag)")
162133
163-
project_regen("/tmp/project<br>(fresh, current version)")
134+
project_regen_current("/tmp/project-old<br>(fresh, current version)")
135+
project_regen_latest("/tmp/project-new<br>(fresh, latest version)")
164136
project_current("current project")
165137
project_half("half migrated<br>project")
166138
project_updated("updated project")
167-
project_applied("updated project<br>(diff applied)")
168139
project_full("fully updated<br>and migrated project")
169140
170-
update["update current<br>project in-place<br>(prompting)<br>+ run tasks again"]
171-
compare["compare to get diff"]
172-
apply["apply diff"]
173-
174-
diff("diff")
141+
update["3-way merge<br>& run tasks again"]
142+
regen_current["generate and run tasks"]
143+
regen_latest["generate and run tasks"]
175144
176145
%% edges ----------------------------------------------------------
177146
template_repo --> |git clone| template_current
178147
template_repo --> |git clone| template_latest
179148
180-
template_current --> |generate and run tasks| project_regen
181-
project_current --> compare
149+
template_current --> regen_current
150+
project_current .-> |use answers| regen_current
151+
regen_current --> project_regen_current
152+
template_latest --> regen_latest
153+
regen_latest --> project_regen_latest
182154
project_current --> |apply pre-migrations| project_half
183-
project_regen --> compare
155+
project_half .-> |use answers| regen_latest
184156
project_half --> update
185-
template_latest --> update
157+
project_regen_current --> update
158+
project_regen_latest --> update
186159
update --> project_updated
187-
compare --> diff
188-
diff --> apply
189-
project_updated --> apply
190-
apply --> project_applied
191-
project_applied --> |apply post-migrations| project_full
160+
project_updated --> |apply post-migrations| project_full
192161
193162
%% style ----------------------------------------------------------
194163
classDef blackborder stroke:#000;
195-
class compare,update,apply blackborder;
164+
class regen_current,regen_latest,update blackborder;
196165
```
197166

198167
As you can see here, `copier` does several things:
@@ -247,15 +216,18 @@ branch. The following strategies won't work:
247216

248217
- `git checkout <branch>` – _error: you need to resolve your current index first_
249218
- `git checkout .` – _error: path '&lt;filename&gt;' is unmerged_
250-
- `git merge --abort` – _fatal: There is no merge to abort (MERGE_HEAD missing)_
251219

252220
Here is what you can do using Git in the terminal to throw away all changes:
253221

254-
```shell
255-
git reset # throw away merge conflict information
256-
git checkout . # restore modified files
257-
git clean -d -i # remove untracked files and folders
258-
```
222+
- ```shell
223+
git merge --abort
224+
```
225+
226+
- ```shell
227+
git reset # throw away merge conflict information
228+
git checkout . # restore modified files
229+
git clean -d -i # remove untracked files and folders
230+
```
259231

260232
If you want fine-grained control to restore files selectively, read the output of the
261233
`git status` command attentively. It shows all the commands you may need as hints!

tests/test_cli.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,6 @@ def test_copy_help() -> None:
377377

378378
def test_update_help() -> None:
379379
_help = COPIER_CMD("update", "--help")
380-
assert "-o, --conflict" in _help
381380
assert "copier update [SWITCHES] [destination_path=.]" in _help
382381
assert "--skip-answered" in _help
383382

tests/test_subdirectory.py

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from pathlib import Path
22
from textwrap import dedent
3-
from typing import Literal
43

54
import pytest
65
from plumbum import local
@@ -177,34 +176,8 @@ def test_update_subdirectory_from_root_path(
177176
assert (dst / "subfolder" / "file1").read_text() == "version 2\nhello\na1\nbye\n"
178177

179178

180-
@pytest.mark.parametrize(
181-
"conflict, readme, expect_reject",
182-
[
183-
(
184-
"rej",
185-
"upstream version 2\n",
186-
True,
187-
),
188-
(
189-
"inline",
190-
dedent(
191-
"""\
192-
<<<<<<< before updating
193-
downstream version 1
194-
=======
195-
upstream version 2
196-
>>>>>>> after updating
197-
"""
198-
),
199-
False,
200-
),
201-
],
202-
)
203179
def test_new_version_uses_subdirectory(
204180
tmp_path_factory: pytest.TempPathFactory,
205-
conflict: Literal["rej", "inline"],
206-
readme: str,
207-
expect_reject: bool,
208181
) -> None:
209182
# Template in v1 doesn't have a _subdirectory;
210183
# in v2 it moves all things into a subdir and adds that key to copier.yml.
@@ -259,15 +232,22 @@ def test_new_version_uses_subdirectory(
259232
git("tag", "v2")
260233

261234
# Finally, update the generated project
262-
copier.run_update(dst_path=dst, defaults=True, overwrite=True, conflict=conflict)
235+
copier.run_update(dst_path=dst, defaults=True, overwrite=True)
263236
assert load_answersfile_data(dst).get("_commit") == "v2"
264237

265238
# Assert that the README still exists, and the conflicts were handled
266239
# correctly.
267240
assert (dst / "README.md").exists()
268241

269-
assert (dst / "README.md").read_text().splitlines() == readme.splitlines()
270-
assert (dst / "README.md.rej").exists() == expect_reject
242+
assert (dst / "README.md").read_text() == dedent(
243+
"""\
244+
<<<<<<< HEAD
245+
downstream version 1
246+
=======
247+
upstream version 2
248+
>>>>>>> copier/after-updating
249+
"""
250+
)
271251

272252
# Also assert the subdirectory itself was not rendered
273253
assert not (dst / subdir).exists()

0 commit comments

Comments
 (0)