From 6a004b6ae7433912c84dc7acc619a94617a56d5e Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Sun, 22 Nov 2015 22:44:02 +0100 Subject: [PATCH 1/2] Added SubversionAdapter --- src/Adapter/SubversionAdapter.php | 154 +++++++++ tests/Adapter/SubversionAdapterTest.php | 407 ++++++++++++++++++++++++ 2 files changed, 561 insertions(+) create mode 100644 src/Adapter/SubversionAdapter.php create mode 100644 tests/Adapter/SubversionAdapterTest.php diff --git a/src/Adapter/SubversionAdapter.php b/src/Adapter/SubversionAdapter.php new file mode 100644 index 0000000..c5fc503 --- /dev/null +++ b/src/Adapter/SubversionAdapter.php @@ -0,0 +1,154 @@ + + */ +class SubversionAdapter extends AbstractAdapter +{ + /** + * The path in the repository representing 'trunk' or 'master'. + * + * @var string + */ + private $trunkPath = 'trunk'; + + /** + * The path in the repository representing 'branches'. + * + * @var string + */ + private $branchesPath = 'branches'; + + /** + * The path in the repository representing 'tags'. + * + * @var string + */ + private $tagsPath = 'tags'; + + /** + * {@inheritdoc} + */ + public function supportsRepository() + { + $result = $this->processExecutor->execute('svn --version'); + if ($result->isSuccessful() === false) { + return false; + } + + if (preg_match('#(^svn://|^svn\+ssh://|svn\.)#i', $this->repositoryUrl)) { + return true; + } + + $result = $this->processExecutor->execute(sprintf('svn info --non-interactive %s', ProcessUtils::escapeArgument($this->repositoryUrl))); + if ($result->isSuccessful()) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getBranches() + { + $branches = array(); + + $result = $this->processExecutor->execute(sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument($this->repositoryUrl.'/'.$this->trunkPath))); + if ($result->isSuccessful()) { + foreach ($result->getOutputAsArray() as $branch) { + $matches = array(); + if (preg_match('#^\s*(\S+).*?(\S+)\s*$#', $branch, $matches) && $matches[2] === './') { + $branches[$matches[1]] = 'master'; + } + } + } + + $result = $this->processExecutor->execute(sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument($this->repositoryUrl.'/'.$this->branchesPath))); + if ($result->isSuccessful()) { + foreach ($result->getOutputAsArray() as $branch) { + $matches = array(); + if (preg_match('#^\s*(\S+).*?(\S+)\s*$#', $branch, $matches)) { + $branches[$matches[1]] = $matches[2]; + } + } + } + + return $branches; + } + + /** + * {@inheritdoc} + */ + public function getTags() + { + $tags = array(); + + $result = $this->processExecutor->execute(sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument($this->repositoryUrl.'/'.$this->tagsPath))); + if ($result->isSuccessful()) { + foreach ($result->getOutputAsArray() as $tag) { + $matches = array(); + if (preg_match('#^\s*(\S+).*?(\S+)\s*$#', $tag, $matches)) { + $tags[$matches[1]] = $matches[2]; + } + } + } + + return $tags; + } + + /** + * {@inheritdoc} + */ + public function checkout($version) + { + $checkoutSuccesful = false; + + $escapedRepositoryUrlWithVersionPath = $this->getRepositoryUrlWithVersionPath($version); + if ($escapedRepositoryUrlWithVersionPath === false) { + return false; + } + + if ($this->processExecutor->isDirectory($this->repositoryDirectory) && $this->processExecutor->execute('svn info --non-interactive', $this->repositoryDirectory)->isSuccessful()) { + $checkoutSuccesful = $this->processExecutor->execute(sprintf('svn switch --non-interactive %s', $escapedRepositoryUrlWithVersionPath), $this->repositoryDirectory)->isSuccessful(); + } else { + $escapedRepositoryDirectory = ProcessUtils::escapeArgument($this->repositoryDirectory); + + $result = $this->processExecutor->execute(sprintf('svn checkout --non-interactive %s %s', $escapedRepositoryUrlWithVersionPath, $escapedRepositoryDirectory)); + $checkoutSuccesful = $result->isSuccessful(); + } + + return $checkoutSuccesful; + } + + /** + * Returns the escaped repository URL with version path. + * Returns false when the repository URL for a version cannot be found. + * + * @param string $version + * + * @return string|bool + */ + private function getRepositoryUrlWithVersionPath($version) + { + $repositoryUrl = $this->repositoryUrl; + if (in_array($version, array('master', $this->trunkPath))) { + $repositoryUrl .= '/'.$this->trunkPath; + } elseif (in_array($version, $this->getBranches())) { + $repositoryUrl .= '/'.$this->branchesPath.'/'.$version; + } elseif (in_array($version, $this->getTags())) { + $repositoryUrl .= '/'.$this->tagsPath.'/'.$version; + } else { + return false; + } + + return ProcessUtils::escapeArgument($repositoryUrl); + } +} diff --git a/tests/Adapter/SubversionAdapterTest.php b/tests/Adapter/SubversionAdapterTest.php new file mode 100644 index 0000000..52a5132 --- /dev/null +++ b/tests/Adapter/SubversionAdapterTest.php @@ -0,0 +1,407 @@ + + */ +class SubversionAdapterTest extends PHPUnit_Framework_TestCase +{ + /** + * Tests if SubversionAdapter::supportsRepository returns the expected result. + * + * @dataProvider provideTestSupportsRepository + * + * @param string $repositoryUrl + * @param ProcessExecutorInterface $processExecutor + * @param bool $expectedResult + */ + public function testSupportsRepository($repositoryUrl, ProcessExecutorInterface $processExecutor, $expectedResult) + { + $subversionAdapter = new SubversionAdapter($repositoryUrl, '', $processExecutor); + + $this->assertSame($expectedResult, $subversionAdapter->supportsRepository()); + } + + /** + * Tests if SubversionAdapter::getBranches returns the expected result. + * + * @dataProvider provideTestGetBranches + * + * @param ProcessExecutorInterface $processExecutor + * @param array $expectedResult + */ + public function testGetBranches(ProcessExecutorInterface $processExecutor, array $expectedResult) + { + $subversionAdapter = new SubversionAdapter('https://github.com/accompli/chrono', '', $processExecutor); + + $this->assertSame($expectedResult, $subversionAdapter->getBranches()); + } + + /** + * Tests if SubversionAdapter::getTags returns the expected result. + * + * @dataProvider provideTestGetTags + * + * @param ProcessExecutorInterface $processExecutor + * @param array $expectedResult + */ + public function testGetTags(ProcessExecutorInterface $processExecutor, array $expectedResult) + { + $subversionAdapter = new SubversionAdapter('https://github.com/accompli/chrono', '', $processExecutor); + + $this->assertSame($expectedResult, $subversionAdapter->getTags()); + } + + /** + * Tests if SubversionAdapter::checkout returns the expected result. + * + * @dataProvider provideTestCheckout + * + * @param ProcessExecutorInterface $processExecutor + * @param bool $expectedResult + * @param string $version + */ + public function testCheckout(ProcessExecutorInterface $processExecutor, $expectedResult, $version = '0.1.0') + { + $subversionAdapter = new SubversionAdapter('https://github.com/accompli/chrono', '/svn/working-directory', $processExecutor); + + $this->assertSame($expectedResult, $subversionAdapter->checkout($version)); + } + + /** + * Returns the test data and expected results for testing SubversionAdapter::supportsRepository. + * + * @return array + */ + public function provideTestSupportsRepository() + { + $infoCommand = 'svn info --non-interactive %s'; + + $provideTest = array(); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('execute') + ->with($this->equalTo('svn --version')) + ->willReturn(new ProcessExecutionResult(1, '', '')); + $provideTest[] = array('https://github.com/accompli/chrono.git', $processExecutorMock, false); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->exactly(2)) + ->method('execute') + ->withConsecutive( + array($this->equalTo('svn --version')), + array($this->equalTo(sprintf($infoCommand, ProcessUtils::escapeArgument('https://github.com/accompli/chrono.git')))) + ) + ->willReturn(new ProcessExecutionResult(0, '', '')); + $provideTest[] = array('https://github.com/accompli/chrono.git', $processExecutorMock, true); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->exactly(2)) + ->method('execute') + ->withConsecutive( + array($this->equalTo('svn --version')), + array($this->equalTo(sprintf($infoCommand, ProcessUtils::escapeArgument('git@github.com:accompli/chrono.git')))) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, '', ''), + new ProcessExecutionResult(1, '', '') + ); + $provideTest[] = array('git@github.com:accompli/chrono.git', $processExecutorMock, false); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->exactly(2)) + ->method('execute') + ->withConsecutive( + array($this->equalTo('svn --version')), + array($this->equalTo(sprintf($infoCommand, ProcessUtils::escapeArgument('user@example.com:accompli/chrono')))) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, '', ''), + new ProcessExecutionResult(1, '', '') + ); + $provideTest[] = array('user@example.com:accompli/chrono', $processExecutorMock, false); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('execute') + ->with($this->equalTo('svn --version')) + ->willReturn(new ProcessExecutionResult(0, '', '')); + $provideTest[] = array('svn+ssh://example.com/accompli/chrono', $processExecutorMock, true); + + return $provideTest; + } + + /** + * Returns the test data and expected results for testing SubversionAdapter::getBranches. + * + * @return array + */ + public function provideTestGetBranches() + { + $trunkCommand = sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/trunk')); + $branchesCommand = sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/branches')); + + $provideTest = array(); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->exactly(2)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($trunkCommand)), + array($this->equalTo($branchesCommand)) + ) + ->willReturn(new ProcessExecutionResult(1, '', '')); + $provideTest[] = array($processExecutorMock, array()); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->exactly(2)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($trunkCommand)), + array($this->equalTo($branchesCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(1, '', ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 1.0\n", '') + ); + $provideTest[] = array($processExecutorMock, array('35' => '1.0')); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->exactly(2)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($trunkCommand)), + array($this->equalTo($branchesCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, " 34 niels.ni nov 22 22:10 ./\n 34 niels.ni nov 22 22:10 file\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 1.0\n", '') + ); + $provideTest[] = array($processExecutorMock, array('34' => 'master', '35' => '1.0')); + + return $provideTest; + } + + /** + * Returns the test data and expected results for testing SubversionAdapter::getTags. + * + * @return array + */ + public function provideTestGetTags() + { + $tagsCommand = sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/tags')); + + $provideTest = array(); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('execute') + ->with($this->equalTo($tagsCommand)) + ->willReturn(new ProcessExecutionResult(1, '', '')); + $provideTest[] = array($processExecutorMock, array()); + + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('execute') + ->with($this->equalTo($tagsCommand)) + ->willReturn(new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 0.1.0\n", '')); + $provideTest[] = array($processExecutorMock, array('35' => '0.1.0')); + + return $provideTest; + } + + /** + * Returns the test data and expected results for testing SubversionAdapter::checkout. + * + * @return array + */ + public function provideTestCheckout() + { + $getTrunkCommand = sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/trunk')); + $getBranchesCommand = sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/branches')); + $getTagsCommand = sprintf('svn ls --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/tags')); + $infoCommand = 'svn info --non-interactive'; + $trunkSwitchCommand = sprintf('svn switch --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/trunk')); + $branchSwitchCommand = sprintf('svn switch --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/branches/1.0')); + $tagSwitchCommand = sprintf('svn switch --non-interactive %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/tags/0.1.0')); + $checkoutCommand = sprintf('svn checkout --non-interactive %s %s', ProcessUtils::escapeArgument('https://github.com/accompli/chrono/tags/0.1.0'), ProcessUtils::escapeArgument('/svn/working-directory')); + + $provideTest = array(); + + // directory false, no branches, no tags + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(false); + $processExecutorMock->expects($this->exactly(3)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($getTrunkCommand)), + array($this->equalTo($getBranchesCommand)), + array($this->equalTo($getTagsCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(1, '', ''), + new ProcessExecutionResult(1, '', ''), + new ProcessExecutionResult(1, '', '') + ); + $provideTest[] = array($processExecutorMock, false); + + // directory false, branches, tags, checkout failed + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(false); + $processExecutorMock->expects($this->exactly(4)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($getTrunkCommand)), + array($this->equalTo($getBranchesCommand)), + array($this->equalTo($getTagsCommand)), + array($this->equalTo($checkoutCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, " 34 niels.ni nov 22 22:10 ./\n 34 niels.ni nov 22 22:10 file\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 1.0\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 0.1.0\n", ''), + new ProcessExecutionResult(1, '', '') + ); + $provideTest[] = array($processExecutorMock, false); + + // directory false, branches, tags, checkout success + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(false); + $processExecutorMock->expects($this->exactly(4)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($getTrunkCommand)), + array($this->equalTo($getBranchesCommand)), + array($this->equalTo($getTagsCommand)), + array($this->equalTo($checkoutCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, " 34 niels.ni nov 22 22:10 ./\n 34 niels.ni nov 22 22:10 file\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 1.0\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 0.1.0\n", ''), + new ProcessExecutionResult(0, '', '') + ); + $provideTest[] = array($processExecutorMock, true); + + // directory true, no branches, no tags + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(true); + $processExecutorMock->expects($this->exactly(3)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($getTrunkCommand)), + array($this->equalTo($getBranchesCommand)), + array($this->equalTo($getTagsCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(1, '', ''), + new ProcessExecutionResult(1, '', ''), + new ProcessExecutionResult(1, '', '') + ); + $provideTest[] = array($processExecutorMock, false); + + // directory true, branches, tags, switch failed + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(true); + $processExecutorMock->expects($this->exactly(5)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($getTrunkCommand)), + array($this->equalTo($getBranchesCommand)), + array($this->equalTo($getTagsCommand)), + array($this->equalTo($infoCommand)), + array($this->equalTo($tagSwitchCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, " 34 niels.ni nov 22 22:10 ./\n 34 niels.ni nov 22 22:10 file\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 1.0\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 0.1.0\n", ''), + new ProcessExecutionResult(0, '', ''), + new ProcessExecutionResult(1, '', '') + ); + $provideTest[] = array($processExecutorMock, false); + + // directory true, branches, tags, switch success + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(true); + $processExecutorMock->expects($this->exactly(5)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($getTrunkCommand)), + array($this->equalTo($getBranchesCommand)), + array($this->equalTo($getTagsCommand)), + array($this->equalTo($infoCommand)), + array($this->equalTo($tagSwitchCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, " 34 niels.ni nov 22 22:10 ./\n 34 niels.ni nov 22 22:10 file\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 1.0\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 0.1.0\n", ''), + new ProcessExecutionResult(0, '', ''), + new ProcessExecutionResult(0, '', '') + ); + $provideTest[] = array($processExecutorMock, true); + + // directory true, branches, tags, switch to 'master' + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(true); + $processExecutorMock->expects($this->exactly(3)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($infoCommand)), + array($this->equalTo($trunkSwitchCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, '', ''), + new ProcessExecutionResult(0, '', '') + ); + $provideTest[] = array($processExecutorMock, true, 'master'); + + // directory true, branches, switch to '1.0' + $processExecutorMock = $this->getMockBuilder('Accompli\Chrono\Process\ProcessExecutorInterface')->getMock(); + $processExecutorMock->expects($this->once()) + ->method('isDirectory') + ->willReturn(true); + $processExecutorMock->expects($this->exactly(4)) + ->method('execute') + ->withConsecutive( + array($this->equalTo($getTrunkCommand)), + array($this->equalTo($getBranchesCommand)), + array($this->equalTo($infoCommand)), + array($this->equalTo($branchSwitchCommand)) + ) + ->willReturnOnConsecutiveCalls( + new ProcessExecutionResult(0, " 34 niels.ni nov 22 22:10 ./\n 34 niels.ni nov 22 22:10 file\n", ''), + new ProcessExecutionResult(0, " 35 niels.ni nov 22 22:10 ./\n 35 niels.ni nov 22 22:10 1.0\n", ''), + new ProcessExecutionResult(0, '', ''), + new ProcessExecutionResult(0, '', '') + ); + $provideTest[] = array($processExecutorMock, true, '1.0'); + + return $provideTest; + } +} From 493751ac85cd9408c3d5d20f81f4cf34a6149a33 Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Sun, 22 Nov 2015 22:44:53 +0100 Subject: [PATCH 2/2] Added SubversionAdapter to list of adapters in Repository --- src/Repository.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Repository.php b/src/Repository.php index 39164f6..09236af 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -41,6 +41,7 @@ class Repository implements RepositoryInterface */ private $adapters = array( 'Accompli\Chrono\Adapter\GitAdapter', + 'Accompli\Chrono\Adapter\SubversionAdapter', ); /**