diff --git a/README.md b/README.md index 03733ac..5da7dec 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,41 @@ current root directory the whole command simplifies to: ./vendor/bin/tailor ter:publish 1.2.0 ``` +The version can be skipped as well, since Tailor is able to +determine it from your extension: + +```bash +./vendor/bin/tailor ter:publish +``` + +The following sources are used, in this order: + +1. The **version argument**, if given. +2. The **tag of the checked out commit**, no matter whether it's + prefixed with `v` (e.g. `v1.2.0`) or not. +3. The version in **`ext_emconf.php`**. +4. The version in **`composer.json`**, either on root level or at + `[extra][typo3/cms][version]`. + +The last two are the ones written by the +[`set-version`](#update-the-version-in-your-extension-files) +command. Tailor tells you which version it uses and where it +comes from, e.g. +`Using version 1.2.0 from the tag of the checked out commit.` + +Since an extension key never looks like a version, it can still +be passed as only argument: + +```bash +./vendor/bin/tailor ter:publish my_extension +``` + +> [!NOTE] +> The version has to be stated as argument if the checked out +> commit is tagged with more than one version, or if you publish +> an `--artefact` which does not belong to the extension in your +> current working directory. + > [!IMPORTANT] > A couple of directories and files are excluded from packaging > by default. Read more about @@ -469,9 +504,14 @@ git push origin --tags **Step 4: Push this version to TER** ```bash -./vendor/bin/tailor ter:publish 1.5.0 +./vendor/bin/tailor ter:publish ``` +Since **Step 1** wrote the version into your extension files and +**Step 2** tagged the commit with it, the version does not have to +be repeated here. State it as argument, e.g. `ter:publish 1.5.0`, +if you want to publish another version. + > [!NOTE] > Both `set-version` and `ter:publish` provide options > to specify the location of your extension. If, like in the example diff --git a/src/Command/Extension/UploadExtensionVersionCommand.php b/src/Command/Extension/UploadExtensionVersionCommand.php index 3c06f10..f5965f0 100644 --- a/src/Command/Extension/UploadExtensionVersionCommand.php +++ b/src/Command/Extension/UploadExtensionVersionCommand.php @@ -48,19 +48,30 @@ protected function configure(): void $this ->setDescription('Publishes a new version of an extension to TER') ->setResultFormat(ConsoleFormatter::FORMAT_DETAIL) - ->addArgument('version', InputArgument::REQUIRED, 'The version to publish, e.g. 1.2.3') + ->addArgument('version', InputArgument::OPTIONAL, 'The version to publish, e.g. 1.2.3. Defaults to the tag of the checked out commit.') ->addArgument('extensionkey', InputArgument::OPTIONAL, 'The extension key') - ->addOption('path', '', InputOption::VALUE_OPTIONAL, 'Path to the extension folder') + ->addOption('path', '', InputOption::VALUE_OPTIONAL, 'Path to the extension folder. Defaults to the current working directory.') ->addOption('artefact', '', InputOption::VALUE_OPTIONAL, 'Path or URL to a zip file') ->addOption('comment', '', InputOption::VALUE_OPTIONAL, 'Upload comment of the new version (e.g. release notes)'); } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->version = $input->getArgument('version'); + CommandHelper::normalizeVersionAndExtensionKeyArguments($input); + + $resolvedVersion = CommandHelper::getVersionFromInput($input); + $this->version = $resolvedVersion->getVersion(); $this->extensionKey = CommandHelper::getExtensionKeyFromInput($input); $this->transactionPath = rtrim(realpath(getcwd() ?: './'), '/') . '/tailor-version-upload'; + if (!$resolvedVersion->isFromArgument() && $input->getOption('raw') === false) { + $output->writeln(sprintf( + 'Using version %s from %s.', + $resolvedVersion->getVersion(), + $resolvedVersion->getSource() + )); + } + if (!(new Filesystem\Directory())->create($this->transactionPath)) { throw new \RuntimeException(sprintf('Directory could not be created.')); } diff --git a/src/Dto/ResolvedVersion.php b/src/Dto/ResolvedVersion.php new file mode 100644 index 0000000..74ebd92 --- /dev/null +++ b/src/Dto/ResolvedVersion.php @@ -0,0 +1,54 @@ +version = $version; + $this->source = $source; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getSource(): string + { + return $this->source; + } + + public function isFromArgument(): bool + { + return $this->source === self::SOURCE_ARGUMENT; + } +} diff --git a/src/Exception/VersionMissingException.php b/src/Exception/VersionMissingException.php new file mode 100644 index 0000000..f9b0e33 --- /dev/null +++ b/src/Exception/VersionMissingException.php @@ -0,0 +1,15 @@ +composerSchema['extra']['typo3/cms']['extension-key'] ?? ''; } + + /** + * The version of the extension, as maintained by the `set-version` command. + * Both the root level and the TYPO3 specific section are taken into account. + */ + public function getVersion(): string + { + $version = $this->composerSchema['version'] + ?? $this->composerSchema['extra']['typo3/cms']['version'] + ?? ''; + + return trim((string)$version); + } } diff --git a/src/Filesystem/EmConfReader.php b/src/Filesystem/EmConfReader.php new file mode 100644 index 0000000..d86e2f5 --- /dev/null +++ b/src/Filesystem/EmConfReader.php @@ -0,0 +1,47 @@ +configuration = $configuration; + } + } + + public function getVersion(): string + { + return trim((string)($this->configuration['version'] ?? '')); + } +} diff --git a/src/Helper/CommandHelper.php b/src/Helper/CommandHelper.php index f15a806..33446a6 100644 --- a/src/Helper/CommandHelper.php +++ b/src/Helper/CommandHelper.php @@ -13,9 +13,14 @@ namespace TYPO3\Tailor\Helper; use Symfony\Component\Console\Input\InputInterface; +use TYPO3\Tailor\Dto\ResolvedVersion; use TYPO3\Tailor\Environment\Variables; use TYPO3\Tailor\Exception\ExtensionKeyMissingException; +use TYPO3\Tailor\Exception\VersionMissingException; use TYPO3\Tailor\Filesystem\ComposerReader; +use TYPO3\Tailor\Filesystem\EmConfReader; +use TYPO3\Tailor\Service\GitService; +use TYPO3\Tailor\Validation\VersionValidator; /** * Helper class for console commands. @@ -47,6 +52,91 @@ final class CommandHelper 'typo3extension', ]; + /** + * The version is looked up in the extension itself if not given as argument, + * so a release does not have to repeat its version on the command line. + */ + public static function getVersionFromInput(InputInterface $input): ResolvedVersion + { + // 1. CLI argument has highest priority + $version = (string)($input->getArgument('version') ?? ''); + if ($version !== '') { + return new ResolvedVersion($version, ResolvedVersion::SOURCE_ARGUMENT); + } + + $path = self::getPathFromInput($input); + + // 2. The tag of the checked out commit marks the released version + $versions = (new GitService())->getVersionsFromTagsOfHead($path); + + if (count($versions) > 1) { + throw new VersionMissingException( + sprintf( + 'The checked out commit is tagged with more than one version (%s). Please state the version to use as argument.', + implode(', ', $versions) + ), + 1786492801 + ); + } + + if ($versions !== []) { + return new ResolvedVersion($versions[0], ResolvedVersion::SOURCE_GIT_TAG); + } + + // 3. The version files, as written by the `set-version` command + $versionValidator = new VersionValidator(); + + $version = (new EmConfReader($path))->getVersion(); + if ($versionValidator->isValid($version)) { + return new ResolvedVersion($version, ResolvedVersion::SOURCE_EMCONF); + } + + $version = (new ComposerReader($path))->getVersion(); + if ($versionValidator->isValid($version)) { + return new ResolvedVersion($version, ResolvedVersion::SOURCE_COMPOSER); + } + + throw new VersionMissingException( + 'The version must either be set as argument, or be available in the tag of the checked out commit, ' + . 'in `ext_emconf.php` or in `composer.json`.', + 1786492802 + ); + } + + /** + * Move the extension key to its argument if it was passed as only argument. + * + * Since the version argument comes first and is optional, a single argument + * is ambiguous. Extension keys never look like a version though, so + * `ter:publish my_extension` can safely be told apart from `ter:publish 1.2.3`. + */ + public static function normalizeVersionAndExtensionKeyArguments(InputInterface $input): void + { + if (!$input->hasArgument('version') || !$input->hasArgument('extensionkey')) { + return; + } + + $version = (string)($input->getArgument('version') ?? ''); + $extensionKey = (string)($input->getArgument('extensionkey') ?? ''); + + if ($extensionKey !== '' || !preg_match('/^[a-z][a-z0-9_]+$/', $version)) { + return; + } + + $input->setArgument('extensionkey', $version); + $input->setArgument('version', null); + } + + /** + * The path of the extension to work with. Defaults to the current working directory. + */ + public static function getPathFromInput(InputInterface $input): string + { + $path = $input->hasOption('path') ? (string)($input->getOption('path') ?? '') : ''; + + return $path !== '' ? $path : (string)(getcwd() ?: '.'); + } + public static function getExtensionKeyFromInput(InputInterface $input): string { // 1. CLI argument has highest priority @@ -56,8 +146,8 @@ public static function getExtensionKeyFromInput(InputInterface $input): string return $key; } - // 2. composer.json is the recommended source - $extensionKeyFromComposer = (new ComposerReader())->getExtensionKey(); + // 2. composer.json of the extension is the recommended source + $extensionKeyFromComposer = (new ComposerReader(self::getPathFromInput($input)))->getExtensionKey(); if ($extensionKeyFromComposer !== '') { return $extensionKeyFromComposer; } diff --git a/src/Service/GitService.php b/src/Service/GitService.php new file mode 100644 index 0000000..bfba6a3 --- /dev/null +++ b/src/Service/GitService.php @@ -0,0 +1,76 @@ + The versions, without duplicates + */ + public function getVersionsFromTagsOfHead(string $path): array + { + $versionValidator = new VersionValidator(); + $versions = []; + + foreach ($this->getTagsOfHead($path) as $tag) { + $version = (string)preg_replace('/^v/i', '', $tag); + + if ($versionValidator->isValid($version) && !in_array($version, $versions, true)) { + $versions[] = $version; + } + } + + return $versions; + } + + /** + * Return all tags of the currently checked out commit. + * + * Anything preventing us from asking git - a missing binary, a disabled + * exec() or a path which is no repository at all - just means there is + * no tag to work with. + * + * @param string $path A path within the git repository + * @return array + */ + protected function getTagsOfHead(string $path): array + { + if (!function_exists('exec') || !is_dir($path)) { + return []; + } + + $output = []; + $exitCode = 0; + + // stderr is redirected into the captured output, so failures of the + // command (e.g. no repository) do not show up on the console. + @exec(sprintf('git -C %s tag --points-at HEAD 2>&1', escapeshellarg($path)), $output, $exitCode); + + if ($exitCode !== 0) { + return []; + } + + return array_values(array_filter(array_map('trim', $output))); + } +} diff --git a/tests/Unit/Command/Extension/UploadExtensionVersionCommandTest.php b/tests/Unit/Command/Extension/UploadExtensionVersionCommandTest.php index 1e90327..ee5a8d8 100644 --- a/tests/Unit/Command/Extension/UploadExtensionVersionCommandTest.php +++ b/tests/Unit/Command/Extension/UploadExtensionVersionCommandTest.php @@ -14,11 +14,14 @@ use PHPUnit\Framework\Attributes\Test; use TYPO3\Tailor\Command\Extension\UploadExtensionVersionCommand; +use TYPO3\Tailor\Exception\VersionMissingException; use TYPO3\Tailor\Tests\Unit\Command\AbstractCommandTestCase; +use TYPO3\Tailor\Tests\Unit\GitRepositoryTrait; class UploadExtensionVersionCommandTest extends AbstractCommandTestCase { use ExtensionDirectoryTrait; + use GitRepositoryTrait; protected function setUp(): void { @@ -110,6 +113,60 @@ public function transactionDirectoryIsRemovedAfterwards(): void self::assertDirectoryDoesNotExist($this->workingDirectory . '/tailor-version-upload'); } + #[Test] + public function versionIsTakenFromTheTagOfTheCheckedOutCommit(): void + { + $this->createGitRepository($this->extensionDirectory, '1.2.3'); + $tester = $this->apiTester($this->command(), self::jsonResponse(['number' => '1.2.3'], 201)); + + self::assertSame(0, $tester->execute(['--path' => $this->extensionDirectory])); + self::assertSame(self::BASE_URI . 'extension/my_ext/1.2.3', $this->request()['url']); + self::assertDisplayContains('Using version 1.2.3 from the tag of the checked out commit.', $tester); + } + + #[Test] + public function versionTagMayBePrefixed(): void + { + $this->createGitRepository($this->extensionDirectory, 'v1.2.3'); + $tester = $this->apiTester($this->command(), self::jsonResponse([], 201)); + + self::assertSame(0, $tester->execute(['--path' => $this->extensionDirectory])); + self::assertSame(self::BASE_URI . 'extension/my_ext/1.2.3', $this->request()['url']); + } + + #[Test] + public function extensionKeyCanBeGivenAsSoleArgument(): void + { + $this->createGitRepository($this->extensionDirectory, '1.2.3'); + $tester = $this->apiTester($this->command(), self::jsonResponse([], 201)); + + self::assertSame(0, $tester->execute(['version' => 'my_ext', '--path' => $this->extensionDirectory])); + self::assertSame(self::BASE_URI . 'extension/my_ext/1.2.3', $this->request()['url']); + } + + #[Test] + public function versionIsTakenFromExtEmconfIfTheCommitIsNotTagged(): void + { + $this->createGitRepository($this->extensionDirectory); + $tester = $this->apiTester($this->command(), self::jsonResponse([], 201)); + + self::assertSame(0, $tester->execute(['--path' => $this->extensionDirectory])); + self::assertSame(self::BASE_URI . 'extension/my_ext/1.2.3', $this->request()['url']); + self::assertDisplayContains('Using version 1.2.3 from ext_emconf.php.', $tester); + } + + #[Test] + public function ambiguousVersionTagsAreRejected(): void + { + $this->createGitRepository($this->extensionDirectory, '1.2.3', '1.2.4'); + $tester = $this->apiTester($this->command(), self::jsonResponse([], 201)); + + $this->expectException(VersionMissingException::class); + $this->expectExceptionMessage('tagged with more than one version (1.2.3, 1.2.4)'); + + $tester->execute(['--path' => $this->extensionDirectory]); + } + #[Test] public function failingRequestReturnsFailure(): void { diff --git a/tests/Unit/Filesystem/ComposerReaderTest.php b/tests/Unit/Filesystem/ComposerReaderTest.php index 781b249..45e9b64 100644 --- a/tests/Unit/Filesystem/ComposerReaderTest.php +++ b/tests/Unit/Filesystem/ComposerReaderTest.php @@ -70,4 +70,33 @@ public function readCorrectExtensionKeyFromGivenComposerJsonFile(): void $subject = new ComposerReader('tmp'); self::assertSame('my-extension', $subject->getExtensionKey()); } + + #[Test] + public function returnEmptyStringIfVersionNotGiven(): void + { + $composerContent = file_get_contents(__DIR__ . '/../Fixtures/Composer/composer_no_extension_key.json'); + file_put_contents(self::COMPOSER_FILE, $composerContent); + $subject = new ComposerReader('tmp'); + self::assertEmpty($subject->getVersion()); + } + + #[Test] + public function readCorrectVersionFromGivenComposerJsonFile(): void + { + $composerContent = file_get_contents(__DIR__ . '/../Fixtures/Composer/composer_with_extension_key.json'); + file_put_contents(self::COMPOSER_FILE, $composerContent); + $subject = new ComposerReader('tmp'); + self::assertSame('1.0.0', $subject->getVersion()); + } + + #[Test] + public function versionOnRootLevelIsPreferred(): void + { + file_put_contents(self::COMPOSER_FILE, (string)json_encode([ + 'version' => '2.0.0', + 'extra' => ['typo3/cms' => ['version' => '1.0.0']], + ])); + $subject = new ComposerReader('tmp'); + self::assertSame('2.0.0', $subject->getVersion()); + } } diff --git a/tests/Unit/Filesystem/EmConfReaderTest.php b/tests/Unit/Filesystem/EmConfReaderTest.php new file mode 100644 index 0000000..f13f8d1 --- /dev/null +++ b/tests/Unit/Filesystem/EmConfReaderTest.php @@ -0,0 +1,81 @@ +getVersion()); + } + + #[Test] + public function returnEmptyStringIfVersionNotGiven(): void + { + $this->useFixture('emconf_no_version.php'); + + self::assertEmpty((new EmConfReader(self::EMCONF_DIRECTORY))->getVersion()); + } + + #[Test] + public function returnEmptyStringIfEmConfHasNoValidStructure(): void + { + $this->useFixture('emconf_invalid.php'); + + self::assertEmpty((new EmConfReader(self::EMCONF_DIRECTORY))->getVersion()); + } + + #[Test] + public function readCorrectVersionFromGivenEmConfFile(): void + { + $this->useFixture('emconf_valid.php'); + + self::assertSame('1.0.0', (new EmConfReader(self::EMCONF_DIRECTORY))->getVersion()); + } + + #[Test] + public function surroundingWhitespaceOfTheVersionIsStripped(): void + { + file_put_contents(self::EMCONF_FILE, " '1.0.0 '];\n"); + + self::assertSame('1.0.0', (new EmConfReader(self::EMCONF_DIRECTORY))->getVersion()); + } + + private function useFixture(string $filename): void + { + copy(__DIR__ . '/../Fixtures/EmConf/' . $filename, self::EMCONF_FILE); + } +} diff --git a/tests/Unit/GitRepositoryTrait.php b/tests/Unit/GitRepositoryTrait.php new file mode 100644 index 0000000..138f226 --- /dev/null +++ b/tests/Unit/GitRepositoryTrait.php @@ -0,0 +1,65 @@ +git($path, 'init -q'); + $this->git($path, 'add -A'); + $this->git($path, 'commit -q -m "Add extension"'); + + foreach ($tags as $tag) { + $this->git($path, 'tag ' . escapeshellarg($tag)); + } + } + + private function git(string $path, string $arguments): void + { + $command = sprintf( + 'git -C %s -c user.name=Tailor -c user.email=tailor@example.org -c commit.gpgsign=false %s 2>&1', + escapeshellarg($path), + $arguments + ); + + $output = []; + $exitCode = 0; + exec($command, $output, $exitCode); + + if ($exitCode !== 0) { + self::fail(sprintf('Command "%s" failed: %s', $command, implode(PHP_EOL, $output))); + } + } + + private static function isGitAvailable(): bool + { + $output = []; + $exitCode = 0; + exec('git --version 2>&1', $output, $exitCode); + + return $exitCode === 0; + } +} diff --git a/tests/Unit/Helper/CommandHelperTest.php b/tests/Unit/Helper/CommandHelperTest.php index 894d4aa..70459f6 100644 --- a/tests/Unit/Helper/CommandHelperTest.php +++ b/tests/Unit/Helper/CommandHelperTest.php @@ -18,11 +18,18 @@ use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use TYPO3\Tailor\Dto\ResolvedVersion; use TYPO3\Tailor\Exception\ExtensionKeyMissingException; +use TYPO3\Tailor\Exception\VersionMissingException; use TYPO3\Tailor\Helper\CommandHelper; +use TYPO3\Tailor\Tests\Unit\GitRepositoryTrait; final class CommandHelperTest extends TestCase { + use GitRepositoryTrait; + /** * @var InputDefinition */ @@ -33,16 +40,24 @@ final class CommandHelperTest extends TestCase */ private $input; + /** + * @var string + */ + private $extensionPath = ''; + protected function setUp(): void { $this->definition = new InputDefinition(); $this->input = new ArrayInput([], $this->definition); + $this->extensionPath = sys_get_temp_dir() . '/tailor-helper-' . bin2hex(random_bytes(6)); + mkdir($this->extensionPath, 0777, true); } protected function tearDown(): void { // Clean up environment variable after each test putenv('TYPO3_EXTENSION_KEY'); + $this->removeExtensionPath(); } #[Test] @@ -100,6 +115,127 @@ public function getExtensionKeyFromInputReturnsExtensionKeyFromEnvironmentVariab } } + #[Test] + public function getVersionFromInputPrefersTheArgumentOverEveryOtherSource(): void + { + $this->writeEmConf('1.2.3'); + $this->createGitRepository($this->extensionPath, '2.0.0'); + + $version = CommandHelper::getVersionFromInput($this->versionInput(['version' => '3.0.0'])); + + self::assertSame('3.0.0', $version->getVersion()); + self::assertSame(ResolvedVersion::SOURCE_ARGUMENT, $version->getSource()); + self::assertTrue($version->isFromArgument()); + } + + #[Test] + public function getVersionFromInputPrefersTheTagOverTheVersionFiles(): void + { + $this->writeEmConf('1.2.3'); + $this->writeComposerJson('1.2.3'); + $this->createGitRepository($this->extensionPath, '2.0.0'); + + $version = CommandHelper::getVersionFromInput($this->versionInput()); + + self::assertSame('2.0.0', $version->getVersion()); + self::assertSame(ResolvedVersion::SOURCE_GIT_TAG, $version->getSource()); + } + + #[Test] + public function getVersionFromInputReturnsEmConfVersionForUntaggedCommits(): void + { + $this->writeEmConf('1.2.3'); + $this->writeComposerJson('2.0.0'); + $this->createGitRepository($this->extensionPath); + + $version = CommandHelper::getVersionFromInput($this->versionInput()); + + self::assertSame('1.2.3', $version->getVersion()); + self::assertSame(ResolvedVersion::SOURCE_EMCONF, $version->getSource()); + } + + #[Test] + public function getVersionFromInputFallsBackToComposerJson(): void + { + $this->writeEmConf(null); + $this->writeComposerJson('2.0.0'); + + $version = CommandHelper::getVersionFromInput($this->versionInput()); + + self::assertSame('2.0.0', $version->getVersion()); + self::assertSame(ResolvedVersion::SOURCE_COMPOSER, $version->getSource()); + } + + #[Test] + public function getVersionFromInputTakesTheTypo3SectionOfComposerJsonIntoAccount(): void + { + file_put_contents($this->extensionPath . '/composer.json', (string)json_encode([ + 'name' => 'vendor/my-ext', + 'extra' => ['typo3/cms' => ['extension-key' => 'my_ext', 'version' => '2.0.0']], + ])); + + self::assertSame('2.0.0', CommandHelper::getVersionFromInput($this->versionInput())->getVersion()); + } + + #[Test] + public function getVersionFromInputIgnoresValuesWhichAreNoVersion(): void + { + $this->writeEmConf('dev-main'); + $this->writeComposerJson('2.0.0'); + + self::assertSame('2.0.0', CommandHelper::getVersionFromInput($this->versionInput())->getVersion()); + } + + #[Test] + public function getVersionFromInputThrowsExceptionIfNoVersionCanBeDetermined(): void + { + $this->writeEmConf(null); + $this->writeComposerJson(null); + + $this->expectException(VersionMissingException::class); + $this->expectExceptionCode(1786492802); + + CommandHelper::getVersionFromInput($this->versionInput()); + } + + #[Test] + public function getVersionFromInputThrowsExceptionIfTheCommitIsTaggedWithSeveralVersions(): void + { + $this->writeEmConf('1.2.3'); + $this->createGitRepository($this->extensionPath, '2.0.0', '2.0.1'); + + $this->expectException(VersionMissingException::class); + $this->expectExceptionCode(1786492801); + + CommandHelper::getVersionFromInput($this->versionInput()); + } + + #[Test] + public function normalizeVersionAndExtensionKeyArgumentsMovesASoleExtensionKeyToItsArgument(): void + { + $input = $this->versionInput(['version' => 'my_ext']); + CommandHelper::normalizeVersionAndExtensionKeyArguments($input); + + self::assertNull($input->getArgument('version')); + self::assertSame('my_ext', $input->getArgument('extensionkey')); + } + + #[Test] + public function normalizeVersionAndExtensionKeyArgumentsKeepsGivenArguments(): void + { + $input = $this->versionInput(['version' => '1.2.3', 'extensionkey' => 'my_ext']); + CommandHelper::normalizeVersionAndExtensionKeyArguments($input); + + self::assertSame('1.2.3', $input->getArgument('version')); + self::assertSame('my_ext', $input->getArgument('extensionkey')); + } + + #[Test] + public function getPathFromInputFallsBackToTheCurrentWorkingDirectory(): void + { + self::assertSame((string)getcwd(), CommandHelper::getPathFromInput($this->input)); + } + /** * @param string[] $expected */ @@ -137,4 +273,60 @@ public static function tagsImpliedByTerDataProvider(): array 'domain tag with separator survives' => ['e-commerce,tt_news', []], ]; } + + /** + * @param array $parameters + */ + private function versionInput(array $parameters = []): InputInterface + { + return new ArrayInput($parameters + ['--path' => $this->extensionPath], new InputDefinition([ + new InputArgument('version', InputArgument::OPTIONAL), + new InputArgument('extensionkey', InputArgument::OPTIONAL), + new InputOption('path', '', InputOption::VALUE_OPTIONAL), + ])); + } + + private function writeEmConf(?string $version): void + { + $version = $version === null ? '' : sprintf(" 'version' => '%s',\n", $version); + + file_put_contents($this->extensionPath . '/ext_emconf.php', << 'My extension', + {$version}]; + PHP); + } + + private function writeComposerJson(?string $version): void + { + $composerSchema = ['name' => 'vendor/my-ext', 'type' => 'typo3-cms-extension']; + + if ($version !== null) { + $composerSchema['version'] = $version; + } + + file_put_contents($this->extensionPath . '/composer.json', (string)json_encode($composerSchema)); + } + + private function removeExtensionPath(): void + { + if ($this->extensionPath === '' || !is_dir($this->extensionPath)) { + return; + } + + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->extensionPath, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $file) { + $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); + } + + rmdir($this->extensionPath); + $this->extensionPath = ''; + } + } diff --git a/tests/Unit/Service/GitServiceTest.php b/tests/Unit/Service/GitServiceTest.php new file mode 100644 index 0000000..584188f --- /dev/null +++ b/tests/Unit/Service/GitServiceTest.php @@ -0,0 +1,112 @@ +repositoryPath = sys_get_temp_dir() . '/tailor-git-' . bin2hex(random_bytes(6)); + mkdir($this->repositoryPath, 0777, true); + file_put_contents($this->repositoryPath . '/ext_emconf.php', 'repositoryPath === '' || !is_dir($this->repositoryPath)) { + return; + } + + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->repositoryPath, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $file) { + $file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); + } + + rmdir($this->repositoryPath); + $this->repositoryPath = ''; + } + + #[Test] + public function tagOfTheCheckedOutCommitIsReturned(): void + { + $this->createGitRepository($this->repositoryPath, '1.2.3'); + + self::assertSame(['1.2.3'], (new GitService())->getVersionsFromTagsOfHead($this->repositoryPath)); + } + + #[Test] + public function versionPrefixIsStripped(): void + { + $this->createGitRepository($this->repositoryPath, 'v1.2.3'); + + self::assertSame(['1.2.3'], (new GitService())->getVersionsFromTagsOfHead($this->repositoryPath)); + } + + #[Test] + public function tagsWhichAreNoVersionAreIgnored(): void + { + $this->createGitRepository($this->repositoryPath, 'latest', '1.2.3', 'release-1.2.3'); + + self::assertSame(['1.2.3'], (new GitService())->getVersionsFromTagsOfHead($this->repositoryPath)); + } + + #[Test] + public function prefixedAndUnprefixedTagOfTheSameVersionAreReturnedOnce(): void + { + $this->createGitRepository($this->repositoryPath, '1.2.3', 'v1.2.3'); + + self::assertSame(['1.2.3'], (new GitService())->getVersionsFromTagsOfHead($this->repositoryPath)); + } + + #[Test] + public function allVersionsOfTheCheckedOutCommitAreReturned(): void + { + $this->createGitRepository($this->repositoryPath, '1.2.3', '1.2.4'); + + self::assertSame(['1.2.3', '1.2.4'], (new GitService())->getVersionsFromTagsOfHead($this->repositoryPath)); + } + + #[Test] + public function untaggedRepositoryReturnsNoVersion(): void + { + $this->createGitRepository($this->repositoryPath); + + self::assertSame([], (new GitService())->getVersionsFromTagsOfHead($this->repositoryPath)); + } + + #[Test] + public function pathWithoutRepositoryReturnsNoVersion(): void + { + self::assertSame([], (new GitService())->getVersionsFromTagsOfHead($this->repositoryPath)); + } + + #[Test] + public function nonExistingPathReturnsNoVersion(): void + { + self::assertSame([], (new GitService())->getVersionsFromTagsOfHead($this->repositoryPath . '/does-not-exist')); + } +}