Addon Manager: Refactor to test git and eliminate GitPython
This commit is contained in:
@@ -26,6 +26,7 @@ import os
|
||||
import shutil
|
||||
import stat
|
||||
import tempfile
|
||||
import time
|
||||
from zipfile import ZipFile
|
||||
import FreeCAD
|
||||
|
||||
@@ -40,6 +41,7 @@ class TestGit(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the test case: called by the unit test system"""
|
||||
self.cwd = os.getcwd()
|
||||
test_data_dir = os.path.join(
|
||||
FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data"
|
||||
)
|
||||
@@ -49,7 +51,9 @@ class TestGit(unittest.TestCase):
|
||||
)
|
||||
os.makedirs(self.test_dir, exist_ok=True)
|
||||
self.test_repo_remote = os.path.join(self.test_dir, "TEST_REPO_REMOTE")
|
||||
self._rmdir(self.test_repo_remote)
|
||||
if os.path.exists(self.test_repo_remote):
|
||||
# Make sure any old copy that got left around is deleted
|
||||
self._rmdir(self.test_repo_remote)
|
||||
|
||||
if not os.path.exists(git_repo_zip):
|
||||
self.skipTest("Can't find test repo")
|
||||
@@ -66,13 +70,16 @@ class TestGit(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after the test"""
|
||||
# self._rmdir(self.test_dir)
|
||||
os.chdir(self.cwd)
|
||||
#self._rmdir(self.test_dir)
|
||||
os.rename(self.test_dir, self.test_dir + ".old." + str(time.time()))
|
||||
|
||||
def test_clone(self):
|
||||
"""Test git clone"""
|
||||
checkout_dir = self._clone_test_repo()
|
||||
self.assertTrue(os.path.exists(checkout_dir))
|
||||
self.assertTrue(os.path.exists(os.path.join(checkout_dir, ".git")))
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_checkout(self):
|
||||
"""Test git checkout"""
|
||||
@@ -82,6 +89,7 @@ class TestGit(unittest.TestCase):
|
||||
status = self.git.status(checkout_dir).strip()
|
||||
expected_status = "## HEAD (no branch)"
|
||||
self.assertEqual(status, expected_status)
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_update(self):
|
||||
"""Test using git to update the local repo"""
|
||||
@@ -91,6 +99,7 @@ class TestGit(unittest.TestCase):
|
||||
self.assertTrue(self.git.update_available(checkout_dir))
|
||||
self.git.update(checkout_dir)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_tag_and_branch(self):
|
||||
"""Test checking the currently checked-out tag"""
|
||||
@@ -108,12 +117,40 @@ class TestGit(unittest.TestCase):
|
||||
self.assertEqual(found_branch, expected_branch)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
|
||||
expected_branch = "master"
|
||||
expected_branch = "main"
|
||||
self.git.checkout(checkout_dir, expected_branch)
|
||||
found_branch = self.git.current_branch(checkout_dir)
|
||||
self.assertEqual(found_branch, expected_branch)
|
||||
self.assertFalse(self.git.update_available(checkout_dir))
|
||||
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_get_remote(self):
|
||||
""" Test getting the remote location """
|
||||
checkout_dir = self._clone_test_repo()
|
||||
expected_remote = self.test_repo_remote
|
||||
returned_remote = self.git.get_remote(checkout_dir)
|
||||
self.assertEqual(expected_remote, returned_remote)
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
def test_repair(self):
|
||||
""" Test the repair feature (and some exception throwing) """
|
||||
checkout_dir = self._clone_test_repo()
|
||||
remote = self.git.get_remote(checkout_dir)
|
||||
git_dir = os.path.join(checkout_dir,".git")
|
||||
self.assertTrue(os.path.exists(git_dir))
|
||||
self._rmdir(git_dir)
|
||||
|
||||
# Make sure that we've truly broken the install
|
||||
with self.assertRaises(GitFailed):
|
||||
self.git.status(checkout_dir)
|
||||
|
||||
self.git.repair(remote, checkout_dir)
|
||||
status = self.git.status(checkout_dir)
|
||||
self.assertEqual(status,"## main...origin/main\n")
|
||||
self.assertEqual(os.getcwd(),self.cwd, "We should be left in the same CWD we started")
|
||||
|
||||
|
||||
def _rmdir(self, path):
|
||||
try:
|
||||
shutil.rmtree(path, onerror=self._remove_readonly)
|
||||
|
||||
Binary file not shown.
@@ -26,6 +26,13 @@ import json
|
||||
import unittest
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
have_git = True
|
||||
try:
|
||||
import git
|
||||
except ImportError:
|
||||
have_git = False
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from PySide2 import QtCore
|
||||
@@ -36,6 +43,11 @@ from addonmanager_workers_startup import (
|
||||
CreateAddonListWorker,
|
||||
LoadPackagesFromCacheWorker,
|
||||
LoadMacrosFromCacheWorker,
|
||||
CheckSingleUpdateWorker,
|
||||
)
|
||||
|
||||
from addonmanager_workers_installation import (
|
||||
InstallWorkbenchWorker,
|
||||
)
|
||||
|
||||
class TestWorkersStartup(unittest.TestCase):
|
||||
@@ -159,8 +171,49 @@ class TestWorkersStartup(unittest.TestCase):
|
||||
def test_update_checker(self):
|
||||
""" Test the code that checks a single addon for available updates. """
|
||||
|
||||
# First, install a specific Addon of each kind
|
||||
if not have_git:
|
||||
return
|
||||
|
||||
# Populate the test's Addon List:
|
||||
self.test_create_addon_list_worker()
|
||||
|
||||
# First, install a specific Addon of each kind into a temp location
|
||||
location = os.path.join(tempfile.gettempdir(),"FreeCADTesting")
|
||||
|
||||
self._test_workbench_update_checker(location)
|
||||
|
||||
|
||||
|
||||
# Preference Pack
|
||||
# Macro
|
||||
|
||||
# Arrange for those addons to be out-of-date
|
||||
|
||||
# Check for updates
|
||||
# Check for updates
|
||||
|
||||
def _test_workbench_update_checker(self, location):
|
||||
|
||||
# Workbench: use the FreeCAD-Help workbench for testing purposes
|
||||
help_addon = None
|
||||
for addon in self.addon_list:
|
||||
if addon.name == "Help":
|
||||
help_addon = addon
|
||||
break
|
||||
if not help_addon:
|
||||
print("Unable to locate the FreeCAD-Help addon to test with")
|
||||
return
|
||||
|
||||
addon_location = os.path.join(location, help_addon.name)
|
||||
worker = InstallWorkbenchWorker(addon, addon_location)
|
||||
worker.run() # Synchronous call, blocks until complete
|
||||
gitrepo = git.Git(addon_location)
|
||||
gitrepo.reset("--hard", "HEAD^")
|
||||
print (addon_location)
|
||||
|
||||
# At this point the addon should be "out of date", checked out to one revision behind
|
||||
# the most recent.
|
||||
|
||||
worker = CheckSingleUpdateWorker(help_addon)
|
||||
worker.do_work() # Synchronous call
|
||||
|
||||
self.assertEqual(help_addon.status(), Addon.Status.UPDATE_AVAILABLE)
|
||||
@@ -29,8 +29,11 @@ import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import List
|
||||
import time
|
||||
import FreeCAD
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class NoGitFound(RuntimeError):
|
||||
"""Could not locate the git executable on this system."""
|
||||
@@ -80,15 +83,47 @@ class GitManager:
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
self._synchronous_call_git(["fetch"])
|
||||
self._synchronous_call_git(["pull"])
|
||||
self._synchronous_call_git(["submodule", "update", "--init", "--recursive"])
|
||||
try:
|
||||
self._synchronous_call_git(["pull"])
|
||||
self._synchronous_call_git(["submodule", "update", "--init", "--recursive"])
|
||||
except GitFailed as e:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Basic git update failed with the following message:",
|
||||
)
|
||||
+ str(e)
|
||||
+ "\n"
|
||||
)
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
"Backing up the original directory and re-cloning",
|
||||
)
|
||||
+ "...\n"
|
||||
)
|
||||
remote = self.get_remote(local_path)
|
||||
with open(os.path.join(local_path, "ADDON_DISABLED"), "w") as f:
|
||||
f.write(
|
||||
"This is a backup of an addon that failed to update cleanly so was re-cloned. "
|
||||
+ "It was disabled by the Addon Manager's git update facility and can be safely "
|
||||
+ "deleted if the addon is working properly."
|
||||
)
|
||||
os.chdir("..")
|
||||
os.rename(local_path, local_path + ".backup" + str(time.time()))
|
||||
self.clone(remote, local_path)
|
||||
os.chdir(old_dir)
|
||||
|
||||
def status(self, local_path) -> str:
|
||||
"""Gets the v1 porcelain status"""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
status = self._synchronous_call_git(["status", "-sb", "--porcelain"])
|
||||
try:
|
||||
status = self._synchronous_call_git(["status", "-sb", "--porcelain"])
|
||||
except GitFailed as e:
|
||||
os.chdir(old_dir)
|
||||
raise e
|
||||
|
||||
os.chdir(old_dir)
|
||||
return status
|
||||
|
||||
@@ -99,7 +134,11 @@ class GitManager:
|
||||
final_args = ["reset"]
|
||||
if args:
|
||||
final_args.extend(args)
|
||||
self._synchronous_call_git(final_args)
|
||||
try:
|
||||
self._synchronous_call_git(final_args)
|
||||
except GitFailed as e:
|
||||
os.chdir(old_dir)
|
||||
raise e
|
||||
os.chdir(old_dir)
|
||||
|
||||
def async_fetch_and_update(self, local_path, progress_monitor, args=None):
|
||||
@@ -109,8 +148,12 @@ class GitManager:
|
||||
"""Returns True if an update is available from the remote, or false if not"""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
self._synchronous_call_git(["fetch"])
|
||||
status = self._synchronous_call_git(["status", "-sb", "--porcelain"])
|
||||
try:
|
||||
self._synchronous_call_git(["fetch"])
|
||||
status = self._synchronous_call_git(["status", "-sb", "--porcelain"])
|
||||
except GitFailed as e:
|
||||
os.chdir(old_dir)
|
||||
raise e
|
||||
os.chdir(old_dir)
|
||||
return "behind" in status
|
||||
|
||||
@@ -118,7 +161,11 @@ class GitManager:
|
||||
"""Get the name of the currently checked-out tag if HEAD is detached"""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
tag = self._synchronous_call_git(["describe", "--tags"]).strip()
|
||||
try:
|
||||
tag = self._synchronous_call_git(["describe", "--tags"]).strip()
|
||||
except GitFailed as e:
|
||||
os.chdir(old_dir)
|
||||
raise e
|
||||
os.chdir(old_dir)
|
||||
return tag
|
||||
|
||||
@@ -126,7 +173,11 @@ class GitManager:
|
||||
"""Get the name of the current branch"""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
branch = self._synchronous_call_git(["branch", "--show-current"]).strip()
|
||||
try:
|
||||
branch = self._synchronous_call_git(["branch", "--show-current"]).strip()
|
||||
except GitFailed as e:
|
||||
os.chdir(old_dir)
|
||||
raise e
|
||||
os.chdir(old_dir)
|
||||
return branch
|
||||
|
||||
@@ -135,14 +186,55 @@ class GitManager:
|
||||
ensures that it is. Note that any local changes in local_path will be destroyed. This
|
||||
is achieved by archiving the old path, cloning an entirely new copy, and then deleting
|
||||
the old directory."""
|
||||
old_path = local_path + ".bak"
|
||||
os.rename(local_path, old_path)
|
||||
|
||||
original_cwd = os.getcwd()
|
||||
|
||||
# Make sure we are not currently in that directory, otherwise on Windows the rename
|
||||
# will fail. To guarantee we aren't in it, change to it, then shift up one.
|
||||
os.chdir(local_path)
|
||||
os.chdir("..")
|
||||
backup_path = local_path + ".backup" + str(time.time())
|
||||
os.rename(local_path, backup_path)
|
||||
try:
|
||||
self.clone(remote, local_path)
|
||||
except GitFailed as e:
|
||||
shutil.rmtree(local_path)
|
||||
os.rename(old_path, local_path)
|
||||
FreeCAD.Console.PrintError(
|
||||
translate(
|
||||
"AddonsInstaller", "Failed to clone {} into {} using git"
|
||||
).format(remote, local_path)
|
||||
)
|
||||
os.chdir(original_cwd)
|
||||
raise e
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def get_remote(self, local_path) -> str:
|
||||
"""Get the repository that this local path is set to fetch from"""
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(local_path)
|
||||
try:
|
||||
response = self._synchronous_call_git(["remote", "-v", "show"])
|
||||
except GitFailed as e:
|
||||
os.chdir(old_dir)
|
||||
raise e
|
||||
lines = response.split("\n")
|
||||
result = "(unknown remote)"
|
||||
for line in lines:
|
||||
if line.endswith("(fetch)"):
|
||||
|
||||
# The line looks like:
|
||||
# origin https://some/sort/of/path (fetch)
|
||||
|
||||
segments = line.split()
|
||||
if len(segments) == 3:
|
||||
result = segments[1]
|
||||
break
|
||||
else:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"Error parsing the results from git remote -v show:\n"
|
||||
)
|
||||
FreeCAD.Console.PrintWarning(line + "\n")
|
||||
os.chdir(old_dir)
|
||||
return result
|
||||
|
||||
def _find_git(self):
|
||||
# Find git. In preference order
|
||||
|
||||
@@ -49,22 +49,17 @@ import addonmanager_utilities as utils
|
||||
from addonmanager_macro import Macro
|
||||
from Addon import Addon
|
||||
import NetworkManager
|
||||
from addonmanager_git import GitManager, GitFailed, NoGitFound
|
||||
|
||||
have_git = False
|
||||
try:
|
||||
import git
|
||||
|
||||
# Some types of Python installation will fall back to finding a directory called "git"
|
||||
# in certain locations instead of a Python package called git: that directory is unlikely
|
||||
# to have the "Repo" attribute unless it is a real installation, however, so this check
|
||||
# should catch that. (Bug #4072)
|
||||
have_git = hasattr(git, "Repo")
|
||||
if not have_git:
|
||||
FreeCAD.Console.PrintMessage(
|
||||
"Unable to locate a viable GitPython installation: falling back to ZIP installation."
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
git_manager = None
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
disable_git = pref.GetBool("disableGit", False)
|
||||
if not disable_git:
|
||||
try:
|
||||
git_manager = GitManager()
|
||||
except NoGitFound:
|
||||
pass # A log messsage was already printed by the startup code
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
@@ -73,8 +68,6 @@ translate = FreeCAD.Qt.translate
|
||||
# \brief Multithread workers for the addon manager
|
||||
# @{
|
||||
|
||||
NOGIT = False # for debugging purposes, set this to True to always use http downloads
|
||||
|
||||
|
||||
class InstallWorkbenchWorker(QtCore.QThread):
|
||||
"This worker installs a workbench"
|
||||
@@ -84,7 +77,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
||||
success = QtCore.Signal(Addon, str)
|
||||
failure = QtCore.Signal(Addon, str)
|
||||
|
||||
def __init__(self, repo: Addon):
|
||||
def __init__(self, repo: Addon, location=None):
|
||||
|
||||
QtCore.QThread.__init__(self)
|
||||
self.repo = repo
|
||||
@@ -93,13 +86,22 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
||||
self.update_timer.timeout.connect(self.update_status)
|
||||
self.update_timer.start()
|
||||
|
||||
if location:
|
||||
self.clone_directory = location
|
||||
else:
|
||||
basedir = FreeCAD.getUserAppDataDir()
|
||||
self.clone_directory = os.path.join(basedir, "Mod", repo.name)
|
||||
|
||||
if not os.path.exists(self.clone_directory):
|
||||
os.makedirs(self.clone_directory)
|
||||
|
||||
def run(self):
|
||||
"installs or updates the selected addon"
|
||||
|
||||
if not self.repo:
|
||||
return
|
||||
|
||||
if not have_git or NOGIT:
|
||||
if not git_manager:
|
||||
FreeCAD.Console.PrintLog(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
@@ -117,13 +119,9 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
||||
)
|
||||
return
|
||||
|
||||
basedir = FreeCAD.getUserAppDataDir()
|
||||
moddir = basedir + os.sep + "Mod"
|
||||
if not os.path.exists(moddir):
|
||||
os.makedirs(moddir)
|
||||
target_dir = moddir + os.sep + self.repo.name
|
||||
target_dir = self.clone_directory
|
||||
|
||||
if have_git and not NOGIT:
|
||||
if git_manager:
|
||||
# Do the git process...
|
||||
self.run_git(target_dir)
|
||||
else:
|
||||
@@ -144,7 +142,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
||||
|
||||
def run_git(self, clonedir: str) -> None:
|
||||
|
||||
if NOGIT or not have_git:
|
||||
if not git_manager:
|
||||
FreeCAD.Console.PrintLog(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
@@ -165,10 +163,9 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
||||
self.status_message.emit("Updating module...")
|
||||
with self.repo.git_lock:
|
||||
if not os.path.exists(clonedir + os.sep + ".git"):
|
||||
utils.repair_git_repo(self.repo.url, clonedir)
|
||||
repo = git.Git(clonedir)
|
||||
git_manager.repair(self.repo.url, clonedir)
|
||||
try:
|
||||
repo.pull("--ff-only") # Refuses to take a progress object?
|
||||
git_manager.update(clonedir)
|
||||
if self.repo.contains_workbench():
|
||||
answer = translate(
|
||||
"AddonsInstaller",
|
||||
@@ -179,7 +176,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
||||
"AddonsInstaller",
|
||||
"Workbench successfully updated.",
|
||||
)
|
||||
except Exception as e:
|
||||
except GitFailed as e:
|
||||
answer = (
|
||||
translate("AddonsInstaller", "Error updating module")
|
||||
+ " "
|
||||
@@ -190,14 +187,8 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
||||
)
|
||||
answer += str(e)
|
||||
self.failure.emit(self.repo, answer)
|
||||
else:
|
||||
# Update the submodules for this repository
|
||||
repo_sms = git.Repo(clonedir)
|
||||
self.status_message.emit("Updating submodules...")
|
||||
for submodule in repo_sms.submodules:
|
||||
submodule.update(init=True, recursive=True)
|
||||
self.update_metadata()
|
||||
self.success.emit(self.repo, answer)
|
||||
self.update_metadata()
|
||||
self.success.emit(self.repo, answer)
|
||||
|
||||
def run_git_clone(self, clonedir: str) -> None:
|
||||
self.status_message.emit("Cloning module...")
|
||||
@@ -216,27 +207,14 @@ class InstallWorkbenchWorker(QtCore.QThread):
|
||||
|
||||
with self.repo.git_lock:
|
||||
FreeCAD.Console.PrintMessage("Lock acquired...\n")
|
||||
# NOTE: There is no way to interrupt this process in GitPython: someday we should
|
||||
# support pygit2/libgit2 so we can actually interrupt this properly.
|
||||
repo = git.Repo.clone_from(
|
||||
self.repo.url, clonedir, progress=self.git_progress
|
||||
)
|
||||
git_manager.clone(self.repo.url, clonedir)
|
||||
FreeCAD.Console.PrintMessage("Initial clone complete...\n")
|
||||
if current_thread.isInterruptionRequested():
|
||||
return
|
||||
|
||||
# Make sure to clone all the submodules as well
|
||||
if repo.submodules:
|
||||
FreeCAD.Console.PrintMessage("Updating submodules...\n")
|
||||
repo.submodule_update(recursive=True)
|
||||
|
||||
if current_thread.isInterruptionRequested():
|
||||
return
|
||||
|
||||
if self.repo.branch in repo.heads:
|
||||
FreeCAD.Console.PrintMessage("Checking out HEAD...\n")
|
||||
repo.heads[self.repo.branch].checkout()
|
||||
|
||||
FreeCAD.Console.PrintMessage("Clone complete\n")
|
||||
|
||||
if self.repo.contains_workbench():
|
||||
@@ -728,13 +706,12 @@ class UpdateMetadataCacheWorker(QtCore.QThread):
|
||||
repo.cached_icon_filename = cache_file
|
||||
|
||||
|
||||
if have_git and not NOGIT:
|
||||
if git_manager:
|
||||
|
||||
class GitProgressMonitor(git.RemoteProgress):
|
||||
class GitProgressMonitor:
|
||||
"""An object that receives git progress updates and stores them for later display"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.current = 0
|
||||
self.total = 100
|
||||
self.message = ""
|
||||
|
||||
@@ -40,29 +40,25 @@ import addonmanager_utilities as utils
|
||||
from addonmanager_macro import Macro
|
||||
from Addon import Addon
|
||||
import NetworkManager
|
||||
|
||||
have_git = False
|
||||
try:
|
||||
import git
|
||||
|
||||
# Some types of Python installation will fall back to finding a directory called "git"
|
||||
# in certain locations instead of a Python package called git: that directory is unlikely
|
||||
# to have the "Repo" attribute unless it is a real installation, however, so this check
|
||||
# should catch that. (Bug #4072)
|
||||
have_git = hasattr(git, "Repo")
|
||||
if not have_git:
|
||||
FreeCAD.Console.PrintMessage(
|
||||
"Unable to locate a viable GitPython installation: falling back to ZIP installation."
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
NOGIT = False # for debugging purposes, set this to True to always use http downloads
|
||||
from addonmanager_git import GitManager, GitFailed, NoGitFound
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
git_manager = None
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
|
||||
disable_git = pref.GetBool("disableGit", False)
|
||||
if not disable_git:
|
||||
try:
|
||||
git_manager = GitManager()
|
||||
except NoGitFound:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate("AddonsInstaller", "Could not locate a suitable git executable")
|
||||
)
|
||||
|
||||
# Workers only have one public method by design
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
class CreateAddonListWorker(QtCore.QThread):
|
||||
"""This worker updates the list of available workbenches, emitting an "addon_repo"
|
||||
signal for each Addon as they are processed."""
|
||||
@@ -133,7 +129,7 @@ class CreateAddonListWorker(QtCore.QThread):
|
||||
raise ConnectionError
|
||||
|
||||
def _process_deprecated(self, deprecated_addons):
|
||||
""" Parse the section on deprecated addons """
|
||||
"""Parse the section on deprecated addons"""
|
||||
|
||||
fc_major = int(FreeCAD.Version()[0])
|
||||
fc_minor = int(FreeCAD.Version()[1])
|
||||
@@ -146,9 +142,7 @@ class CreateAddonListWorker(QtCore.QThread):
|
||||
minor = int(version_components[1])
|
||||
else:
|
||||
minor = 0
|
||||
if major < fc_major or (
|
||||
major == fc_major and minor <= fc_minor
|
||||
):
|
||||
if major < fc_major or (major == fc_major and minor <= fc_minor):
|
||||
if "kind" not in item or item["kind"] == "mod":
|
||||
self.obsolete.append(item["name"])
|
||||
elif item["kind"] == "macro":
|
||||
@@ -268,10 +262,10 @@ class CreateAddonListWorker(QtCore.QThread):
|
||||
|
||||
macro_cache_location = utils.get_cache_file_name("Macros")
|
||||
|
||||
if not have_git or NOGIT:
|
||||
if not git_manager:
|
||||
message = translate(
|
||||
"AddonsInstaller",
|
||||
"Failed to execute Git Python command: check installation of GitPython and/or git",
|
||||
"Failed to execute git command: check installation of git",
|
||||
)
|
||||
self.status_message_signal.emit(message)
|
||||
FreeCAD.Console.PrintWarning(message + "\n")
|
||||
@@ -316,16 +310,17 @@ class CreateAddonListWorker(QtCore.QThread):
|
||||
"Attempting to change non-git Macro setup to use git\n",
|
||||
)
|
||||
)
|
||||
utils.repair_git_repo(
|
||||
"https://github.com/FreeCAD/FreeCAD-macros.git", macro_cache_location
|
||||
git_manager.repair(
|
||||
"https://github.com/FreeCAD/FreeCAD-macros.git",
|
||||
macro_cache_location,
|
||||
)
|
||||
gitrepo = git.Git(macro_cache_location)
|
||||
gitrepo.pull("--ff-only")
|
||||
git_manager.update(macro_cache_location)
|
||||
else:
|
||||
git.Repo.clone_from(
|
||||
"https://github.com/FreeCAD/FreeCAD-macros.git", macro_cache_location
|
||||
git_manager.clone(
|
||||
"https://github.com/FreeCAD/FreeCAD-macros.git",
|
||||
macro_cache_location,
|
||||
)
|
||||
except git.exc.GitError as e:
|
||||
except GitFailed as e:
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate(
|
||||
"AddonsInstaller",
|
||||
@@ -340,13 +335,14 @@ class CreateAddonListWorker(QtCore.QThread):
|
||||
)
|
||||
try:
|
||||
shutil.rmtree(macro_cache_location, onerror=self._remove_readonly)
|
||||
git.Repo.clone_from(
|
||||
"https://github.com/FreeCAD/FreeCAD-macros.git", macro_cache_location
|
||||
git_manager.clone(
|
||||
"https://github.com/FreeCAD/FreeCAD-macros.git",
|
||||
macro_cache_location,
|
||||
)
|
||||
FreeCAD.Console.PrintMessage(
|
||||
translate("AddonsInstaller", "Clean checkout succeeded") + "\n"
|
||||
)
|
||||
except git.exc.GitError as e2:
|
||||
except GitFailed as e2:
|
||||
# The Qt Python translation extractor doesn't support splitting this string (yet)
|
||||
# pylint: disable=line-too-long
|
||||
FreeCAD.Console.PrintWarning(
|
||||
@@ -413,7 +409,8 @@ class CreateAddonListWorker(QtCore.QThread):
|
||||
|
||||
|
||||
class LoadPackagesFromCacheWorker(QtCore.QThread):
|
||||
""" A subthread worker that loads package information from its cache file. """
|
||||
"""A subthread worker that loads package information from its cache file."""
|
||||
|
||||
addon_repo = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, cache_file: str):
|
||||
@@ -424,12 +421,12 @@ class LoadPackagesFromCacheWorker(QtCore.QThread):
|
||||
)
|
||||
|
||||
def override_metadata_cache_path(self, path):
|
||||
""" For testing purposes, override the location to fetch the package metadata from. """
|
||||
"""For testing purposes, override the location to fetch the package metadata from."""
|
||||
self.metadata_cache_path = path
|
||||
|
||||
def run(self):
|
||||
""" Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread """
|
||||
"""Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread"""
|
||||
with open(self.cache_file, "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
if data:
|
||||
@@ -456,7 +453,7 @@ class LoadPackagesFromCacheWorker(QtCore.QThread):
|
||||
|
||||
|
||||
class LoadMacrosFromCacheWorker(QtCore.QThread):
|
||||
""" A worker object to load macros from a cache file """
|
||||
"""A worker object to load macros from a cache file"""
|
||||
|
||||
add_macro_signal = QtCore.Signal(object)
|
||||
|
||||
@@ -465,8 +462,8 @@ class LoadMacrosFromCacheWorker(QtCore.QThread):
|
||||
self.cache_file = cache_file
|
||||
|
||||
def run(self):
|
||||
""" Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread """
|
||||
"""Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread"""
|
||||
|
||||
with open(self.cache_file, "r", encoding="utf-8") as f:
|
||||
data = f.read()
|
||||
@@ -491,8 +488,8 @@ class CheckSingleUpdateWorker(QtCore.QObject):
|
||||
self.repo = repo
|
||||
|
||||
def do_work(self):
|
||||
""" Use the UpdateChecker class to do the work of this function, depending on the
|
||||
type of Addon """
|
||||
"""Use the UpdateChecker class to do the work of this function, depending on the
|
||||
type of Addon"""
|
||||
|
||||
checker = UpdateChecker()
|
||||
if self.repo.repo_type == Addon.Kind.WORKBENCH:
|
||||
@@ -520,8 +517,8 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
|
||||
self.moddir = os.path.join(self.basedir, "Mod")
|
||||
|
||||
def run(self):
|
||||
""" Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread """
|
||||
"""Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread"""
|
||||
|
||||
self.current_thread = QtCore.QThread.currentThread()
|
||||
checker = UpdateChecker()
|
||||
@@ -544,22 +541,22 @@ class CheckWorkbenchesForUpdatesWorker(QtCore.QThread):
|
||||
|
||||
|
||||
class UpdateChecker:
|
||||
""" A utility class used by the CheckWorkbenchesForUpdatesWorker class. Each function is
|
||||
"""A utility class used by the CheckWorkbenchesForUpdatesWorker class. Each function is
|
||||
designed for a specific Addon type, and modifies the passed-in Addon with the determined
|
||||
update status. """
|
||||
update status."""
|
||||
|
||||
def __init__(self):
|
||||
self.basedir = FreeCAD.getUserAppDataDir()
|
||||
self.moddir = os.path.join(self.basedir, "Mod")
|
||||
|
||||
def override_mod_directory(self, moddir):
|
||||
""" Primarily for use when testing, sets an alternate directory to use for mods """
|
||||
"""Primarily for use when testing, sets an alternate directory to use for mods"""
|
||||
self.moddir = moddir
|
||||
|
||||
def check_workbench(self, wb):
|
||||
""" Given a workbench Addon wb, check it for updates using git. If git is not
|
||||
available, does nothing. """
|
||||
if not have_git or NOGIT:
|
||||
"""Given a workbench Addon wb, check it for updates using git. If git is not
|
||||
available, does nothing."""
|
||||
if not git_manager:
|
||||
wb.set_status(Addon.Status.CANNOT_CHECK)
|
||||
return
|
||||
clonedir = os.path.join(self.moddir, wb.name)
|
||||
@@ -567,21 +564,17 @@ class UpdateChecker:
|
||||
# mark as already installed AND already checked for updates
|
||||
if not os.path.exists(os.path.join(clonedir, ".git")):
|
||||
with wb.git_lock:
|
||||
utils.repair_git_repo(wb.url, clonedir)
|
||||
git_manager.repair(wb.url, clonedir)
|
||||
with wb.git_lock:
|
||||
gitrepo = git.Repo(clonedir)
|
||||
try:
|
||||
if gitrepo.head.is_detached:
|
||||
status = git_manager.status(clonedir)
|
||||
if "(no branch)" in git_manager.status(clonedir):
|
||||
# By definition, in a detached-head state we cannot
|
||||
# update, so don't even bother checking.
|
||||
wb.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
|
||||
if hasattr(gitrepo.head, "ref"):
|
||||
wb.branch = gitrepo.head.ref.name
|
||||
else:
|
||||
wb.branch = gitrepo.head.name
|
||||
wb.branch = git_manager.current_branch(clonedir)
|
||||
return
|
||||
gitrepo.git.fetch()
|
||||
except git.exc.GitError as e:
|
||||
except GitFailed as e:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"AddonManager: "
|
||||
+ translate(
|
||||
@@ -594,11 +587,11 @@ class UpdateChecker:
|
||||
wb.set_status(Addon.Status.CANNOT_CHECK)
|
||||
else:
|
||||
try:
|
||||
if "git pull" in gitrepo.git.status():
|
||||
if git_manager.update_available(clonedir):
|
||||
wb.set_status(Addon.Status.UPDATE_AVAILABLE)
|
||||
else:
|
||||
wb.set_status(Addon.Status.NO_UPDATE_AVAILABLE)
|
||||
except git.exc.GitError:
|
||||
except GitFailed:
|
||||
FreeCAD.Console.PrintWarning(
|
||||
translate(
|
||||
"AddonsInstaller", "git status failed for {}"
|
||||
@@ -608,7 +601,7 @@ class UpdateChecker:
|
||||
wb.set_status(Addon.Status.CANNOT_CHECK)
|
||||
|
||||
def check_package(self, package: Addon) -> None:
|
||||
""" Given a packaged Addon package, check it for updates. If git is available that is
|
||||
"""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."""
|
||||
|
||||
@@ -616,7 +609,7 @@ class UpdateChecker:
|
||||
if os.path.exists(clonedir):
|
||||
|
||||
# First, try to just do a git-based update, which will give the most accurate results:
|
||||
if have_git and not NOGIT:
|
||||
if git_manager:
|
||||
self.check_workbench(package)
|
||||
if package.status() != Addon.Status.CANNOT_CHECK:
|
||||
# It worked, just exit now
|
||||
@@ -653,7 +646,7 @@ class UpdateChecker:
|
||||
package.set_status(Addon.Status.CANNOT_CHECK)
|
||||
|
||||
def check_macro(self, macro_wrapper: Addon) -> None:
|
||||
""" Check to see if the online copy of the macro's code differs from the local copy. """
|
||||
"""Check to see if the online copy of the macro's code differs from the local copy."""
|
||||
|
||||
# Make sure this macro has its code downloaded:
|
||||
try:
|
||||
@@ -724,8 +717,8 @@ class CacheMacroCodeWorker(QtCore.QThread):
|
||||
self.repo_queue = None
|
||||
|
||||
def run(self):
|
||||
""" Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread """
|
||||
"""Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread"""
|
||||
|
||||
self.status_message.emit(translate("AddonsInstaller", "Caching macro code..."))
|
||||
|
||||
@@ -769,8 +762,8 @@ class CacheMacroCodeWorker(QtCore.QThread):
|
||||
)
|
||||
|
||||
def _process_queue(self, num_macros) -> bool:
|
||||
""" Spools up six network connections and downloads the macro code. Returns True if
|
||||
it was interrupted by user request, or False if it ran to completion. """
|
||||
"""Spools up six network connections and downloads the macro code. Returns True if
|
||||
it was interrupted by user request, or False if it ran to completion."""
|
||||
|
||||
# Emulate QNetworkAccessManager and spool up six connections:
|
||||
for _ in range(6):
|
||||
@@ -800,7 +793,7 @@ class CacheMacroCodeWorker(QtCore.QThread):
|
||||
return False
|
||||
|
||||
def update_and_advance(self, repo: Addon) -> None:
|
||||
""" Emit the updated signal and launch the next item from the queue. """
|
||||
"""Emit the updated signal and launch the next item from the queue."""
|
||||
if repo is not None:
|
||||
if repo.macro.name not in self.failed:
|
||||
self.update_macro.emit(repo)
|
||||
@@ -835,7 +828,7 @@ class CacheMacroCodeWorker(QtCore.QThread):
|
||||
pass
|
||||
|
||||
def terminate(self, worker) -> None:
|
||||
""" Shut down all running workers and exit the thread """
|
||||
"""Shut down all running workers and exit the thread"""
|
||||
if not worker.isFinished():
|
||||
macro_name = worker.macro.name
|
||||
FreeCAD.Console.PrintWarning(
|
||||
@@ -871,8 +864,8 @@ class GetMacroDetailsWorker(QtCore.QThread):
|
||||
self.macro = repo.macro
|
||||
|
||||
def run(self):
|
||||
""" Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread """
|
||||
"""Rarely called directly: create an instance and call start() on it instead to
|
||||
launch in a new thread"""
|
||||
|
||||
self.status_message.emit(
|
||||
translate("AddonsInstaller", "Retrieving macro description...")
|
||||
|
||||
Reference in New Issue
Block a user