diff --git a/otterwiki/preferences.py b/otterwiki/preferences.py index da92dae7..7049edf9 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,33 @@ 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 to 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 to remote", + "success": False, + "output": "Repository manager not available", + } + + 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): if not has_permission("ADMIN"): abort(403) diff --git a/otterwiki/repomgmt.py b/otterwiki/repomgmt.py index 7f4edccb..113214b6 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_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. 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 @@