From b6da08ef8b29d43fccc5d512f62a12e505fccb18 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Tue, 16 Aug 2022 17:47:21 -0500 Subject: [PATCH] Addon Manager: Create new git handling mechanism --- .../AddonManagerTest/app/test_git.py | 138 +++++++++++++ .../AddonManagerTest/data/test_repo.zip | Bin 0 -> 22966 bytes src/Mod/AddonManager/CMakeLists.txt | 3 + src/Mod/AddonManager/TestAddonManagerApp.py | 4 + src/Mod/AddonManager/addonmanager_git.py | 191 ++++++++++++++++++ 5 files changed, 336 insertions(+) create mode 100644 src/Mod/AddonManager/AddonManagerTest/app/test_git.py create mode 100644 src/Mod/AddonManager/AddonManagerTest/data/test_repo.zip create mode 100644 src/Mod/AddonManager/addonmanager_git.py diff --git a/src/Mod/AddonManager/AddonManagerTest/app/test_git.py b/src/Mod/AddonManager/AddonManagerTest/app/test_git.py new file mode 100644 index 0000000000..7ad29344ed --- /dev/null +++ b/src/Mod/AddonManager/AddonManagerTest/app/test_git.py @@ -0,0 +1,138 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2022 FreeCAD Project Association * +# * * +# * This file is part of FreeCAD. * +# * * +# * FreeCAD is free software: you can redistribute it and/or modify it * +# * under the terms of the GNU Lesser General Public License as * +# * published by the Free Software Foundation, either version 2.1 of the * +# * License, or (at your option) any later version. * +# * * +# * FreeCAD is distributed in the hope that it will be useful, but * +# * WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * +# * Lesser General Public License for more details. * +# * * +# * You should have received a copy of the GNU Lesser General Public * +# * License along with FreeCAD. If not, see * +# * . * +# * * +# *************************************************************************** + + +import unittest +import os +import shutil +import stat +import tempfile +from zipfile import ZipFile +import FreeCAD + +from typing import Dict + +from addonmanager_git import GitManager, NoGitFound, GitFailed + + +class TestGit(unittest.TestCase): + + MODULE = "test_git" # file name without extension + + def setUp(self): + """Set up the test case: called by the unit test system""" + test_data_dir = os.path.join( + FreeCAD.getHomePath(), "Mod", "AddonManager", "AddonManagerTest", "data" + ) + git_repo_zip = os.path.join(test_data_dir, "test_repo.zip") + self.test_dir = os.path.join( + tempfile.gettempdir(), "FreeCADTesting", "AddonManagerTests", "Git" + ) + 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 not os.path.exists(git_repo_zip): + self.skipTest("Can't find test repo") + return + + with ZipFile(git_repo_zip, "r") as zip_repo: + zip_repo.extractall(self.test_repo_remote) + self.test_repo_remote = os.path.join(self.test_repo_remote, "test_repo") + + try: + self.git = GitManager() + except NoGitFound: + self.skipTest("No git found") + + def tearDown(self): + """Clean up after the test""" + # self._rmdir(self.test_dir) + + 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"))) + + def test_checkout(self): + """Test git checkout""" + checkout_dir = self._clone_test_repo() + + self.git.checkout(checkout_dir, "HEAD~1") + status = self.git.status(checkout_dir).strip() + expected_status = "## HEAD (no branch)" + self.assertEqual(status, expected_status) + + def test_update(self): + """Test using git to update the local repo""" + checkout_dir = self._clone_test_repo() + + self.git.reset(checkout_dir, ["--hard", "HEAD~1"]) + self.assertTrue(self.git.update_available(checkout_dir)) + self.git.update(checkout_dir) + self.assertFalse(self.git.update_available(checkout_dir)) + + def test_tag_and_branch(self): + """Test checking the currently checked-out tag""" + checkout_dir = self._clone_test_repo() + + expected_tag = "TestTag" + self.git.checkout(checkout_dir, expected_tag) + found_tag = self.git.current_tag(checkout_dir) + self.assertEqual(found_tag, expected_tag) + self.assertFalse(self.git.update_available(checkout_dir)) + + expected_branch = "TestBranch" + 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)) + + expected_branch = "master" + 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)) + + def _rmdir(self, path): + try: + shutil.rmtree(path, onerror=self._remove_readonly) + except Exception as e: + print(e) + + def _remove_readonly(self, func, path, _) -> None: + """Remove a read-only file.""" + + os.chmod(path, stat.S_IWRITE) + func(path) + + def _clone_test_repo(self) -> str: + checkout_dir = os.path.join(self.test_dir, "test_repo") + try: + # Git won't clone to an existing directory, so make sure to remove it first + if os.path.exists(checkout_dir): + self._rmdir(checkout_dir) + self.git.clone(self.test_repo_remote, checkout_dir) + except GitFailed as e: + self.fail(str(e)) + return checkout_dir diff --git a/src/Mod/AddonManager/AddonManagerTest/data/test_repo.zip b/src/Mod/AddonManager/AddonManagerTest/data/test_repo.zip new file mode 100644 index 0000000000000000000000000000000000000000..c35876021c3c6af263a049652b648a3be2540de2 GIT binary patch literal 22966 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. * +# * * +# *************************************************************************** + +""" Wrapper around git executable to simplify calling git commands from Python. """ + +# pylint: disable=too-few-public-methods + +import os +import platform +import shutil +import subprocess +from typing import List +import FreeCAD + + +class NoGitFound(RuntimeError): + """Could not locate the git executable on this system.""" + + +class GitFailed(RuntimeError): + """The call to git returned an error of some kind""" + + +class GitManager: + """A class to manage access to git: mostly just provides a simple wrapper around the basic + command-line calls. Provides optional asynchronous access to clone and update.""" + + def __init__(self): + self.git_exe = None + self._find_git() + if not self.git_exe: + raise NoGitFound() + + def clone(self, remote, local_path, args: List[str] = None): + """Clones the remote to the local path""" + final_args = ["clone", "--recurse-submodules"] + if args: + final_args.extend(args) + final_args.extend([remote, local_path]) + self._synchronous_call_git(final_args) + + def async_clone(self, remote, local_path, progress_monitor, args: List[str] = None): + """Clones the remote to the local path, sending periodic progress updates + to the passed progress_monitor. Returns a handle that can be used to + cancel the job.""" + + def checkout(self, local_path, spec, args: List[str] = None): + """Checks out a specific git revision, tag, or branch. Any valid argument to + git checkout can be submitted.""" + old_dir = os.getcwd() + os.chdir(local_path) + final_args = ["checkout"] + if args: + final_args.extend(args) + final_args.append(spec) + self._synchronous_call_git(final_args) + os.chdir(old_dir) + + def update(self, local_path): + """Fetches and pulls the local_path from its remote""" + 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"]) + 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"]) + os.chdir(old_dir) + return status + + def reset(self, local_path, args: List[str] = None): + """Executes the git reset command""" + old_dir = os.getcwd() + os.chdir(local_path) + final_args = ["reset"] + if args: + final_args.extend(args) + self._synchronous_call_git(final_args) + os.chdir(old_dir) + + def async_fetch_and_update(self, local_path, progress_monitor, args=None): + """Same as fetch_and_update, but asynchronous""" + + def update_available(self, local_path) -> bool: + """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"]) + os.chdir(old_dir) + return "behind" in status + + def current_tag(self, local_path) -> str: + """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() + os.chdir(old_dir) + return tag + + def current_branch(self, local_path) -> str: + """Get the name of the current branch""" + old_dir = os.getcwd() + os.chdir(local_path) + branch = self._synchronous_call_git(["branch", "--show-current"]).strip() + os.chdir(old_dir) + return branch + + def repair(self, remote, local_path): + """Assumes that local_path is supposed to be a local clone of the given remote, and + 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) + try: + self.clone(remote, local_path) + except GitFailed as e: + shutil.rmtree(local_path) + os.rename(old_path, local_path) + raise e + + def _find_git(self): + # Find git. In preference order + # A) The value of the GitExecutable user preference + # B) The executable located in the same bin directory as FreeCAD and called "git" + # C) The result of an shutil search for your system's "git" executable + prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons") + git_exe = prefs.GetString("GitExecutable", "Not set") + if not git_exe or git_exe == "Not set" or not os.path.exists(git_exe): + fc_dir = FreeCAD.getHomePath() + git_exe = os.path.join(fc_dir, "bin", "git") + if "Windows" in platform.system(): + git_exe += ".exe" + + if not git_exe or not os.path.exists(git_exe): + git_exe = shutil.which("git") + + if not git_exe or not os.path.exists(git_exe): + return + + prefs.SetString("GitExecutable", git_exe) + self.git_exe = git_exe + + def _synchronous_call_git(self, args: List[str]) -> str: + """Calls git and returns its output.""" + final_args = [self.git_exe] + final_args.extend(args) + try: + proc = subprocess.run( + final_args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + check=True, + ) + except subprocess.CalledProcessError as e: + raise GitFailed(str(e)) from e + + if proc.returncode != 0: + raise GitFailed( + f"Git returned a non-zero exit status: {proc.returncode}\n" + + f"Called with: {' '.join(final_args)}\n\n" + + f"Returned stderr:\n{proc.stderr.decode()}" + ) + + return proc.stdout.decode()