Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ typecheck:
poetry run dmypy run -- src tests

pre-commit: fmt lint typecheck

test:
poetry run pytest tests
1 change: 1 addition & 0 deletions docs/source/tutorials/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

write-template-from-scratch
backwards-compatible-template-changes
reset-incarnation
```
14 changes: 14 additions & 0 deletions docs/source/tutorials/reset-incarnation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Reset incarnation
There is an option to reset an incarnation by removing all customizations that were done to it and bring it back to
a pristine state as if it was just created freshly from the template.

By default, the target version and data is taken from the last change that was successfully applied
to the incarnation, but they can be overridden. For the data, partial overrides are also allowed.

## Ignore some files from reset
Sometimes, you want to keep some files in the incarnation that were created or modified after the initial
creation of the incarnation. For example, you might have a `pipeline.yaml` which is created by different process.
To achieve this, you can create a file called `.foxops-reset-ignore` in the root of your template. It would propagate it
to the root of your incarnation repository.

This file should contain a list of file paths (one per line) that should be ignored during the reset process.
19 changes: 19 additions & 0 deletions src/foxops/services/change.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,11 +772,30 @@ def _construct_merge_request_conflict_description(
return "\n\n".join(description_paragraphs)


def _load_fengine_reset_ignore(directory: Path) -> frozenset[str]:
"""Load the content of .fengine-reset-ignore file from the given directory.
The file contains a list of file/folder names (one per line) that should be
skipped during file deletion in delete_all_files_in_local_git_repository.
"""
ignore_file = directory / ".fengine-reset-ignore"
if not ignore_file.exists():
return frozenset()

content = ignore_file.read_text()
ignore_list = frozenset(line.strip() for line in content.splitlines() if line.strip())
return ignore_list


def delete_all_files_in_local_git_repository(directory: Path) -> None:
ignore_list = _load_fengine_reset_ignore(directory)

for file in directory.glob("*"):
if file.name == ".git":
continue

if file.name in ignore_list:
continue

if file.is_dir():
shutil.rmtree(file)
else:
Expand Down
66 changes: 66 additions & 0 deletions tests/services/test_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,3 +714,69 @@ async def test_diff_should_not_include_gitrepository(
diff = await change_service.diff_incarnation(initialized_incarnation.id)

assert diff == ""


def test_delete_all_files_in_local_git_repository_respects_fengine_reset_ignore_for_files(tmp_path):
# GIVEN
(tmp_path / ".fengine-reset-ignore").write_text("keep_me.txt\nkeep_me_too.md")
(tmp_path / "keep_me.txt").write_text("I should remain")
(tmp_path / "keep_me_too.md").write_text("I should also remain")
(tmp_path / "delete_me.txt").write_text("I should be deleted")

# WHEN
delete_all_files_in_local_git_repository(tmp_path)

# THEN
assert (tmp_path / "keep_me.txt").exists()
assert (tmp_path / "keep_me_too.md").exists()
assert not (tmp_path / "delete_me.txt").exists()
assert not (tmp_path / ".fengine-reset-ignore").exists()


def test_delete_all_files_in_local_git_repository_respects_fengine_reset_ignore_for_directories(tmp_path):
# GIVEN
(tmp_path / ".fengine-reset-ignore").write_text("keep_folder")
(tmp_path / "keep_folder").mkdir()
(tmp_path / "keep_folder" / "nested_file.txt").write_text("Nested content")
(tmp_path / "delete_folder").mkdir()
(tmp_path / "delete_folder" / "some_file.txt").write_text("To be deleted")

# WHEN
delete_all_files_in_local_git_repository(tmp_path)

# THEN
assert (tmp_path / "keep_folder").exists()
assert (tmp_path / "keep_folder" / "nested_file.txt").exists()
assert not (tmp_path / "delete_folder").exists()


def test_delete_all_files_in_local_git_repository_handles_empty_lines_in_fengine_reset_ignore(tmp_path):
# GIVEN
(tmp_path / ".fengine-reset-ignore").write_text("keep_me.txt\n\n \n\nkeep_me_too.md\n")
(tmp_path / "keep_me.txt").write_text("I should remain")
(tmp_path / "keep_me_too.md").write_text("I should also remain")
(tmp_path / "delete_me.txt").write_text("I should be deleted")

# WHEN
delete_all_files_in_local_git_repository(tmp_path)

# THEN
assert (tmp_path / "keep_me.txt").exists()
assert (tmp_path / "keep_me_too.md").exists()
assert not (tmp_path / "delete_me.txt").exists()


def test_delete_all_files_in_local_git_repository_handles_whitespace_in_fengine_reset_ignore(tmp_path):
# GIVEN
(tmp_path / ".fengine-reset-ignore").write_text(" keep_me.txt \n keep_folder ")
(tmp_path / "keep_me.txt").write_text("I should remain")
(tmp_path / "keep_folder").mkdir()
(tmp_path / "delete_me.txt").write_text("I should be deleted")

# WHEN
delete_all_files_in_local_git_repository(tmp_path)

# THEN
assert (tmp_path / "keep_me.txt").exists()
assert (tmp_path / "keep_folder").exists()
assert not (tmp_path / "delete_me.txt").exists()