From 94cd2b766754184028911ff9ad832aa9e476f5ec Mon Sep 17 00:00:00 2001
From: Getslow6 <43093176+Getslow6@users.noreply.github.com>
Date: Wed, 15 Jul 2026 09:22:40 +0000
Subject: [PATCH 1/4] Reset to remote function
---
otterwiki/repomgmt.py | 66 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/otterwiki/repomgmt.py b/otterwiki/repomgmt.py
index 7f4edccb..61de6153 100644
--- a/otterwiki/repomgmt.py
+++ b/otterwiki/repomgmt.py
@@ -217,6 +217,72 @@ def pull_from_remote(self, remote_url, private_key=None):
key_path, original_ssh_command, original_ssh_auth_sock
)
+ def reset_hard_to_remote(self, remote_url, private_key=None):
+ """
+ Fetch from a remote repository and hard-reset the current branch to it,
+ discarding all local commits and uncommitted changes.
+ Returns (success, output) tuple.
+ """
+ if not remote_url:
+ return False, "No remote URL provided"
+
+ with self.git_push_pull_Lock:
+ key_path, original_ssh_command, original_ssh_auth_sock = (
+ self._setup_ssh_environment(private_key)
+ )
+
+ try:
+ from otterwiki.server import app
+
+ try:
+ current_branch = self.storage.repo.active_branch.name
+ except TypeError:
+ # if there is no current branch we should probably just stop
+ return False, "No active branch found"
+
+ app.logger.info(
+ f"[RepositoryManager] Fetching from remote for hard reset: {remote_url}"
+ )
+
+ # fetch the remote branch into FETCH_HEAD without touching the working tree
+ fetch_result = self.storage.repo.git.fetch(
+ remote_url, current_branch
+ )
+ if fetch_result:
+ app.logger.info(
+ f"[RepositoryManager] Fetch result: {fetch_result}"
+ )
+
+ app.logger.warning(
+ f"[RepositoryManager] Hard-resetting '{current_branch}' to FETCH_HEAD "
+ f"from {remote_url} (local changes will be discarded)"
+ )
+ reset_result = self.storage.repo.git.reset(
+ '--hard', 'FETCH_HEAD'
+ )
+
+ self.storage.notify_repository_changed_from_external()
+
+ return (
+ True,
+ reset_result or "Hard reset completed successfully",
+ )
+
+ except Exception as e:
+ try:
+ from otterwiki.server import app
+
+ app.logger.error(
+ f"[RepositoryManager] Hard reset from remote failed: {e}"
+ )
+ except ImportError:
+ pass
+ return False, str(e)
+ finally:
+ self._restore_ssh_environment(
+ key_path, original_ssh_command, original_ssh_auth_sock
+ )
+
def push_to_remote_async(self, remote_url, private_key=None):
"""
Asynchronously push to remote repository.
From 4969390e69feb3a1ea0206c6bb6e42c62ebc740a Mon Sep 17 00:00:00 2001
From: Getslow6 <43093176+Getslow6@users.noreply.github.com>
Date: Wed, 15 Jul 2026 09:47:38 +0000
Subject: [PATCH 2/4] Add reset to remote function to frontend
---
otterwiki/preferences.py | 31 +++++++++++++++++++
.../admin/repository_management.html | 1 +
2 files changed, 32 insertions(+)
diff --git a/otterwiki/preferences.py b/otterwiki/preferences.py
index da92dae7..0a81173d 100644
--- a/otterwiki/preferences.py
+++ b/otterwiki/preferences.py
@@ -264,6 +264,8 @@ def handle_repository_management(form):
git_action_result = _handle_git_force_push()
elif form.get("git_pull"):
git_action_result = _handle_git_pull()
+ elif form.get("git_reset_remote"):
+ git_action_result = _handle_git_reset_remote()
# If it was a git action, return to form with results
if git_action_result:
@@ -439,6 +441,35 @@ def _handle_git_pull():
return {"action": "pull", "success": success, "output": output}
+def _handle_git_reset_remote():
+ """Handle git reset to remote action."""
+ from otterwiki.repomgmt import get_repo_manager
+ from otterwiki.server import app
+
+ if not app.config.get('GIT_REMOTE_PULL_ENABLED'):
+ return {
+ "action": "reset_remote",
+ "success": False,
+ "output": "Pull functionality is not enabled",
+ }
+
+ remote_url = app.config.get('GIT_REMOTE_PULL_URL')
+ private_key = app.config.get('GIT_REMOTE_PULL_PRIVATE_KEY')
+
+ repo_manager = get_repo_manager()
+ if not repo_manager:
+ return {
+ "action": "reset_remote",
+ "success": False,
+ "output": "Repository manager not available",
+ }
+
+ success, output = repo_manager.reset_hard_to_remote(
+ remote_url, private_key
+ )
+ return {"action": "reset_remote", "success": success, "output": output}
+
+
def handle_test_mail_preferences(form):
if not has_permission("ADMIN"):
abort(403)
diff --git a/otterwiki/templates/admin/repository_management.html b/otterwiki/templates/admin/repository_management.html
index 4a2e6b16..4261ff0e 100644
--- a/otterwiki/templates/admin/repository_management.html
+++ b/otterwiki/templates/admin/repository_management.html
@@ -146,6 +146,7 @@
Repository Management
{% endif %}
{% if config.GIT_REMOTE_PULL_ENABLED %}
+
{% endif %}
{% endif %}
From 7be726c86d7234b9f9f1ba9c40f1eb7d3a8b852e Mon Sep 17 00:00:00 2001
From: Getslow6 <43093176+Getslow6@users.noreply.github.com>
Date: Wed, 15 Jul 2026 10:03:09 +0000
Subject: [PATCH 3/4] Improve action result text
---
otterwiki/preferences.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/otterwiki/preferences.py b/otterwiki/preferences.py
index 0a81173d..f57bf033 100644
--- a/otterwiki/preferences.py
+++ b/otterwiki/preferences.py
@@ -448,7 +448,7 @@ def _handle_git_reset_remote():
if not app.config.get('GIT_REMOTE_PULL_ENABLED'):
return {
- "action": "reset_remote",
+ "action": "Reset remote",
"success": False,
"output": "Pull functionality is not enabled",
}
@@ -459,7 +459,7 @@ def _handle_git_reset_remote():
repo_manager = get_repo_manager()
if not repo_manager:
return {
- "action": "reset_remote",
+ "action": "Reset remote",
"success": False,
"output": "Repository manager not available",
}
@@ -467,7 +467,7 @@ def _handle_git_reset_remote():
success, output = repo_manager.reset_hard_to_remote(
remote_url, private_key
)
- return {"action": "reset_remote", "success": success, "output": output}
+ return {"action": "Reset remote", "success": success, "output": output}
def handle_test_mail_preferences(form):
From cf50d445313684cf0203fd04f46d37c8eeda3ee8 Mon Sep 17 00:00:00 2001
From: Getslow6 <43093176+Getslow6@users.noreply.github.com>
Date: Wed, 15 Jul 2026 10:43:12 +0000
Subject: [PATCH 4/4] Add and resolve tests for reset to remote functionality
---
otterwiki/preferences.py | 10 ++++----
otterwiki/repomgmt.py | 2 +-
tests/test_repository_management.py | 37 +++++++++++++++++++++++++++++
3 files changed, 42 insertions(+), 7 deletions(-)
diff --git a/otterwiki/preferences.py b/otterwiki/preferences.py
index f57bf033..7049edf9 100644
--- a/otterwiki/preferences.py
+++ b/otterwiki/preferences.py
@@ -448,7 +448,7 @@ def _handle_git_reset_remote():
if not app.config.get('GIT_REMOTE_PULL_ENABLED'):
return {
- "action": "Reset remote",
+ "action": "reset to remote",
"success": False,
"output": "Pull functionality is not enabled",
}
@@ -459,15 +459,13 @@ def _handle_git_reset_remote():
repo_manager = get_repo_manager()
if not repo_manager:
return {
- "action": "Reset remote",
+ "action": "reset to remote",
"success": False,
"output": "Repository manager not available",
}
- success, output = repo_manager.reset_hard_to_remote(
- remote_url, private_key
- )
- return {"action": "Reset remote", "success": success, "output": output}
+ success, output = repo_manager.reset_to_remote(remote_url, private_key)
+ return {"action": "reset to remote", "success": success, "output": output}
def handle_test_mail_preferences(form):
diff --git a/otterwiki/repomgmt.py b/otterwiki/repomgmt.py
index 61de6153..113214b6 100644
--- a/otterwiki/repomgmt.py
+++ b/otterwiki/repomgmt.py
@@ -217,7 +217,7 @@ def pull_from_remote(self, remote_url, private_key=None):
key_path, original_ssh_command, original_ssh_auth_sock
)
- def reset_hard_to_remote(self, remote_url, private_key=None):
+ def reset_to_remote(self, remote_url, private_key=None):
"""
Fetch from a remote repository and hard-reset the current branch to it,
discarding all local commits and uncommitted changes.
diff --git a/tests/test_repository_management.py b/tests/test_repository_management.py
index cea2b7d0..f5151cf9 100644
--- a/tests/test_repository_management.py
+++ b/tests/test_repository_management.py
@@ -428,6 +428,7 @@ def test_button_visibility(self, app_with_user, admin_client, test_data):
assert soup.find('input', {'name': 'git_push'}) is None
assert soup.find('input', {'name': 'git_force_push'}) is None
assert soup.find('input', {'name': 'git_pull'}) is None
+ assert soup.find('input', {'name': 'git_reset_remote'}) is None
# Enable push functionality
self._setup_features(admin_client, test_data, enable_push=True)
@@ -437,6 +438,7 @@ def test_button_visibility(self, app_with_user, admin_client, test_data):
assert soup.find('input', {'name': 'git_push'}) is not None
assert soup.find('input', {'name': 'git_force_push'}) is not None
assert soup.find('input', {'name': 'git_pull'}) is None
+ assert soup.find('input', {'name': 'git_reset_remote'}) is None
# Enable both push and pull
self._setup_features(
@@ -448,6 +450,7 @@ def test_button_visibility(self, app_with_user, admin_client, test_data):
assert soup.find('input', {'name': 'git_push'}) is not None
assert soup.find('input', {'name': 'git_force_push'}) is not None
assert soup.find('input', {'name': 'git_pull'}) is not None
+ assert soup.find('input', {'name': 'git_reset_remote'}) is not None
@patch('otterwiki.repomgmt.RepositoryManager.push_to_remote')
def test_push_button_functionality(
@@ -521,6 +524,30 @@ def test_pull_button_functionality(
mock_pull.assert_called_with(test_data['remote_url'], "")
+ @patch('otterwiki.repomgmt.RepositoryManager.reset_to_remote')
+ def test_reset_to_remote_button_functionality(
+ self, mock_pull, app_with_user, admin_client, test_data
+ ):
+ """Test that the git pull button works correctly."""
+ mock_pull.return_value = (True, "HEAD is now at")
+
+ # Enable pull functionality
+ self._setup_features(admin_client, test_data, enable_pull=True)
+
+ # Test pull button
+ rv = admin_client.post(
+ ADMIN_REPO_MGMT_URL,
+ data={"git_reset_remote": "Reset to Remote"},
+ follow_redirects=True,
+ )
+ assert rv.status_code == 200
+
+ html = rv.data.decode()
+ assert "Reset To Remote Results" in html
+ assert "HEAD is now at" in html
+
+ mock_pull.assert_called_with(test_data['remote_url'], "")
+
def test_error_handling_and_styling(
self, app_with_user, admin_client, test_data
):
@@ -592,6 +619,16 @@ def test_disabled_feature_error_handling(
assert "Pull Results" in html
assert "Pull functionality is not enabled" in html
+ # Test reset remote button when disabled
+ rv = admin_client.post(
+ ADMIN_REPO_MGMT_URL,
+ data={"git_reset_remote": "Reset to Remote"},
+ follow_redirects=True,
+ )
+ html = rv.data.decode()
+ assert "Reset To Remote Results" in html
+ assert "Pull functionality is not enabled" in html
+
@patch('otterwiki.repomgmt.get_repo_manager')
def test_unavailable_repo_manager_handling(
self, mock_get_repo_manager, app_with_user, admin_client, test_data