diff --git a/README.md b/README.md index 4a714625..5cfda22c 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ Commands: pull Fetch changes from Mergin Maps repository push Upload local changes into Mergin Maps repository remove Remove project from server. + rename Rename project in Mergin Maps repository. + reset Reset local changes in project. share Fetch permissions to project share-add Add permissions to [users] to project share-remove Remove [users] permissions from project diff --git a/mergin/cli.py b/mergin/cli.py index c127fb3b..5607b51a 100755 --- a/mergin/cli.py +++ b/mergin/cli.py @@ -602,5 +602,57 @@ def resolve_unfinished_pull(ctx): _print_unhandled_exception() +@cli.command() +@click.argument("project_path") +@click.argument("new_project_name") +@click.pass_context +def rename(ctx, project_path: str, new_project_name: str): + """Rename project in Mergin Maps repository.""" + mc = ctx.obj["client"] + if mc is None: + return + + if "/" not in project_path: + click.secho(f"Specify `project_path` as full name (/) instead.", fg="red") + return + + if "/" in new_project_name: + old_workspace, old_project_name = project_path.split("/") + new_workspace, new_project_name = new_project_name.split("/") + + if old_workspace != new_workspace: + click.secho( + "`new_project_name` should not contain namespace, project can only be rename within their namespace.\nTo move project to another workspace use web dashboard.", + fg="red", + ) + return + + try: + mc.rename_project(project_path, new_project_name) + click.echo("Project renamed") + except ClientError as e: + click.secho("Error: " + str(e), fg="red") + except Exception as e: + _print_unhandled_exception() + + +@cli.command() +@click.pass_context +def reset(ctx): + """Reset local changes in project.""" + directory = os.getcwd() + mc: MerginClient = ctx.obj["client"] + if mc is None: + return + try: + mc.reset_local_changes(directory) + except InvalidProject as e: + click.secho("Invalid project directory ({})".format(str(e)), fg="red") + except ClientError as e: + click.secho("Error: " + str(e), fg="red") + except Exception as e: + _print_unhandled_exception() + + if __name__ == "__main__": cli() diff --git a/mergin/client.py b/mergin/client.py index 169b2198..4acf8ffe 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -1131,6 +1131,36 @@ def has_writing_permissions(self, project_path): info = self.project_info(project_path) return info["permissions"]["upload"] + def rename_project(self, project_path: str, new_project_name: str) -> None: + """ + Rename project on server. + + :param project_path: Project's full name (/) + :type project_path: String + :param new_project_name: Project's new name () + :type new_project_name: String + """ + # TODO: this version check should be replaced by checking against the list + # of endpoints that server publishes in /config (once implemented) + if not is_version_acceptable(self.server_version(), "2023.5.4"): + raise NotImplementedError("This needs server at version 2023.5.4 or later") + + if "/" in new_project_name: + raise ClientError( + "Project's new name should be without workspace specification (). Project can only be renamed within its current workspace." + ) + + project_info = self.project_info(project_path) + project_id = project_info["id"] + path = "/v2/projects/" + project_id + url = urllib.parse.urljoin(self.url, urllib.parse.quote(path)) + json_headers = {"Content-Type": "application/json"} + data = {"name": new_project_name} + request = urllib.request.Request( + url, data=json.dumps(data).encode("utf-8"), headers=json_headers, method="PATCH" + ) + self._do_request(request) + def reset_local_changes(self, directory: str, files_to_reset: typing.List[str] = None) -> None: """ Reset local changes to either all files or only listed files. diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index e4ca10d8..37a37d2f 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -2161,6 +2161,56 @@ def test_project_metadata(mc): assert mp.version() == "v0" +def test_project_rename(mc: MerginClient): + """Check project can be renamed""" + + test_project = "test_project_rename" + test_project_renamed = "test_project_renamed" + project = API_USER + "/" + test_project + project_renamed = API_USER + "/" + test_project_renamed + + project_dir = os.path.join(TMP_DIR, test_project) # primary project dir + + cleanup(mc, project, [project_dir]) + cleanup(mc, project_renamed, []) + + shutil.copytree(TEST_DATA_DIR, project_dir) + mc.create_project_and_push(project, project_dir) + + # renamed project does not exist + with pytest.raises(ClientError, match="The requested URL was not found on the server"): + info = mc.project_info(project_renamed) + + # rename + mc.rename_project(project, test_project_renamed) + + # validate project info + project_info = mc.project_info(project_renamed) + assert project_info["version"] == "v1" + assert project_info["name"] == test_project_renamed + assert project_info["namespace"] == API_USER + with pytest.raises(ClientError, match="The requested URL was not found on the server"): + mc.project_info(project) + + # recreate project + cleanup(mc, project, [project_dir]) + shutil.copytree(TEST_DATA_DIR, project_dir) + mc.create_project_and_push(project, project_dir) + + # rename to existing name - created previously + mc.project_info(project_renamed) + with pytest.raises(ClientError, match="Name already exist within workspace"): + mc.rename_project(project, test_project_renamed) + + # cannot rename project that does not exist + with pytest.raises(ClientError, match="The requested URL was not found on the server."): + mc.rename_project(API_USER + "/" + "non_existing_project", "new_project") + + # cannot rename with full project name + with pytest.raises(ClientError, match="Project's new name should be without workspace specification"): + mc.rename_project(project, "workspace" + "/" + test_project_renamed) + + def test_download_files(mc: MerginClient): """Test downloading files at specified versions.""" test_project = "test_download_files"