Skip to content
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

Fix for video #47

Merged
merged 11 commits into from
Feb 15, 2024
163 changes: 132 additions & 31 deletions src/qw/changes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Compares changes between remote and local data, allowing the user to make decisions."""
from abc import ABC, abstractmethod
from collections import OrderedDict, defaultdict

from rich.console import Console
Expand All @@ -10,6 +11,73 @@
from qw.remote_repo.service import GitService


class LocalChangeDeterminer(ABC):
"""
Determines what should happen to local objects.

When a remote change is detected, the user needs to say what
should happen to the equivalent local object. The user could
respond interactively or select a policy to apply to all
changes.
"""

@abstractmethod
def should_remove_deleted_object(self, name):
"""Ask the user if the object should be removed locally."""
...

@abstractmethod
def version_increment(self):
"""
Ask the user if the version should be incremented.

Possible return values are: 0 (update but don't increment),
1 (update and increment), None (don't increment).
"""
...


class LocalChangeInteractive(LocalChangeDeterminer, str):
"""Asks the user every time."""

def should_remove_deleted_object(self, name):
"""Ask the user if they want to delete the object."""
return Confirm.ask(
f"{name} no longer exists in remote,"
" would you like to remove it from the local store?",
)

def version_increment(self):
"""Ask the user if they want the version incremented."""
prompt = "\n".join(
[
"Would you like to:",
"n (Don't save the update)",
"u (Update, but trivial change so don't increment the version)",
"i (Update and increment the version)",
"",
],
)
response = Prompt.ask(prompt, choices=["n", "u", "i"])
if response == "n":
return None
if response == "i":
return 1
return 0


class LocalChangeNone(LocalChangeDeterminer, str):
"""Always says not to change anything."""

def should_remove_deleted_object(self, _name):
"""Say not to remove."""
return False

def version_increment(self):
"""Say not to update."""
return


class ChangeHandler:
"""Allow user interaction to manage changes between local and remote design stage data."""

Expand Down Expand Up @@ -39,7 +107,28 @@ def __init__(
def show(self):
"""Show this difference on the screen."""
if not bool(self._diff):
return
if self._local_item is None:
Console().print(
"[green]New item ({}) #{}: {}[/]".format(
self._remote_item.stage.value,
self._remote_item.internal_id,
self._remote_item.title.replace("[", "\\["),
),
)
return self._remote_item
if self._remote_item is not None:
# Both exist, but no difference
return self._local_item
if not self._local_item.is_marked_deleted():
# Only a local item, and it has not yet been marked as deleted
Console().print(
"[red]Removed item ({}) #{}: {}[/]".format(
self._local_item.stage.value,
self._local_item.internal_id,
self._local_item.title.replace("[", "\\["),
),
)
return None
table = Table(
title=f"Changes detected for {self._local_item}:",
show_lines=True,
Expand All @@ -56,45 +145,50 @@ def show(self):
table.add_row(field, differences["self"], differences["other"])

Console().print(table)
return None

def prompt_for_version_change(self):
def _version_change_where_remote_deleted(
self,
determiner: LocalChangeDeterminer,
) -> DesignStages:
"""Prompt the user for a deleted object, if appropriate."""
if self._local_item is None:
return None
if self._local_item.is_marked_deleted():
# User has already requested to keep it
return self._local_item
# User has not requested to keep it yet
if determiner.should_remove_deleted_object(self._local_item):
# Remove the local item
return None
# Keep the local item
self._local_item.mark_as_deleted()
return self._local_item

def prompt_for_version_change(
self,
determiner: LocalChangeDeterminer,
) -> DesignStages:
"""
Prompt the user for what they want to do with this diff.

:return: The item so be stored in the local store; either
the local item (for no change) or the remote item (possibly
with the version number incremented).
"""
if self._local_item is None:
# New remote item, no prompt required
return self._remote_item
if self._remote_item is None:
return self._version_change_where_remote_deleted(determiner)
if not bool(self._diff):
if self._local_item is None:
# New remote item, no prompt required
return self._remote_item
if self._remote_item is not None:
# Both exist, but no difference
return self._local_item
# Only local exists, remote has been deleted
if Confirm.ask(
f"{self._local_item} no longer exists in remote, would you like to remove it from the local store?",
):
# Remove the local item
return None
# Keep the local item
# Both exist, but no difference
return self._local_item
prompt = "\n".join(
[
"Would you like to:",
"n (Don't save the update)",
"u (Update, but trivial change so don't increment the version)",
"i (Update and increment the version)",
"",
],
)

response = Prompt.ask(prompt, choices=["n", "u", "i"])
if response == "n":
# Both exist and there is a difference
update_decision = determiner.version_increment()
if update_decision is None:
return self._local_item
if response == "i":
self._remote_item.version += 1
self._remote_item.version = self._local_item.version + update_decision
return self._remote_item

def __init__(self, service: GitService, store: LocalStore):
Expand All @@ -105,7 +199,10 @@ def __init__(self, service: GitService, store: LocalStore):
def combine_local_and_remote_items(self) -> list[DesignStages]:
"""Compare local and remote design stages and prompt on any differences."""
diff_elements = self.diff_remote_and_local_items()
return self.get_local_items_from_diffs(diff_elements)
return self.get_local_items_from_diffs(
diff_elements,
LocalChangeInteractive(),
)

def diff_remote_and_local_items(self) -> list[DiffElement]:
"""
Expand All @@ -132,19 +229,23 @@ def diff_remote_and_local_items(self) -> list[DiffElement]:
def get_local_items_from_diffs(
cls,
diff_elements: list[DiffElement],
determiner: LocalChangeDeterminer,
) -> list[DesignStages]:
"""
Transform DiffElements into local items to be stored.

:param diff_elements: An iterable of DiffElements, each
representing a difference between the remote and local
items.
:param determiner: Determines what to do with each change.
:return: A list of local items to be set in the store.
"""
output_items = []
for diff_element in diff_elements:
diff_element.show()
output_item = diff_element.prompt_for_version_change()
output_item = diff_element.prompt_for_version_change(
determiner,
)
if output_item is not None:
output_items.append(output_item)

Expand Down
Loading
Loading