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'));
+ }
+}