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"
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/addon.py b/client/version_control/addon.py
index da0796f..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,17 +62,16 @@ 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.backends.perforce.api.rest_stub import \
+ from version_control.rest.perforce.rest_stub import \
PerforceRestStub
PerforceRestStub.login(host=conn_info["host"],
@@ -82,7 +83,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"],
@@ -96,7 +97,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):
@@ -104,7 +105,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()
@@ -123,6 +124,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 absolute 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/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/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
diff --git a/client/version_control/backends/perforce/rest_routes.py b/client/version_control/backends/perforce/rest_routes.py
index b899f58..a51416a 100644
--- a/client/version_control/backends/perforce/rest_routes.py
+++ b/client/version_control/backends/perforce/rest_routes.py
@@ -1,11 +1,10 @@
import json
import datetime
-from bson.objectid import ObjectId
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.tools.tray.webserver.base_routes import RestApiEndpoint
from version_control.backends.perforce.backend import (
VersionControlPerforce
@@ -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)
@@ -68,7 +65,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 +80,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,11 +94,13 @@ 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()
+ 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),
@@ -112,7 +111,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 +127,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 +142,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 +156,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 +170,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 +184,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"])
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/changes_viewer/control.py b/client/version_control/changes_viewer/control.py
index a308386..99ee417 100644
--- a/client/version_control/changes_viewer/control.py
+++ b/client/version_control/changes_viewer/control.py
@@ -1,13 +1,10 @@
-import ayon_api
-
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.pipeline import (
registered_host,
get_current_context,
)
-from ayon_core.tools.ayon_utils.models import HierarchyModel
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:
@@ -16,17 +13,18 @@ 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)
self._event_system = self._create_event_system()
def emit_event(self, topic, data=None, source=None):
@@ -37,27 +35,14 @@ 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_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()
)
- 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"],
@@ -70,48 +55,22 @@ 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):
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..bed6dd5 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,19 @@ 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)
+
+ message_label_widget = QtWidgets.QLabel(self)
+
sync_btn = QtWidgets.QPushButton("Sync to", self)
self._block_changes = False
@@ -29,13 +51,54 @@ def __init__(self, model, 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)
- # 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
+ self._message_label_widget = message_label_widget
+
+ 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._message_label_widget.setText(f"Syncing to '{change_id}'...")
+
+ self.sync_btn.setEnabled(False)
+ thread = SyncThread(self._controller, change_id)
+ thread.finished.connect(lambda: self._on_thread_finished(change_id))
+ thread.start()
+
+ self._thread = thread
+
+ def _on_thread_finished(self, change_id):
+ self._message_label_widget.setText(
+ f"Synced to '{change_id}'. "
+ "Please close Viewer to continue."
+ )
+ 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 db5cd85..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__]
@@ -25,7 +17,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,50 +29,24 @@ 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
- 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/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/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..ec0226c
--- /dev/null
+++ b/client/version_control/launch_hooks/perforce/pre_load_sync_project.py
@@ -0,0 +1,96 @@
+"""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(launch_data=self.data)
+ changes_tool.show()
+ changes_tool.raise_()
+ changes_tool.activateWindow()
+ changes_tool.showNormal()
+
+ 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
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/plugins/create/unreal/publish_commit.py b/client/version_control/plugins/create/unreal/changelist_metadata.py
similarity index 51%
rename from client/version_control/plugins/create/unreal/publish_commit.py
rename to client/version_control/plugins/create/unreal/changelist_metadata.py
index 5660f21..3be9440 100644
--- a/client/version_control/plugins/create/unreal/publish_commit.py
+++ b/client/version_control/plugins/create/unreal/changelist_metadata.py
@@ -1,17 +1,8 @@
-from ayon_core.pipeline import (
- CreatedInstance
-)
-from ayon_core.client import get_asset_by_name
-import unreal
-
-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.pipeline import CreatedInstance
+from ayon_api import get_folder_by_path
+
+from ayon_unreal.api.plugin import UnrealBaseAutoCreator
+from ayon_unreal.api.pipeline import create_publish_instance, imprint
class UnrealPublishCommit(UnrealBaseAutoCreator):
@@ -23,11 +14,11 @@ 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"
- label = "Publish commit"
+ identifier = "io.ayon.creators.unreal.changelist_metadata"
+ product_type = "changelist_metadata"
+ label = "Publish Changelist Metadata"
- default_variant = ""
+ default_variant = "Main"
def create(self, options=None):
existing_instance = None
@@ -38,27 +29,32 @@ def create(self, options=None):
context = self.create_context
project_name = context.get_current_project_name()
- asset_name = context.get_current_asset_name()
- task_name = context.get_current_task_name()
+ folder_path = context.get_current_folder_path()
+ 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_asset = None
- else:
- existing_instance_asset = existing_instance["folderPath"]
- if existing_instance is None:
- asset_doc = get_asset_by_name(project_name, asset_name)
+
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": asset_name,
+ "folderPath": folder_path,
"task": task_name,
- "variant": self.default_variant
+ "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
@@ -73,9 +69,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())
@@ -83,14 +76,13 @@ def create(self, options=None):
return pub_instance
elif (
- existing_instance_asset != asset_name
- or existing_instance["task"] != task_name
+ existing_instance["folderPath"] != folder_path
+ or existing_instance.get("task") != task_name
):
- asset_doc = get_asset_by_name(project_name, asset_name)
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"] = asset_name
+ existing_instance["folderPath"] = folder_path
existing_instance["task"] = task_name
- existing_instance["product_name"] = product_name
+ existing_instance["productName"] = product_name
diff --git a/client/version_control/plugins/publish/collect_latest_changelist.py b/client/version_control/plugins/publish/collect_latest_changelist.py
index 0b45a7a..d9e360b 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
)
@@ -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/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):
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..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,9 +8,9 @@
import pyblish.api
-from openpype.modules import ModulesManager
+from ayon_core.addon import AddonsManager
-from version_control.backends.perforce.api.rest_stub import PerforceRestStub
+from version_control.rest.perforce.rest_stub import PerforceRestStub
class CollectVersionControlLogin(pyblish.api.ContextPlugin):
@@ -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
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..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,14 +4,14 @@
change list
Provides:
- new representation with name == "publish_commit"
+ new representation with name == "changelist_metadata"
"""
import os
import json
import tempfile
-from openpype.pipeline import publish
+from ayon_core.pipeline import publish
class ExtractChangeListInfo(publish.Extractor):
@@ -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/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/plugins/publish/validate_stream.py b/client/version_control/plugins/publish/validate_stream.py
new file mode 100644
index 0000000..7ddd3a9
--- /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 = ["changelist_metadata"]
+ 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)
diff --git a/client/version_control/plugins/publish/validate_workspace.py b/client/version_control/plugins/publish/validate_workspace.py
index afc133c..c51d283 100644
--- a/client/version_control/plugins/publish/validate_workspace.py
+++ b/client/version_control/plugins/publish/validate_workspace.py
@@ -1,19 +1,14 @@
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):
"""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
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)
diff --git a/client/version_control/backends/perforce/rest_api.py b/client/version_control/rest/perforce/rest_api.py
similarity index 98%
rename from client/version_control/backends/perforce/rest_api.py
rename to client/version_control/rest/perforce/rest_api.py
index 4069765..eb617eb 100644
--- a/client/version_control/backends/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
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):
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)