From 32e01859dcda88da34a1b1efc8aacc8f77db0a0e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Apr 2024 12:58:13 +0200 Subject: [PATCH 01/32] Fix import --- client/version_control/changes_viewer/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/version_control/changes_viewer/control.py b/client/version_control/changes_viewer/control.py index a308386..22b1f58 100644 --- a/client/version_control/changes_viewer/control.py +++ b/client/version_control/changes_viewer/control.py @@ -5,7 +5,7 @@ registered_host, get_current_context, ) -from ayon_core.tools.ayon_utils.models import HierarchyModel +from ayon_core.tools.common_models import HierarchyModel from ayon_core.modules import ModulesManager from version_control.backends.perforce.api.rest_stub import PerforceRestStub From c094b90d3d5eb3049decf932fc65f2aae9faa738 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Apr 2024 13:23:56 +0200 Subject: [PATCH 02/32] Removed development variables --- client/version_control/changes_viewer/control.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/version_control/changes_viewer/control.py b/client/version_control/changes_viewer/control.py index 22b1f58..d56c521 100644 --- a/client/version_control/changes_viewer/control.py +++ b/client/version_control/changes_viewer/control.py @@ -55,9 +55,7 @@ def login(self): # TODO push to controller conn_info = version_control_addon.get_connection_info( project_name=self.get_current_project_name() ) - conn_info = {"host": "localhost", "port": 1666, "username": "admin", - "password": "pass12349ers", - "workspace_dir": "c:/projects/ayon_test/unreal/admin_ygor_7550"} # TEMP!!!! + if conn_info: self._conn_info = conn_info PerforceRestStub.login(host=conn_info["host"], From bf81b6aa6bdc4fe9b69a1952c0da72cc78aae2c7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Apr 2024 16:28:56 +0200 Subject: [PATCH 03/32] Added new hook to show Sync dialog Allows artists to synch to specific change list before Unreal is launched. --- client/version_control/addon.py | 10 ++ .../perforce/pre_load_sync_project.py | 97 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 client/version_control/launch_hooks/perforce/pre_load_sync_project.py diff --git a/client/version_control/addon.py b/client/version_control/addon.py index da0796f..102c646 100644 --- a/client/version_control/addon.py +++ b/client/version_control/addon.py @@ -123,6 +123,16 @@ def get_publish_plugin_paths(self, host_name): return [os.path.join(VERSION_CONTROL_ADDON_DIR, "plugins", "publish")] + def get_launch_hook_paths(self, _app): + """Implementation for applications launch hooks. + + Returns: + (str): full absolut path to directory with hooks for the module + """ + + return os.path.join(VERSION_CONTROL_ADDON_DIR, "launch_hooks", + self.active_version_control_system) + @click.group("version_control", help="Version Control module related commands.") def cli_main(): diff --git a/client/version_control/launch_hooks/perforce/pre_load_sync_project.py b/client/version_control/launch_hooks/perforce/pre_load_sync_project.py new file mode 100644 index 0000000..1d781a5 --- /dev/null +++ b/client/version_control/launch_hooks/perforce/pre_load_sync_project.py @@ -0,0 +1,97 @@ +"""Shows dialog to sync Unreal project + +Requires: + None + +Provides: + self.data["last_workfile_path"] + +""" +import os + +from ayon_applications import ( + PreLaunchHook, + ApplicationLaunchFailed, + LaunchTypes, +) + +from ayon_core.tools.utils import qt_app_context +from ayon_core.modules import ModulesManager + +from version_control.changes_viewer import ChangesWindows + + +class SyncUnrealProject(PreLaunchHook): + """Show dialog for artist to sync to specific change list. + + It is triggered before Unreal launch as syncing inside would likely + lead to locks. + It is called before `pre_workfile_preparation` which is using + self.data["last_workfile_path"]. + + It is expected that workspace would be created, connected + and first version of project would be synced before. + """ + + order = -5 + app_groups = ["unreal"] + launch_types = {LaunchTypes.local} + + def execute(self): + version_control_addon = self._get_enabled_version_control_addon() + if not version_control_addon: + self.log.info("Version control is not enabled, skipping") + return + + self.data["last_workfile_path"] = self._get_unreal_project_path( + version_control_addon) + + with qt_app_context(): + changes_tool = ChangesWindows() + changes_tool.show() + changes_tool.raise_() + changes_tool.activateWindow() + changes_tool.showNormal() + + changes_tool.refresh() + changes_tool.exec_() + + def _get_unreal_project_path(self, version_control_addon): + conn_info = version_control_addon.get_connection_info( + project_name=self.data["project_name"] + ) + workdir = conn_info["workspace_dir"] + if not os.path.exists(workdir): + raise RuntimeError(f"{workdir} must exists for using version " + "control") + project_files = self._find_uproject_files(workdir) + if len(project_files) != 1: + raise RuntimeError("Found unexpected number of projects " + f"'{project_files}.\n" + "Expected only single Unreal project.") + return project_files[0] + + def _get_enabled_version_control_addon(self): + manager = ModulesManager() + version_control_addon = manager.get("version_control") + if version_control_addon and version_control_addon.enabled: + return version_control_addon + return None + + def _find_uproject_files(self, start_dir): + """ + This function searches for files with the .uproject extension recursively + within a starting directory and its subdirectories. + + Args: + start_dir: The starting directory from where the search begins. + + Returns: + A list of full paths to all the found .uproject files. + """ + uproject_files = [] + for dirpath, dirnames, filenames in os.walk(start_dir): + for filename in filenames: + if filename.endswith(".uproject"): + uproject_files.append(os.path.join(dirpath, filename)) + return uproject_files From 528abc5d2522f21dc49796a239404e45d6cbfe08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 29 Apr 2024 16:53:34 +0200 Subject: [PATCH 04/32] :bug: fix call --- client/version_control/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/version_control/addon.py b/client/version_control/addon.py index 102c646..97c45ac 100644 --- a/client/version_control/addon.py +++ b/client/version_control/addon.py @@ -96,7 +96,7 @@ def sync_to_version(self, conn_info, change_id): def tray_exit(self): if self.enabled and \ - self.webserver and self.webserver.server_is_running(): + self.webserver and self.webserver.server_is_running: self.webserver.stop() def tray_init(self): From 847c9b940e6923cd6d85e85b64d50d41210835ef Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Apr 2024 11:13:14 +0200 Subject: [PATCH 05/32] Pass launch data to changes viewer Launch context data must be passed to the Viewer to query connection info properly. --- .../version_control/changes_viewer/control.py | 58 +++++-------------- .../version_control/changes_viewer/window.py | 4 +- .../perforce/pre_load_sync_project.py | 2 +- 3 files changed, 17 insertions(+), 47 deletions(-) diff --git a/client/version_control/changes_viewer/control.py b/client/version_control/changes_viewer/control.py index d56c521..f1cd88b 100644 --- a/client/version_control/changes_viewer/control.py +++ b/client/version_control/changes_viewer/control.py @@ -1,5 +1,3 @@ -import ayon_api - from ayon_core.lib.events import QueuedEventSystem from ayon_core.pipeline import ( registered_host, @@ -16,14 +14,17 @@ class ChangesViewerController: Goal of this controller is to provide a way to get current context. """ - def __init__(self, host=None): + def __init__(self, launch_data, host=None): if host is None: host = registered_host() self._host = host - self._current_context = None - self._current_project = None - self._current_folder_id = None - self._current_folder_set = False + self._current_project = launch_data["project_name"] + self._current_folder_id = launch_data["folder_entity"]["id"] + + manager = ModulesManager() + version_control_addon = manager.get("version_control") + self._version_control_addon = version_control_addon + self.enabled = version_control_addon and version_control_addon.enabled # Switch dialog requirements self._hierarchy_model = HierarchyModel(self) @@ -38,21 +39,17 @@ def register_event_callback(self, topic, callback): self._event_system.add_callback(topic, callback) def reset(self): - self._current_context = None self._current_project = None self._current_folder_id = None - self._current_folder_set = False self._conn_info = None self._hierarchy_model.reset() - def login(self): # TODO push to controller - manager = ModulesManager() - version_control_addon = manager.get("version_control") - if not version_control_addon or not version_control_addon.enabled: + def login(self): + if not self.enabled: return - conn_info = version_control_addon.get_connection_info( + conn_info = self._version_control_addon.get_connection_info( project_name=self.get_current_project_name() ) @@ -68,47 +65,20 @@ def get_changes(self): return PerforceRestStub.get_changes() def sync_to(self, change_id): - manager = ModulesManager() - version_control_addon = manager.get("version_control") - if not version_control_addon or not version_control_addon.enabled: + if not self.enabled: return - conn_info = version_control_addon.get_connection_info( + conn_info = self._version_control_addon.get_connection_info( project_name=self.get_current_project_name() ) if conn_info: self._conn_info = conn_info - version_control_addon.sync_to_version(conn_info, change_id) - - def get_current_context(self): - if self._current_context is None: - if hasattr(self._host, "get_current_context"): - self._current_context = self._host.get_current_context() - else: - self._current_context = get_current_context() - return self._current_context + self._version_control_addon.sync_to_version(conn_info, change_id) def get_current_project_name(self): - return "ayon_test" # TEMP!!! - if self._current_project is None: - self._current_project = self.get_current_context()["project_name"] return self._current_project def get_current_folder_id(self): - if self._current_folder_set: - return self._current_folder_id - - context = self.get_current_context() - project_name = context["project_name"] - folder_name = context.get("asset_name") - folder_id = None - if folder_name: - folder = ayon_api.get_folder_by_path(project_name, folder_name) - if folder: - folder_id = folder["id"] - - self._current_folder_id = folder_id - self._current_folder_set = True return self._current_folder_id def _create_event_system(self): diff --git a/client/version_control/changes_viewer/window.py b/client/version_control/changes_viewer/window.py index db5cd85..25b445f 100644 --- a/client/version_control/changes_viewer/window.py +++ b/client/version_control/changes_viewer/window.py @@ -25,7 +25,7 @@ class ChangesWindows(QtWidgets.QDialog): - def __init__(self, controller=None, parent=None): + def __init__(self, controller=None, parent=None, launch_data=None): super(ChangesWindows, self).__init__(parent=parent) self.setWindowTitle("Changes Viewer") self.setObjectName("ChangesViewer") @@ -37,7 +37,7 @@ def __init__(self, controller=None, parent=None): self.resize(780, 430) if controller is None: - controller = ChangesViewerController() + controller = ChangesViewerController(launch_data=launch_data) # Trigger refresh on first called show self._first_show = True diff --git a/client/version_control/launch_hooks/perforce/pre_load_sync_project.py b/client/version_control/launch_hooks/perforce/pre_load_sync_project.py index 1d781a5..6d21930 100644 --- a/client/version_control/launch_hooks/perforce/pre_load_sync_project.py +++ b/client/version_control/launch_hooks/perforce/pre_load_sync_project.py @@ -47,7 +47,7 @@ def execute(self): version_control_addon) with qt_app_context(): - changes_tool = ChangesWindows() + changes_tool = ChangesWindows(launch_data=self.data) changes_tool.show() changes_tool.raise_() changes_tool.activateWindow() From 0b3abe474107b2a5d06698140a56654e0ea0fa4c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Apr 2024 11:14:25 +0200 Subject: [PATCH 06/32] Changed logging to debug --- .../backends/perforce/rest_routes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/version_control/backends/perforce/rest_routes.py b/client/version_control/backends/perforce/rest_routes.py index b899f58..be6fd2d 100644 --- a/client/version_control/backends/perforce/rest_routes.py +++ b/client/version_control/backends/perforce/rest_routes.py @@ -68,7 +68,7 @@ async def post(self, request) -> Response: class AddEndpoint(PerforceRestApiEndpoint): """Returns list of dict with project info (id, name).""" async def post(self, request) -> Response: - log.info("AddEndpoint called") + log.debug("AddEndpoint called") content = await request.json() result = VersionControlPerforce.add(content["path"], @@ -83,7 +83,7 @@ async def post(self, request) -> Response: class SyncLatestEndpoint(PerforceRestApiEndpoint): """Returns list of dict with project info (id, name).""" async def post(self, request) -> Response: - log.info("SyncLatestEndpoint called") + log.debug("SyncLatestEndpoint called") content = await request.json() result = VersionControlPerforce.sync_latest_version(content["path"]) @@ -97,7 +97,7 @@ async def post(self, request) -> Response: class SyncVersionEndpoint(PerforceRestApiEndpoint): """Returns list of dict with project info (id, name).""" async def post(self, request) -> Response: - log.info("SyncVersionEndpoint called") + log.debug("SyncVersionEndpoint called") content = await request.json() result = VersionControlPerforce.sync_to_version(content["path"], @@ -112,7 +112,7 @@ async def post(self, request) -> Response: class CheckoutEndpoint(PerforceRestApiEndpoint): """Returns list of dict with project info (id, name).""" async def post(self, request) -> Response: - log.info("CheckoutEndpoint called") + log.debug("CheckoutEndpoint called") content = await request.json() @@ -128,7 +128,7 @@ async def post(self, request) -> Response: class IsCheckoutedEndpoint(PerforceRestApiEndpoint): """Checks if file is checkouted by sameone.""" async def post(self, request) -> Response: - log.info("CheckoutEndpoint called") + log.debug("CheckoutEndpoint called") content = await request.json() @@ -143,7 +143,7 @@ async def post(self, request) -> Response: class GetChanges(PerforceRestApiEndpoint): """Returns list of submitted changes.""" async def post(self, request) -> Response: - log.info("GetChanges called") + log.debug("GetChanges called") content = await request.json() result = VersionControlPerforce.get_changes() @@ -157,7 +157,7 @@ async def post(self, request) -> Response: class GetLastChangelist(PerforceRestApiEndpoint): """Returns list of dict with project info (id, name).""" async def post(self, request) -> Response: - log.info("GetLatestChangelist called") + log.debug("GetLatestChangelist called") content = await request.json() result = VersionControlPerforce.get_last_change_list() @@ -171,7 +171,7 @@ async def post(self, request) -> Response: class SubmitChangelist(PerforceRestApiEndpoint): """Returns list of dict with project info (id, name).""" async def post(self, request) -> Response: - log.info("SubmitChangelist called") + log.debug("SubmitChangelist called") content = await request.json() result = VersionControlPerforce.submit_change_list(content["comment"]) @@ -185,7 +185,7 @@ async def post(self, request) -> Response: class ExistsOnServer(PerforceRestApiEndpoint): """Returns information about file on 'path'.""" async def post(self, request) -> Response: - log.info("exists_on_server called") + log.debug("exists_on_server called") content = await request.json() result = VersionControlPerforce.exists_on_server(content["path"]) From e7cf82f6e5bfaed67890ac29fedaafc565a431bc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Apr 2024 11:18:23 +0200 Subject: [PATCH 07/32] Added debug logging --- client/version_control/backends/perforce/rest_routes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/version_control/backends/perforce/rest_routes.py b/client/version_control/backends/perforce/rest_routes.py index be6fd2d..81d9b28 100644 --- a/client/version_control/backends/perforce/rest_routes.py +++ b/client/version_control/backends/perforce/rest_routes.py @@ -100,8 +100,10 @@ async def post(self, request) -> Response: log.debug("SyncVersionEndpoint called") content = await request.json() + log.debug(f"Syncing '{content['path']}' to {content['version']}") result = VersionControlPerforce.sync_to_version(content["path"], content["version"]) + log.debug("Synced") return Response( status=200, body=self.encode(result), From a838bc10da520e4ac6294befffaa0e8702ca2d31 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 30 Apr 2024 15:56:56 +0200 Subject: [PATCH 08/32] Reworked changes dialog Disables Sync button when synchronizing. Cleaned up model, introduced proxy for integer sorting. --- .../version_control/changes_viewer/control.py | 11 +--- .../version_control/changes_viewer/model.py | 44 +++++++------ .../version_control/changes_viewer/widgets.py | 64 +++++++++++++++++-- .../version_control/changes_viewer/window.py | 44 ++----------- .../perforce/pre_load_sync_project.py | 1 - 5 files changed, 90 insertions(+), 74 deletions(-) diff --git a/client/version_control/changes_viewer/control.py b/client/version_control/changes_viewer/control.py index f1cd88b..8e1caf0 100644 --- a/client/version_control/changes_viewer/control.py +++ b/client/version_control/changes_viewer/control.py @@ -3,7 +3,6 @@ registered_host, get_current_context, ) -from ayon_core.tools.common_models import HierarchyModel from ayon_core.modules import ModulesManager from version_control.backends.perforce.api.rest_stub import PerforceRestStub @@ -26,8 +25,6 @@ def __init__(self, launch_data, host=None): self._version_control_addon = version_control_addon self.enabled = version_control_addon and version_control_addon.enabled - # Switch dialog requirements - self._hierarchy_model = HierarchyModel(self) self._event_system = self._create_event_system() def emit_event(self, topic, data=None, source=None): @@ -38,13 +35,6 @@ def emit_event(self, topic, data=None, source=None): def register_event_callback(self, topic, callback): self._event_system.add_callback(topic, callback) - def reset(self): - self._current_project = None - self._current_folder_id = None - self._conn_info = None - - self._hierarchy_model.reset() - def login(self): if not self.enabled: return @@ -83,3 +73,4 @@ def get_current_folder_id(self): def _create_event_system(self): return QueuedEventSystem() + diff --git a/client/version_control/changes_viewer/model.py b/client/version_control/changes_viewer/model.py index 19264e9..2411482 100644 --- a/client/version_control/changes_viewer/model.py +++ b/client/version_control/changes_viewer/model.py @@ -1,4 +1,5 @@ from qtpy import QtCore, QtGui +from datetime import datetime CHANGE_ROLE = QtCore.Qt.UserRole + 1 DESC_ROLE = QtCore.Qt.UserRole + 2 @@ -8,13 +9,13 @@ class ChangesModel(QtGui.QStandardItemModel): column_labels = [ - "Change number", + "Change", "Description", "Author", "Date submitted", ] - def __init__(self, controller, *args, parent=None, **kwargs): + def __init__(self, controller, *args, **kwargs): super(ChangesModel, self).__init__(*args, **kwargs) self._changes_by_item_id = {} @@ -29,31 +30,38 @@ def __init__(self, controller, *args, parent=None, **kwargs): def refresh(self): self.removeRows(0, self.rowCount()) # Clear existing data changes = self._controller.get_changes() - for i, change in enumerate(changes): + + for change in changes: + date_time = datetime.fromtimestamp(int(change["time"])) + date_string = date_time.strftime("%Y%m%dT%H%M%SZ") + number_item = QtGui.QStandardItem(change["change"]) number_item.setData(int(change["change"]), CHANGE_ROLE) # Store number for sorting - # number_item.setData(change["user"], DESC_ROLE) # Store number for sorting - # number_item.setData(change["user"], AUTHOR_ROLE) # Store number for sorting - # number_item.setData(change["time"], CREATED_ROLE) # Store number for sorting - # self.appendRow(number_item) desc_item = QtGui.QStandardItem(change["desc"]) author_item = QtGui.QStandardItem(change["user"]) - date_item = QtGui.QStandardItem(change["time"]) + date_item = QtGui.QStandardItem(date_string) self.appendRow([number_item, desc_item, author_item, date_item]) def data(self, index, role=QtGui.Qt.DisplayRole): - if role == QtGui.Qt.DisplayRole: - return super().data(index, role) - elif role == CHANGE_ROLE: + if role == CHANGE_ROLE: # Return actual data stored for sorting return index.model().item(index.row(), 0).data(CHANGE_ROLE) - return None - - def sort(self, column, order=QtGui.Qt.AscendingOrder): - if column == 0: # Sort by number (stored in user role) - self.sortItems(0, order, CHANGE_ROLE) - else: - super().sort(column, order) + return super().data(index, role) def get_change_by_id(self, item_id): return self._changes_by_item_id.get(item_id) + + +class CustomSortProxyModel(QtCore.QSortFilterProxyModel): + def lessThan(self, source_left, source_right): + first_column = 0 + + # Use different sort roles for the first column and others + SORT_ROLE = QtGui.Qt.DisplayRole + if source_left.column() == first_column: + SORT_ROLE = CHANGE_ROLE + + left_data = self.sourceModel().data(source_left, SORT_ROLE) + right_data = self.sourceModel().data(source_right, SORT_ROLE) + + return left_data < right_data diff --git a/client/version_control/changes_viewer/widgets.py b/client/version_control/changes_viewer/widgets.py index 62e6af8..8bbd97d 100644 --- a/client/version_control/changes_viewer/widgets.py +++ b/client/version_control/changes_viewer/widgets.py @@ -1,14 +1,26 @@ from qtpy import QtWidgets, QtCore from ayon_core.tools.utils import TreeView +from ayon_core.tools.utils.delegates import PrettyTimeDelegate +from .model import ( + ChangesModel, + CHANGE_ROLE, + CustomSortProxyModel +) -class ChangesDetail(QtWidgets.QWidget): + +class ChangesDetailWidget(QtWidgets.QWidget): """Table printing list of changes from Perforce""" sync_triggered = QtCore.Signal() - def __init__(self, model, parent=None): - super(ChangesDetail, self).__init__(parent) + def __init__(self, controller, parent=None): + super().__init__(parent) + + model = ChangesModel(controller=controller, parent=self) + proxy = CustomSortProxyModel() + proxy.setSourceModel(model) + proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) changes_view = TreeView(self) changes_view.setSelectionMode( @@ -17,9 +29,17 @@ def __init__(self, model, parent=None): changes_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) changes_view.setSortingEnabled(True) changes_view.setAlternatingRowColors(True) - changes_view.setModel(model) + changes_view.setModel(proxy) changes_view.setIndentation(0) + changes_view.setColumnWidth(0, 70) + changes_view.setColumnWidth(1, 430) + changes_view.setColumnWidth(2, 100) + changes_view.setColumnWidth(3, 120) + + time_delegate = PrettyTimeDelegate() + changes_view.setItemDelegateForColumn(3, time_delegate) + sync_btn = QtWidgets.QPushButton("Sync to", self) self._block_changes = False @@ -32,10 +52,42 @@ def __init__(self, model, parent=None): layout.addWidget(sync_btn, 0, QtCore.Qt.AlignRight) sync_btn.clicked.connect(self._on_sync_clicked) - # changes_view.textChanged.connect(self._on_text_change) + self._model = model + self._controller = controller self._changes_view = changes_view self.sync_btn = sync_btn + self._thread = None + self._time_delegate = time_delegate + + def reset(self): + self._model.refresh() def _on_sync_clicked(self): - self.sync_triggered.emit() + selection_model = self._changes_view.selectionModel() + current_index = selection_model.currentIndex() + if not current_index.isValid(): + return + + change_id = current_index.data(CHANGE_ROLE) + + self.sync_btn.setEnabled(False) + thread = SyncThread(self._controller, change_id) + thread.finished.connect(self._on_thread_finished) + thread.start() + + self._thread = thread + + def _on_thread_finished(self): + self.sync_btn.setEnabled(True) + + +class SyncThread(QtCore.QThread): + + def __init__(self, controller, change_id): + super().__init__() + self._controller = controller + self._change_id = change_id + + def run(self): + self._controller.sync_to(self._change_id) diff --git a/client/version_control/changes_viewer/window.py b/client/version_control/changes_viewer/window.py index 25b445f..8dc36ef 100644 --- a/client/version_control/changes_viewer/window.py +++ b/client/version_control/changes_viewer/window.py @@ -1,23 +1,15 @@ -import os import sys from qtpy import QtWidgets, QtCore -import qtawesome from ayon_core import style -from ayon_core.pipeline import registered_host -from ayon_core.tools.utils import PlaceholderLineEdit from ayon_core.tools.utils.lib import ( iter_model_rows, qt_app_context ) -from ayon_core.tools.utils.models import RecursiveSortFilterProxyModel from .control import ChangesViewerController -from .model import ( - ChangesModel, - CHANGE_ROLE -) -from .widgets import ChangesDetail + +from .widgets import ChangesDetailWidget module = sys.modules[__name__] @@ -39,48 +31,22 @@ def __init__(self, controller=None, parent=None, launch_data=None): if controller is None: controller = ChangesViewerController(launch_data=launch_data) - # Trigger refresh on first called show self._first_show = True - model = ChangesModel(controller=controller, parent=self) - proxy = RecursiveSortFilterProxyModel() - proxy.setSourceModel(model) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - proxy.setSortRole(CHANGE_ROLE) + details_widget = ChangesDetailWidget(controller, self) - details_widget = ChangesDetail(proxy, self) - details_widget.sync_triggered.connect(self._on_sync_to) - - layout = QtWidgets.QHBoxLayout() + layout = QtWidgets.QHBoxLayout(self) layout.addWidget(details_widget, stretch=1) - self.setLayout(layout) self._controller = controller - self._model = model - self._proxy = proxy self._details_widget = details_widget - def _on_sync_to(self): - current_index = ( - self._details_widget._changes_view.selectionModel().currentIndex()) - if not current_index.isValid(): - return - - change_id = current_index.data(0) - self._controller.sync_to(change_id) - - def _on_refresh_clicked(self): - self.refresh() - - def refresh(self): - self._model.refresh() - def showEvent(self, *args, **kwargs): super(ChangesWindows, self).showEvent(*args, **kwargs) if self._first_show: self._first_show = False self.setStyleSheet(style.load_stylesheet()) - self.refresh() + self._details_widget.reset() def show(root=None, debug=False, parent=None): diff --git a/client/version_control/launch_hooks/perforce/pre_load_sync_project.py b/client/version_control/launch_hooks/perforce/pre_load_sync_project.py index 6d21930..ec0226c 100644 --- a/client/version_control/launch_hooks/perforce/pre_load_sync_project.py +++ b/client/version_control/launch_hooks/perforce/pre_load_sync_project.py @@ -53,7 +53,6 @@ def execute(self): changes_tool.activateWindow() changes_tool.showNormal() - changes_tool.refresh() changes_tool.exec_() def _get_unreal_project_path(self, version_control_addon): From 21f0b4c5c31ed73dabfa20e312c8dc023d1b1ee8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 May 2024 14:36:09 +0200 Subject: [PATCH 09/32] Removed unnecessary import Copied from OP which used MongoDB --- client/version_control/backends/perforce/rest_routes.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/version_control/backends/perforce/rest_routes.py b/client/version_control/backends/perforce/rest_routes.py index 81d9b28..b5c395d 100644 --- a/client/version_control/backends/perforce/rest_routes.py +++ b/client/version_control/backends/perforce/rest_routes.py @@ -1,6 +1,5 @@ import json import datetime -from bson.objectid import ObjectId from aiohttp.web_response import Response @@ -24,8 +23,6 @@ def __init__(self): def json_dump_handler(value): if isinstance(value, datetime.datetime): return value.isoformat() - if isinstance(value, ObjectId): - return str(value) if isinstance(value, set): return list(value) raise TypeError(value) From 6ccb75f8e5fb1d726a6df691106de40ee30c747d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 27 May 2024 16:00:56 +0200 Subject: [PATCH 10/32] Fix ayon imports Missed updates because of ayon-core:0.3.0 --- .../plugins/create/unreal/publish_commit.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/client/version_control/plugins/create/unreal/publish_commit.py b/client/version_control/plugins/create/unreal/publish_commit.py index 5660f21..2b5ca78 100644 --- a/client/version_control/plugins/create/unreal/publish_commit.py +++ b/client/version_control/plugins/create/unreal/publish_commit.py @@ -1,9 +1,8 @@ -from ayon_core.pipeline import ( - CreatedInstance -) -from ayon_core.client import get_asset_by_name import unreal +from ayon_core.pipeline import CreatedInstance +from ayon_api import get_folder_by_path + try: from ayon_core.hosts.unreal.api.plugin import UnrealBaseAutoCreator from ayon_core.hosts.unreal.api.pipeline import ( @@ -38,21 +37,21 @@ def create(self, options=None): context = self.create_context project_name = context.get_current_project_name() - asset_name = context.get_current_asset_name() + folder_path = context.get_current_folder_path() task_name = context.get_current_task_name() host_name = context.host_name if existing_instance is None: - existing_instance_asset = None + existing_instance_folder_path = None else: - existing_instance_asset = existing_instance["folderPath"] + existing_instance_folder_path = existing_instance["folderPath"] if existing_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + asset_doc = get_folder_by_path(project_name, folder_path) product_name = self.get_product_name( project_name, asset_doc, task_name, self.default_variant, host_name ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": self.default_variant } @@ -83,14 +82,14 @@ def create(self, options=None): return pub_instance elif ( - existing_instance_asset != asset_name + existing_instance_folder_path != folder_path or existing_instance["task"] != task_name ): - asset_doc = get_asset_by_name(project_name, asset_name) + asset_doc = folder_path(project_name, folder_path) product_name = self.get_product_name( project_name, asset_doc, task_name, self.default_variant, host_name ) - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_path existing_instance["task"] = task_name existing_instance["product_name"] = product_name From 49f31ce95fc042385ba0472c2898c5aa0b52c888 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 27 May 2024 16:01:22 +0200 Subject: [PATCH 11/32] Move p4 to runtime dependencies Shouldn't be imported in DCC --- client/pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/pyproject.toml b/client/pyproject.toml index 3ccbced..2d6406e 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -1,3 +1,5 @@ [tool.poetry.dependencies] python = "^3.9" + +[ayon.runtimeDependencies] p4python = "^2023.1.2454917" From c9e6f6082606c21c75576c95b2a009103d695d97 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 27 May 2024 16:02:32 +0200 Subject: [PATCH 12/32] Move rest to separate folder This shouldn't be dependent on any backend to limit cross imports of backend libraries. --- .../version_control/{backends => rest}/perforce/rest_api.py | 0 .../{backends/perforce/api => rest/perforce}/rest_stub.py | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) rename client/version_control/{backends => rest}/perforce/rest_api.py (100%) rename client/version_control/{backends/perforce/api => rest/perforce}/rest_stub.py (97%) diff --git a/client/version_control/backends/perforce/rest_api.py b/client/version_control/rest/perforce/rest_api.py similarity index 100% rename from client/version_control/backends/perforce/rest_api.py rename to client/version_control/rest/perforce/rest_api.py diff --git a/client/version_control/backends/perforce/api/rest_stub.py b/client/version_control/rest/perforce/rest_stub.py similarity index 97% rename from client/version_control/backends/perforce/api/rest_stub.py rename to client/version_control/rest/perforce/rest_stub.py index ccf8408..1cd9404 100644 --- a/client/version_control/backends/perforce/api/rest_stub.py +++ b/client/version_control/rest/perforce/rest_stub.py @@ -2,8 +2,6 @@ import os import requests -from version_control.backends import abstract - if six.PY2: import pathlib2 as pathlib else: @@ -16,7 +14,7 @@ del _typing -class PerforceRestStub(abstract.VersionControl): +class PerforceRestStub: @staticmethod def _wrap_call(command, **kwargs): From 9f65610e4688ea748d9b1708abcffee70b1bd24e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 27 May 2024 16:03:08 +0200 Subject: [PATCH 13/32] Removed unneeded files --- client/version_control/__init__.py | 61 ------ client/version_control/api.py | 250 ------------------------ client/version_control/api.pyi | 296 ----------------------------- client/version_control/hosts.py | 44 ----- client/version_control/lib.py | 130 ------------- client/version_control/widgets.py | 203 -------------------- 6 files changed, 984 deletions(-) delete mode 100644 client/version_control/api.py delete mode 100644 client/version_control/api.pyi delete mode 100644 client/version_control/hosts.py delete mode 100644 client/version_control/lib.py delete mode 100644 client/version_control/widgets.py diff --git a/client/version_control/__init__.py b/client/version_control/__init__.py index f84ae6a..559c745 100644 --- a/client/version_control/__init__.py +++ b/client/version_control/__init__.py @@ -2,71 +2,10 @@ Package for interfacing with version control systems """ -_compatible_dcc = True - -import six - from .addon import VERSION_CONTROL_ADDON_DIR from .addon import VersionControlAddon -if six.PY3: - # This is a clever hack to get python to import in a lazy (sensible) way - # whilst allowing static analysis to work correctly. - # Effectively this is forcing python to see these sub-packages without - # importing any packages until they are needed, this also helps - # avoid triggering potential dependency loops. - # The module level __getattr__ will handle lazy imports in this syntax: - - # ``` - # import version_control - # version_control.backends.perforce.sync - # ``` - - import importlib - import pathlib - - # this is used instead of typing.TYPE_CHECKING as it - # avoids needing to import the typing module at all: - _typing = False - if _typing: - from typing import Any - - from . import api - from . import backends - from . import hosts - from . import lib - from . import widgets - del _typing - - def __getattr__(name): - # type: (str) -> Any - current_file = pathlib.Path(__file__) - current_directory = current_file.parent - for path in current_directory.iterdir(): - if path.stem != name: - continue - - try: - return importlib.import_module("{0}.{1}".format(__package__, name)) - except ImportError as error: - if "No module named P4API" not in str(error): - raise - - global _compatible_dcc - _compatible_dcc = False - - raise AttributeError("{0} has no attribute named: {0}".format(__package__, name)) - -else: - raise RuntimeError("Version control is not supported on Python2") - __all__ = ( - "api", - "backends", - "hosts", - "lib", - "widgets", "VersionControlAddon", "VERSION_CONTROL_ADDON_DIR", - "_compatible_dcc" ) diff --git a/client/version_control/api.py b/client/version_control/api.py deleted file mode 100644 index ebe0a60..0000000 --- a/client/version_control/api.py +++ /dev/null @@ -1,250 +0,0 @@ -from . lib import get_active_version_control_backend -from . lib import is_version_control_enabled -from . lib import NoActiveVersionControlError -from . lib import VersionControlDisabledError - -import pathlib - -_typing = False -if _typing: - import datetime - - from . import backends - from typing import Any - from typing import Union - from typing import Sequence - - T_P4PATH = Union[pathlib.Path, str, Sequence[Union[pathlib.Path, str]]] -del _typing - - -_active_backend = None # type: backends.abstract.VersionControl - - -def _with_active_backend(function): - def wrapper(*args, **kwargs): - global _active_backend - try: - _active_backend = _active_backend or get_active_version_control_backend() - except (VersionControlDisabledError, NoActiveVersionControlError): - pass - - return function(*args, **kwargs) - - return wrapper - - -@_with_active_backend -def get_server_version(path): - # type: (T_P4PATH) -> int - if not _active_backend: - return 0 - - return _active_backend.get_server_version(path) - - -@_with_active_backend -def get_local_version(path): - # type: (T_P4PATH) -> int | None - if not _active_backend: - return - - return _active_backend.get_local_version(path) - - -@_with_active_backend -def get_version_info(path): - # type: (T_P4PATH) -> tuple[int | None, int | None] - if not _active_backend: - return (None, None) - - return _active_backend.get_version_info(path) - - -@_with_active_backend -def is_checkedout(path): - # type: (T_P4PATH) -> bool | None - if not _active_backend: - return - - return _active_backend.is_checkedout(path) - - -@_with_active_backend -def is_latest_version(path): - # type: (T_P4PATH) -> bool | None - if not _active_backend: - return - - return _active_backend.is_latest_version(path) - - -@_with_active_backend -def exists_on_server(path): - # type: (T_P4PATH) -> bool - if not _active_backend: - return True - - return _active_backend.exists_on_server(path) - - -@_with_active_backend -def sync_latest_version(path): - # type: (T_P4PATH) -> bool - if not _active_backend: - return True - - return _active_backend.sync_latest_version(path) - - -@_with_active_backend -def sync_to_version(path, version): - # type: (T_P4PATH, int) -> bool - if not _active_backend: - return True - - return _active_backend.sync_to_version(path, version) - - -@_with_active_backend -def add(path, comment=""): - # type: (T_P4PATH, str) -> bool - if not _active_backend: - return True - - return _active_backend.add(path, comment=comment) - - -@_with_active_backend -def add_to_change_list(path, comment=""): - # type: (T_P4PATH, str) -> bool - if not _active_backend: - return True - - return _active_backend.add_to_change_list(path, comment=comment) - - -@_with_active_backend -def checkout(path, comment=""): - # type: (T_P4PATH, str) -> bool - - if not _active_backend: - return True - - return _active_backend.checkout(path, comment=comment) - - -@_with_active_backend -def revert(path): - # type: (T_P4PATH) -> bool - - if not _active_backend: - return True - - return _active_backend.revert(path) - - -@_with_active_backend -def move(path, new_path, change_description=None): - # type: (T_P4PATH, T_P4PATH, str | None) -> bool - - if not _active_backend: - return True - - return _active_backend.move(path, new_path, change_description=change_description) - - -@_with_active_backend -def checked_out_by(path, other_users_only=False): - # type: (T_P4PATH, bool) -> list[str] | None - - if not _active_backend: - return - - return _active_backend.checked_out_by(path, other_users_only=other_users_only) - - -@_with_active_backend -def get_existing_change_list(comment): - # type: (str) -> dict[str, Any] | None - if not _active_backend: - return {} - - return _active_backend.get_existing_change_list(comment) - - -@_with_active_backend -def get_newest_file_in_folder(path, name_pattern=None, extensions=None): - # type: (T_P4PATH, str | None, Sequence[str] | None) -> pathlib.Path | None - if not _active_backend: - return - - return _active_backend.get_newest_file_in_folder(path, name_pattern=name_pattern, extensions=extensions) - - -@_with_active_backend -def get_files_in_folder_in_date_order(path, name_pattern=None, extensions=None): - # type: (T_P4PATH, str | None, Sequence[str] | None) -> list[tuple[pathlib.Path, datetime.datetime]] | None - if not _active_backend: - return - - return _active_backend.get_files_in_folder_in_date_order(path, name_pattern=name_pattern, extensions=extensions) - - -@_with_active_backend -def submit_change_list(comment): - # type: (str) -> int | None - if not _active_backend: - return True - - return _active_backend.submit_change_list(comment) - - -@_with_active_backend -def update_change_list_description(comment, new_comment): - # type: (str, str) -> bool - if not _active_backend: - return True - - return _active_backend.update_change_list_description(comment, new_comment) - - -@_with_active_backend -def get_change_list_description(): - # type: () -> str - if not _active_backend: - return "" - - return _active_backend.change_list_description - - -@_with_active_backend -def get_change_list_description_with_tags(description): - # type: (str) -> str - """ - Get the current change list but with tags ([tag1][tag2]) as a prefix. - This is the convention for submitting files to perforce for use with - Unreal Game Sync. - """ - global _active_backend - if not _active_backend: - return "" - - _active_backend.change_list_description = description - return _active_backend.change_list_description - - -__all__ = ( - "get_active_version_control_backend", - "is_version_control_enabled", - "get_server_version", - "get_local_version", - "is_latest_version", - "exists_on_server", - "sync_latest_version", - "add", - "checkout", - "get_existing_change_list", - "submit_change_list", - "update_change_list_description", -) diff --git a/client/version_control/api.pyi b/client/version_control/api.pyi deleted file mode 100644 index 88e6c91..0000000 --- a/client/version_control/api.pyi +++ /dev/null @@ -1,296 +0,0 @@ -import six - -from . backends.perforce.api import P4PathDateData -from . lib import get_active_version_control_backend -from . lib import is_version_control_enabled -from typing import Any -from typing import overload -from typing import Sequence - -if six.PY2: - import pathlib2 as pathlib -else: - import pathlib - - -@overload -def checked_out_by( - path: pathlib.Path | str, other_users_only: bool = False -) -> list[str] | None: - ... - - -@overload -def checked_out_by( - path: Sequence[pathlib.Path | str], other_users_only: bool = False -) -> dict[str, list[str] | None]: - ... - - -@overload -def get_server_version(path: pathlib.Path | str) -> int | None: - """ - Get the current server revision numbers for the given path(s) - - Arguments: - ---------- - - `path`: The file path(s) to get the server revision of. - - Returns: - -------- - - If a single file is provided: - The server version number. `None` if the file does not exist on the server. - - If a list of files are provided: - A dictionary where each key is the path and each value is - the server version number or `None` if the file does not exist on the server. - """ - ... - - -@overload -def get_server_version(path: Sequence[pathlib.Path | str]) -> dict[str, int | None]: - ... - - -@overload -def get_local_version(path: pathlib.Path | str) -> int | None: - """ - Get the current local (client) revision numbers for the given path(s) - - Arguments: - ---------- - - `path`: The file path(s) to get the client revision of. - - Returns: - -------- - - If a single file is provided: - The local version number. Returns `0` if the file does not exist locally - or `None` if the file does not exist on the server. - - If a list of files are provided: - A dictionary where each key is the path and each value is - the local version number or `0` if the file does not exist locally - or `None` if the file does not exist on the server. - """ - ... - - -@overload -def get_local_version(path: Sequence[pathlib.Path | str]) -> dict[str, int | None]: - ... - - -@overload -def get_version_info(path: pathlib.Path | str) -> tuple[int | None, int | None]: - """ - Get client and server versions for the given path(s). - - Arguments: - ---------- - - `path`: The file path(s) to get the versions of. - - Returns: - -------- - - If a single file: - A tuple with the client and server versions. Values are None if the file - does not exist on the server. - - If a list of files: - A dictionary where each key is the path and each value is - a tuple with the client and server versions. Values are None if the file - does not exist on the server. - """ - ... - - -@overload -def get_version_info(path: Sequence[pathlib.Path | str]) -> dict[str, tuple[int | None, int | None]]: - ... - - -@overload -def is_checkedout(path: pathlib.Path | str) -> bool | None: - ... - - -@overload -def is_checkedout(path: Sequence[pathlib.Path | str]) -> dict[str, bool | None]: - ... - - -@overload -def is_latest_version(path: pathlib.Path | str) -> bool | None: - ... - - -@overload -def is_latest_version(path: Sequence[pathlib.Path | str]) -> dict[str, bool | None]: - ... - - -@overload -def exists_on_server(path: pathlib.Path | str) -> bool: - ... - - -@overload -def exists_on_server(path: Sequence[pathlib.Path | str]) -> dict[str, bool]: - ... - - -@overload -def sync_latest_version(path: pathlib.Path | str) -> bool | None: - ... - - -@overload -def sync_latest_version(path: Sequence[pathlib.Path | str]) -> dict[str, bool | None]: - ... - - -@overload -def sync_to_version(path: pathlib.Path | str, version: int) -> bool | None: - ... - - -@overload -def sync_to_version(path: Sequence[pathlib.Path | str], version: int) -> dict[str, bool | None]: - ... - - -@overload -def add(path: pathlib.Path | str, comment: str = "") -> bool: - ... - - -@overload -def add(path: Sequence[pathlib.Path | str], comment: str = "") -> dict[str, bool]: - ... - - -def add_to_change_list(path: Sequence[pathlib.Path | str], comment: str = "") -> dict[str, bool]: - """ - Add the given path(s) to the existing change list - with the given description. - - Arguments: - ---------- - - `path`: The path(s) to add. - - `description` : The description of the change list to add the - file(s) to. - - `workspace_override` (optional): If provided, uses the specific workspace - to first run the command under. If `None`, will use the current workspace - define by the local perforce settings. If the function fails, will - iterate over all other workspaces, running the function to see - if it will run successfully. - Defaults to `None` - - Returns: - -------- - `True` if paths where successfully added, `False` if not. - """ - ... - - -@overload -def checkout(path: pathlib.Path | str, comment: str = "") -> bool: - ... - - -@overload -def checkout(path: Sequence[pathlib.Path | str], comment: str = "") -> dict[str, bool]: - ... - - -@overload -def revert(path: pathlib.Path | str, comment: str = "") -> bool: - ... - - -@overload -def revert(path: Sequence[pathlib.Path | str], comment: str = "") -> dict[str, bool]: - ... - - -def move(path: pathlib.Path | str, new_path: pathlib.Path | str, change_description: str | None = None) -> bool | None: - ... - - -def get_existing_change_list(comment): - # type: (str) -> dict[str, Any] | None - ... - - -@overload -def get_newest_file_in_folder( - path: pathlib.Path | str, - name_pattern: str | None = None, - extensions: str | None = None -) -> pathlib.Path | None: - ... - - -@overload -def get_newest_file_in_folder( - path: Sequence[pathlib.Path | str], - name_pattern: str | None = None, - extensions: str | None = None -) -> dict[str, pathlib.Path | None]: - ... - - -@overload -def get_files_in_folder_in_date_order( - path: pathlib.Path | str, - name_pattern: str | None = None, - extensions: str | None = None -) -> list[pathlib.Path] | None: - ... - - -@overload -def get_files_in_folder_in_date_order( - path: Sequence[pathlib.Path | str], - name_pattern: str | None = None, - extensions: str | None = None -) -> dict[str, list[pathlib.Path] | None]: - ... - - -def submit_change_list(comment): - # type: (str) -> int | None - ... - - -def update_change_list_description(comment, new_comment): - # type: (str, str) -> bool - ... - - -def get_change_list_description(): - # type: () -> str - ... - - -def get_change_list_description_with_tags(description): - # type: (str) -> str - """ - Get the current change list but with tags ([tag1][tag2]) as a prefix. - This is the convention for submitting files to perforce for use with - Unreal Game Sync. - """ - ... - - -__all__ = ( - "get_active_version_control_backend", - "is_version_control_enabled", - "get_server_version", - "get_local_version", - "is_latest_version", - "exists_on_server", - "sync_latest_version", - "add", - "checkout", - "get_existing_change_list", - "submit_change_list", - "update_change_list_description", -) diff --git a/client/version_control/hosts.py b/client/version_control/hosts.py deleted file mode 100644 index 4672eed..0000000 --- a/client/version_control/hosts.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -import functools - -from . import api - -_typing = False -if _typing: - from typing import Any - from typing import Callable - - -def pre_save(function: Callable[..., Any]): - """ - Decorator wrapping a hosts `workio.save_file` function, - checking out the file being saved if version control - is active. - """ - - @functools.wraps(function) - def wrapper(*args: Any, **kwargs: Any): - if api.is_version_control_enabled(): - api.checkout(args[0]) - - return function(*args, **kwargs) - - return wrapper - - -def pre_load(function: Callable[..., Any]): - """ - Decorator wrapping a hosts `workio.save_file` function, - checking out the file being saved if version control - is active. - """ - - @functools.wraps(function) - def wrapper(*args, **kwargs): - if api.is_version_control_enabled(): - api.sync_latest_version(args[0]) - - return function(*args, **kwargs) - - return wrapper diff --git a/client/version_control/lib.py b/client/version_control/lib.py deleted file mode 100644 index 127ea0f..0000000 --- a/client/version_control/lib.py +++ /dev/null @@ -1,130 +0,0 @@ -from openpype.settings import lib as op_settings_lib - -_typing = False -if _typing: - from typing import Any - - from . import backends -del _typing - - -class VersionControlDisabledError(Exception): - def __init__(self): - # type: () -> None - super().__init__("Version control is disabled!") - - -class NoActiveVersionControlError(Exception): - def __init__(self, message="No version control set!"): - # type: (str) -> None - super().__init__(message) - - -class NoVersionControlWithNameFoundError(NoActiveVersionControlError): - def __init__(self, vcs_name): - # type: (str) -> None - super().__init__("No version control named: '{}'' found!".format(vcs_name)) - - -class NoVersionControlBackendFoundError(NoActiveVersionControlError): - def __init__(self, vcs_name): - # type: (str) -> None - super().__init__("Version control: '{}'' has no backend attribute!".format(vcs_name)) - - -class NoVersionControlClassFoundError(NoActiveVersionControlError): - def __init__(self, vcs_name, vcs_class_name): - # type: (str, str) -> None - super().__init__("Version control: '{}'' has no class named {}!".format(vcs_name, vcs_class_name)) - - -def get_version_control_settings(): - # type: () -> dict[str, Any] - - system_settings = op_settings_lib.get_system_settings() - module_settings = system_settings["modules"] - if "version_control" not in module_settings: - return {} - - return module_settings["version_control"] - - -def is_version_control_enabled(): - # type: () -> bool - from .. import version_control - - if not version_control._compatible_dcc: - return False - - version_control_settings = get_version_control_settings() - if not version_control_settings: - return False - - return version_control_settings["enabled"] - - -def get_active_version_control_system(): - # type: () -> str | None - - if not is_version_control_enabled(): - return - - version_control_settings = get_version_control_settings() - return version_control_settings["active_version_control_system"] - - -_active_version_control_backend = None # type: backends.abstract.VersionControl | None - - -def get_active_version_control_backend(): - # type: () -> backends.abstract.VersionControl | None - """ - Get the active version control backend. - - Raises VersionControlDisabledError if version control is disabled - or NoActiveVersionControlError if no backend is set. - - Returned object is a static sub-class of `backends.abstract.VersionControl`. - """ - global _active_version_control_backend - - if _active_version_control_backend is not None: - return _active_version_control_backend - - try: - from . import backends - except ImportError as error: - if "No module named P4API" not in str(error): - raise - return - - active_vcs = get_active_version_control_system() - if active_vcs is None: - raise VersionControlDisabledError() - - try: - backend_module = getattr(backends, active_vcs) - except AttributeError as error: - if active_vcs in str(error): - raise NoVersionControlWithNameFoundError(active_vcs) - raise - - try: - backend_sub_module = getattr(backend_module, "backend") - except AttributeError as error: - if "backend" in str(error): - raise NoVersionControlBackendFoundError(active_vcs) - - raise - - try: - backend_class = getattr( - backend_sub_module, f"VersionControl{active_vcs.title()}" - ) # type: type[backends.abstract.VersionControl] - _active_version_control_backend = backend_class() - return _active_version_control_backend - except AttributeError as error: - if f"VersionControl{active_vcs.title()}" in str(error): - raise NoVersionControlClassFoundError(active_vcs, f"VersionControl{active_vcs.title()}") - - raise diff --git a/client/version_control/widgets.py b/client/version_control/widgets.py deleted file mode 100644 index 54add47..0000000 --- a/client/version_control/widgets.py +++ /dev/null @@ -1,203 +0,0 @@ - -import collections -import Qt.QtCore as QtCore # type: ignore -import Qt.QtWidgets as QtWidgets # type: ignore - - -class VersionControlLabel(QtWidgets.QLabel): - def __init__(self, text="", parent=None): - super().__init__(parent=parent) - self.setText(text) - self.setObjectName("VersionControlLabel") - self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Maximum) - - -class VersionControlTextEdit(QtWidgets.QPlainTextEdit): - def __init__(self, placeholder_text="", parent=None): - super().__init__(parent=parent) - self.setPlaceholderText(placeholder_text) - self.setObjectName("VersionControlTextEdit") - self._valid = "invalid" - - @QtCore.Property(str) - def valid(self): - # type: () -> str - return self._valid - - @valid.setter - def valid(self, value): - # type: (str) -> None - update = self._valid != value - self._valid = value - if not update: - return - - self.style().unpolish(self) - self.style().polish(self) - self.update() - - -class VersionControlCommentWidget(QtWidgets.QWidget): - # Signals: - textChanged = QtCore.Signal(str) - textIsValid = QtCore.Signal(bool) - returnPressed = QtCore.Signal() - - text_changed = textChanged - text_is_valid = textIsValid - - def __init__(self, parent=None): - super(VersionControlCommentWidget, self).__init__(parent=parent) - - self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Maximum) - - self.setObjectName("VersionControlCommentWidget") - self._character_count = 25 - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - - text_edit = VersionControlTextEdit( - "Enter a detailed comment to submit", self - ) - - characters_to_go_text = "Characters Required: {}" - characters_to_go_label = VersionControlLabel( - text=characters_to_go_text.format(self._character_count), - parent=self - ) - - output_text = "Comment Output: {}" - output_text_label = VersionControlLabel( - text=output_text.format("Invalid"), - parent=self - ) - output_text_label.setWordWrap(True) - self.style().unpolish(output_text_label) - self.style().polish(output_text_label) - output_text_label.update() - - layout.addWidget(text_edit) - layout.addWidget(characters_to_go_label) - layout.addWidget(output_text_label) - - text_update_timer = QtCore.QTimer() - text_update_timer.setInterval(200) - text_update_timer.timeout.connect(self._on_text_update_timer_timeout) - - text_edit.textChanged.connect(self._on_text_edit_text_changed) - - self._text_edit = text_edit - self._characters_to_go_label = characters_to_go_label - self._output_text_label = output_text_label - self._output_text = output_text - self._characters_to_go_text = characters_to_go_text - self._text_update_timer = text_update_timer - - self._previous_heights = collections.defaultdict(lambda: 0) # type: collections.defaultdict[str, int] - self._vc_api = None - - self._adjust_ui_height() - - # Slots: - @property - def vc_api(self): - """ - Version Control api module - """ - if self._vc_api is None: - import version_control.api as api - self._vc_api = api - - return self._vc_api - - @QtCore.Slot() - def _on_text_update_timer_timeout(self): - # type: () -> None - self._text_changed() - self._text_update_timer.stop() - - @QtCore.Slot() - def _on_text_edit_text_changed(self): - # type: () -> None - if self._text_update_timer.isActive(): - self._text_update_timer.stop() - - self._text_update_timer.start() - self._adjust_ui_height() - - # Private Methods: - def _text_changed(self): - # type: () -> None - text = self._text_edit.toPlainText() - text_length = len(text) - valid_length = text_length >= self._character_count - charactes_to_go = 0 if valid_length else self._character_count - text_length - label_text = self._characters_to_go_text.format(charactes_to_go) - self._characters_to_go_label.setText(label_text) - self.textIsValid.emit(valid_length) - self.textChanged.emit(text) - self._text_edit.valid = "valid" if valid_length else "invalid" - _text = ( - self.vc_api.get_change_list_description_with_tags(text) - if valid_length - else "Invalid" - ) - self._output_text_label.setText(self._output_text.format(_text)) - - def _adjust_widget_height_to_fit_text(self, widget, text): - # type: (QtWidgets.QWidget, str) -> bool - font_metrics = widget.fontMetrics() - text = text or "Test String" - line_count = len(text.splitlines()) + 1 - bounding_rect = font_metrics.boundingRect(text) - contents_margins = widget.contentsMargins() - widget_width = widget.width() - if widget_width: - word_wrap_count = abs(int((bounding_rect.width() / widget_width) - 1)) - line_count += word_wrap_count - - new_height = ( - (bounding_rect.height() * line_count) + contents_margins.top() + contents_margins.bottom() - ) - previous_height = self._previous_heights[str(widget)] - if new_height != previous_height: - widget.setFixedHeight(new_height) - self._previous_heights[str(widget)] = new_height - return True - - return False - - def _adjust_text_edit_height(self): - if self._adjust_widget_height_to_fit_text( - self._text_edit, self._text_edit.toPlainText() - ): - self._text_edit.verticalScrollBar().setValue(0) - self._text_edit.ensureCursorVisible() - - def _adjust_label_height(self): - self._adjust_widget_height_to_fit_text( - self._output_text_label, self._output_text_label.text() - ) - - def _adjust_ui_height(self): - self._adjust_text_edit_height() - self._adjust_label_height() - - # Public Methods: - def text(self): - # type: () -> str - - return self._text_edit.toPlainText() - - def setText(self, text): - # type: (str | None) -> None - - text = text or "" - self._text_edit.setPlainText(text) - - # Qt Override Methods: - def keyPressEvent(self, event): - if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): - self.returnPressed.emit() - - return super().keyPressEvent(event) From 94d62a553f0a7523356e46170ecf5391222fd725 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 27 May 2024 16:04:04 +0200 Subject: [PATCH 14/32] Changed imports --- client/version_control/addon.py | 6 +++--- .../backends/perforce/rest_routes.py | 4 ++-- .../version_control/changes_viewer/control.py | 2 +- .../publish/collect_latest_changelist.py | 2 +- .../publish/collect_version_control_login.py | 2 +- .../plugins/publish/integrate_perforce.py | 15 +++++++++++---- .../perforce => rest}/communication_server.py | 17 +---------------- 7 files changed, 20 insertions(+), 28 deletions(-) rename client/version_control/{backends/perforce => rest}/communication_server.py (97%) diff --git a/client/version_control/addon.py b/client/version_control/addon.py index 97c45ac..1545038 100644 --- a/client/version_control/addon.py +++ b/client/version_control/addon.py @@ -70,7 +70,7 @@ def get_connection_info(self, project_name, project_settings=None): return conn_info def sync_to_latest(self, conn_info): - from version_control.backends.perforce.api.rest_stub import \ + from version_control.rest.perforce.rest_stub import \ PerforceRestStub PerforceRestStub.login(host=conn_info["host"], @@ -82,7 +82,7 @@ def sync_to_latest(self, conn_info): return def sync_to_version(self, conn_info, change_id): - from version_control.backends.perforce.api.rest_stub import \ + from version_control.rest.perforce.rest_stub import \ PerforceRestStub PerforceRestStub.login(host=conn_info["host"], @@ -104,7 +104,7 @@ def tray_init(self): def tray_start(self): if self.enabled: - from .backends.perforce.communication_server import WebServer + from version_control.rest.communication_server import WebServer self.webserver = WebServer() self.webserver.start() diff --git a/client/version_control/backends/perforce/rest_routes.py b/client/version_control/backends/perforce/rest_routes.py index b5c395d..9a54c78 100644 --- a/client/version_control/backends/perforce/rest_routes.py +++ b/client/version_control/backends/perforce/rest_routes.py @@ -3,8 +3,8 @@ from aiohttp.web_response import Response -from openpype.lib import Logger -from openpype.modules.webserver.base_routes import RestApiEndpoint +from ayon_core.lib import Logger +from ayon_core.modules.webserver.base_routes import RestApiEndpoint from version_control.backends.perforce.backend import ( VersionControlPerforce diff --git a/client/version_control/changes_viewer/control.py b/client/version_control/changes_viewer/control.py index 8e1caf0..99ee417 100644 --- a/client/version_control/changes_viewer/control.py +++ b/client/version_control/changes_viewer/control.py @@ -4,7 +4,7 @@ get_current_context, ) from ayon_core.modules import ModulesManager -from version_control.backends.perforce.api.rest_stub import PerforceRestStub +from version_control.rest.perforce.rest_stub import PerforceRestStub class ChangesViewerController: diff --git a/client/version_control/plugins/publish/collect_latest_changelist.py b/client/version_control/plugins/publish/collect_latest_changelist.py index 0b45a7a..c60fb74 100644 --- a/client/version_control/plugins/publish/collect_latest_changelist.py +++ b/client/version_control/plugins/publish/collect_latest_changelist.py @@ -11,7 +11,7 @@ """ import pyblish.api -from version_control.backends.perforce.api.rest_stub import ( +from version_control.rest.perforce.rest_stub import ( PerforceRestStub ) diff --git a/client/version_control/plugins/publish/collect_version_control_login.py b/client/version_control/plugins/publish/collect_version_control_login.py index 310165a..e52ad45 100644 --- a/client/version_control/plugins/publish/collect_version_control_login.py +++ b/client/version_control/plugins/publish/collect_version_control_login.py @@ -10,7 +10,7 @@ from openpype.modules import ModulesManager -from version_control.backends.perforce.api.rest_stub import PerforceRestStub +from version_control.rest.perforce.rest_stub import PerforceRestStub class CollectVersionControlLogin(pyblish.api.ContextPlugin): diff --git a/client/version_control/plugins/publish/integrate_perforce.py b/client/version_control/plugins/publish/integrate_perforce.py index 1a66783..dd83661 100644 --- a/client/version_control/plugins/publish/integrate_perforce.py +++ b/client/version_control/plugins/publish/integrate_perforce.py @@ -5,9 +5,9 @@ import pyblish.api -from openpype.lib import StringTemplate +from ayon_core.lib import StringTemplate -from version_control.backends.perforce.api.rest_stub import ( +from version_control.rest.perforce.rest_stub import ( PerforceRestStub ) @@ -28,12 +28,19 @@ def process(self, instance): if not version_template_key: raise RuntimeError("Instance data missing 'version_control[template_name]'") # noqa + if "_" in version_template_key: + template_area, template_name = version_template_key.split("_") + else: + template_area = version_template_key + template_name = "default" anatomy = instance.context.data["anatomy"] - template = anatomy.templates_obj.templates[version_template_key]["path"] # noqa + template = anatomy.templates_obj.templates[template_area][template_name] # noqa if not template: raise RuntimeError("Anatomy is missing configuration for '{}'". format(version_template_key)) + template_file_path = os.path.join(template["directory"], + template["file"]) anatomy_data = copy.deepcopy(instance.data["anatomyData"]) anatomy_data["root"] = instance.data["version_control"]["roots"] # anatomy_data["output"] = '' @@ -44,7 +51,7 @@ def process(self, instance): anatomy_data["ext"] = repre["ext"] version_control_path = StringTemplate.format_template( - template, anatomy_data + template_file_path, anatomy_data ) source_path = repre["published_path"] diff --git a/client/version_control/backends/perforce/communication_server.py b/client/version_control/rest/communication_server.py similarity index 97% rename from client/version_control/backends/perforce/communication_server.py rename to client/version_control/rest/communication_server.py index 9a8dd92..05798d0 100644 --- a/client/version_control/backends/perforce/communication_server.py +++ b/client/version_control/rest/communication_server.py @@ -1,28 +1,13 @@ import os -import json -import time -import subprocess -import collections import asyncio import logging import socket import threading -from queue import Queue from contextlib import closing -import aiohttp from aiohttp import web -from aiohttp_json_rpc import JsonRpc -from aiohttp_json_rpc.protocol import ( - encode_request, encode_error, decode_msg, JsonRpcMsgTyp -) -from aiohttp_json_rpc.exceptions import RpcError -from openpype.lib import emit_event - -from version_control.backends.perforce.rest_api import ( - PerforceModuleRestAPI -) +from version_control.rest.perforce.rest_api import PerforceModuleRestAPI log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) From 5f81f0c2fe86118fe07c103a4d2e8552fed2db63 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 11 Jun 2024 15:56:46 +0200 Subject: [PATCH 15/32] Fix import --- .../version_control/plugins/publish/collect_version_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/version_control/plugins/publish/collect_version_control.py b/client/version_control/plugins/publish/collect_version_control.py index 0fccec7..62380a2 100644 --- a/client/version_control/plugins/publish/collect_version_control.py +++ b/client/version_control/plugins/publish/collect_version_control.py @@ -6,7 +6,7 @@ instance -> families ([]) """ import pyblish.api -from openpype.lib import filter_profiles +from ayon_core.lib import filter_profiles class CollectVersionControl(pyblish.api.InstancePlugin): From 0e1c8db628606cdd4e51bf92ebe1067e53bece6e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 11 Jun 2024 15:56:56 +0200 Subject: [PATCH 16/32] Fix import --- .../plugins/publish/collect_version_control_login.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/version_control/plugins/publish/collect_version_control_login.py b/client/version_control/plugins/publish/collect_version_control_login.py index e52ad45..d5af93e 100644 --- a/client/version_control/plugins/publish/collect_version_control_login.py +++ b/client/version_control/plugins/publish/collect_version_control_login.py @@ -8,7 +8,7 @@ import pyblish.api -from openpype.modules import ModulesManager +from ayon_core.addon import AddonsManager from version_control.rest.perforce.rest_stub import PerforceRestStub @@ -22,7 +22,7 @@ class CollectVersionControlLogin(pyblish.api.ContextPlugin): def process(self, context): - version_control = ModulesManager().get("version_control") + version_control = AddonsManager().get("version_control") if not version_control or not version_control.enabled: self.log.info("No version control enabled") return From 5f349744bf4cf93913d4df1d29a1d943753cbba8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 11 Jun 2024 15:57:03 +0200 Subject: [PATCH 17/32] Fix import --- .../version_control/plugins/publish/extract_change_list_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/version_control/plugins/publish/extract_change_list_info.py b/client/version_control/plugins/publish/extract_change_list_info.py index 823371a..e33b5f9 100644 --- a/client/version_control/plugins/publish/extract_change_list_info.py +++ b/client/version_control/plugins/publish/extract_change_list_info.py @@ -11,7 +11,7 @@ import json import tempfile -from openpype.pipeline import publish +from ayon_core.pipeline import publish class ExtractChangeListInfo(publish.Extractor): From 74162942aff48ff8c155e32e40bb9815b14dac34 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 11 Jun 2024 15:57:13 +0200 Subject: [PATCH 18/32] Fix import --- client/version_control/rest/perforce/rest_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/version_control/rest/perforce/rest_api.py b/client/version_control/rest/perforce/rest_api.py index 4069765..eb617eb 100644 --- a/client/version_control/rest/perforce/rest_api.py +++ b/client/version_control/rest/perforce/rest_api.py @@ -1,5 +1,5 @@ from aiohttp.web_response import Response -from openpype.lib import Logger +from ayon_core.lib import Logger from version_control.backends.perforce import rest_routes From 7353434c66c8b6f1328df8b24d5cdad71a819ea0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 11 Jun 2024 15:57:28 +0200 Subject: [PATCH 19/32] Fix import --- .../version_control/plugins/publish/validate_workspace.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/version_control/plugins/publish/validate_workspace.py b/client/version_control/plugins/publish/validate_workspace.py index afc133c..e250a12 100644 --- a/client/version_control/plugins/publish/validate_workspace.py +++ b/client/version_control/plugins/publish/validate_workspace.py @@ -1,12 +1,8 @@ import os import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder -from openpype.pipeline.publish import ( - PublishXmlValidationError, -) - -from version_control.backends.perforce.api.rest_stub import PerforceRestStub +from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline import PublishXmlValidationError class ValidateWorkspaceDir(pyblish.api.InstancePlugin): From 26fc1c3504abbb18b244c63719aaa3ba05c7916e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 11 Jun 2024 15:57:47 +0200 Subject: [PATCH 20/32] Fix settings query --- client/version_control/backends/abstract.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/version_control/backends/abstract.py b/client/version_control/backends/abstract.py index 1167088..f84ee15 100644 --- a/client/version_control/backends/abstract.py +++ b/client/version_control/backends/abstract.py @@ -4,8 +4,6 @@ import pathlib import six -from openpype.lib import local_settings - # @sharkmob-shea.richardson: # This need to be evaluated at runtime to provide # the correct type annotations for @class_property @@ -98,10 +96,6 @@ def __init__(self): # Public Properties: @property def settings(self): - # type: () -> local_settings.OpenPypeSettingsRegistry - if self._settings is None: - self._settings = local_settings.OpenPypeSettingsRegistry("version_control") - return self._settings @property From 99c35db5f740a5c6bec6f4258d7b93538993740a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 12 Jun 2024 12:47:20 +0200 Subject: [PATCH 21/32] Provide rudimentary notification about synchronization --- client/version_control/changes_viewer/widgets.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/version_control/changes_viewer/widgets.py b/client/version_control/changes_viewer/widgets.py index 8bbd97d..31216d1 100644 --- a/client/version_control/changes_viewer/widgets.py +++ b/client/version_control/changes_viewer/widgets.py @@ -40,6 +40,8 @@ def __init__(self, controller, parent=None): time_delegate = PrettyTimeDelegate() changes_view.setItemDelegateForColumn(3, time_delegate) + message_label_widget = QtWidgets.QLabel(self) + sync_btn = QtWidgets.QPushButton("Sync to", self) self._block_changes = False @@ -49,6 +51,8 @@ def __init__(self, controller, parent=None): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(changes_view, 1) + layout.addWidget(message_label_widget, 0, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) layout.addWidget(sync_btn, 0, QtCore.Qt.AlignRight) sync_btn.clicked.connect(self._on_sync_clicked) @@ -59,6 +63,7 @@ def __init__(self, controller, parent=None): self.sync_btn = sync_btn self._thread = None self._time_delegate = time_delegate + self._message_label_widget = message_label_widget def reset(self): self._model.refresh() @@ -71,14 +76,17 @@ def _on_sync_clicked(self): change_id = current_index.data(CHANGE_ROLE) + self._message_label_widget.setText(f"Syncing to '{change_id}'...") + self.sync_btn.setEnabled(False) thread = SyncThread(self._controller, change_id) - thread.finished.connect(self._on_thread_finished) + thread.finished.connect(lambda: self._on_thread_finished(change_id)) thread.start() self._thread = thread - def _on_thread_finished(self): + def _on_thread_finished(self, change_id): + self._message_label_widget.setText(f"Synced to '{change_id}'") self.sync_btn.setEnabled(True) From b3c85452ff35ac1991f7485e366e535eb7e04e7a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 12 Jun 2024 12:49:16 +0200 Subject: [PATCH 22/32] Update rudimentary notification about synchronization --- client/version_control/changes_viewer/widgets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/version_control/changes_viewer/widgets.py b/client/version_control/changes_viewer/widgets.py index 31216d1..bed6dd5 100644 --- a/client/version_control/changes_viewer/widgets.py +++ b/client/version_control/changes_viewer/widgets.py @@ -86,7 +86,10 @@ def _on_sync_clicked(self): self._thread = thread def _on_thread_finished(self, change_id): - self._message_label_widget.setText(f"Synced to '{change_id}'") + self._message_label_widget.setText( + f"Synced to '{change_id}'. " + "Please close Viewer to continue." + ) self.sync_btn.setEnabled(True) From d0e3ea2a68ce3af83b714c1a023b258d923c543d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 23 Jul 2024 19:17:38 +0200 Subject: [PATCH 23/32] Fixes to names, imports for publish_commit --- .../plugins/create/unreal/publish_commit.py | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/client/version_control/plugins/create/unreal/publish_commit.py b/client/version_control/plugins/create/unreal/publish_commit.py index 2b5ca78..9e50cc2 100644 --- a/client/version_control/plugins/create/unreal/publish_commit.py +++ b/client/version_control/plugins/create/unreal/publish_commit.py @@ -1,16 +1,11 @@ -import unreal - from ayon_core.pipeline import CreatedInstance from ayon_api import get_folder_by_path -try: - from ayon_core.hosts.unreal.api.plugin import UnrealBaseAutoCreator - from ayon_core.hosts.unreal.api.pipeline import ( - create_publish_instance, imprint) -except ImportError: - # should be used after splitting unreal host to separate addon - from ayon_unreal.api.plugin import UnrealBaseAutoCreator - from ayon_unreal.api.pipeline import create_publish_instance, imprint +from ayon_core.hosts.unreal.api.plugin import UnrealBaseAutoCreator +from ayon_core.hosts.unreal.api.pipeline import ( + create_publish_instance, + imprint +) class UnrealPublishCommit(UnrealBaseAutoCreator): @@ -24,7 +19,7 @@ class UnrealPublishCommit(UnrealBaseAutoCreator): """ identifier = "io.ayon.creators.unreal.publish_commit" product_type = "publish_commit" - label = "Publish commit" + label = "Publish Changelist Metadata" default_variant = "" @@ -38,26 +33,31 @@ def create(self, options=None): context = self.create_context project_name = context.get_current_project_name() folder_path = context.get_current_folder_path() - task_name = context.get_current_task_name() + folder_entity = get_folder_by_path(project_name, folder_path) + task_entity = context.get_current_task_entity() + task_name = task_entity["name"] host_name = context.host_name + if existing_instance is None: - existing_instance_folder_path = None - else: - existing_instance_folder_path = existing_instance["folderPath"] - if existing_instance is None: - asset_doc = get_folder_by_path(project_name, folder_path) + product_name = self.get_product_name( - project_name, asset_doc, task_name, self.default_variant, + project_name, folder_entity, task_entity, self.default_variant, host_name ) + data = { "folderPath": folder_path, - "task": task_name, - "variant": self.default_variant + "task_name": task_name, + "variant": self.default_variant, + "productName": product_name } data.update(self.get_dynamic_data( - self.default_variant, task_name, asset_doc, - project_name, host_name, None + project_name, + folder_entity, + task_entity, + self.default_variant, + host_name, + None )) # TODO enable when Settings available @@ -72,9 +72,6 @@ def create(self, options=None): pub_instance = create_publish_instance(instance_name, self.root) pub_instance.set_editor_property('add_external_assets', True) - assets = pub_instance.get_editor_property('asset_data_external') - - ar = unreal.AssetRegistryHelpers.get_asset_registry() imprint(f"{self.root}/{instance_name}", new_instance.data_to_store()) @@ -82,14 +79,15 @@ def create(self, options=None): return pub_instance elif ( - existing_instance_folder_path != folder_path - or existing_instance["task"] != task_name + existing_instance["folderPath"] != folder_path + # 'task' just for backward compatibility + or (existing_instance.get("task") or + existing_instance["task_name"]) != task_name ): - asset_doc = folder_path(project_name, folder_path) product_name = self.get_product_name( - project_name, asset_doc, task_name, self.default_variant, + project_name, folder_entity, task_entity, self.default_variant, host_name ) existing_instance["folderPath"] = folder_path - existing_instance["task"] = task_name - existing_instance["product_name"] = product_name + existing_instance["task_name"] = task_name + existing_instance["productName"] = product_name From 05a2e0aec53236b27233335d6e5c2e944b4fef27 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 23 Jul 2024 19:19:55 +0200 Subject: [PATCH 24/32] Fix wrong import in publish_commit --- .../plugins/create/unreal/publish_commit.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/version_control/plugins/create/unreal/publish_commit.py b/client/version_control/plugins/create/unreal/publish_commit.py index 9e50cc2..58fe67d 100644 --- a/client/version_control/plugins/create/unreal/publish_commit.py +++ b/client/version_control/plugins/create/unreal/publish_commit.py @@ -1,12 +1,8 @@ from ayon_core.pipeline import CreatedInstance from ayon_api import get_folder_by_path -from ayon_core.hosts.unreal.api.plugin import UnrealBaseAutoCreator -from ayon_core.hosts.unreal.api.pipeline import ( - create_publish_instance, - imprint -) - +from ayon_unreal.api.plugin import UnrealBaseAutoCreator +from ayon_unreal.api.pipeline import create_publish_instance, imprint class UnrealPublishCommit(UnrealBaseAutoCreator): """Auto creator to mark current version of project as published. From 41e1da2bbb8813e86794bc0690cd7e2fff5dd20b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 23 Jul 2024 19:23:46 +0200 Subject: [PATCH 25/32] Update docstring of validator for P4 commits --- client/version_control/plugins/publish/validate_workspace.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/version_control/plugins/publish/validate_workspace.py b/client/version_control/plugins/publish/validate_workspace.py index e250a12..c51d283 100644 --- a/client/version_control/plugins/publish/validate_workspace.py +++ b/client/version_control/plugins/publish/validate_workspace.py @@ -8,8 +8,7 @@ class ValidateWorkspaceDir(pyblish.api.InstancePlugin): """Validates if workspace_dir was collected and is valid. - Login will overrride P4CONFIG env variables if present on systems with - P4V installed. + Used for committing to P4 directly from AYON. """ order = ValidateContentsOrder From 0255ec174b53d5b2c47c9deee04e289a32d35df0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 23 Jul 2024 19:25:12 +0200 Subject: [PATCH 26/32] Added validator that workspace has P4 stream connected --- .../plugins/publish/validate_stream.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 client/version_control/plugins/publish/validate_stream.py diff --git a/client/version_control/plugins/publish/validate_stream.py b/client/version_control/plugins/publish/validate_stream.py new file mode 100644 index 0000000..6f2f3be --- /dev/null +++ b/client/version_control/plugins/publish/validate_stream.py @@ -0,0 +1,28 @@ +import pyblish.api + +from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline import PublishXmlValidationError + + +class ValidateStream(pyblish.api.InstancePlugin): + """Validates if Perforce stream is collected. + + Current Deadline implementation requires P4 depots to be of type 'stream' + and workspace to be assigned to a stream + """ + + order = ValidateContentsOrder + label = "Validate P4 Stream" + families = ["publish_commit"] + targets = ["local"] + + def process(self, instance): + stream = instance.context.data["version_control"]["stream"] + + if not stream: + msg = ( + "Deadline implementation require depot with `streams`. " + "Please let your Perforce admin set up your workspace with " + "stream connected." + ) + raise PublishXmlValidationError(self, msg) From a66a1bc3e98f46bff925745544cf8ca6095bb09c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 26 Jul 2024 15:38:31 +0200 Subject: [PATCH 27/32] Fix imports of webserver Moved to Tray tool --- client/version_control/backends/perforce/rest_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/version_control/backends/perforce/rest_routes.py b/client/version_control/backends/perforce/rest_routes.py index 9a54c78..a51416a 100644 --- a/client/version_control/backends/perforce/rest_routes.py +++ b/client/version_control/backends/perforce/rest_routes.py @@ -4,7 +4,7 @@ from ayon_core.lib import Logger -from ayon_core.modules.webserver.base_routes import RestApiEndpoint +from ayon_core.tools.tray.webserver.base_routes import RestApiEndpoint from version_control.backends.perforce.backend import ( VersionControlPerforce From 322742e8ee9df79bd93fba796d265946936a0929 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 29 Jul 2024 18:28:05 +0200 Subject: [PATCH 28/32] Tweak typo --- client/version_control/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/version_control/addon.py b/client/version_control/addon.py index 1545038..2c9cdf4 100644 --- a/client/version_control/addon.py +++ b/client/version_control/addon.py @@ -127,7 +127,7 @@ def get_launch_hook_paths(self, _app): """Implementation for applications launch hooks. Returns: - (str): full absolut path to directory with hooks for the module + (str): full absolute path to directory with hooks for the module """ return os.path.join(VERSION_CONTROL_ADDON_DIR, "launch_hooks", From c71f4573d8c9c9517ce4fd2a9e1de584c4859ffd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 29 Jul 2024 18:31:37 +0200 Subject: [PATCH 29/32] Renamed publish_commit to changelist_metadata --- client/version_control/changes_viewer/README.md | 2 +- .../version_control/plugins/create/unreal/publish_commit.py | 4 ++-- .../plugins/publish/collect_latest_changelist.py | 2 +- .../plugins/publish/extract_change_list_info.py | 6 +++--- client/version_control/plugins/publish/validate_stream.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/version_control/changes_viewer/README.md b/client/version_control/changes_viewer/README.md index a6d416c..8f2bded 100644 --- a/client/version_control/changes_viewer/README.md +++ b/client/version_control/changes_viewer/README.md @@ -1,7 +1,7 @@ Change list viewer ------------------ -Simple UI showing list of `publish_commit` references marking change list submission as particular +Simple UI showing list of `changelist_metadata` references marking change list submission as particular version of Unreal project for rendering. It should also list all change list and allow to checkout any of those for republish/rerendering. \ No newline at end of file diff --git a/client/version_control/plugins/create/unreal/publish_commit.py b/client/version_control/plugins/create/unreal/publish_commit.py index 58fe67d..79a8bc3 100644 --- a/client/version_control/plugins/create/unreal/publish_commit.py +++ b/client/version_control/plugins/create/unreal/publish_commit.py @@ -13,8 +13,8 @@ class UnrealPublishCommit(UnrealBaseAutoCreator): This logic should be eventually moved to UnrealBaseAutoCreator class in unreal addon andd only be imported from there. """ - identifier = "io.ayon.creators.unreal.publish_commit" - product_type = "publish_commit" + identifier = "io.ayon.creators.unreal.changelist_metadata" + product_type = "changelist_metadata" label = "Publish Changelist Metadata" default_variant = "" diff --git a/client/version_control/plugins/publish/collect_latest_changelist.py b/client/version_control/plugins/publish/collect_latest_changelist.py index c60fb74..d9e360b 100644 --- a/client/version_control/plugins/publish/collect_latest_changelist.py +++ b/client/version_control/plugins/publish/collect_latest_changelist.py @@ -23,7 +23,7 @@ class CollectLatestChangeList(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.4995 targets = ["local"] - families = ["publish_commit"] + families = ["changelist_metadata"] def process(self, instance): if not instance.context.data.get("version_control"): diff --git a/client/version_control/plugins/publish/extract_change_list_info.py b/client/version_control/plugins/publish/extract_change_list_info.py index e33b5f9..3e50d1c 100644 --- a/client/version_control/plugins/publish/extract_change_list_info.py +++ b/client/version_control/plugins/publish/extract_change_list_info.py @@ -4,7 +4,7 @@ change list Provides: - new representation with name == "publish_commit" + new representation with name == "changelist_metadata" """ import os @@ -19,7 +19,7 @@ class ExtractChangeListInfo(publish.Extractor): order = publish.Extractor.order label = "Extract Change List Info" - families = ["publish_commit"] + families = ["changelist_metadata"] targets = ["local"] @@ -36,7 +36,7 @@ def process(self, instance): json.dump(change_info, fp) repre_data = { - "name": "publish_commit", + "name": "changelist_metadata", "ext": "json", "files": file_name, "stagingDir": staging_dir diff --git a/client/version_control/plugins/publish/validate_stream.py b/client/version_control/plugins/publish/validate_stream.py index 6f2f3be..7ddd3a9 100644 --- a/client/version_control/plugins/publish/validate_stream.py +++ b/client/version_control/plugins/publish/validate_stream.py @@ -13,7 +13,7 @@ class ValidateStream(pyblish.api.InstancePlugin): order = ValidateContentsOrder label = "Validate P4 Stream" - families = ["publish_commit"] + families = ["changelist_metadata"] targets = ["local"] def process(self, instance): From 314fb65e43d4ab843e7ba9acb1b993ed920c3141 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 31 Jul 2024 11:29:44 +0200 Subject: [PATCH 30/32] Ranamend creator to changelist_metadata --- .../create/unreal/{publish_commit.py => changelist_metadata.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/version_control/plugins/create/unreal/{publish_commit.py => changelist_metadata.py} (100%) diff --git a/client/version_control/plugins/create/unreal/publish_commit.py b/client/version_control/plugins/create/unreal/changelist_metadata.py similarity index 100% rename from client/version_control/plugins/create/unreal/publish_commit.py rename to client/version_control/plugins/create/unreal/changelist_metadata.py From 50b1484c455a72aa507dbececc96a41f04cd258e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 31 Jul 2024 12:25:44 +0200 Subject: [PATCH 31/32] Revert back task_name to task Publisher expects only 'task' --- .../plugins/create/unreal/changelist_metadata.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/client/version_control/plugins/create/unreal/changelist_metadata.py b/client/version_control/plugins/create/unreal/changelist_metadata.py index 79a8bc3..3be9440 100644 --- a/client/version_control/plugins/create/unreal/changelist_metadata.py +++ b/client/version_control/plugins/create/unreal/changelist_metadata.py @@ -4,6 +4,7 @@ from ayon_unreal.api.plugin import UnrealBaseAutoCreator from ayon_unreal.api.pipeline import create_publish_instance, imprint + class UnrealPublishCommit(UnrealBaseAutoCreator): """Auto creator to mark current version of project as published. @@ -17,7 +18,7 @@ class UnrealPublishCommit(UnrealBaseAutoCreator): product_type = "changelist_metadata" label = "Publish Changelist Metadata" - default_variant = "" + default_variant = "Main" def create(self, options=None): existing_instance = None @@ -33,7 +34,6 @@ def create(self, options=None): task_entity = context.get_current_task_entity() task_name = task_entity["name"] host_name = context.host_name - if existing_instance is None: product_name = self.get_product_name( @@ -43,10 +43,11 @@ def create(self, options=None): data = { "folderPath": folder_path, - "task_name": task_name, + "task": task_name, "variant": self.default_variant, "productName": product_name } + data.update(self.get_dynamic_data( project_name, folder_entity, @@ -76,14 +77,12 @@ def create(self, options=None): elif ( existing_instance["folderPath"] != folder_path - # 'task' just for backward compatibility - or (existing_instance.get("task") or - existing_instance["task_name"]) != task_name + or existing_instance.get("task") != task_name ): product_name = self.get_product_name( project_name, folder_entity, task_entity, self.default_variant, host_name ) existing_instance["folderPath"] = folder_path - existing_instance["task_name"] = task_name + existing_instance["task"] = task_name existing_instance["productName"] = product_name From ad07f3e871ffb7204588e0583f584210205075f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 7 Aug 2024 11:09:33 +0200 Subject: [PATCH 32/32] :dog: small style fixes --- client/version_control/addon.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/client/version_control/addon.py b/client/version_control/addon.py index 2c9cdf4..33c9661 100644 --- a/client/version_control/addon.py +++ b/client/version_control/addon.py @@ -16,6 +16,8 @@ class VersionControlAddon(AYONAddon, ITrayService, IPluginPaths): # _icon_name = "mdi.jira" # _icon_scale = 1.3 + webserver = None + active_version_control_system = None # Properties: @property @@ -34,7 +36,7 @@ def initialize(self, settings): assert self.name in settings, ( "{} not found in settings - make sure they are defined in the defaults".format(self.name) ) - vc_settings = settings[self.name] # type: dict[str, Any] + vc_settings = settings[self.name] # type: dict[str, Any] enabled = vc_settings["enabled"] # type: bool active_version_control_system = vc_settings["active_version_control_system"] # type: str self.active_version_control_system = active_version_control_system @@ -60,14 +62,13 @@ def get_connection_info(self, project_name, project_settings=None): if workspace_dir: workspace_dir = os.path.normpath(workspace_dir) - conn_info = {} - conn_info["host"] = version_settings["host_name"] - conn_info["port"] = version_settings["port"] - conn_info["username"] = local_setting["username"] - conn_info["password"] = local_setting["password"] - conn_info["workspace_dir"] = workspace_dir - - return conn_info + return { + "host": version_settings["host_name"], + "port": version_settings["port"], + "username": local_setting["username"], + "password": local_setting["password"], + "workspace_dir": workspace_dir + } def sync_to_latest(self, conn_info): from version_control.rest.perforce.rest_stub import \