From 399482c315ba688850bc7a7e76bf573657b6ed83 Mon Sep 17 00:00:00 2001 From: Logan Bailey Date: Mon, 20 Feb 2017 09:19:39 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Abstract=20Out=20Version=20Logic=20?= =?UTF-8?q?Into=20Library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will allow teams that don't use SemVer to define their own Version and use the same bump commands. --- .gitignore | 2 +- bin/bump | 66 +++---------- composer.json | 6 ++ phpunit.xml.dist | 18 ++++ src/Compare.php | 39 ++++++++ src/Version.php | 41 ++++++++ src/Version/SemVer.php | 156 ++++++++++++++++++++++++++++++ tests/unit/CompareTest.php | 81 ++++++++++++++++ tests/unit/Version/SemVerTest.php | 82 ++++++++++++++++ 9 files changed, 438 insertions(+), 53 deletions(-) create mode 100644 phpunit.xml.dist create mode 100644 src/Compare.php create mode 100644 src/Version.php create mode 100644 src/Version/SemVer.php create mode 100644 tests/unit/CompareTest.php create mode 100644 tests/unit/Version/SemVerTest.php diff --git a/.gitignore b/.gitignore index de6b8eb..1cd379b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ vendor/ # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file -# composer.lock \ No newline at end of file +composer.lock diff --git a/bin/bump b/bin/bump index a742a52..cbcc8f7 100644 --- a/bin/bump +++ b/bin/bump @@ -1,69 +1,31 @@ #! /usr/bin/php $major) { - $major = $parts[0]; - $minor = $parts[1]; - $patch = $parts[2]; - } elseif ($parts[0] == $major) { - if ($parts[1] > $minor) { - $minor = $parts[1]; - $patch = $parts[2]; - } elseif ($parts[1] == $minor && $parts[2] > $patch) { - $patch = $parts[2]; - } - } -} - -$current_version = implode('.', [$major, $minor, $patch]); +$comparison = new \Bump\Compare(function ($tag) { + return new \Bump\Version\SemVer($tag); +}); -switch ($bump) { - case 'major': - $major++; - $minor = 0; - $patch = 0; - break; - - case 'minor': - $minor++; - $patch = 0; - break; - - case 'patch': - $patch++; - break; -} +$current_tag = $comparison->getLatestTag(explode(PHP_EOL, shell_exec('git tag'))); +$next_tag = $current_tag->bump($bump); -$bumped_version = implode('.', [$major, $minor, $patch]); -$cmd = 'git shortlog ' . $current_version . '..'; -echo PHP_EOL . 'git shortlog ' . $current_version . '..' . PHP_EOL; +$cmd = 'git shortlog ' . $current_tag->formatTag() . '..'; +echo PHP_EOL . 'git shortlog ' . $current_tag->formatTag() . '..' . PHP_EOL; passthru($cmd); -$tagit = readline('Do you want to create and push a new tag (' . $bumped_version . ') with the above changes? (y/n):'); +$tagit = readline('Do you want to create and push a new tag (' . $next_tag->formatTag() . ') with the above changes? (y/n):'); if (strtolower($tagit) !== 'y') { - echo 'It\'s cool. I understand.' . PHP_EOL; - exit; + echo 'It\'s cool. I understand.' . PHP_EOL; + exit; } -passthru('git tag ' . $bumped_version); -passthru('git push origin ' . $bumped_version); +passthru('git tag ' . $next_tag->formatTag()); +passthru('git push origin ' . $next_tag->formatTag()); echo $bumped_version . ' has been tagged.' . PHP_EOL; diff --git a/composer.json b/composer.json index cc7a521..3e05c78 100644 --- a/composer.json +++ b/composer.json @@ -10,5 +10,11 @@ } ], "require": {}, + "require-dev": { + "phpunit/phpunit": "4.8.*" + }, + "autoload": { + "psr-4": { "Bump\\": "src/" } + }, "bin": ["bin/bump"] } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..6324588 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + test/unit + + + diff --git a/src/Compare.php b/src/Compare.php new file mode 100644 index 0000000..d25fa75 --- /dev/null +++ b/src/Compare.php @@ -0,0 +1,39 @@ +tag_factory = $tag_factory; + } + + /** + * @param string[] $repository_tags The list of tags from your source code repository. + * @param string $starting_tag The earliest possible tag for your tagging system, 0.0.0 for SemVer. + * + * @return Version The current tag. + */ + public function getLatestTag(array $repository_tags, $starting_tag = '0.0.0') + { + $current = call_user_func($this->tag_factory, $starting_tag); + foreach ($repository_tags as $repository_tag) { + /** @var Version $tag */ + $tag = call_user_func($this->tag_factory, $repository_tag); + + if ($tag->exclude()) { + continue; + } + + if ($current->compare($tag) === Version::LESSER) { + $current = $tag; + } + } + + return $current; + } +} diff --git a/src/Version.php b/src/Version.php new file mode 100644 index 0000000..97dae4f --- /dev/null +++ b/src/Version.php @@ -0,0 +1,41 @@ +init($tag)) { + $this->is_valid_format = true; + } + } + + /** + * @inheritdoc + */ + public function exclude() + { + return !$this->is_valid_format; + } + + /** + * @inheritdoc + */ + public function bump($touple) + { + $touple_map = [ + 'patch' => 'bumpPatch', + 'minor' => 'bumpMinor', + 'major' => 'bumpMajor' + ]; + + if (!isset($touple_map[$touple])) { + throw new \InvalidArgumentException($touple . ' is not an allowed touple'); + } + + return $this->{$touple_map[$touple]}(); + } + + /** + * Generates the patch bump for the given tag. + * + * @return self + */ + protected function bumpPatch() + { + $version = clone $this; + $version->patch++; + return $version; + } + + /** + * Generates the minor bump for the given tag. + * + * @return string + */ + protected function bumpMinor() + { + $version = clone $this; + $version->minor++; + $version->patch = 0; + return $version; + } + + /** + * Generates the major bump for the given tag. + * + * @return string + */ + protected function bumpMajor() + { + $version = clone $this; + $version->major++; + $version->patch = 0; + $version->minor = 0; + return $version; + } + + /** + * @inheritdoc + */ + public function getTag() + { + return sprintf('%d.%d.%d', $this->major, $this->minor, $this->patch); + } + + /** + * @inheritdoc + */ + public function compare(Version $version) + { + $major = $this->spaceShip($this->major, $version->major); + if ($major !== VERSION::EQUAL) { + return $major; + } + + $minor = $this->spaceShip($this->minor, $version->minor); + if ($minor !== Version::EQUAL) { + return $minor; + } + + return $this->spaceShip($this->patch, $version->patch); + } + + /** + * Initializes the major, minor, and patch properties. + * + * @param string $tag + * + * @return bool True if the raw_tag is formatted correctly, false if the format is incorrect. + */ + protected function init($tag) + { + $parts = explode('.', $tag); + + if (count($parts) !== 3) { + return false; + } + + list($this->major, $this->minor, $this->patch) = $parts; + + return true; + } + + /** + * Performs the same action as `<=>` operation in php 7. + * + * @param string|int $instance + * @param string|int $parameter + * + * @return int -1 when instance is less than parameter, 0 if they're equal and 1 if instance is greater than + * parameter. + */ + private function spaceShip($instance, $parameter) + { + if ($instance == $parameter) { + return 0; + } + + return $instance > $parameter ? 1 : -1; + } +} diff --git a/tests/unit/CompareTest.php b/tests/unit/CompareTest.php new file mode 100644 index 0000000..f25e8dd --- /dev/null +++ b/tests/unit/CompareTest.php @@ -0,0 +1,81 @@ +tag_factory = $this->getMock(TagFactory::class); + $this->compare = new Compare($this->tag_factory); + } + + public function testWithNoTagsReturnsTheCurrentVersion() + { + $expected = $this->getMock(Version::class); + $this->tag_factory->expects($this->once()) + ->method('__invoke') + ->with('0.0.0') + ->willReturn($expected); + + $this->assertSame($expected, $this->compare->getLatestTag([])); + } + + public function testExcludedTagsAreNotIncludedInComparison() + { + $base_tag = $this->getMock(Version::class); + $excluded_tag = $this->getMock(Version::class); + + $this->tag_factory->expects($this->exactly(2)) + ->method('__invoke') + ->withConsecutive(['0.0.0'], ['irregular-tag']) + ->willReturnOnConsecutiveCalls($base_tag, $excluded_tag); + + $base_tag->expects($this->never())->method('compare'); + $excluded_tag->expects($this->once()) + ->method('exclude') + ->willReturn(true); + + $this->assertSame($base_tag, $this->compare->getLatestTag(['irregular-tag'])); + } + + public function testCompare() + { + $base_tag = $this->getMock(Version::class); + $lower_tag = $this->getMock(Version::class); + $upper_tag = $this->getMock(Version::class); + + + $this->tag_factory->expects($this->exactly(3)) + ->method('__invoke') + ->withConsecutive(['0.0.0'], ['1.0.1'], ['1.0.2']) + ->willReturnOnConsecutiveCalls($base_tag, $lower_tag, $upper_tag); + + $base_tag->expects($this->once()) + ->method('compare') + ->with($lower_tag) + ->willReturn(-1); + + $lower_tag->expects($this->once()) + ->method('compare') + ->with($upper_tag) + ->willReturn(-1); + + $this->assertSame($upper_tag, $this->compare->getLatestTag(['1.0.1', '1.0.2'])); + } +} + +class TagFactory +{ + public function __invoke($tag) {} +} diff --git a/tests/unit/Version/SemVerTest.php b/tests/unit/Version/SemVerTest.php new file mode 100644 index 0000000..5a64f3c --- /dev/null +++ b/tests/unit/Version/SemVerTest.php @@ -0,0 +1,82 @@ +assertTrue($version->exclude()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testBumpThrowsAnExceptionWhenSuppliedInvalidTouple() + { + $version = new SemVer('1.0.1'); + $version->bump('your-face'); + } + + /** + * @param string $touple The touple to bump, can either be patch, minor, or major + * @param string $starting_version The starting version + * @param string $expected_version The expected final version + * + * @dataProvider bumpDataProvider + */ + public function testBumpIncreasesTouplesCorrectly($touple, $starting_version, $expected_version) + { + $expected = new SemVer($expected_version); + $starting = new SemVer($starting_version); + + $this->assertEquals($expected, $starting->bump($touple)); + } + + public function bumpDataProvider() + { + return [ + 'patch' => ['patch', '1.5.1', '1.5.2'], + 'minor' => ['minor', '1.5.2', '1.6.0'], + 'major' => ['major', '1.5.2', '2.0.0'], + ]; + } + + public function testGetTag() + { + $version = new SemVer('123.456.789'); + $this->assertEquals('123.456.789', $version->getTag()); + } + + /** + * @param Semver $instance The object compare is being called on + * @param Semver $comparator The object being passed into the comparison + * @param int $expected either 1, 0, or -1 + * + * @dataProvider comparisonDataProvider + */ + public function testCompare(Semver $instance, Semver $comparator, $expected) + { + $this->assertEquals($expected, $instance->compare($comparator)); + } + + public function comparisonDataProvider() + { + return [ + 'Major Less Than' => [new SemVer('0.1.1'), new SemVer('1.0.0'), Version::LESSER], + 'Major Greater Than' => [new SemVer('2.1.1'), new SemVer('1.0.0'), Version::GREATER], + + 'Minor Less Than' => [new SemVer('0.1.1'), new SemVer('0.2.0'), Version::LESSER], + 'Minor Greater Than' => [new SemVer('2.1.1'), new SemVer('2.0.0'), Version::GREATER], + + 'Patch Less Than' => [new SemVer('0.0.1'), new SemVer('0.0.3'), Version::LESSER], + 'Patch Greater Than' => [new SemVer('2.1.1'), new SemVer('2.1.0'), Version::GREATER], + + 'Patch Greater Than' => [new SemVer('2.1.1'), new SemVer('2.1.1'), Version::EQUAL], + ]; + } +}