From b5bc03e71190e576637f3d100088f5c3c146b7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Wed, 24 Aug 2016 19:02:39 +0200 Subject: [PATCH 1/9] First throw at release channels We start out with master (Stable), rc/maintenance (Maintenance) and rc/devel (Devel) --- .versioneer-lookup | 4 +- CONTRIBUTING.md | 26 +-- .../plugins/softwareupdate/__init__.py | 70 +++++++- .../scripts/update-octoprint.py | 34 +++- .../static/js/softwareupdate.js | 18 ++- .../templates/softwareupdate_settings.jinja2 | 6 + .../softwareupdate/updaters/update_script.py | 14 +- .../version_checks/github_release.py | 152 ++++++++++++++---- 8 files changed, 263 insertions(+), 61 deletions(-) diff --git a/.versioneer-lookup b/.versioneer-lookup index 5a20c7e4..ed486827 100644 --- a/.versioneer-lookup +++ b/.versioneer-lookup @@ -7,10 +7,10 @@ # The file is processed from top to bottom, the first matching line wins. If or are left out, # the lookup table does not apply to the matched branches -# master, prerelease and rc shall not use the lookup table, only tags +# master and rc shall not use the lookup table, only tags master +rc/.* prerelease -rc # neither should disconnected checkouts, e.g. 'git checkout ' HEAD diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23984c22..0d466a21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -299,14 +299,9 @@ There are three main branches in OctoPrint: * `master`: The master branch always contains the current stable release. It is *only* updated on new releases. Will have a version number following - the scheme `x.y.z` (e.g. `1.2.9`) or - if it's absolutely necessary to - add a commit after release to this branch - `x.y.z.post` + the scheme `..` (e.g. `1.2.9`) or - if it's absolutely necessary to + add a commit after release to this branch - `...post` (e.g. `1.2.9.post1`). - * `prerelease`: This branch is only used during the short period where a - future release has "graduated" from the `maintenance` branch and is already - tagged, but still marked on Github as a pre-release. This is mostly used for - update testing just before new releases. Version number follows the scheme - `x.y.z` (e.g. `1.2.9`), just like the `master` branch. * `maintenance`: Improvements and fixes of the current release that make up the next release go here. More or less continously updated. You can consider this a preview of the next release version. It should be very stable at all @@ -314,7 +309,7 @@ There are three main branches in OctoPrint: next stable release, so if you want to help out development, running the `maintenance` branch and reporting back anything you find is a very good way to do that. Will usually have a version number following the scheme - `x.y.z+1.dev.` for an OctoPrint version of `x.y.z` + `...dev` for an OctoPrint version of `..` (e.g. `1.2.10.dev12`). * `devel`: Ongoing development of new features that will go into the next bigger release (MINOR version number increases) will happen on this branch. Usually @@ -322,8 +317,16 @@ There are three main branches in OctoPrint: temporarily. Can be considered the "bleeding edge". All PRs should target *this* branch. Important improvements and fixes from PRs here are backported to `maintenance` as needed. Will usually have a version number following the - scheme `x.y+1.0.dev` for an OctoPrint version - of `x.y.z` (e.g. `1.3.0.dev123`). + scheme `..0.dev` for a current OctoPrint version + of `..` (e.g. `1.3.0.dev123`). + * `rc/maintenance`: This branch is reserved for future releases that have graduated from + the `maintenance` branch and are now being pushed on the "Maintenance" + pre release channel for further testing. Version number follows the scheme + `..rc` (e.g. `1.2.9rc1`). + * `rc/devel`: This branch is reserved for future releases that have graduated from + the `devel` branch and are now being pushed on the "Devel" pre release channel + for further testing. Version number follows the scheme `..0rc` (e.g. `1.3.0rc1`) + for a current stable OctoPrint version of `..`. Additionally, from time to time you might see other branches pop up in the repository. Those usually have one of the following prefixes: @@ -334,9 +337,6 @@ Those usually have one of the following prefixes: `maintenance` and `devel` branches. * `dev/...` or `feature/...`: New functionality under development that is to be merged into the `devel` branch. - * `rc`: A branch similar in nature to the `prerelease` branch, only that it will be - used to provide current release candidates of the next stable version to be derived - from the `devel` branch. There is also the `gh-pages` branch, which holds OctoPrint's web page, and a couple of older development branches that are slowly being migrated or deleted. diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 4a1e1c95..c1d1d820 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -18,7 +18,7 @@ from . import version_checks, updaters, exceptions, util from octoprint.server.util.flask import restricted_access -from octoprint.server import admin_permission, VERSION, REVISION +from octoprint.server import admin_permission, VERSION, REVISION, BRANCH from octoprint.util import dict_merge import octoprint.settings @@ -138,19 +138,26 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, #~~ SettingsPlugin API def get_settings_defaults(self): + update_script = os.path.join(self._basefolder, "scripts", "update-octoprint.py") return { "checks": { "octoprint": { "type": "github_release", "user": "foosel", "repo": "OctoPrint", - "update_script": "{{python}} \"{update_script}\" --python=\"{{python}}\" \"{{folder}}\" {{target}}".format(update_script=os.path.join(self._basefolder, "scripts", "update-octoprint.py")), + "update_script": "{{python}} \"{update_script}\" --branch={{branch}} --force={{force}} \"{{folder}}\" {{target}}".format(update_script=update_script), "restart": "octoprint" }, }, "pip_command": None, "check_providers": {}, + "octoprint_stable_branch": dict(branch="master", name="Stable"), + "octoprint_prerelease_branches": [ + dict(branch="rc/maintenance", name="Maintenance"), + dict(branch="rc/devel", name="Devel") + ], + "cache_ttl": 24 * 60, } @@ -159,6 +166,19 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, if "checks" in data: del data["checks"] + branch_mappings = [] + if "octoprint_stable_branch" in data: + branch_mappings.append(data["octoprint_stable_branch"]) + del data["octoprint_stable_branch"] + if "octoprint_prerelease_branches" in data: + for mapping in data["octoprint_prerelease_branches"]: + branch_mappings.append(mapping) + del data["octoprint_prerelease_branches"] + data["octoprint_branch_mappings"] = branch_mappings + + if "check_providers" in data: + del data["check_providers"] + checks = self._get_configured_checks() if "octoprint" in checks: if "checkout_folder" in checks["octoprint"]: @@ -168,6 +188,12 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, else: data["octoprint_checkout_folder"] = None data["octoprint_type"] = checks["octoprint"].get("type", None) + + data["octoprint_release_channel"] = self._settings.get(["octoprint_stable_branch", "branch"]) + if checks["octoprint"].get("prerelease", False): + channel = checks["octoprint"].get("prerelease_channel", BRANCH) + if channel in [x["branch"] for x in self._settings.get(["octoprint_prerelease_branches"])]: + data["octoprint_release_channel"] = channel else: data["octoprint_checkout_folder"] = None data["octoprint_type"] = None @@ -191,6 +217,11 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, update_type = check.get("type", None) checkout_folder = check.get("checkout_folder", None) update_folder = check.get("update_folder", None) + prerelease = check.get("prerelease", False) + prerelease_channel = check.get("prerelease_channel", None) + else: + update_type = checkout_folder = update_folder = prerelease_channel = None + prerelease = False defaults = dict( plugins=dict(softwareupdate=dict( @@ -198,7 +229,9 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, octoprint=dict( type=update_type, checkout_folder=checkout_folder, - update_folder=update_folder + update_folder=update_folder, + prerelease=prerelease, + prerelease_channel=prerelease_channel ) ) )) @@ -214,6 +247,16 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, self._settings.set(["checks", "octoprint", "type"], data["octoprint_type"], defaults=defaults, force=True) self._refresh_configured_checks = True + if "octoprint_release_channel" in data: + if data["octoprint_release_channel"] in [x["branch"] for x in self._settings.get(["octoprint_prerelease_branches"])]: + self._settings.set(["checks", "octoprint", "prerelease"], True, defaults=defaults, force=True) + self._settings.set(["checks", "octoprint", "prerelease_channel"], data["octoprint_release_channel"], defaults=defaults, force=True) + self._refresh_configured_checks = True + else: + self._settings.set(["checks", "octoprint", "prerelease"], False, defaults=defaults, force=True) + self._settings.set(["checks", "octoprint", "prerelease_channel"], None, defaults=defaults, force=True) + self._refresh_configured_checks = True + def get_settings_version(self): return 4 @@ -704,10 +747,29 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, result["current"] = REVISION if REVISION else "unknown" else: result["current"] = VERSION + + if check["type"] == "github_release" and (check["prerelease"] or BRANCH != self._settings.get(["octoprint_stable_branch", "branch"])): + # we force python unequality check here because that will also allow us to + # downgrade on a prerelease channel change + result["release_compare"] = "python_unequal" + + # also we compare versions fully, not just the base so that we see a difference + # between RCs for the same version release + result["force_base"] = False + + if check.get("update_script", None): + channel = result["prerelease_channel"] = check.get("prerelease_channel", BRANCH) + if channel: + # if we have a release channel, we also set our update_branch here to our release channel + result["update_branch"] = check.get("update_branch", channel) + + # we also force our target version in the update + result["force_exact_version"] = True + else: result["displayName"] = check.get("displayName", target) result["displayVersion"] = check.get("displayVersion", check.get("current", "unknown")) - if check["type"] in ("github_commit"): + if check["type"] in ("github_commit",): result["current"] = check.get("current", None) else: result["current"] = check.get("current", check.get("displayVersion", None)) diff --git a/src/octoprint/plugins/softwareupdate/scripts/update-octoprint.py b/src/octoprint/plugins/softwareupdate/scripts/update-octoprint.py index 7ccef16d..1934a205 100644 --- a/src/octoprint/plugins/softwareupdate/scripts/update-octoprint.py +++ b/src/octoprint/plugins/softwareupdate/scripts/update-octoprint.py @@ -71,7 +71,7 @@ def _python(args, cwd, python_executable, sudo=False): return p.returncode, stdout -def update_source(git_executable, folder, target, force=False): +def _rescue_changes(git_executable, folder): print(">>> Running: git diff --shortstat") returncode, stdout = _git(["diff", "--shortstat"], folder, git_executable=git_executable) if returncode != 0: @@ -91,11 +91,30 @@ def update_source(git_executable, folder, target, force=False): with open(patch, "wb") as f: f.write(stdout) + return True + + return False + + +def update_source(git_executable, folder, target, force=False, branch=None): + if _rescue_changes(git_executable, folder): print(">>> Running: git reset --hard") returncode, stdout = _git(["reset", "--hard"], folder, git_executable=git_executable) if returncode != 0: raise RuntimeError("Could not update, \"git reset --hard\" failed with returncode %d: %s" % (returncode, stdout)) + print(">>> Running: git fetch") + returncode, stdout = _git(["fetch"], folder, git_executable=git_executable) + if returncode != 0: + raise RuntimeError("Could not update, \"git fetch\" failed with returncode %d: %s" % (returncode, stdout)) + print(stdout) + + if branch is not None and branch.strip() != "": + print(">>> Running: git checkout {}".format(branch)) + returncode, stdout = _git(["checkout", branch], folder, git_executable=git_executable) + if returncode != 0: + raise RuntimeError("Could not update, \"git checkout\" failed with returncode %d: %s" % (returncode, stdout)) + print(">>> Running: git pull") returncode, stdout = _git(["pull"], folder, git_executable=git_executable) if returncode != 0: @@ -134,18 +153,24 @@ def install_source(python_executable, folder, user=False, sudo=False): def parse_arguments(): import argparse + boolean_trues = ["true", "yes", "1"] + boolean_falses = ["false", "no", "0"] + parser = argparse.ArgumentParser(prog="update-octoprint.py") parser.add_argument("--git", action="store", type=str, dest="git_executable", help="Specify git executable to use") parser.add_argument("--python", action="store", type=str, dest="python_executable", help="Specify python executable to use") - parser.add_argument("--force", action="store_true", dest="force", - help="Set this to force the update to only the specified version (nothing newer)") + parser.add_argument("--force", action="store", type=lambda x: x in boolean_trues, + dest="force", default=False, + help="Set this to true to force the update to only the specified version (nothing newer, nothing older)") parser.add_argument("--sudo", action="store_true", dest="sudo", help="Install with sudo") parser.add_argument("--user", action="store_true", dest="user", help="Install to the user site directory instead of the general site directory") + parser.add_argument("--branch", action="store", type=str, dest="branch", default=None, + help="Specify the branch to make sure is checked out") parser.add_argument("folder", type=str, help="Specify the base folder of the OctoPrint installation to update") parser.add_argument("target", type=str, @@ -167,13 +192,12 @@ def main(): python_executable = args.python_executable folder = args.folder - target = args.target import os if not os.access(folder, os.W_OK): raise RuntimeError("Could not update, base folder is not writable") - update_source(git_executable, folder, target, force=args.force) + update_source(git_executable, folder, args.target, force=args.force, branch=args.branch) install_source(python_executable, folder, user=args.user, sudo=args.sudo) if __name__ == "__main__": diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index 58c6e227..ef0cc9c1 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -21,6 +21,7 @@ $(function() { self.config_cacheTtl = ko.observable(); self.config_checkoutFolder = ko.observable(); self.config_checkType = ko.observable(); + self.config_releaseChannel = ko.observable(); self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog"); self.confirmationDialog = $("#softwareupdate_confirmation_dialog"); @@ -29,6 +30,7 @@ $(function() { {"key": "github_release", "name": gettext("Release")}, {"key": "git_commit", "name": gettext("Commit")} ]; + self.config_availableReleaseChannels = ko.observableArray([]); self.reloadOverlay = $("#reloadui_overlay"); @@ -97,7 +99,8 @@ $(function() { softwareupdate: { cache_ttl: parseInt(self.config_cacheTtl()), octoprint_checkout_folder: self.config_checkoutFolder(), - octoprint_type: self.config_checkType() + octoprint_type: self.config_checkType(), + octoprint_release_channel: self.config_releaseChannel() } } }; @@ -112,6 +115,13 @@ $(function() { self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); self.config_checkoutFolder(self.settings.settings.plugins.softwareupdate.octoprint_checkout_folder()); self.config_checkType(self.settings.settings.plugins.softwareupdate.octoprint_type()); + self.config_releaseChannel(self.settings.settings.plugins.softwareupdate.octoprint_release_channel()); + + var availableReleaseChannels = []; + _.each(self.settings.settings.plugins.softwareupdate.octoprint_branch_mappings(), function(mapping) { + availableReleaseChannels.push({"key": mapping.branch, "name": gettext(mapping.name || mapping.branch)}); + }); + self.config_availableReleaseChannels(availableReleaseChannels); }; self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) { @@ -145,7 +155,7 @@ $(function() { var octoprint = data.information["octoprint"]; if (octoprint && octoprint.hasOwnProperty("check")) { var check = octoprint.check; - if (BRANCH != "master" && check["type"] == "github_release") { + if (check["release_branches"] && !_.contains(check["release_branches"], BRANCH) && check["type"] == "github_release") { self.octoprintUnreleased(true); } else { self.octoprintUnreleased(false); @@ -153,8 +163,8 @@ $(function() { var checkoutFolder = (check["checkout_folder"] || "").trim(); var updateFolder = (check["update_folder"] || "").trim(); - var checkType = check["type"] || ""; - if ((checkType == "github_release" || checkType == "git_commit") && checkoutFolder == "" && updateFolder == "") { + var needsFolder = check["update_script"] || false; + if (needsFolder && checkoutFolder == "" && updateFolder == "") { self.octoprintUnconfigured(true); } else { self.octoprintUnconfigured(false); diff --git a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 index 251c1e86..3c3f18bf 100644 --- a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 +++ b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 @@ -86,6 +86,12 @@ +
+ +
+ +
+
diff --git a/src/octoprint/plugins/softwareupdate/updaters/update_script.py b/src/octoprint/plugins/softwareupdate/updaters/update_script.py index db3a114b..050b1968 100644 --- a/src/octoprint/plugins/softwareupdate/updaters/update_script.py +++ b/src/octoprint/plugins/softwareupdate/updaters/update_script.py @@ -33,9 +33,11 @@ def perform_update(target, check, target_version): raise ConfigurationInvalid("checkout_folder and update_folder are missing for update target %s, one is needed" % target) update_script = check["update_script"] - folder = check["update_folder"] if "update_folder" in check else check["checkout_folder"] - pre_update_script = check["pre_update_script"] if "pre_update_script" in check else None - post_update_script = check["post_update_script"] if "post_update_script" in check else None + update_branch = check.get("update_branch", "") + force_exact_version = check.get("force_exact_version", False) + folder = check.get("update_folder", check["checkout_folder"]) + pre_update_script = check.get("pre_update_script", None) + post_update_script = check.get("post_update_script", None) update_stdout = "" update_stderr = "" @@ -56,7 +58,11 @@ def perform_update(target, check, target_version): ### update try: - update_command = update_script.format(python=sys.executable, folder=folder, target=target_version) + update_command = update_script.format(python=sys.executable, + folder=folder, + target=target_version, + branch=update_branch, + force="true" if force_exact_version else "false") logger.debug("Target %s, running update script: %s" % (target, update_command)) returncode, stdout, stderr = execute(update_command, cwd=folder) diff --git a/src/octoprint/plugins/softwareupdate/version_checks/github_release.py b/src/octoprint/plugins/softwareupdate/version_checks/github_release.py index 6960d2e6..a16f89b6 100644 --- a/src/octoprint/plugins/softwareupdate/version_checks/github_release.py +++ b/src/octoprint/plugins/softwareupdate/version_checks/github_release.py @@ -14,7 +14,60 @@ RELEASE_URL = "https://api.github.com/repos/{user}/{repo}/releases" logger = logging.getLogger("octoprint.plugins.softwareupdate.version_checks.github_release") -def _get_latest_release(user, repo, include_prerelease=False): +def _filter_out_latest(releases, include_prerelease=False, prerelease_channel=None): + """ + Filters out the newest of all matching releases. + + Tests: + + >>> release_1_2_15 = dict(name="1.2.15", tag_name="1.2.15", html_url="some_url", published_at="2016-07-29T19:53:29Z", prerelease=False, draft=False, target_commitish="prerelease") + >>> release_1_2_16rc1 = dict(name="1.2.16rc1", tag_name="1.2.16rc1", html_url="some_url", published_at="2016-08-29T12:00:00Z", prerelease=True, draft=False, target_commitish="rc/maintenance") + >>> release_1_2_16rc2 = dict(name="1.2.16rc2", tag_name="1.2.16rc2", html_url="some_url", published_at="2016-08-30T12:00:00Z", prerelease=True, draft=False, target_commitish="rc/maintenance") + >>> release_1_2_17rc1 = dict(name="1.2.17rc1", tag_name="1.2.17rc1", html_url="some_url", published_at="2016-08-31T12:00:00Z", prerelease=True, draft=True, target_commitish="rc/maintenance") + >>> release_1_3_0rc1 = dict(name="1.3.0rc1", tag_name="1.3.0rc1", html_url="some_url", published_at="2016-12-12T12:00:00Z", prerelease=True, draft=False, target_commitish="rc/devel") + >>> release_1_4_0rc1 = dict(name="1.4.0rc1", tag_name="1.4.0rc1", html_url="some_url", published_at="2017-12-12T12:00:00Z", prerelease=True, draft=False, target_commitish="rc/future") + >>> releases = [release_1_2_15, release_1_2_16rc1, release_1_2_16rc2, release_1_2_17rc1, release_1_3_0rc1, release_1_4_0rc1] + >>> _filter_out_latest(releases, include_prerelease=False, prerelease_channel=None) + ('1.2.15', '1.2.15', 'some_url') + >>> _filter_out_latest(releases, include_prerelease=True, prerelease_channel="rc/maintenance") + ('1.2.16rc2', '1.2.16rc2', 'some_url') + >>> _filter_out_latest(releases, include_prerelease=True, prerelease_channel="rc/devel") + ('1.3.0rc1', '1.3.0rc1', 'some_url') + >>> _filter_out_latest(releases, include_prerelease=True, prerelease_channel=None) + ('1.4.0rc1', '1.4.0rc1', 'some_url') + >>> _filter_out_latest(releases, include_prerelease=True, prerelease_channel="rc/doesntexist") + ('1.2.15', '1.2.15', 'some_url') + >>> _filter_out_latest([release_1_2_17rc1]) + (None, None, None) + >>> _filter_out_latest([release_1_2_16rc1, release_1_2_16rc2]) + (None, None, None) + """ + + nothing = None, None, None + + # filter out prereleases and drafts + filter_function = lambda rel: not rel["prerelease"] and not rel["draft"] + if include_prerelease: + if prerelease_channel: + filter_function = lambda rel: not rel["draft"] and ( + not rel["prerelease"] or rel["target_commitish"] == prerelease_channel) + else: + filter_function = lambda rel: not rel["draft"] + + releases = filter(filter_function, releases) + if not releases: + return nothing + + # sort by date + releases = sorted(releases, key=lambda release: release.get("published_at", None)) + + # latest release = last in list + latest = releases[-1] + + return latest["name"], latest["tag_name"], latest.get("html_url", None) + + +def _get_latest_release(user, repo, include_prerelease=False, prerelease_channel=None): nothing = None, None, None r = requests.get(RELEASE_URL.format(user=user, repo=repo)) @@ -27,36 +80,48 @@ def _get_latest_release(user, repo, include_prerelease=False): releases = r.json() # sanitize - required_fields = {"name", "tag_name", "html_url", "draft", "prerelease", "published_at"} + required_fields = {"name", "tag_name", "html_url", "draft", "prerelease", "published_at", "target_commitish"} releases = filter(lambda rel: set(rel.keys()) & required_fields == required_fields, releases) - # filter out prereleases and drafts - if include_prerelease: - releases = filter(lambda rel: not rel["draft"], releases) - else: - releases = filter(lambda rel: not rel["prerelease"] and not rel["draft"], - releases) - - if not releases: - return nothing - - # sort by date - comp = lambda a, b: cmp(a.get("published_at", None), b["published_at"]) - releases = sorted(releases, cmp=comp) - - # latest release = last in list - latest = releases[-1] - - return latest["name"], latest["tag_name"], latest.get("html_url", None) + return _filter_out_latest(releases, include_prerelease=include_prerelease, prerelease_channel=prerelease_channel) def _get_sanitized_version(version_string): + """ + Removes "-..." prefix from version strings. + + Tests: + >>> _get_sanitized_version("1.2.15") + '1.2.15' + >>> _get_sanitized_version("1.2.15-dev12") + '1.2.15' + """ if "-" in version_string: version_string = version_string[:version_string.find("-")] return version_string +def _get_base_from_version_tuple(version_tuple): + """ + Reduces version tuple to base version. + + Tests: + + >>> _get_base_from_version_tuple(("1", "2", "15")) + ('1', '2', '15') + >>> _get_base_from_version_tuple(("1", "2", "15", "*", "dev12")) + ('1', '2', '15') + """ + + base_version = [] + for part in version_tuple: + if part.startswith("*"): + break + base_version.append(part) + return tuple(base_version) + + def _get_comparable_version_pkg_resources(version_string, force_base=True): import pkg_resources @@ -65,12 +130,7 @@ def _get_comparable_version_pkg_resources(version_string, force_base=True): if force_base: if isinstance(version, tuple): # old setuptools - base_version = [] - for part in version: - if part.startswith("*"): - break - base_version.append(part) - version = tuple(base_version) + version = _get_base_from_version_tuple(version) else: # new setuptools version = pkg_resources.parse_version(version.base_version) @@ -91,10 +151,32 @@ def _get_comparable_version_semantic(version_string, force_base=True): def _is_current(release_information, compare_type, custom=None, force_base=True): + """ + Checks if the provided release information indicates the version being the most current one. + + Tests: + + >>> _is_current(dict(remote=dict(value=None)) + True + >>> _is_current(dict(local=dict(value="1.2.15"), remote=dict(value="1.2.16"))) + False + >>> _is_current(dict(local=dict(value="1.2.16dev1"), remote=dict(value="1.2.16dev2"))) + True + >>> _is_current(dict(local=dict(value="1.2.16dev1"), remote=dict(value="1.2.16dev2")), force_base=False) + False + >>> _is_current(dict(local=dict(value="1.2.16dev3"), remote=dict(value="1.2.16dev2")), force_base=False) + True + >>> _is_current(dict(local=dict(value="1.2.16dev3"), remote=dict(value="1.2.16dev2")), force_base=False, compare_type="python_unequal") + False + + """ + if release_information["remote"]["value"] is None: return True - if not compare_type in ("python", "semantic", "unequal", "custom") or compare_type == "custom" and custom is None: + if not compare_type in ("python", "python_unequal", + "semantic", "semantic_unequal", + "unequal", "custom") or compare_type == "custom" and custom is None: compare_type = "python" sanitized_local = _get_sanitized_version(release_information["local"]["value"]) @@ -106,11 +188,21 @@ def _is_current(release_information, compare_type, custom=None, force_base=True) remote_version = _get_comparable_version_pkg_resources(sanitized_remote, force_base=force_base) return local_version >= remote_version + elif compare_type == "python_unequal": + local_version = _get_comparable_version_pkg_resources(sanitized_local, force_base=force_base) + remote_version = _get_comparable_version_pkg_resources(sanitized_remote, force_base=force_base) + return local_version == remote_version + elif compare_type == "semantic": local_version = _get_comparable_version_semantic(sanitized_local, force_base=force_base) remote_version = _get_comparable_version_semantic(sanitized_remote, force_base=force_base) return local_version >= remote_version + elif compare_type == "semantic_unequal": + local_version = _get_comparable_version_semantic(sanitized_local, force_base=force_base) + remote_version = _get_comparable_version_semantic(sanitized_remote, force_base=force_base) + return local_version == remote_version + elif compare_type == "custom": return custom(sanitized_local, sanitized_remote) @@ -127,12 +219,14 @@ def get_latest(target, check, custom_compare=None): current = check.get("current", None) include_prerelease = check.get("prerelease", False) + prerelease_channel = check.get("prerelease_channel", None) force_base = check.get("force_base", True) remote_name, remote_tag, release_notes = _get_latest_release(check["user"], check["repo"], - include_prerelease=include_prerelease) - compare_type = check["release_compare"] if "release_compare" in check else "python" + include_prerelease=include_prerelease, + prerelease_channel=prerelease_channel) + compare_type = check.get("release_compare", "python") information =dict( local=dict(name=current, value=current), From f8386649bf7b4e945045d07b8d7d6dcd8a26b695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 25 Aug 2016 13:32:18 +0200 Subject: [PATCH 2/9] Fix release channel selector --- src/octoprint/plugins/softwareupdate/__init__.py | 4 ++-- .../softwareupdate/static/js/softwareupdate.js | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index c1d1d820..137b9bc4 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -154,8 +154,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, "octoprint_stable_branch": dict(branch="master", name="Stable"), "octoprint_prerelease_branches": [ - dict(branch="rc/maintenance", name="Maintenance"), - dict(branch="rc/devel", name="Devel") + dict(branch="rc/maintenance", name="Maintenance RCs"), + dict(branch="rc/devel", name="Development RCs") ], "cache_ttl": 24 * 60, diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index ef0cc9c1..142ae8f8 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -112,16 +112,18 @@ $(function() { }; self._copyConfig = function() { + var availableReleaseChannels = []; + _.each(self.settings.settings.plugins.softwareupdate.octoprint_branch_mappings(), function(mapping) { + availableReleaseChannels.push({"key": mapping.branch(), "name": gettext(mapping.name() || mapping.branch())}); + }); + self.config_availableReleaseChannels(availableReleaseChannels); + self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); self.config_checkoutFolder(self.settings.settings.plugins.softwareupdate.octoprint_checkout_folder()); self.config_checkType(self.settings.settings.plugins.softwareupdate.octoprint_type()); self.config_releaseChannel(self.settings.settings.plugins.softwareupdate.octoprint_release_channel()); - var availableReleaseChannels = []; - _.each(self.settings.settings.plugins.softwareupdate.octoprint_branch_mappings(), function(mapping) { - availableReleaseChannels.push({"key": mapping.branch, "name": gettext(mapping.name || mapping.branch)}); - }); - self.config_availableReleaseChannels(availableReleaseChannels); + log.info("releaseChannel:", self.config_releaseChannel(), ", availableReleaseChannels:", availableReleaseChannels); }; self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) { From d538b3fd38394b3cb3d45bbdbbbbf5296229c37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 25 Aug 2016 15:45:25 +0200 Subject: [PATCH 3/9] Adjust swu plugin settings to selected update method --- .../plugins/softwareupdate/__init__.py | 52 +++++++++++++++---- .../static/js/softwareupdate.js | 20 ++++--- .../templates/softwareupdate_settings.jinja2 | 6 +-- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/octoprint/plugins/softwareupdate/__init__.py b/src/octoprint/plugins/softwareupdate/__init__.py index 137b9bc4..7b0dcd55 100644 --- a/src/octoprint/plugins/softwareupdate/__init__.py +++ b/src/octoprint/plugins/softwareupdate/__init__.py @@ -189,6 +189,11 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, data["octoprint_checkout_folder"] = None data["octoprint_type"] = checks["octoprint"].get("type", None) + try: + data["octoprint_method"] = self._get_update_method("octoprint", checks["octoprint"]) + except exceptions.UnknownUpdateType: + data["octoprint_method"] = "unknown" + data["octoprint_release_channel"] = self._settings.get(["octoprint_stable_branch", "branch"]) if checks["octoprint"].get("prerelease", False): channel = checks["octoprint"].get("prerelease_channel", BRANCH) @@ -774,6 +779,10 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, else: result["current"] = check.get("current", check.get("displayVersion", None)) + if "pip" in result: + if not "pip_command" in check and self._settings.get(["pip_command"]) is not None: + result["pip_command"] = self._settings.get(["pip_command"]) + return result def _get_version_checker(self, target, check): @@ -799,22 +808,45 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin, else: raise exceptions.UnknownCheckType() + def _get_update_method(self, target, check, valid_methods=None): + """ + Determines the update method for the given target and check. + + If ``valid_methods`` is provided, determine method must be contained + therein to be considered valid. + + Raises an ``UnknownUpdateType`` exception if method cannot be determined + or validated. + """ + + method = None + if "method" in check: + method = check["method"] + else: + if "update_script" in check: + method = "update_script" + elif "pip" in check: + method = "pip" + elif "python_updater" in check: + method = "python_updated" + + if method is None or (valid_methods and not method in valid_methods): + raise exceptions.UnknownUpdateType() + + return method + def _get_updater(self, target, check): """ Retrieves the updater for the given target and check configuration. Will raise an UnknownUpdateType if updater cannot be determined. """ - if "update_script" in check: - return updaters.update_script - elif "pip" in check: - if not "pip_command" in check and self._settings.get(["pip_command"]) is not None: - check["pip_command"] = self._settings.get(["pip_command"]) - return updaters.pip - elif "python_updater" in check: - return updaters.python_updater - else: - raise exceptions.UnknownUpdateType() + mapping = dict(update_script=updaters.update_script, + pip=updaters.pip, + python_updater=updaters.python_updater) + + method = self._get_update_method(target, check, valid_methods=mapping.keys()) + return mapping[method] __plugin_name__ = "Software Update" __plugin_author__ = "Gina Häußge" diff --git a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js index 142ae8f8..4bbc86ae 100644 --- a/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js +++ b/src/octoprint/plugins/softwareupdate/static/js/softwareupdate.js @@ -21,15 +21,13 @@ $(function() { self.config_cacheTtl = ko.observable(); self.config_checkoutFolder = ko.observable(); self.config_checkType = ko.observable(); + self.config_updateMethod = ko.observable(); self.config_releaseChannel = ko.observable(); self.configurationDialog = $("#settings_plugin_softwareupdate_configurationdialog"); self.confirmationDialog = $("#softwareupdate_confirmation_dialog"); - self.config_availableCheckTypes = [ - {"key": "github_release", "name": gettext("Release")}, - {"key": "git_commit", "name": gettext("Commit")} - ]; + self.config_availableCheckTypes = ko.observableArray([]); self.config_availableReleaseChannels = ko.observableArray([]); self.reloadOverlay = $("#reloadui_overlay"); @@ -112,18 +110,28 @@ $(function() { }; self._copyConfig = function() { + var updateMethod = self.settings.settings.plugins.softwareupdate.octoprint_method(); + + var availableCheckTypes = []; + if (updateMethod == "update_script" || updateMethod == "python") { + availableCheckTypes = [{"key": "github_release", "name": gettext("Release")}, + {"key": "git_commit", "name": gettext("Commit")}]; + } else { + availableCheckTypes = []; + } + self.config_availableCheckTypes(availableCheckTypes); + var availableReleaseChannels = []; _.each(self.settings.settings.plugins.softwareupdate.octoprint_branch_mappings(), function(mapping) { availableReleaseChannels.push({"key": mapping.branch(), "name": gettext(mapping.name() || mapping.branch())}); }); self.config_availableReleaseChannels(availableReleaseChannels); + self.config_updateMethod(updateMethod); self.config_cacheTtl(self.settings.settings.plugins.softwareupdate.cache_ttl()); self.config_checkoutFolder(self.settings.settings.plugins.softwareupdate.octoprint_checkout_folder()); self.config_checkType(self.settings.settings.plugins.softwareupdate.octoprint_type()); self.config_releaseChannel(self.settings.settings.plugins.softwareupdate.octoprint_release_channel()); - - log.info("releaseChannel:", self.config_releaseChannel(), ", availableReleaseChannels:", availableReleaseChannels); }; self.fromCheckResponse = function(data, ignoreSeen, showIfNothingNew) { diff --git a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 index 3c3f18bf..fc90e531 100644 --- a/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 +++ b/src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2 @@ -74,19 +74,19 @@