From 002f246989459f2dc737dc278eeb66422f39a962 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Tue, 30 Jan 2024 14:25:08 +0100 Subject: [PATCH] Addon Manager: Allow primary branch name change --- src/Mod/AddonManager/Addon.py | 13 +++++- src/Mod/AddonManager/addonmanager_git.py | 24 ++++++++++ .../AddonManager/addonmanager_installer.py | 8 +++- src/Mod/AddonManager/addonmanager_metadata.py | 7 +++ .../addonmanager_workers_startup.py | 45 ++++++++++++++----- .../manage_python_dependencies.py | 1 - src/Mod/AddonManager/package_details.py | 43 ++++++++++++------ src/Mod/AddonManager/package_list.py | 7 ++- 8 files changed, 117 insertions(+), 31 deletions(-) diff --git a/src/Mod/AddonManager/Addon.py b/src/Mod/AddonManager/Addon.py index e67614de85..5289f2752b 100644 --- a/src/Mod/AddonManager/Addon.py +++ b/src/Mod/AddonManager/Addon.py @@ -192,6 +192,7 @@ class Addon: self.macro = None # Bridge to Gaël Écorchard's macro management class self.updated_timestamp = None self.installed_version = None + self.installed_metadata = None # Each repo is also a node in a directed dependency graph (referenced by name so # they can be serialized): @@ -254,6 +255,8 @@ class Addon: if os.path.isfile(cached_package_xml_file): instance.load_metadata_file(cached_package_xml_file) + instance._load_installed_metadata() + if "requires" in cache_dict: instance.requires = set(cache_dict["requires"]) instance.blocks = set(cache_dict["blocks"]) @@ -293,6 +296,14 @@ class Addon: else: fci.Console.PrintLog(f"Internal error: {file} does not exist") + def _load_installed_metadata(self) -> None: + # If it is actually installed, there is a SECOND metadata file, in the actual installation, + # that may not match the cached one if the Addon has not been updated but the cache has. + mod_dir = os.path.join(self.mod_directory, self.name) + installed_metadata_path = os.path.join(mod_dir, "package.xml") + if os.path.isfile(installed_metadata_path): + self.installed_metadata = MetadataReader.from_file(installed_metadata_path) + def set_metadata(self, metadata: Metadata) -> None: """Set the given metadata object as this object's metadata, updating the object's display name and package type information to match, as well as @@ -683,9 +694,7 @@ class Addon: filename, extension = os.path.splitext(current_file) if extension == ".py": wb_classname = self._find_classname_in_file(current_file) - print(f"Current file: {current_file} ") if wb_classname: - print(f"Found name {wb_classname} \n") return wb_classname return "" diff --git a/src/Mod/AddonManager/addonmanager_git.py b/src/Mod/AddonManager/addonmanager_git.py index 7bbcd542e5..3a63d74619 100644 --- a/src/Mod/AddonManager/addonmanager_git.py +++ b/src/Mod/AddonManager/addonmanager_git.py @@ -387,6 +387,30 @@ class GitManager: result_dict[author]["count"] += 1 return result_dict + def migrate_branch(self, local_path: str, old_branch: str, new_branch: str) -> None: + """Rename a branch (used when the remote branch name changed). Assumes that "origin" + exists.""" + old_dir = os.getcwd() + os.chdir(local_path) + try: + self._synchronous_call_git(["branch", "-m", old_branch, new_branch]) + self._synchronous_call_git(["fetch", "origin"]) + self._synchronous_call_git(["branch", "--unset-upstream"]) + self._synchronous_call_git(["branch", f"--set-upstream-to=origin/{new_branch}"]) + self._synchronous_call_git(["pull"]) + except GitFailed as e: + fci.Console.PrintWarning( + translate( + "AddonsInstaller", + "Git branch rename failed with the following message:", + ) + + str(e) + + "\n" + ) + os.chdir(old_dir) + raise e + os.chdir(old_dir) + def _find_git(self): # Find git. In preference order # A) The value of the GitExecutable user preference diff --git a/src/Mod/AddonManager/addonmanager_installer.py b/src/Mod/AddonManager/addonmanager_installer.py index d35a3bc4b9..cd9b2bf9e6 100644 --- a/src/Mod/AddonManager/addonmanager_installer.py +++ b/src/Mod/AddonManager/addonmanager_installer.py @@ -40,6 +40,7 @@ from PySide import QtCore from Addon import Addon import addonmanager_utilities as utils +from addonmanager_metadata import get_branch_from_metadata from addonmanager_git import initialize_git, GitFailed if FreeCAD.GuiUp: @@ -281,7 +282,12 @@ class AddonInstaller(QtCore.QObject): install_path = os.path.join(self.installation_path, self.addon_to_install.name) try: if os.path.isdir(install_path): - self.git_manager.update(install_path) + old_branch = get_branch_from_metadata(self.addon_to_install.installed_metadata) + new_branch = get_branch_from_metadata(self.addon_to_install.metadata) + if old_branch != new_branch: + self.git_manager.migrate_branch(install_path, old_branch, new_branch) + else: + self.git_manager.update(install_path) else: self.git_manager.clone(self.addon_to_install.url, install_path) self.git_manager.checkout(install_path, self.addon_to_install.branch) diff --git a/src/Mod/AddonManager/addonmanager_metadata.py b/src/Mod/AddonManager/addonmanager_metadata.py index 115f5f29fe..213b0e2c5e 100644 --- a/src/Mod/AddonManager/addonmanager_metadata.py +++ b/src/Mod/AddonManager/addonmanager_metadata.py @@ -234,6 +234,13 @@ def get_first_supported_freecad_version(metadata: Metadata) -> Optional[Version] return current_earliest +def get_branch_from_metadata(metadata: Metadata) -> str: + for url in metadata.url: + if url.type == UrlType.repository: + return url.branch + return "master" # Legacy default + + class MetadataReader: """Read metadata XML data and construct a Metadata object""" diff --git a/src/Mod/AddonManager/addonmanager_workers_startup.py b/src/Mod/AddonManager/addonmanager_workers_startup.py index a6a8e986b1..7769216e04 100644 --- a/src/Mod/AddonManager/addonmanager_workers_startup.py +++ b/src/Mod/AddonManager/addonmanager_workers_startup.py @@ -43,7 +43,7 @@ from addonmanager_macro import Macro from Addon import Addon import NetworkManager from addonmanager_git import initialize_git, GitFailed -from addonmanager_metadata import MetadataReader +from addonmanager_metadata import MetadataReader, get_branch_from_metadata translate = FreeCAD.Qt.translate @@ -193,8 +193,8 @@ class CreateAddonListWorker(QtCore.QThread): repo = Addon(name, addon["url"], state, addon["branch"]) md_file = os.path.join(addondir, "package.xml") if os.path.isfile(md_file): - repo.load_metadata_file(md_file) - repo.installed_version = repo.metadata.version + repo.installed_metadata = MetadataReader.from_file(md_file) + repo.installed_version = repo.installed_metadata.version repo.updated_timestamp = os.path.getmtime(md_file) repo.verify_url_and_branch(addon["url"], addon["branch"]) @@ -236,8 +236,8 @@ class CreateAddonListWorker(QtCore.QThread): repo = Addon(name, url, state, branch) md_file = os.path.join(addondir, "package.xml") if os.path.isfile(md_file): - repo.load_metadata_file(md_file) - repo.installed_version = repo.metadata.version + repo.installed_metadata = MetadataReader.from_file(md_file) + repo.installed_version = repo.installed_metadata.version repo.updated_timestamp = os.path.getmtime(md_file) repo.verify_url_and_branch(url, branch) @@ -453,8 +453,6 @@ class LoadPackagesFromCacheWorker(QtCore.QThread): if os.path.isfile(repo_metadata_cache_path): try: repo.load_metadata_file(repo_metadata_cache_path) - repo.installed_version = repo.metadata.version - repo.updated_timestamp = os.path.getmtime(repo_metadata_cache_path) except Exception as e: FreeCAD.Console.PrintLog(f"Failed loading {repo_metadata_cache_path}\n") FreeCAD.Console.PrintLog(str(e) + "\n") @@ -608,15 +606,36 @@ class UpdateChecker: ) wb.set_status(Addon.Status.CANNOT_CHECK) + def _branch_name_changed(self, package: Addon) -> bool: + clone_dir = os.path.join(self.moddir, package.name) + installed_metadata_file = os.path.join(clone_dir, "package.xml") + if not os.path.isfile(installed_metadata_file): + return False + try: + installed_metadata = MetadataReader.from_file(installed_metadata_file) + installed_default_branch = get_branch_from_metadata(installed_metadata) + remote_default_branch = get_branch_from_metadata(package.metadata) + if installed_default_branch != remote_default_branch: + return True + except Exception: + return False + return False + def check_package(self, package: Addon) -> None: """Given a packaged Addon package, check it for updates. If git is available that is used. If not, the package's metadata is examined, and if the metadata file has changed - compared to the installed copy, an update is flagged.""" + compared to the installed copy, an update is flagged. In addition, a change to the + default branch name triggers an update.""" - clonedir = self.moddir + os.sep + package.name - if os.path.exists(clonedir): + clone_dir = self.moddir + os.sep + package.name + if os.path.exists(clone_dir): - # First, try to just do a git-based update, which will give the most accurate results: + # First, see if the branch name changed, which automatically triggers an update + if self._branch_name_changed(package): + package.set_status(Addon.Status.UPDATE_AVAILABLE) + return + + # Next, try to just do a git-based update, which will give the most accurate results: if self.git_manager: self.check_workbench(package) if package.status() != Addon.Status.CANNOT_CHECK: @@ -624,7 +643,7 @@ class UpdateChecker: return # If we were unable to do a git-based update, try using the package.xml file instead: - installed_metadata_file = os.path.join(clonedir, "package.xml") + installed_metadata_file = os.path.join(clone_dir, "package.xml") if not os.path.isfile(installed_metadata_file): # If there is no package.xml file, then it's because the package author added it # after the last time the local installation was updated. By definition, then, @@ -852,6 +871,8 @@ class CacheMacroCodeWorker(QtCore.QThread): ) with self.lock: self.failed.append(macro_name) + self.repo_queue.task_done() + self.counter += 1 class GetMacroDetailsWorker(QtCore.QThread): diff --git a/src/Mod/AddonManager/manage_python_dependencies.py b/src/Mod/AddonManager/manage_python_dependencies.py index 4b42cd9298..ccfceddb57 100644 --- a/src/Mod/AddonManager/manage_python_dependencies.py +++ b/src/Mod/AddonManager/manage_python_dependencies.py @@ -415,7 +415,6 @@ class PythonPackageManager: pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") known_python_versions_string = pref.GetString("KnownPythonVersions", "[]") known_python_versions = json.loads(known_python_versions_string) - print(json.dumps(known_python_versions, indent=4)) return known_python_versions @classmethod diff --git a/src/Mod/AddonManager/package_details.py b/src/Mod/AddonManager/package_details.py index 62f8cb665d..b66100ba82 100644 --- a/src/Mod/AddonManager/package_details.py +++ b/src/Mod/AddonManager/package_details.py @@ -31,10 +31,14 @@ from PySide import QtCore, QtGui, QtWidgets import addonmanager_freecad_interface as fci import addonmanager_utilities as utils -from addonmanager_metadata import Version, UrlType, get_first_supported_freecad_version +from addonmanager_metadata import ( + Version, + get_first_supported_freecad_version, + get_branch_from_metadata, +) from addonmanager_workers_startup import GetMacroDetailsWorker, CheckSingleUpdateWorker from addonmanager_readme_viewer import ReadmeViewer -from addonmanager_git import GitManager, NoGitFound, GitFailed +from addonmanager_git import GitManager, NoGitFound from Addon import Addon from change_branch import ChangeBranchDialog @@ -150,16 +154,30 @@ class PackageDetails(QtWidgets.QWidget): if status == Addon.Status.UPDATE_AVAILABLE: if repo.metadata: - installed_version_string += ( - "" - + translate( - "AddonsInstaller", - "On branch {}, update available to version", - ).format(repo.branch) - + " " - ) - installed_version_string += str(repo.metadata.version) - installed_version_string += "." + name_change = False + if repo.installed_metadata: + old_branch = get_branch_from_metadata(repo.installed_metadata) + new_branch = get_branch_from_metadata(repo.metadata) + if old_branch != new_branch: + installed_version_string += ( + "" + + translate( + "AddonsInstaller", + "Currently on branch {}, name changed to {}", + ).format(old_branch, new_branch) + ) + ". " + name_change = True + if not name_change: + installed_version_string += ( + "" + + translate( + "AddonsInstaller", + "On branch {}, update available to version", + ).format(repo.branch) + + " " + ) + installed_version_string += str(repo.metadata.version) + installed_version_string += "." elif repo.macro and repo.macro.version: installed_version_string += ( "" + translate("AddonsInstaller", "Update available to version") + " " @@ -364,7 +382,6 @@ class PackageDetails(QtWidgets.QWidget): # If all four above checks passed, then it's possible for us to switch # branches, if there are any besides the one we are on: show the button - print("Showing the button") self.ui.buttonChangeBranch.show() def set_disable_button_state(self): diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index d26b4f5489..f6676feaa5 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -454,11 +454,14 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate): installed_version_string = "" if repo.status() != Addon.Status.NOT_INSTALLED: - if repo.installed_version: + if repo.installed_version or repo.installed_metadata: installed_version_string = ( "
" + translate("AddonsInstaller", "Installed version") + ": " ) - installed_version_string += str(repo.installed_version) + if repo.installed_metadata: + installed_version_string += str(repo.installed_metadata.version) + elif repo.installed_version: + installed_version_string += str(repo.installed_version) else: installed_version_string = "
" + translate("AddonsInstaller", "Unknown version")