Merge branch 'maintenance' into devel

Conflicts:
	src/octoprint/plugins/softwareupdate/templates/softwareupdate_settings.jinja2
	src/octoprint/plugins/softwareupdate/updaters/update_script.py
This commit is contained in:
Gina Häußge 2016-08-26 14:22:14 +02:00
commit efca776102
11 changed files with 395 additions and 95 deletions

View file

@ -7,10 +7,10 @@
# The file is processed from top to bottom, the first matching line wins. If <tag> or <reference commit> 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 <tag>'
HEAD

View file

@ -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<commits since x.y.z>`
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<commits since x.y.z>`
(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.<commits since increase of z>` for an OctoPrint version of `x.y.z`
`<x>.<y>.<z+1>.dev<commits since increase of z>` for an OctoPrint version of `<x>.<y>.<z>`
(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<commits since increase of y>` for an OctoPrint version
of `x.y.z` (e.g. `1.3.0.dev123`).
scheme `<x>.<y+1>.0.dev<commits since increase of y>` for a current OctoPrint version
of `<x>.<y>.<z>` (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
`<x>.<y>.<z>rc<n>` (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 `<x>.<y+1>.0rc<n>` (e.g. `1.3.0rc1`)
for a current stable OctoPrint version of `<x>.<y>.<z>`.
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.

View file

@ -20,7 +20,7 @@ from . import version_checks, updaters, exceptions, util, cli
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
@ -155,14 +155,18 @@ 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")),
"restart": "octoprint"
"update_script": "{{python}} \"{update_script}\" --branch={{branch}} --force={{force}} \"{{folder}}\" {{target}}".format(update_script=update_script),
"restart": "octoprint",
"stable_branch": dict(branch="master", name="Stable"),
"prerelease_branches": [dict(branch="rc/maintenance", name="Maintenance RCs"),
dict(branch="rc/devel", name="Devel RCs")]
},
},
"pip_command": None,
@ -176,19 +180,47 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
if "checks" in data:
del data["checks"]
if "check_providers" in data:
del data["check_providers"]
checks = self._get_configured_checks()
if "octoprint" in checks:
data["octoprint_checkout_folder"] = self._get_octoprint_checkout_folder(checks=checks)
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"
stable_branch = None
prerelease_branches = []
branch_mappings = []
if "stable_branch" in checks["octoprint"]:
branch_mappings.append(checks["octoprint"]["stable_branch"])
stable_branch = checks["octoprint"]["stable_branch"]["branch"]
if "prerelease_branches" in checks["octoprint"]:
for mapping in checks["octoprint"]["prerelease_branches"]:
branch_mappings.append(mapping)
prerelease_branches.append(mapping["branch"])
data["octoprint_branch_mappings"] = branch_mappings
data["octoprint_release_channel"] = stable_branch
if checks["octoprint"].get("prerelease", False):
channel = checks["octoprint"].get("prerelease_channel", BRANCH)
if channel in prerelease_branches:
data["octoprint_release_channel"] = channel
else:
data["octoprint_checkout_folder"] = None
data["octoprint_type"] = None
data["octoprint_branch_mappings"] = []
return data
def on_settings_save(self, data):
for key in self.get_settings_defaults():
if key == "checks" or key == "cache_ttl" or key == "octoprint_checkout_folder" or key == "octoprint_type":
if key in ("checks", "cache_ttl", "octoprint_checkout_folder", "octoprint_type", "octoprint_release_channel"):
continue
if key in data:
self._settings.set([key], data[key])
@ -203,6 +235,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(
@ -210,7 +247,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
)
)
))
@ -236,6 +275,17 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
pass
self._version_cache_dirty = True
if "octoprint_release_channel" in data:
prerelease_branches = self._settings.get(["checks", "octoprint", "prerelease_branches"])
if prerelease_branches and data["octoprint_release_channel"] in [x["branch"] for x in 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
@ -368,7 +418,8 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
try:
information, update_available, update_possible = self.get_current_versions(check_targets=check_targets, force=force)
return flask.jsonify(dict(status="updatePossible" if update_available and update_possible else "updateAvailable" if update_available else "current", information=information))
return flask.jsonify(dict(status="updatePossible" if update_available and update_possible else "updateAvailable" if update_available else "current",
information=information))
except exceptions.ConfigurationInvalid as e:
flask.make_response("Update not properly configured, can't proceed: %s" % e.message, 500)
@ -621,6 +672,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
# one of our updates requires a restart of either type "octoprint" or "environment". Let's see if
# we can actually perform that
restart_command = None
if restart_type == "octoprint":
restart_command = self._settings.global_get(["server", "commands", "serverRestartCommand"])
elif restart_type == "environment":
@ -729,18 +781,70 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
result["displayName"] = check.get("displayName", gettext("OctoPrint"))
result["displayVersion"] = check.get("displayVersion", "{octoprint_version}")
stable_branch = "master"
release_branches = []
if "stable_branch" in check:
release_branches.append(check["stable_branch"]["branch"])
stable_branch = check["stable_branch"]["branch"]
if "prerelease_branches" in check:
release_branches += [x["branch"] for x in check["prerelease_branches"]]
result["released_version"] = not release_branches or BRANCH in release_branches
if check["type"] == "github_commit":
result["current"] = REVISION if REVISION else "unknown"
else:
result["current"] = VERSION
if check["type"] == "github_release" and (check["prerelease"] or BRANCH != stable_branch):
# we are tracking github releases and are either also tracking prerelease OR are currently installed
# from something that is not the stable (master) branch => we need to change some parameters
# we compare versions fully, not just the base so that we see a difference
# between RCs + stable for the same version release
result["force_base"] = False
if check.get("update_script", None):
# if we are using the update_script, we need to set our update_branch and force
# to install the exact version we requested
if check["prerelease"]:
# we are tracking prereleases => we want to be on the correct prerelease channel/branch
channel = check.get("prerelease_channel", None)
if channel:
# if we have a release channel, we also set our update_branch here to our release channel
# in case it's not already set
result["update_branch"] = check.get("update_branch", channel)
# we also force our target version in the update
result["force_exact_version"] = True
else:
# we are not tracking prereleases, but aren't on the stable branch either => switch back
# to stable branch on update
result["update_branch"] = check.get("update_branch", stable_branch)
if BRANCH != result.get("prerelease_channel"):
# we force python unequality check here because that will also allow us to
# downgrade on a prerelease channel change (rc/devel => rc/maintenance)
#
# we detect channel changes by comparing the current branch with the target
# branch of the release channel - unequality means we might have to handle
# a downgrade
result["release_compare"] = "python_unequal"
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))
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 _log(self, lines, prefix=None, stream=None, strip=True):
@ -777,22 +881,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]
def _get_octoprint_checkout_folder(self, checks=None):
if checks is None:

