diff --git a/.gitignore b/.gitignore index 9cfbdffc..1e7fd755 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ /tests/WorkspaceHome /tests/Test/Updater/fixtures/generated /.php-cs-fixer.cache +.idea/ \ No newline at end of file diff --git a/composer.json b/composer.json index e13da467..cea58a8f 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "symfony/finder": "^6.1", "symfony/yaml": "^6.1", "twig/twig": "^2.13", - "composer/semver": "^3.3" + "composer/semver": "^3.3", + "czproject/git-php": "^4.2.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", diff --git a/composer.lock b/composer.lock index 1be7ef6b..9fcc51c2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c0e6bd3ea771120e163c7cbb1adcf80b", + "content-hash": "e2d31ab744dc6a55ca9a4e1d5c69856b", "packages": [ { "name": "composer/semver", @@ -87,6 +87,58 @@ ], "time": "2022-04-01T19:23:25+00:00" }, + { + "name": "czproject/git-php", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/czproject/git-php.git", + "reference": "e257f2c3b43fe8fef19ddb5727b604416b423107" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/czproject/git-php/zipball/e257f2c3b43fe8fef19ddb5727b604416b423107", + "reference": "e257f2c3b43fe8fef19ddb5727b604416b423107", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "require-dev": { + "nette/tester": "^2.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jan Pecha", + "email": "janpecha@email.cz" + } + ], + "description": "Library for work with Git repository in PHP.", + "keywords": [ + "git" + ], + "support": { + "issues": "https://github.com/czproject/git-php/issues", + "source": "https://github.com/czproject/git-php/tree/v4.2.0" + }, + "funding": [ + { + "url": "https://www.janpecha.cz/donate/git-php/", + "type": "other" + } + ], + "time": "2023-07-12T09:14:30+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -4670,5 +4722,5 @@ "php": "^8.1" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/services.yml b/config/services.yml index 76dabc27..b4136197 100644 --- a/config/services.yml +++ b/config/services.yml @@ -47,13 +47,19 @@ services: my127\Console\: resource: '../packages/Console/src/*' - my127\Workspace\Types\Harness\Repository\AggregateRepository: ~ + my127\Workspace\Types\Harness\Repository\AggregateRepository: + arguments: + - '@my127\Workspace\Types\Harness\Repository\PackageRepository' + - '@my127\Workspace\Types\Harness\Repository\LocalSyncRepository' + - '@my127\Workspace\Types\Harness\Repository\GithubRepository' + - '@my127\Workspace\Types\Harness\Repository\ArchiveRepository' # this is catch-all and must stay last + my127\Workspace\Types\Harness\Repository\ArchiveRepository: ~ my127\Workspace\Types\Harness\Repository\PackageRepository: ~ + my127\Workspace\Types\Harness\Repository\LocalSyncRepository: ~ + my127\Workspace\Types\Harness\Repository\GithubRepository: ~ my127\Workspace\Types\Harness\Repository\Repository $packages: '@my127\Workspace\Types\Harness\Repository\AggregateRepository' - my127\Workspace\Types\Harness\Repository\Repository $archiveRepository: '@my127\Workspace\Types\Harness\Repository\ArchiveRepository' - my127\Workspace\Types\Harness\Repository\Repository $packageRepository: '@my127\Workspace\Types\Harness\Repository\PackageRepository' my127\Workspace\Updater\Updater: arguments: diff --git a/src/Types/Harness/Repository/AggregateRepository.php b/src/Types/Harness/Repository/AggregateRepository.php index e5f00e3b..44672b1a 100644 --- a/src/Types/Harness/Repository/AggregateRepository.php +++ b/src/Types/Harness/Repository/AggregateRepository.php @@ -6,25 +6,25 @@ class AggregateRepository implements Repository { - /** @var Repository */ - private $packageRepository; + /** @var HandlingRepository[] */ + private array $repositories; - /** @var Repository */ - private $archiveRepository; - - public function __construct(Repository $packageRepository, Repository $archiveRepository) + public function __construct(HandlingRepository ...$repositories) { - $this->packageRepository = $packageRepository; - $this->archiveRepository = $archiveRepository; + $this->repositories = $repositories; } + /** + * @throws \Exception + */ public function get(string $package): Package { - $parts = parse_url($package); - if ($parts === false || (empty($parts['scheme']) && str_contains($parts['path'], ':'))) { - return $this->packageRepository->get($package); + foreach ($this->repositories as $repository) { + if ($repository->handles($package)) { + return $repository->get($package); + } } - return $this->archiveRepository->get($package); + throw new \Exception(sprintf('No handler found for URI "%s"', $package)); } } diff --git a/src/Types/Harness/Repository/ArchiveRepository.php b/src/Types/Harness/Repository/ArchiveRepository.php index c8cd57e1..c451da13 100644 --- a/src/Types/Harness/Repository/ArchiveRepository.php +++ b/src/Types/Harness/Repository/ArchiveRepository.php @@ -4,8 +4,13 @@ use my127\Workspace\Types\Harness\Repository\Package\Package; -class ArchiveRepository implements Repository +class ArchiveRepository implements HandlingRepository { + public function handles(string $uri): bool + { + return true; + } + public function get(string $package): Package { return new Package([ diff --git a/src/Types/Harness/Repository/GithubRepository.php b/src/Types/Harness/Repository/GithubRepository.php new file mode 100644 index 00000000..8f7ed203 --- /dev/null +++ b/src/Types/Harness/Repository/GithubRepository.php @@ -0,0 +1,33 @@ +.+):(?[^:]+)$#'; + public const KEY_REF = 'ref'; + public const KEY_URN = 'urn'; + + public function handles(string $uri): bool + { + return (bool) \preg_match(self::PATTERN, $uri); + } + + public function get(string $package): Package + { + if (!\preg_match(self::PATTERN, $package, $matches)) { + throw new \Exception("Package '$package' not matching git URL pattern."); + } + + return new Package([ + 'url' => $matches[self::KEY_URN], + 'ref' => $matches[self::KEY_REF], + 'git' => true, + ]); + } +} diff --git a/src/Types/Harness/Repository/HandlingRepository.php b/src/Types/Harness/Repository/HandlingRepository.php new file mode 100644 index 00000000..43264358 --- /dev/null +++ b/src/Types/Harness/Repository/HandlingRepository.php @@ -0,0 +1,10 @@ + rtrim(\substr($package, 6), '/') . '/', + 'localsync' => true, + ]); + } +} diff --git a/src/Types/Harness/Repository/PackageRepository.php b/src/Types/Harness/Repository/PackageRepository.php index f8e5f566..4486b054 100644 --- a/src/Types/Harness/Repository/PackageRepository.php +++ b/src/Types/Harness/Repository/PackageRepository.php @@ -7,7 +7,7 @@ use my127\Workspace\Types\Harness\Repository\Exception\UnknownPackage; use my127\Workspace\Types\Harness\Repository\Package\Package; -class PackageRepository implements Repository +class PackageRepository implements HandlingRepository { private const HARNESS_PACKAGE_PATTERN = '/^((?P[a-z0-9-]+)\/)?(?P[a-z0-9-]+){1}(:(?P[a-z0-9.-]+))?$/'; private const HARNESS_VERSION_PATTERN = '/^v(?[0-9x]+){1}(\.(?[0-9x]+))?(.(?[0-9x]+))?$/'; @@ -48,6 +48,13 @@ public function __construct(JsonLoader $fileLoader) $this->fileLoader = $fileLoader; } + public function handles(string $uri): bool + { + $parts = parse_url($uri); + + return $parts === false || (empty($parts['scheme']) && str_contains($parts['path'], ':')); + } + public function get(string $package): Package { $this->importPackagesFromSources(); diff --git a/src/Types/Workspace/Builder.php b/src/Types/Workspace/Builder.php index 2ee9cd6c..c53df8ea 100644 --- a/src/Types/Workspace/Builder.php +++ b/src/Types/Workspace/Builder.php @@ -122,14 +122,16 @@ public function build(Environment $environment, DefinitionCollection $definition ->usage('install') ->option('--step= Step from which to start installer. [default: 1]') ->option('--skip-events If set events will not be triggered.') + ->option('--force Force re-download and overwrite (in step=prepare)') ->action(function (Input $input) { $this->workspace->install($input); }); $this->application->section('harness download') ->usage('harness download') + ->option('--force Force re-download and overwrite') ->action(function (Input $input) { - $this->workspace->run('install --step=download'); + $this->workspace->run('install --step=download' . ($input->getOption('force')->value() ? ' --force' : '')); }); $this->application->section('harness prepare') diff --git a/src/Types/Workspace/Creator.php b/src/Types/Workspace/Creator.php index b45129b8..e6d191d3 100644 --- a/src/Types/Workspace/Creator.php +++ b/src/Types/Workspace/Creator.php @@ -2,9 +2,12 @@ namespace my127\Workspace\Types\Workspace; +use CzProject\GitPhp\Git; use my127\Workspace\Types\Crypt\Key; use my127\Workspace\Types\Harness\Repository\Package\Package; use my127\Workspace\Types\Harness\Repository\Repository; +use my127\Workspace\Utility\Filesystem; +use my127\Workspace\Utility\TmpNamType; use Symfony\Component\Yaml\Yaml; class Creator @@ -52,7 +55,7 @@ private function findHarnessLayers(string $harness): array $harnessLayers = [$harness]; - $harnessData = $this->parseYamlMergeStreams($this->downloadAndExtractHarnessYml($package)); + $harnessData = $this->parseYamlMergeStreams($this->acquireAndExtractHarnessYml($package)); if (!is_array($harnessData)) { throw new \Exception('Could not parse the harness\'s harness.yml file'); } @@ -89,8 +92,24 @@ private function parseYamlMergeStreams($content): array return $mergedDocument; } - private function downloadAndExtractHarnessYml(Package $package): string + private function acquireAndExtractHarnessYml(Package $package): string { + if ($package->getDist()['localsync'] ?? false) { + return file_get_contents($package->getDist()['url'] . 'harness.yml'); + } + + if ($package->getDist()['git'] ?? false) { + $packageDirPath = Filesystem::tempname(TmpNamType::PATH); + + $git = new Git(); + $git->cloneRepository($package->getDist()['url'], $packageDirPath, ['-q', '--depth', '1', '--branch', $package->getDist()['ref']]); + + $yaml = file_get_contents($packageDirPath . '/harness.yml'); + Filesystem::rrmdir($packageDirPath); + + return $yaml; + } + $packageTarball = tempnam(sys_get_temp_dir(), 'my127ws'); file_put_contents($packageTarball, file_get_contents($package->getDist()['url'])); @@ -98,7 +117,7 @@ private function downloadAndExtractHarnessYml(Package $package): string if ($packageDir === false) { throw new \Exception('Could not create temporary directory for harness'); } - unlink($packageDir); + \unlink($packageDir); if (!mkdir($packageDir, 0700)) { throw new \Exception('Could not create temporary ' . $packageDir . ' directory for harness'); } @@ -112,7 +131,7 @@ private function downloadAndExtractHarnessYml(Package $package): string if (is_dir($packageDir)) { exec('rm -rf ' . escapeshellarg($packageDir)); } - unlink($packageTarball); + \unlink($packageTarball); if ($harnessYaml === false) { throw new \Exception('Could not parse the harness\'s harness.yml file'); diff --git a/src/Types/Workspace/Installer.php b/src/Types/Workspace/Installer.php index 644b4fbb..1a78dffc 100644 --- a/src/Types/Workspace/Installer.php +++ b/src/Types/Workspace/Installer.php @@ -3,6 +3,7 @@ namespace my127\Workspace\Types\Workspace; use Composer\Semver\Semver; +use CzProject\GitPhp\Git; use my127\Workspace\Application; use my127\Workspace\Path\Path; use my127\Workspace\Terminal\Terminal; @@ -12,6 +13,8 @@ use my127\Workspace\Types\Harness\Harness; use my127\Workspace\Types\Harness\Repository\Package\Package; use my127\Workspace\Types\Harness\Repository\Repository; +use my127\Workspace\Utility\Filesystem; +use my127\Workspace\Utility\TmpNamType; use Symfony\Component\Yaml\Yaml; class Installer @@ -101,7 +104,7 @@ public function getStep(?string $step) return $this->stepMap[$step]; } - public function install($step = null, $cascade = true, $events = true) + public function install($step = null, $cascade = true, $events = true, $force = false) { $packages = array_map( function ($harnessName) { @@ -120,7 +123,7 @@ function ($harnessName) { if ($events) { $this->workspace->trigger('before.harness.install'); } - $this->downloadAndExtractHarnessPackages($packages); + $this->downloadAndExtractHarnessPackages($packages, $force); break; case self::STEP_OVERLAY: if (($overlayPath = $this->workspace->getOverlayPath()) !== null) { @@ -166,24 +169,34 @@ function ($harnessName) { /** * @param Package[] $packages */ - private function downloadAndExtractHarnessPackages(array $packages) + private function downloadAndExtractHarnessPackages(array $packages, bool $force) { $harnessInstallPath = $this->workspace->getPath() . '/.my127ws'; - if (!is_dir($harnessInstallPath)) { - mkdir($harnessInstallPath, 0755, true); + if (!is_dir($harnessInstallPath) || $force) { + @mkdir($harnessInstallPath, 0755, true); foreach ($packages as $package) { - $this->downloadAndExtractHarnessPackage($package); + $this->acquireHarnessPackage($package); } } } - private function downloadAndExtractHarnessPackage(Package $package): void + private function acquireHarnessPackage(Package $package): void { - $packageTarball = tempnam(sys_get_temp_dir(), 'my127ws'); - file_put_contents($packageTarball, file_get_contents($package->getDist()['url'])); - passthru('tar -zxf ' . escapeshellarg($packageTarball) . ' --strip=1 -C .my127ws'); - unlink($packageTarball); + if ($package->getDist()['localsync'] ?? false) { + passthru('rsync -r ' . escapeshellarg($package->getDist()['url']) . ' .my127ws'); + } elseif ($package->getDist()['git'] ?? false) { + $packageDirPath = Filesystem::tempname(TmpNamType::PATH); + $git = new Git(); + $git->cloneRepository($package->getDist()['url'], $packageDirPath, ['-q', '--depth', '1', '--branch', $package->getDist()['ref']]); + passthru('rsync -r --exclude .git/ ' . escapeshellarg($packageDirPath) . '/ .my127ws'); + Filesystem::rrmdir($packageDirPath); + } else { + $packageTarball = tempnam(sys_get_temp_dir(), 'my127ws'); + file_put_contents($packageTarball, file_get_contents($package->getDist()['url'])); + passthru('tar -zxf ' . escapeshellarg($packageTarball) . ' --strip=1 -C .my127ws'); + unlink($packageTarball); + } } private function ensureRequiredAttributesArePresent(array $required): void diff --git a/src/Types/Workspace/Workspace.php b/src/Types/Workspace/Workspace.php index 090de90d..d894aa49 100644 --- a/src/Types/Workspace/Workspace.php +++ b/src/Types/Workspace/Workspace.php @@ -80,6 +80,8 @@ public function install(Input $input): void $step = $step instanceof StringOptionValue ? $step->value() : ''; $cascade = true; $events = $input->getOption('skip-events'); + $force = (bool) $input->getOption('force')->value(); + $events = $events instanceof BooleanOptionValue ? !$events->value() : true; if (!is_numeric($step)) { @@ -87,7 +89,7 @@ public function install(Input $input): void $step = $installer->getStep($step); } - $installer->install($step, $cascade, $events); + $installer->install($step, $cascade, $events, $force); } public function refresh(): void diff --git a/src/Utility/Filesystem.php b/src/Utility/Filesystem.php index 3a516570..643c5c0e 100644 --- a/src/Utility/Filesystem.php +++ b/src/Utility/Filesystem.php @@ -4,6 +4,8 @@ class Filesystem { + public const TMPNAM_DEFAULT_PREFIX = 'my127ws'; + public static function upsearch(string $name, string $startFrom): ?string { $path = null; @@ -44,7 +46,7 @@ public static function rcopy($src, $dst) $dir = opendir($src); if (!is_dir($dst)) { - mkdir($dst); + \mkdir($dst); } while (false !== ($file = readdir($dir))) { @@ -75,4 +77,30 @@ public static function rsearch($folder, $pattern) return $fileList; } + + public static function tempname(TmpNamType $type = TmpNamType::PATH, string $prefix = self::TMPNAM_DEFAULT_PREFIX): string + { + if (false === ($tmpFilePath = tempnam(sys_get_temp_dir(), $prefix))) { + throw new \Exception('Could not create temporary ' . $type->value); + } + + if ($type === TmpNamType::FILE) { + return $tmpFilePath; + } + + if (unlink($tmpFilePath) === false) { + throw new \Exception('Could not remove temporary filepath ' . $tmpFilePath); + } + + if ($type === TmpNamType::PATH) { + return $tmpFilePath; + } + + \mkdir($tmpFilePath, 0777, true); + if (\mkdir($tmpFilePath, 0777, true) === false) { + throw new \Exception('Could not create temporary directory ' . $tmpFilePath); + } + + return $tmpFilePath; + } } diff --git a/src/Utility/TmpNamType.php b/src/Utility/TmpNamType.php new file mode 100644 index 00000000..e294d571 --- /dev/null +++ b/src/Utility/TmpNamType.php @@ -0,0 +1,12 @@ +createStub(Repository::class); - $packageRepo = $this->createStub(Repository::class); - - $package1 = new Package(); - $archiveRepo->method('get')->willReturn($package1); - - $sut = new AggregateRepository($archiveRepo, $packageRepo); - $got = $sut->get('inviqa/go:v0.7.0'); - - $this->assertSame($package1, $got); - } - - /** @test */ - public function itUsesArchiveRepositoryForUrlBasedPackageNames() - { - $archiveRepo = $this->createStub(Repository::class); - $packageRepo = $this->createStub(Repository::class); - - $package1 = new Package(); - $archiveRepo->method('get')->willReturn($package1); - - $sut = new AggregateRepository($archiveRepo, $packageRepo); - $got = $sut->get('https://foo.com/inviqa/go/master.tgz'); - - $this->assertInstanceOf(Package::class, $got); - } - - /** @test */ - public function itUsesArchiveRepositoryForFileUriBasedPackageNames() - { - $archiveRepo = $this->createStub(Repository::class); - $packageRepo = $this->createStub(Repository::class); - - $package1 = new Package(); - $archiveRepo->method('get')->willReturn($package1); - - $sut = new AggregateRepository($archiveRepo, $packageRepo); - $got = $sut->get('file:///home/inviqa/go/master.tgz'); - - $this->assertInstanceOf(Package::class, $got); - } - - /** @test */ - public function itUsesArchiveRepositoryForFileBasedPackageNames() - { - $archiveRepo = $this->createStub(Repository::class); - $packageRepo = $this->createStub(Repository::class); - - $package1 = new Package(); - $archiveRepo->method('get')->willReturn($package1); - - $sut = new AggregateRepository($archiveRepo, $packageRepo); - $got = $sut->get('/home/inviqa/go/master.tgz'); - - $this->assertInstanceOf(Package::class, $got); + $repoNo = $this->createMock(HandlingRepository::class); + $repoNo + ->method('handles') + ->willReturn(false); + $repoNo + ->expects($this->never()) + ->method('get'); + + $repoYes = $this->createMock(HandlingRepository::class); + $repoYes + ->method('handles') + ->willReturn(true); + + $repoYes + ->expects($this->once()) + ->method('get'); + + $sut = new AggregateRepository($repoNo, $repoYes); + + $sut->get('foobar'); } /** @test */ - public function itUsesArchiveRepositoryForUriWithoutPathBasedPackageNames() + public function itThrowsAnExceptionWhenNoRepositoryHandles() { - $archiveRepo = $this->createStub(Repository::class); - $packageRepo = $this->createStub(Repository::class); + $repoNo = $this->createMock(HandlingRepository::class); + $repoNo + ->method('handles') + ->willReturn(false); - $package1 = new Package(); - $archiveRepo->method('get')->willReturn($package1); + $sut = new AggregateRepository($repoNo); - $sut = new AggregateRepository($archiveRepo, $packageRepo); - $got = $sut->get('example:'); + $this->expectException(\Exception::class); - $this->assertInstanceOf(Package::class, $got); + $sut->get('foobar'); } } diff --git a/tests/Test/Types/Harness/Repository/ArchiveRepositoryTest.php b/tests/Test/Types/Harness/Repository/ArchiveRepositoryTest.php index 784b5c7c..c11ca98c 100644 --- a/tests/Test/Types/Harness/Repository/ArchiveRepositoryTest.php +++ b/tests/Test/Types/Harness/Repository/ArchiveRepositoryTest.php @@ -16,4 +16,15 @@ public function itCreatesAPackageFromThePackageUrl() $this->assertEquals(new Package(['url' => 'https://github.com/inviqa/harness-go/archive/master.tar.gz']), $got); } + + /** @test */ + public function itHandlesAllUrls() + { + $sut = new ArchiveRepository(); + + $this->assertTrue($sut->handles('https://foo.com/inviqa/go/master.tgz')); + $this->assertTrue($sut->handles('file:///home/inviqa/go/master.tgz')); + $this->assertTrue($sut->handles('/home/inviqa/go/master.tgz')); + $this->assertTrue($sut->handles('example:')); + } } diff --git a/tests/Test/Types/Harness/Repository/GitRepositoryTest.php b/tests/Test/Types/Harness/Repository/GitRepositoryTest.php new file mode 100644 index 00000000..196dd3ab --- /dev/null +++ b/tests/Test/Types/Harness/Repository/GitRepositoryTest.php @@ -0,0 +1,28 @@ +get('github:git@github.com:inviqa/harness-base-php.git:0.4.x'); + + $this->assertEquals(new Package(['url' => 'git@github.com:inviqa/harness-base-php.git', 'ref' => '0.4.x', 'git' => true]), $got); + } + + /* @test * */ + public function itHandlesOnlyGithubSshUrls() + { + $sut = new GithubRepository(); + + $this->assertTrue($sut->handles('github:git@github.com:inviqa/harness-base-php.git:0.4.x')); + $this->assertFalse($sut->handles('inviqa/harness-foo:v1.2.3')); + } +} diff --git a/tests/Test/Types/Harness/Repository/LocalSyncRepositoryTest.php b/tests/Test/Types/Harness/Repository/LocalSyncRepositoryTest.php new file mode 100644 index 00000000..810ff9fd --- /dev/null +++ b/tests/Test/Types/Harness/Repository/LocalSyncRepositoryTest.php @@ -0,0 +1,28 @@ +get('sync://foo/bar'); + + $this->assertEquals(new Package(['url' => '/foo/bar/', 'localsync' => true]), $got); + } + + /* @test * */ + public function itHandlesOnlySyncUrls() + { + $sut = new LocalSyncRepository(); + + $this->assertTrue($sut->handles('sync://foo/bar')); + $this->assertFalse($sut->handles('file:///foo/bar')); + } +} diff --git a/tests/Test/Types/Harness/Repository/PackageRepositoryTest.php b/tests/Test/Types/Harness/Repository/PackageRepositoryTest.php index bb52d2e5..404c0edd 100644 --- a/tests/Test/Types/Harness/Repository/PackageRepositoryTest.php +++ b/tests/Test/Types/Harness/Repository/PackageRepositoryTest.php @@ -11,6 +11,15 @@ class PackageRepositoryTest extends IntegrationTestCase { + /** @test */ + public function itHandlesPackageNames() + { + $sut = $this->createRepository(); + + $this->assertTrue($sut->handles('test/package:v2.0.0')); + $this->assertFalse($sut->handles('foobar')); + } + /** @test */ public function itThrowsExceptionWhenRequestingAnInvalidPackageName(): void {