diff --git a/composer.json b/composer.json index 6f44a80..c209032 100644 --- a/composer.json +++ b/composer.json @@ -15,15 +15,16 @@ "tracy/tracy": "^2.6.0" }, "require-dev": { + "czproject/git-php": "^4.2", "friendsofphp/php-cs-fixer": "^3.13", "nette/application": "^3.1.0", "nette/bootstrap": "^3.1", "nette/di": "^3.0.10", - "symfony/console": "^5.4 | ^6.0", "nette/tester": "^2.4.3", "phpstan/phpstan": "^1.9", "phpstan/phpstan-nette": "^1.1", - "roave/security-advisories": "dev-latest" + "roave/security-advisories": "dev-latest", + "symfony/console": "^5.4 | ^6.0" }, "suggest": { "nette/di": "For an integration with the Nette Framework.", diff --git a/src/Bridge/Nette/DI/TracyGitVersionExtension.php b/src/Bridge/Nette/DI/TracyGitVersionExtension.php index 6bb052f..78fe5d7 100644 --- a/src/Bridge/Nette/DI/TracyGitVersionExtension.php +++ b/src/Bridge/Nette/DI/TracyGitVersionExtension.php @@ -36,8 +36,12 @@ public function getConfigSchema(): Schema 'source_name' => Expect::string(GitRepositoryInterface::SOURCE_GIT_DIRECTORY), 'command_handlers' => Expect::arrayOf(Expect::anyOf(Expect::type(Statement::class), Expect::string()), 'string') ->default([ - GetHeadCommand::class => new Statement(GetHeadCommandHandler::class), - GetLatestTagCommand::class => new Statement(GetLatestTagCommandHandler::class), + GetHeadCommand::class => new Statement(GetHeadCommandHandler::class, [ + 'useBinary' => true, + ]), + GetLatestTagCommand::class => new Statement(GetLatestTagCommandHandler::class, [ + 'useBinary' => true, + ]), ]) ->mergeDefaults() ->before(static function (array $items) { diff --git a/src/Export/Config.php b/src/Export/Config.php index 857b9dc..c8774d9 100644 --- a/src/Export/Config.php +++ b/src/Export/Config.php @@ -31,13 +31,13 @@ public static function create(): self return new self(); } - public static function createDefault(): self + public static function createDefault(bool $useBinary = true): self { return self::create() ->setGitDirectory(GitDirectory::createAutoDetected()) ->addCommandHandlers([ - GetHeadCommand::class => new GetHeadCommandHandler(), - GetLatestTagCommand::class => new GetLatestTagCommandHandler(), + GetHeadCommand::class => new GetHeadCommandHandler(null, $useBinary), + GetLatestTagCommand::class => new GetLatestTagCommandHandler(null, $useBinary), ]) ->addExporters([ new HeadExporter(), diff --git a/src/Export/PartialExporter/LatestTagExporter.php b/src/Export/PartialExporter/LatestTagExporter.php index fe99225..5b9679b 100644 --- a/src/Export/PartialExporter/LatestTagExporter.php +++ b/src/Export/PartialExporter/LatestTagExporter.php @@ -5,9 +5,9 @@ namespace SixtyEightPublishers\TracyGitVersion\Export\PartialExporter; use SixtyEightPublishers\TracyGitVersion\Exception\BadMethodCallException; +use SixtyEightPublishers\TracyGitVersion\Exception\UnhandledCommandException; use SixtyEightPublishers\TracyGitVersion\Export\Config; use SixtyEightPublishers\TracyGitVersion\Export\ExporterInterface; -use SixtyEightPublishers\TracyGitVersion\Exception\UnhandledCommandException; use SixtyEightPublishers\TracyGitVersion\Repository\Command\GetLatestTagCommand; use SixtyEightPublishers\TracyGitVersion\Repository\Entity\Tag; use SixtyEightPublishers\TracyGitVersion\Repository\GitRepositoryInterface; diff --git a/src/Repository/LocalDirectory/CommandHandler/GetHeadCommandHandler.php b/src/Repository/LocalDirectory/CommandHandler/GetHeadCommandHandler.php index b412425..838cd20 100644 --- a/src/Repository/LocalDirectory/CommandHandler/GetHeadCommandHandler.php +++ b/src/Repository/LocalDirectory/CommandHandler/GetHeadCommandHandler.php @@ -8,6 +8,7 @@ use SixtyEightPublishers\TracyGitVersion\Repository\Command\GetHeadCommand; use SixtyEightPublishers\TracyGitVersion\Repository\Entity\CommitHash; use SixtyEightPublishers\TracyGitVersion\Repository\Entity\Head; +use SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory\GitDirectory; use function explode; use function file_get_contents; use function is_readable; @@ -18,10 +19,27 @@ final class GetHeadCommandHandler extends AbstractLocalDirectoryCommandHandler { + private bool $useBinary; + + public function __construct(?GitDirectory $gitDirectory = null, bool $useBinary = false) + { + parent::__construct($gitDirectory); + + $this->useBinary = $useBinary; + } + /** * @throws GitDirectoryException */ public function __invoke(GetHeadCommand $command): Head + { + return $this->useBinary ? $this->readUsingBinary() : $this->readFromDirectory(); + } + + /** + * @throws GitDirectoryException + */ + private function readFromDirectory(): Head { $headFile = $this->getGitDirectory() . DIRECTORY_SEPARATOR . 'HEAD'; @@ -43,4 +61,31 @@ public function __invoke(GetHeadCommand $command): Head is_readable($commitFile) && false !== ($commitHash = @file_get_contents($commitFile)) ? new CommitHash(trim($commitHash)) : null, ); } + + /** + * @throws GitDirectoryException + */ + private function readUsingBinary(): Head + { + $commitOutput = $this->getGitDirectory()->executeGitCommand([ + 'rev-parse', + 'HEAD', + ]); + + if (0 !== $commitOutput['code']) { + return new Head(null, null); + } + + $branchOutput = $this->getGitDirectory()->executeGitCommand([ + 'rev-parse', + '--abbrev-ref', + 'HEAD', + ]); + + if (0 !== $branchOutput['code'] || 'HEAD' === $branchOutput['out']) { + return new Head(null, new CommitHash($commitOutput['out'])); + } + + return new Head($branchOutput['out'], new CommitHash($commitOutput['out'])); + } } diff --git a/src/Repository/LocalDirectory/CommandHandler/GetLatestTagCommandHandler.php b/src/Repository/LocalDirectory/CommandHandler/GetLatestTagCommandHandler.php index c09e7cc..b0cd29b 100644 --- a/src/Repository/LocalDirectory/CommandHandler/GetLatestTagCommandHandler.php +++ b/src/Repository/LocalDirectory/CommandHandler/GetLatestTagCommandHandler.php @@ -8,6 +8,7 @@ use SixtyEightPublishers\TracyGitVersion\Repository\Command\GetLatestTagCommand; use SixtyEightPublishers\TracyGitVersion\Repository\Entity\CommitHash; use SixtyEightPublishers\TracyGitVersion\Repository\Entity\Tag; +use SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory\GitDirectory; use function current; use function file_exists; use function file_get_contents; @@ -22,10 +23,27 @@ final class GetLatestTagCommandHandler extends AbstractLocalDirectoryCommandHandler { + private bool $useBinary; + + public function __construct(?GitDirectory $gitDirectory = null, bool $useBinary = false) + { + parent::__construct($gitDirectory); + + $this->useBinary = $useBinary; + } + /** * @throws GitDirectoryException */ public function __invoke(GetLatestTagCommand $getLatestTag): ?Tag + { + return $this->useBinary ? $this->readUsingBinary() : $this->readFromDirectory(); + } + + /** + * @throws GitDirectoryException + */ + private function readFromDirectory(): ?Tag { $tagsDirectory = sprintf('%s%srefs%stags', $this->getGitDirectory(), DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR); @@ -64,4 +82,39 @@ public function __invoke(GetLatestTagCommand $getLatestTag): ?Tag return new Tag((string) key($latestTagNames), new CommitHash(trim((string) @file_get_contents(current($latestTagNames))))); } + + /** + * @throws GitDirectoryException + */ + private function readUsingBinary(): ?Tag + { + $tagOutput = $this->getGitDirectory()->executeGitCommand([ + 'describe', + '--tags', + '$(' . $this->getGitDirectory()->createGitCommand([ + 'rev-list', + '--tags', + '--max-count', + '1', + ]) . ')', + ]); + + if (0 !== $tagOutput['code']) { + return null; + } + + $tag = $tagOutput['out']; + + $commitOutput = $this->getGitDirectory()->executeGitCommand([ + 'show-ref', + '-s', + $tag, + ]); + + if (0 !== $commitOutput['code']) { + return null; + } + + return new Tag($tag, new CommitHash($commitOutput['out'])); + } } diff --git a/src/Repository/LocalDirectory/GitDirectory.php b/src/Repository/LocalDirectory/GitDirectory.php index 0687a81..10b050a 100644 --- a/src/Repository/LocalDirectory/GitDirectory.php +++ b/src/Repository/LocalDirectory/GitDirectory.php @@ -5,10 +5,18 @@ namespace SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory; use SixtyEightPublishers\TracyGitVersion\Exception\GitDirectoryException; +use function array_merge; use function dirname; +use function fclose; use function file_exists; +use function implode; use function is_dir; +use function is_resource; +use function proc_close; +use function proc_open; use function realpath; +use function stream_get_contents; +use function trim; final class GitDirectory { @@ -71,6 +79,62 @@ public function __toString(): string throw GitDirectoryException::gitDirectoryNotFound($workingDirectory); } + /** + * @param array $command + */ + public function createGitCommand(array $command): string + { + $gitDir = (string) $this; + $parts = array_merge( + [ + 'git', + '--git-dir', + $gitDir, + '--work-tree', + dirname($gitDir), + ], + $command, + ); + + return implode(' ', $parts); + } + + /** + * @param array $command + * + * @return array{ + * code: int, + * out: string, + * err: string, + * } + */ + public function executeGitCommand(array $command): array + { + $process = proc_open( + $this->createGitCommand($command), + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, + __DIR__, + null, + ); + + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + return [ + 'code' => is_resource($process) ? proc_close($process) : 1, + 'out' => trim((string) $stdout), + 'err' => trim((string) $stderr), + ]; + } + /** * @throws GitDirectoryException */ diff --git a/src/Repository/LocalGitRepository.php b/src/Repository/LocalGitRepository.php index d46e6e5..e831b26 100644 --- a/src/Repository/LocalGitRepository.php +++ b/src/Repository/LocalGitRepository.php @@ -29,13 +29,13 @@ public function __construct(GitDirectory $gitDirectory, array $handlers = [], st parent::__construct($handlers); } - public static function createDefault(?string $workingDirectory = null, string $directoryName = '.git'): self + public static function createDefault(?string $workingDirectory = null, string $directoryName = '.git', bool $useBinary = true): self { return new self( GitDirectory::createAutoDetected($workingDirectory, $directoryName), [ - GetHeadCommand::class => new GetHeadCommandHandler(), - GetLatestTagCommand::class => new GetLatestTagCommandHandler(), + GetHeadCommand::class => new GetHeadCommandHandler(null, $useBinary), + GetLatestTagCommand::class => new GetLatestTagCommandHandler(null, $useBinary), ], ); } diff --git a/tests/Export/LocalDirectoryExporterTest.php b/tests/Export/LocalDirectoryExporterTest.php index 9a3abf6..889337f 100644 --- a/tests/Export/LocalDirectoryExporterTest.php +++ b/tests/Export/LocalDirectoryExporterTest.php @@ -17,7 +17,7 @@ final class LocalDirectoryExporterTest extends TestCase public function testWithDefaultConfig(): void { $exporter = new LocalDirectoryExporter(); - $config = Config::createDefault(); + $config = Config::createDefault(false); # directory must be overridden $config->setGitDirectory(GitDirectory::createFromGitDirectory(__DIR__ . '/../files/test-git')); @@ -39,7 +39,7 @@ public function testWithDefaultConfig(): void public function testWithDefaultConfigAndDetachedHead(): void { $exporter = new LocalDirectoryExporter(); - $config = Config::createDefault(); + $config = Config::createDefault(false); # directory must be override $config->setGitDirectory(GitDirectory::createFromGitDirectory(__DIR__ . '/../files/test-git-detached')); diff --git a/tests/GitHelper.php b/tests/GitHelper.php new file mode 100644 index 0000000..e1f743d --- /dev/null +++ b/tests/GitHelper.php @@ -0,0 +1,53 @@ +init($tempDir); + $repo->execute('config', 'user.email', 'test@68publishers.io'); + $repo->execute('config', 'user.name', 'Test SixtyEightPublishers'); + + return $repo; + } + + public static function destroy(GitRepository $repository): void + { + FileSystem::delete($repository->getRepositoryPath()); + } + + public static function createFile(GitRepository $repository, string $name, string $contents): void + { + $filename = rtrim($repository->getRepositoryPath(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($name, DIRECTORY_SEPARATOR); + + FileSystem::write($filename, $contents); + + $repository->addFile($name); + } + + public static function commit(GitRepository $repository, string $commitMessage): void + { + $repository->commit($commitMessage); + } + + public static function createTag(GitRepository $repository, string $tag): void + { + $repository->createTag($tag); + } +} diff --git a/tests/Repository/LocalDirectory/CommandHandler/GetHeadCommandHandlerTest.php b/tests/Repository/LocalDirectory/CommandHandler/GetHeadCommandHandlerTest.php index a382ec9..72f9c03 100644 --- a/tests/Repository/LocalDirectory/CommandHandler/GetHeadCommandHandlerTest.php +++ b/tests/Repository/LocalDirectory/CommandHandler/GetHeadCommandHandlerTest.php @@ -8,6 +8,7 @@ use SixtyEightPublishers\TracyGitVersion\Repository\Entity\Head; use SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory\CommandHandler\GetHeadCommandHandler; use SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory\GitDirectory; +use SixtyEightPublishers\TracyGitVersion\Tests\GitHelper; use Tester\Assert; use Tester\TestCase; @@ -26,6 +27,26 @@ public function testCommandHandling(): void Assert::false($head->isDetached()); } + public function testCommandHandlingUsingBinary(): void + { + $repository = GitHelper::init(); + + try { + GitHelper::createFile($repository, 'file.txt', 'test'); + $repository->commit('commit message'); + + $handler = new GetHeadCommandHandler(GitDirectory::createAutoDetected($repository->getRepositoryPath()), true); + $head = $handler(new GetHeadCommand()); + + Assert::type(Head::class, $head); + Assert::same($repository->getCurrentBranchName(), $head->getBranch()); + Assert::same($repository->getLastCommitId()->toString(), $head->getCommitHash()->getValue()); + Assert::false($head->isDetached()); + } finally { + GitHelper::destroy($repository); + } + } + public function testCommandHandlingOnDetachedHead(): void { $handler = new GetHeadCommandHandler(GitDirectory::createFromGitDirectory(__DIR__ . '/../../../files/test-git-detached')); @@ -36,6 +57,32 @@ public function testCommandHandlingOnDetachedHead(): void Assert::same('3416c5b1831774dd209e489100b3a7c1e333690d', $head->getCommitHash()->getValue()); Assert::true($head->isDetached()); } + + public function testCommandHandlingOnDetachedHeadUsingBinary(): void + { + $repository = GitHelper::init(); + + try { + GitHelper::createFile($repository, 'file.txt', 'test'); + $repository->commit('commit message'); + + $commitId = $repository->getLastCommitId()->toString(); + + GitHelper::createFile($repository, 'file2.txt', 'test 2'); + $repository->commit('commit message 2'); + $repository->checkout($commitId); + + $handler = new GetHeadCommandHandler(GitDirectory::createAutoDetected($repository->getRepositoryPath()), true); + $head = $handler(new GetHeadCommand()); + + Assert::type(Head::class, $head); + Assert::null($head->getBranch()); + Assert::same($commitId, $head->getCommitHash()->getValue()); + Assert::true($head->isDetached()); + } finally { + GitHelper::destroy($repository); + } + } } (new GetHeadCommandHandlerTest())->run(); diff --git a/tests/Repository/LocalDirectory/CommandHandler/GetLatestTagCommandHandlerTest.php b/tests/Repository/LocalDirectory/CommandHandler/GetLatestTagCommandHandlerTest.php index 358a36f..35ced1d 100644 --- a/tests/Repository/LocalDirectory/CommandHandler/GetLatestTagCommandHandlerTest.php +++ b/tests/Repository/LocalDirectory/CommandHandler/GetLatestTagCommandHandlerTest.php @@ -8,6 +8,7 @@ use SixtyEightPublishers\TracyGitVersion\Repository\Entity\Tag; use SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory\CommandHandler\GetLatestTagCommandHandler; use SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory\GitDirectory; +use SixtyEightPublishers\TracyGitVersion\Tests\GitHelper; use Tester\Assert; use Tester\TestCase; @@ -25,6 +26,29 @@ public function testCommandHandling(): void Assert::same('8f2c308e3a5330b7924634edd7aa38eec97a4114', $tag->getCommitHash()->getValue()); } + public function testCommandHandlingUsingBinary(): void + { + $repository = GitHelper::init(); + + try { + GitHelper::createFile($repository, 'file.txt', 'test'); + $repository->commit('commit message'); + + $commitId = $repository->getLastCommitId(); + + $repository->createTag('v1.0.0'); + + $handler = new GetLatestTagCommandHandler(GitDirectory::createAutoDetected($repository->getRepositoryPath()), true); + $tag = $handler(new GetLatestTagCommand()); + + Assert::type(Tag::class, $tag); + Assert::same('v1.0.0', $tag->getName()); + Assert::same($commitId->toString(), $tag->getCommitHash()->getValue()); + } finally { + GitHelper::destroy($repository); + } + } + public function testCommandHandlingWithoutDefinedTags(): void { $handler = new GetLatestTagCommandHandler(GitDirectory::createFromGitDirectory(__DIR__ . '/../../../files/test-git-detached')); @@ -32,6 +56,23 @@ public function testCommandHandlingWithoutDefinedTags(): void Assert::null($tag); } + + public function testCommandHandlingWithoutDefinedTagsUsingBinary(): void + { + $repository = GitHelper::init(); + + try { + GitHelper::createFile($repository, 'file.txt', 'test'); + $repository->commit('commit message'); + + $handler = new GetLatestTagCommandHandler(GitDirectory::createAutoDetected($repository->getRepositoryPath()), true); + $tag = $handler(new GetLatestTagCommand()); + + Assert::null($tag); + } finally { + GitHelper::destroy($repository); + } + } } (new GetLatestTagCommandHandlerTest())->run(); diff --git a/tests/Repository/LocalDirectory/GitDirectoryTest.phpt b/tests/Repository/LocalDirectory/GitDirectoryTest.phpt index 90957b5..f97d323 100644 --- a/tests/Repository/LocalDirectory/GitDirectoryTest.phpt +++ b/tests/Repository/LocalDirectory/GitDirectoryTest.phpt @@ -4,8 +4,10 @@ declare(strict_types=1); namespace SixtyEightPublishers\TracyGitVersion\Tests\Repository\LocalDirectory; +use CzProject\GitPhp\GitRepository; use SixtyEightPublishers\TracyGitVersion\Exception\GitDirectoryException; use SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory\GitDirectory; +use SixtyEightPublishers\TracyGitVersion\Tests\GitHelper; use Tester\Assert; use Tester\TestCase; use function realpath; @@ -13,8 +15,13 @@ use function sprintf; require __DIR__ . '/../../bootstrap.php'; +/** + * @testCase + */ final class GitDirectoryTest extends TestCase { + private GitRepository $repository; + public function testExceptionShouldBeThrownOnInvalidWorkingDirectory(): void { $workingDirectory = __DIR__ . '/non/existent/directory'; @@ -43,7 +50,7 @@ final class GitDirectoryTest extends TestCase public function testCreateGitDirectoryDirectly(): void { - $dir = __DIR__ . '/../../files/test-git'; + $dir = $this->repository->getRepositoryPath() . '/.git'; $gitDirectory = GitDirectory::createFromGitDirectory($dir); Assert::same(realpath($dir), (string) $gitDirectory); @@ -51,15 +58,48 @@ final class GitDirectoryTest extends TestCase public function testCreateGitDirectoryFromWorkingDirectory(): void { - $realGitDirectoryPath = realpath(__DIR__ . '/../../files/test-git'); + GitHelper::createFile($this->repository, 'nested-directory-1/nested-directory-2/test.txt', ''); + + $workingDir = $this->repository->getRepositoryPath(); + $gitDir = '.git'; + + $gitDirectory = GitDirectory::createAutoDetected($workingDir, $gitDir); - $gitDirectory = GitDirectory::createAutoDetected(__DIR__ . '/../../files', 'test-git'); + Assert::same($workingDir . '/' . $gitDir, (string) $gitDirectory); - Assert::same($realGitDirectoryPath, (string) $gitDirectory); + $gitDirectory = GitDirectory::createAutoDetected($workingDir . '/nested-directory-1/nested-directory-2', $gitDir); - $gitDirectory = GitDirectory::createAutoDetected(__DIR__ . '/../../files/nested-directory-1/nested-directory-2', 'test-git'); + Assert::same($workingDir . '/' . $gitDir, (string) $gitDirectory); + } - Assert::same($realGitDirectoryPath, (string) $gitDirectory); + public function testGitCommandShouldBeExecuted(): void + { + GitHelper::createFile($this->repository, 'test.txt', ''); + GitHelper::commit($this->repository, 'first commit'); + GitHelper::createFile($this->repository, 'test2.txt', ''); + GitHelper::commit($this->repository, 'second commit'); + + $gitDirectory = GitDirectory::createAutoDetected($this->repository->getRepositoryPath()); + + Assert::same([ + 'code' => 0, + 'out' => '2', + 'err' => '', + ], $gitDirectory->executeGitCommand([ + 'rev-list', + '--all', + '--count', + ])); + } + + protected function setUp(): void + { + $this->repository = GitHelper::init(); + } + + protected function tearDown(): void + { + GitHelper::destroy($this->repository); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index eb14668..b976dc7 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -11,7 +11,6 @@ Environment::setup(); Environment::bypassFinals(); -date_default_timezone_set('Europe/Prague'); if (PHP_VERSION_ID >= 80200) { error_reporting(~E_DEPRECATED); diff --git a/tests/files/export/config.php b/tests/files/export/config.php index 9c5fdc7..783378a 100644 --- a/tests/files/export/config.php +++ b/tests/files/export/config.php @@ -5,6 +5,6 @@ use SixtyEightPublishers\TracyGitVersion\Export\Config; use SixtyEightPublishers\TracyGitVersion\Repository\LocalDirectory\GitDirectory; -return Config::createDefault() - ->setGitDirectory(GitDirectory::createFromGitDirectory(__DIR__ . '/../test-git')) - ->setOutputFile(__DIR__ . '/../output/git-repository-export.json'); +return Config::createDefault(false) + ->setGitDirectory(GitDirectory::createFromGitDirectory(__DIR__ . '/../test-git')) + ->setOutputFile(__DIR__ . '/../output/git-repository-export.json');