diff --git a/README.md b/README.md index 23f6ac9..e14fb62 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,16 @@ the version string used by Github. This can follow any standard practice with recognisable pre- and postfixes, e.g. `v1.0.3`, `1.0.3`, `1.1`, `1.3rc`, `1.3.2pl2`. +Note: All version strings must follow [SemVer v2.0.0](http://semver.org/) to allow +accurate comparison. The sole exception is that allowance is made for versions pre- +or post-fixed with `dev` to mark development versions. Development versions are +compared solely on the basis of the version string excluding the `dev` label. As +a convenience, versions formatted using, for example, `git describe` (e.g. +`1.0.0alpha.2-26-ge67g3d`) have a `-dev` postfix added (i.e. becoming +`1.0.0alpha.2-dev`). If you frequently update across development versions, it is +strongly recommended to use the SHA-1 or SHA-256 strategies, or implement a custom +strategy that can compare versions based on such metadata. + If you wish to update to a non-stable version, for example where users want to update according to a development track, you can set the stability flag for the Github strategy. By default this is set to `stable` or, in constant form, diff --git a/composer.json b/composer.json index 20d6b39..ebb8d56 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ ], "require": { "php": "^5.6|^7.0", - "padraic/humbug_get_contents": "^1.0" + "padraic/humbug_get_contents": "^1.0", + "composer/semver": "^1.4.2" }, "require-dev": { "phpunit/phpunit": "^5.5|^6.0" diff --git a/src/Updater.php b/src/Updater.php index 0e03277..e126d58 100644 --- a/src/Updater.php +++ b/src/Updater.php @@ -12,6 +12,7 @@ namespace Humbug\SelfUpdate; +use Humbug\SelfUpdate\VersionParser; use Humbug\SelfUpdate\Exception\RuntimeException; use Humbug\SelfUpdate\Exception\InvalidArgumentException; use Humbug\SelfUpdate\Exception\FilesystemException; @@ -320,13 +321,38 @@ protected function hasPubKey() return $this->hasPubKey; } + /** + * Compares local and remote versions. If version strings do not follow + * Semver, i.e. hashes, the strings are just checked for equality. + * @return bool + */ protected function newVersionAvailable() { $this->newVersion = $this->strategy->getCurrentRemoteVersion($this); $this->oldVersion = $this->strategy->getCurrentLocalVersion($this); - if (!empty($this->newVersion) && ($this->newVersion !== $this->oldVersion)) { - return true; + try { + if (!empty($this->newVersion) + && !VersionParser::equals($this->newVersion, $this->oldVersion) + ) { + return true; + } + } catch (\UnexpectedValueException $e) { + if ($this->getStrategy() instanceof GithubStrategy) { + throw new RuntimeException( + 'The current reported version or the current remote version ' + . 'does not adhere to Semantic Versioning and cannot be compared.' + . PHP_EOL + . sprintf('Current version: %s', $this->oldVersion) + . PHP_EOL + . sprintf('Remote version: %s', $this->newVersion) + ); + } + if (!empty($this->newVersion) + && ($this->newVersion !== $this->oldVersion) + ) { + return true; + } } return false; } diff --git a/src/VersionParser.php b/src/VersionParser.php index 87b1626..59d71e0 100644 --- a/src/VersionParser.php +++ b/src/VersionParser.php @@ -12,6 +12,9 @@ namespace Humbug\SelfUpdate; +use Composer\Semver\Semver; +use Composer\Semver\VersionParser as Parser; + class VersionParser { @@ -20,10 +23,15 @@ class VersionParser */ private $versions; + /** + * @var Composer\VersionParser + */ + private $parser; + /** * @var string */ - private $modifier = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; + const GIT_DATA_MATCH = '/.*(-\d+-g[[:alnum:]]{7})$/'; /** * @param array $versions @@ -31,6 +39,7 @@ class VersionParser public function __construct(array $versions = array()) { $this->versions = $versions; + $this->parser = new Parser; } /** @@ -112,6 +121,20 @@ public function isDevelopment($version) return $this->development($version); } + /** + * Checks if two version strings are the same normalised version. + * + * @param string + * @param string + * @return bool + */ + public static function equals($version1, $version2) + { + $parser = new Parser; + return $parser->normalize(self::stripGitHash($version1)) + === $parser->normalize(self::stripGitHash($version2)); + } + private function selectRecentStable() { $candidates = array(); @@ -159,44 +182,31 @@ private function selectRecentAll() private function findMostRecent(array $candidates) { - $candidate = null; - $tracker = null; - foreach ($candidates as $version) { - if (version_compare($candidate, $version, '<')) { - $candidate = $version; - } - } - return $candidate; + $sorted = Semver::rsort($candidates); + return $sorted[0]; } private function stable($version) { - $version = preg_replace('{#.+$}i', '', $version); - if ($this->development($version)) { - return false; - } - preg_match('{'.$this->modifier.'$}i', strtolower($version), $match); - if (!empty($match[3])) { - return false; - } - if (!empty($match[1])) { - if ('beta' === $match[1] || 'b' === $match[1] - || 'alpha' === $match[1] || 'a' === $match[1] - || 'rc' === $match[1]) { - return false; - } + if ('stable' === Parser::parseStability(self::stripGitHash($version))) { + return true; } - return true; + return false; } private function development($version) { - if ('dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4)) { - return true; - } - if (1 == preg_match("/-\d+-[a-z0-9]{8,}$/", $version)) { + if ('dev' === Parser::parseStability(self::stripGitHash($version))) { return true; } return false; } + + private static function stripGitHash($version) + { + if (preg_match(self::GIT_DATA_MATCH, $version, $matches)) { + $version = str_replace($matches[1], '-dev', $version); + } + return $version; + } } diff --git a/tests/Humbug/Test/SelfUpdate/VersionParserTest.php b/tests/Humbug/Test/SelfUpdate/VersionParserTest.php index b867d69..c31da03 100644 --- a/tests/Humbug/Test/SelfUpdate/VersionParserTest.php +++ b/tests/Humbug/Test/SelfUpdate/VersionParserTest.php @@ -167,4 +167,53 @@ public function testIsDevelopment() $this->assertTrue($parser->isDevelopment('1.0.0-dev')); $this->assertTrue($parser->isDevelopment('1.0.0-alpha1-5-g5b46ad8')); } + + public function testEqualsWithSameSemverVersion() + { + $v1 = '1.2.3'; + $v2 = '1.2.3'; + $this->assertTrue(VersionParser::equals($v1, $v2)); + } + + public function testEqualsWithDifferentSemverVersion() + { + $v1 = '1.2.3'; + $v2 = '1.2.4'; + $this->assertFalse(VersionParser::equals($v1, $v2)); + } + + public function testEqualsWithSameSemverVersionButPrefixed() + { + $v1 = '1.2.3'; + $v2 = 'v1.2.3'; + $this->assertTrue(VersionParser::equals($v1, $v2)); + } + + public function testEqualsWithSameSemverVersionButGitData() + { + $v1 = '1.2.3-5-g5b46ad8'; + $v2 = '1.2.3-5-g5b46ad8'; + $this->assertTrue(VersionParser::equals($v1, $v2)); + } + + public function testEqualsWithDifferentSemverVersionButGitData() + { + $v1 = '1.2.3-5-g5b46ad8'; + $v2 = '1.2.4-5-g5b46ad8'; + $this->assertFalse(VersionParser::equals($v1, $v2)); + } + + public function testEqualsWithSameSemverVersionButStabilityDiffers() + { + $v1 = '1.2.3-alpha'; + $v2 = '1.2.3'; + $this->assertFalse(VersionParser::equals($v1, $v2)); + } + + public function testEqualsWithSameSemverVersionButStabilitySameButNumberedOff() + { + $v1 = '1.2.3-alpha'; + $v2 = '1.2.3-alpha2'; + $this->assertFalse(VersionParser::equals($v1, $v2)); + } }