View file

@ -134,7 +134,7 @@ def _to_error(*lines):
return u"".join(map(lambda x: _to_unicode(x, errors="replace"), lines))
def update_source(git_executable, folder, target, force=False):
def _rescue_changes(git_executable, folder):
print(">>> Running: git diff --shortstat")
returncode, stdout, stderr = _git(["diff", "--shortstat"], folder, git_executable=git_executable)
if returncode != 0:
@ -155,6 +155,13 @@ def update_source(git_executable, folder, target, force=False):
for line in stdout:
f.write(line)
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, stderr = _git(["reset", "--hard"], folder, git_executable=git_executable)
if returncode != 0:
@ -165,6 +172,18 @@ def update_source(git_executable, folder, target, force=False):
if returncode != 0:
raise RuntimeError("Could not update, \"git clean -f\" failed with returcode %d: %s" % (returncode, _to_error(*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, stderr = _git(["pull"], folder, git_executable=git_executable)
if returncode != 0:
@ -199,18 +218,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,
@ -238,13 +263,12 @@ def main():
print("Python executable: {!r}".format(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__":

View file

@ -82,14 +82,14 @@ $(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");
@ -161,7 +161,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()
}
}
};
@ -179,9 +180,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());
};
self._copyConfigBack = function() {
@ -220,7 +240,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["released_version"] === false && check["type"] == "github_release") {
self.octoprintUnreleased(true);
} else {
self.octoprintUnreleased(false);
@ -228,8 +248,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);

View file

@ -1,4 +1,4 @@
<div class="control-group">
<div class="control-group" data-bind="visible: config_updateMethod() == 'update_script' || config_updateMethod() == 'python_updater'">
<label class="control-label">{{ _('OctoPrint checkout folder') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: config_checkoutFolder">

View file

@ -0,0 +1,6 @@
<div class="control-group" data-bind="visible: config_availableCheckTypes().length > 0 && config_checkType() == 'github_release'">
<label class="control-label">{{ _('OctoPrint Release Channel') }}</label>
<div class="controls">
<select data-bind="value: config_releaseChannel, options: config_availableReleaseChannels, optionsText: 'name', optionsValue: 'key'"></select>
</div>
</div>

View file

@ -1,4 +1,4 @@
<div class="control-group">
<div class="control-group" data-bind="visible: config_availableCheckTypes().length > 0">
<label class="control-label">{{ _('OctoPrint version tracking') }}</label>
<div class="controls">
<select data-bind="value: config_checkType, options: config_availableCheckTypes, optionsText: 'name', optionsValue: 'key'"></select>

View file

@ -76,6 +76,7 @@
<form class="form-horizontal">
{% include "_snippets/plugins/softwareupdate/checkoutFolder.jinja2" %}
{% include "_snippets/plugins/softwareupdate/versionTracking.jinja2" %}
{% include "_snippets/plugins/softwareupdate/releaseChannel.jinja2" %}
<div class="control-group">
<label class="control-label">{{ _('Version cache TTL') }}</label>
<div class="controls">

View file

@ -57,9 +57,11 @@ def perform_update(target, check, target_version, log_cb=None):
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)
caller = _get_caller(log_cb=log_cb)
@ -75,7 +77,12 @@ def perform_update(target, check, target_version, log_cb=None):
### 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))
caller.checked_call(update_command, cwd=folder)

View file

@ -14,7 +14,69 @@ 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,
sort_key=None,
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
if sort_key is None:
sort_key = lambda release: release.get("published_at", 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 sort_key
releases = sorted(releases, key=sort_key)
# 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, compare_type,
include_prerelease=False,
prerelease_channel=None,
force_base=True):
nothing = None, None, None
r = requests.get(RELEASE_URL.format(user=user, repo=repo))
@ -27,36 +89,55 @@ 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)
comparable_factory = _get_comparable_factory(compare_type,
force_base=force_base)
sort_key = lambda release: comparable_factory(_get_sanitized_version(release["tag_name"]))
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,
sort_key=sort_key,
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 +146,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)
@ -90,32 +166,66 @@ def _get_comparable_version_semantic(version_string, force_base=True):
return version
def _get_sanitized_compare_type(compare_type, custom=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"
return compare_type
def _get_comparable_factory(compare_type, force_base=True):
if compare_type in ("python", "python_unequal"):
return lambda version: _get_comparable_version_pkg_resources(version, force_base=force_base)
elif compare_type in ("semantic", "semantic_unequal"):
return lambda version: _get_comparable_version_semantic(version, force_base=force_base)
else:
return lambda version: version
def _get_comparator(compare_type, custom=None):
if compare_type in ("python", "semantic"):
return lambda a, b: a >= b
elif compare_type == "custom":
return custom
else:
return lambda a, b: a == b
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:
compare_type = "python"
compare_type = _get_sanitized_compare_type(compare_type, custom=custom)
comparable_factory = _get_comparable_factory(compare_type, force_base=force_base)
comparator = _get_comparator(compare_type, custom=custom)
sanitized_local = _get_sanitized_version(release_information["local"]["value"])
sanitized_remote = _get_sanitized_version(release_information["remote"]["value"])
try:
if compare_type == "python":
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 == "custom":
return custom(sanitized_local, sanitized_remote)
else:
return sanitized_local == sanitized_remote
return comparator(comparable_factory(sanitized_local),
comparable_factory(sanitized_remote))
except:
logger.exception("Could not check if version is current due to an error, assuming it is")
return True
@ -127,12 +237,17 @@ 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)
compare_type = _get_sanitized_compare_type(check.get("release_compare", "python"),
custom=custom_compare)
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"
compare_type,
include_prerelease=include_prerelease,
prerelease_channel=prerelease_channel,
force_base=force_base)
information =dict(
local=dict(name=current, value=current),