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