From 1356b44e45054bc40b56fc092127d11d3da978f8 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Sat, 7 Dec 2024 19:03:35 +1100 Subject: [PATCH] Fully refactored application to correctly use git wrapper. --- .circleci/config.yml | 2 +- composer.json | 4 +- phpcs.xml | 6 + rector.php | 2 + src/Commands/ArtifactCommand.php | 760 ++++-------------- src/Git/ArtifactGit.php | 23 - src/Git/ArtifactGitRepository.php | 420 +++++----- src/Traits/FilesystemTrait.php | 23 +- src/Traits/LogTrait.php | 48 +- src/Traits/TokenTrait.php | 18 +- tests/phpunit/AbstractTestCase.php | 73 -- tests/phpunit/Exception/ErrorException.php | 24 - .../Functional/AbstractFunctionalTestCase.php | 229 ++++-- tests/phpunit/Functional/BranchModeTest.php | 151 ++++ tests/phpunit/Functional/BranchTest.php | 150 ---- .../phpunit/Functional/ForcePushModeTest.php | 311 +++++++ tests/phpunit/Functional/ForcePushTest.php | 306 ------- tests/phpunit/Functional/GeneralTest.php | 51 +- tests/phpunit/Functional/README.md | 6 +- tests/phpunit/Functional/TagTest.php | 26 +- tests/phpunit/Functional/TokenTest.php | 103 --- tests/phpunit/Traits/CommandTrait.php | 538 ------------- tests/phpunit/Traits/FixtureTrait.php | 61 ++ tests/phpunit/Traits/GitTrait.php | 384 +++++++++ tests/phpunit/Traits/MockTrait.php | 67 +- tests/phpunit/Unit/AbstractUnitTestCase.php | 42 +- .../Unit/ArtifactGitRepositoryTest.php | 365 +++------ tests/phpunit/Unit/ArtifactGitTest.php | 27 - tests/phpunit/Unit/ExcludeTest.php | 182 ----- tests/phpunit/Unit/TokenTest.php | 88 +- 30 files changed, 1734 insertions(+), 2756 deletions(-) delete mode 100644 src/Git/ArtifactGit.php delete mode 100644 tests/phpunit/AbstractTestCase.php delete mode 100644 tests/phpunit/Exception/ErrorException.php create mode 100644 tests/phpunit/Functional/BranchModeTest.php delete mode 100644 tests/phpunit/Functional/BranchTest.php create mode 100644 tests/phpunit/Functional/ForcePushModeTest.php delete mode 100644 tests/phpunit/Functional/ForcePushTest.php delete mode 100644 tests/phpunit/Functional/TokenTest.php delete mode 100644 tests/phpunit/Traits/CommandTrait.php create mode 100644 tests/phpunit/Traits/FixtureTrait.php create mode 100644 tests/phpunit/Traits/GitTrait.php delete mode 100644 tests/phpunit/Unit/ArtifactGitTest.php delete mode 100644 tests/phpunit/Unit/ExcludeTest.php diff --git a/.circleci/config.yml b/.circleci/config.yml index c0ee4be..8af941c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ aliases: - &container_config working_directory: /workspace/code docker: - - image: drevops/ci-runner:24.4.0 + - image: drevops/ci-runner:24.11.0 environment: COMPOSER_ALLOW_SUPERUSER: 1 DEPLOY_SSH_FINGERPRINT: *deploy_ssh_fingerprint diff --git a/composer.json b/composer.json index 03df261..095c6c7 100644 --- a/composer.json +++ b/composer.json @@ -62,8 +62,8 @@ ], "lint": [ "phpcs", - "phpmd --exclude vendor,vendor-bin,node_modules . text phpmd.xml", "phpstan --memory-limit=-1", + "phpmd --exclude vendor,vendor-bin,node_modules,TokenTest.php . text phpmd.xml", "rector --clear-cache --dry-run" ], "lint-fix": [ @@ -71,6 +71,6 @@ "phpcbf" ], "reset": "rm -rf vendor vendor-bin composer.lock", - "test": "if [ \"${XDEBUG_MODE}\" = 'coverage' ]; then phpunit; else phpunit --no-coverage; fi" + "test": "phpunit" } } diff --git a/phpcs.xml b/phpcs.xml index 3be9ccb..b0f5c20 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -23,6 +23,12 @@ *.test + + + *.Test\.php + *.TestCase\.php + *.test + *.Test\.php diff --git a/rector.php b/rector.php index d18c2c2..cafede2 100644 --- a/rector.php +++ b/rector.php @@ -14,6 +14,7 @@ use Rector\CodeQuality\Rector\ClassMethod\InlineArrayReturnAssignRector; use Rector\CodeQuality\Rector\Empty_\SimplifyEmptyCheckOnEmptyArrayRector; +use Rector\CodingStyle\Rector\Catch_\CatchExceptionNameMatchingTypeRector; use Rector\CodingStyle\Rector\ClassMethod\NewlineBeforeNewAssignSetRector; use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector; @@ -41,6 +42,7 @@ $rectorConfig->skip([ // Rules added by Rector's rule sets. + CatchExceptionNameMatchingTypeRector::class, CountArrayToEmptyArrayComparisonRector::class, DisallowedEmptyRuleFixerRector::class, InlineArrayReturnAssignRector::class, diff --git a/src/Commands/ArtifactCommand.php b/src/Commands/ArtifactCommand.php index 7f19ecd..a0729dc 100644 --- a/src/Commands/ArtifactCommand.php +++ b/src/Commands/ArtifactCommand.php @@ -4,7 +4,7 @@ namespace DrevOps\GitArtifact\Commands; -use DrevOps\GitArtifact\Git\ArtifactGit; +use CzProject\GitPhp\GitException; use DrevOps\GitArtifact\Git\ArtifactGitRepository; use DrevOps\GitArtifact\Traits\FilesystemTrait; use DrevOps\GitArtifact\Traits\LogTrait; @@ -15,7 +15,6 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\Finder\Finder; /** * Artifact Command. @@ -33,15 +32,21 @@ class ArtifactCommand extends Command { const GIT_REMOTE_NAME = 'dst'; + const MODE_BRANCH = 'branch'; + + const MODE_FORCE_PUSH = 'force-push'; + + const MODE_DIFF = 'diff'; + /** - * Represent to current repository. + * Current Git repository. */ - protected ArtifactGitRepository $gitRepository; + protected ArtifactGitRepository $repo; /** - * Source path of git repository. + * Path to the dir of the source git repository. */ - protected string $sourcePathGitRepository = ''; + protected string $sourceDir = ''; /** * Mode in which current build is going to run. @@ -71,7 +76,7 @@ class ArtifactCommand extends Command { protected string $remoteName = ''; /** - * Remote URL includes uri or local path. + * Remote URL includes URI or local path. */ protected string $remoteUrl = ''; @@ -85,7 +90,7 @@ class ArtifactCommand extends Command { /** * Commit message with optional tokens. */ - protected string $message = ''; + protected string $commitMessage = ''; /** * Flag to specify if using dry run. @@ -98,7 +103,7 @@ class ArtifactCommand extends Command { protected bool $needCleanup = TRUE; /** - * Path to log file. + * Path to the log file. */ protected string $logFile = ''; @@ -108,9 +113,9 @@ class ArtifactCommand extends Command { protected bool $showChanges = FALSE; /** - * Artifact build result. + * Flag to specify if push was successful. */ - protected bool $result = FALSE; + protected bool $pushSuccessful = FALSE; /** * Internal option to set current timestamp. @@ -118,33 +123,24 @@ class ArtifactCommand extends Command { protected int $now; /** - * Output. + * Output interface. */ protected OutputInterface $output; - /** - * Git wrapper. - */ - protected ArtifactGit $git; - /** * Artifact constructor. * - * @param \DrevOps\GitArtifact\Git\ArtifactGit $gitWrapper - * Git wrapper. - * @param \Symfony\Component\Filesystem\Filesystem $fsFileSystem - * File system. * @param string|null $name + * File system. + * @param \Symfony\Component\Filesystem\Filesystem $fs * Command name. */ public function __construct( - ?ArtifactGit $gitWrapper = NULL, - ?Filesystem $fsFileSystem = NULL, ?string $name = NULL, + ?Filesystem $fs = NULL, ) { parent::__construct($name); - $this->fs = is_null($fsFileSystem) ? new Filesystem() : $fsFileSystem; - $this->git = is_null($gitWrapper) ? new ArtifactGit() : $gitWrapper; + $this->fs = is_null($fs) ? new Filesystem() : $fs; } /** @@ -157,50 +153,24 @@ protected function configure(): void { $this->addArgument('remote', InputArgument::REQUIRED, 'Path to the remote git repository.'); + // @formatter:off + // phpcs:disable Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma + // phpcs:disable Drupal.WhiteSpace.Comma.TooManySpaces $this - ->addOption('branch', NULL, InputOption::VALUE_REQUIRED, 'Destination branch with optional tokens.', '[branch]') - ->addOption( - 'gitignore', - NULL, - InputOption::VALUE_REQUIRED, - 'Path to gitignore file to replace current .gitignore.' - ) - ->addOption( - 'message', - NULL, - InputOption::VALUE_REQUIRED, - 'Commit message with optional tokens.', - 'Deployment commit' - ) - ->addOption( - 'mode', - NULL, - InputOption::VALUE_REQUIRED, - 'Mode of artifact build: branch, force-push or diff. Defaults to force-push.', - 'force-push' - ) - ->addOption('no-cleanup', NULL, InputOption::VALUE_NONE, 'Do not cleanup after run.') - ->addOption('now', NULL, InputOption::VALUE_REQUIRED, 'Internal value used to set internal time.') - ->addOption('dry-run', NULL, InputOption::VALUE_NONE, 'Run without pushing to the remote repository.') - ->addOption('log', NULL, InputOption::VALUE_REQUIRED, 'Path to the log file.') - ->addOption( - 'root', - NULL, - InputOption::VALUE_REQUIRED, - 'Path to the root for file path resolution. If not specified, current directory is used.' - ) - ->addOption( - 'show-changes', - NULL, - InputOption::VALUE_NONE, - 'Show changes made to the repo by the build in the output.' - ) - ->addOption( - 'src', - NULL, - InputOption::VALUE_REQUIRED, - 'Directory where source repository is located. If not specified, root directory is used.' - ); + ->addOption('branch', NULL, InputOption::VALUE_REQUIRED, 'Destination branch with optional tokens.', '[branch]') + ->addOption('dry-run', NULL, InputOption::VALUE_NONE, 'Run without pushing to the remote repository.') + ->addOption('gitignore', NULL, InputOption::VALUE_REQUIRED, 'Path to gitignore file to replace current .gitignore.') + ->addOption('message', NULL, InputOption::VALUE_REQUIRED, 'Commit message with optional tokens.', 'Deployment commit') + ->addOption('mode', NULL, InputOption::VALUE_REQUIRED, 'Mode of artifact build: branch, force-push or diff. Defaults to force-push.', 'force-push') + ->addOption('no-cleanup', NULL, InputOption::VALUE_NONE, 'Do not cleanup after run.') + ->addOption('now', NULL, InputOption::VALUE_REQUIRED, 'Internal value used to set internal time.') + ->addOption('log', NULL, InputOption::VALUE_REQUIRED, 'Path to the log file.') + ->addOption('root', NULL, InputOption::VALUE_REQUIRED, 'Path to the root for file path resolution. If not specified, current directory is used.') + ->addOption('show-changes', NULL, InputOption::VALUE_NONE, 'Show changes made to the repo by the build in the output.') + ->addOption('src', NULL, InputOption::VALUE_REQUIRED, 'Directory where source repository is located. If not specified, root directory is used.'); + // @formatter:on + // phpcs:enable Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma + // phpcs:enable Drupal.WhiteSpace.Comma.TooManySpaces } /** @@ -217,39 +187,25 @@ protected function configure(): void { * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { - if ($input->getOption('log')) { - $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); - } - $this->output = $output; - $logfile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . time() . '-artifact-log.log'; - $this->logger = self::loggerCreate((string) $this->getName(), $output, $logfile); + $this->logPrepare((string) $this->getName(), $input, $output); $remote = $input->getArgument('remote'); - if (!is_string($remote)) { - throw new \RuntimeException('Remote argument must be a string'); + if (empty($remote) || !is_string($remote)) { + throw new \RuntimeException('Remote argument must be a non-empty string'); } try { - // Now we have all what we need. - // Let process artifact function. $this->checkRequirements(); - $this->processArtifact($remote, $input->getOptions()); - - // Dump log file and clean tmp log file. - if ($this->fs->exists($logfile)) { - if (!empty($this->logFile)) { - $this->fs->copy($logfile, $this->logFile); - } + $this->resolveOptions($remote, $input->getOptions()); - $this->fs->remove($logfile); - } + $this->doExecute(); } catch (\Exception $exception) { $this->output->writeln([ - 'Deployment failed.', + 'Processing failed with an error:', '' . $exception->getMessage() . '', ]); @@ -264,207 +220,81 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Assemble a code artifact from your codebase. * - * @param string $remote - * Path to the remote git repository. - * @param array $options - * Options. - * - * @throws \Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - protected function processArtifact(string $remote, array $options): void { + protected function doExecute(): void { + $error = NULL; + try { - $error = NULL; - $this->logDebug('Debug messages enabled'); - // Let resolve options into properties first. - $this->resolveOptions($remote, $options); - $this->setupRemoteForRepository(); + $this->repo->addRemote($this->remoteName, $this->remoteUrl); + $this->showInfo(); - $this->prepareArtifact(); + + // Do not optimize this into a chained call to make it easier to debug. + $repo = $this->repo; + $repo->switchToBranch($this->artifactBranch, TRUE); + $repo->removeSubRepositories(); + $repo->disableLocalExclude(); + $repo->removeIgnoredFiles(); + $repo->removeOtherFiles(); + $changes = $repo->commitAllChanges($this->commitMessage); + + if ($this->showChanges) { + $this->output->writeln(sprintf('Added changes: %s', implode("\n", $changes))); + $this->logNotice(sprintf('Added changes: %s', implode("\n", $changes))); + } if ($this->isDryRun) { - $this->output->writeln('Cowardly refusing to push to remote. Use without --dry-run to perform an actual push.'); + $this->output->writeln('Cowardly refusing to push to remote. Use without --dry-run to perform an actual push.'); } else { - $this->doPush(); + $ref = sprintf('refs/heads/%s:refs/heads/%s', $this->artifactBranch, $this->destinationBranch); + + if ($this->mode === self::MODE_FORCE_PUSH) { + $this->repo->push([$this->remoteName, $ref], ['--force']); + } + else { + $this->repo->push([$this->remoteName, $ref]); + } + + $this->output->writeln(sprintf('Pushed branch "%s" with commit message "%s"', $this->destinationBranch, $this->commitMessage)); + } + } + catch (GitException $exception) { + $result = $exception->getRunnerResult(); + if (!$result) { + throw new \Exception('Unknown error occurred', $exception->getCode(), $exception); + } + + $error = $result->getOutputAsString(); + if (!empty($result->hasErrorOutput())) { + $error .= PHP_EOL . $result->getErrorOutputAsString(); } - $this->result = TRUE; } catch (\Exception $exception) { // Capture message and allow to rollback. $error = $exception->getMessage(); } - $this->logReport(); + $this->showReport(is_null($error)); if ($this->needCleanup) { - $this->cleanup(); + $this->logNotice('Cleaning up'); + $this->repo + ->restoreLocalExclude() + ->switchToBranch($this->originalBranch) + ->removeBranch($this->artifactBranch, TRUE) + ->removeRemote($this->remoteName); } - if (!$this->result) { - throw new \Exception((string) $error); - } - } - - /** - * Get source path git repository. - * - * @return string - * Source path. - */ - public function getSourcePathGitRepository(): string { - return $this->sourcePathGitRepository; - } - - /** - * Branch mode. - * - * @return string - * Branch mode name. - */ - public static function modeBranch(): string { - return 'branch'; - } - - /** - * Force-push mode. - * - * @return string - * Force-push mode name. - */ - public static function modeForcePush(): string { - return 'force-push'; - } - - /** - * Diff mode. - * - * @return string - * Diff mode name. - */ - public static function modeDiff(): string { - return 'diff'; - } - - /** - * Prepare artifact to be then deployed. - * - * @throws \Exception - */ - protected function prepareArtifact(): void { - // Switch to artifact branch. - $this->switchToArtifactBranchInGitRepository(); - // Remove sub-repositories. - $this->removeSubReposInGitRepository(); - // Disable local exclude. - $this->disableLocalExclude($this->getSourcePathGitRepository()); - // Add files. - $this->addAllFilesInGitRepository(); - // Remove other files. - $this->removeOtherFilesInGitRepository(); - // Commit all changes. - $result = $this->commitAllChangesInGitRepository(); - // Show all changes if needed. - if ($this->showChanges) { - $this->output->writeln(sprintf('Added changes: %s', implode("\n", $result))); - $this->logNotice(sprintf('Added changes: %s', implode("\n", $result))); - } - } - - /** - * Switch to artifact branch. - * - * @throws \CzProject\GitPhp\GitException - */ - protected function switchToArtifactBranchInGitRepository(): void { - $this - ->gitRepository - ->switchToBranch($this->artifactBranch, TRUE); - } - - /** - * Commit all changes. - * - * @return string[] - * The files committed. - * - * @throws \CzProject\GitPhp\GitException - */ - protected function commitAllChangesInGitRepository(): array { - return $this - ->gitRepository - ->commitAllChanges($this->message); - - } - - /** - * Add all files in current git repository. - * - * @throws \CzProject\GitPhp\GitException - * @throws \Exception - */ - protected function addAllFilesInGitRepository(): void { - if (!empty($this->gitignoreFile)) { - $this->replaceGitignoreInGitRepository($this->gitignoreFile); - $this->gitRepository->addAllChanges(); - $this->removeIgnoredFiles($this->getSourcePathGitRepository()); + // Dump log to a file. + if (!empty($this->logFile)) { + $this->logDump($this->logFile); } - else { - $this->gitRepository->addAllChanges(); - } - } - - /** - * Cleanup after build. - * - * @throws \Exception - */ - protected function cleanup(): void { - $this - ->restoreLocalExclude($this->getSourcePathGitRepository()); - $this - ->gitRepository - ->switchToBranch($this->originalBranch); - - $this - ->gitRepository - ->removeBranch($this->artifactBranch, TRUE); - - $this - ->gitRepository - ->removeRemote($this->remoteName); - } - - /** - * Perform actual push to remote. - * - * @throws \Exception - */ - protected function doPush(): void { - try { - $refSpec = sprintf('refs/heads/%s:refs/heads/%s', $this->artifactBranch, $this->destinationBranch); - if ($this->mode === self::modeForcePush()) { - $this - ->gitRepository - ->pushForce($this->remoteName, $refSpec); - } - else { - $this->gitRepository->push([$this->remoteName, $refSpec]); - } - - $this->output->writeln(sprintf('Pushed branch "%s" with commit message "%s"', $this->destinationBranch, $this->message)); - } - catch (\Exception $exception) { - // Re-throw the message with additional context. - throw new \Exception( - sprintf( - 'Error occurred while pushing branch "%s" with commit message "%s"', - $this->destinationBranch, - $this->message - ), - $exception->getCode(), - $exception - ); + if (!is_null($error)) { + $error = empty($error) ? 'Unknown error occurred' : $error; + throw new \Exception($error); } } @@ -491,41 +321,40 @@ protected function resolveOptions(string $url, array $options): void { $this->setMode($options['mode'], $options); - $srcPath = empty($options['src']) ? $this->fsGetRootDir() : $this->fsGetAbsolutePath($options['src']); - $this->sourcePathGitRepository = $srcPath; + $this->sourceDir = empty($options['src']) ? $this->fsGetRootDir() : $this->fsGetAbsolutePath($options['src']); // Setup Git repository from source path. - $this->initGitRepository($srcPath); + $this->repo = new ArtifactGitRepository($this->sourceDir, NULL, $this->logger); + + // Set original, destination, artifact branch names. + $this->originalBranch = $this->repo->getOriginalBranch(); + + $branch = $this->tokenProcess($options['branch']); + if (!ArtifactGitRepository::isValidBranchName($branch)) { + throw new \RuntimeException(sprintf('Incorrect value "%s" specified for git remote branch', $branch)); + } + $this->destinationBranch = $branch; - // Set original, destination, artifact branch name. - $this->originalBranch = $this->resolveOriginalBranch(); - $this->setDstBranch($options['branch']); $this->artifactBranch = $this->destinationBranch . '-artifact'; - // Set commit message. - $this->setMessage($options['message']); + $this->commitMessage = $this->tokenProcess($options['message']); if (!empty($options['gitignore'])) { - $this->setGitignoreFile($options['gitignore']); - } - } + $gitignore = $this->fsGetAbsolutePath($options['gitignore']); + $this->fsAssertPathsExist($gitignore); - /** - * Setup git repository. - * - * @param string $sourcePath - * Source path. - * - * @return \DrevOps\GitArtifact\Git\ArtifactGitRepository - * Current git repository. - * - * @throws \CzProject\GitPhp\GitException - * @throws \Exception - */ - protected function initGitRepository(string $sourcePath): ArtifactGitRepository { - $this->gitRepository = $this->git->open($sourcePath); + $contents = file_get_contents($gitignore); + if (!$contents) { + throw new \Exception('Unable to load contents of ' . $gitignore); + } + + $this->logDebug('-----.gitignore---------'); + $this->logDebug($contents); + $this->logDebug('-----.gitignore---------'); - return $this->gitRepository; + $this->gitignoreFile = $gitignore; + $this->repo->setGitignoreFile($gitignore); + } } /** @@ -537,7 +366,7 @@ protected function showInfo(): void { $lines[] = ('----------------------------------------------------------------------'); $lines[] = (' Build timestamp: ' . date('Y/m/d H:i:s', $this->now)); $lines[] = (' Mode: ' . $this->mode); - $lines[] = (' Source repository: ' . $this->getSourcePathGitRepository()); + $lines[] = (' Source repository: ' . $this->sourceDir); $lines[] = (' Remote repository: ' . $this->remoteUrl); $lines[] = (' Remote branch: ' . $this->destinationBranch); $lines[] = (' Gitignore file: ' . ($this->gitignoreFile ?: 'No')); @@ -545,6 +374,7 @@ protected function showInfo(): void { $lines[] = ('----------------------------------------------------------------------'); $this->output->writeln($lines); + foreach ($lines as $line) { $this->logNotice($line); } @@ -553,18 +383,18 @@ protected function showInfo(): void { /** * Dump artifact report to a file. */ - protected function logReport(): void { + protected function showReport(bool $result): void { $lines[] = '----------------------------------------------------------------------'; $lines[] = ' Artifact report'; $lines[] = '----------------------------------------------------------------------'; $lines[] = ' Build timestamp: ' . date('Y/m/d H:i:s', $this->now); $lines[] = ' Mode: ' . $this->mode; - $lines[] = ' Source repository: ' . $this->getSourcePathGitRepository(); + $lines[] = ' Source repository: ' . $this->sourceDir; $lines[] = ' Remote repository: ' . $this->remoteUrl; $lines[] = ' Remote branch: ' . $this->destinationBranch; $lines[] = ' Gitignore file: ' . ($this->gitignoreFile ?: 'No'); - $lines[] = ' Commit message: ' . $this->message; - $lines[] = ' Push result: ' . ($this->result ? 'Success' : 'Failure'); + $lines[] = ' Commit message: ' . $this->commitMessage; + $lines[] = ' Push result: ' . ($result ? 'Success' : 'Failure'); $lines[] = '----------------------------------------------------------------------'; foreach ($lines as $line) { @@ -582,11 +412,11 @@ protected function logReport(): void { */ protected function setMode(string $mode, array $options): void { switch ($mode) { - case self::modeForcePush(): + case self::MODE_FORCE_PUSH: // Intentionally empty. break; - case self::modeBranch(): + case self::MODE_BRANCH: if (is_scalar($options['branch'] ?? NULL) && !self::tokenExists(strval($options['branch']))) { $this->output->writeln('WARNING! Provided branch name does not have a token. Pushing of the artifact into this branch will fail on second and follow-up pushes to remote. @@ -594,300 +424,32 @@ protected function setMode(string $mode, array $options): void { } break; - case self::modeDiff(): + case self::MODE_DIFF: throw new \RuntimeException('Diff mode is not yet implemented.'); default: throw new \RuntimeException(sprintf('Invalid mode provided. Allowed modes are: %s', implode(', ', [ - self::modeForcePush(), - self::modeBranch(), - self::modeDiff(), + self::MODE_FORCE_PUSH, + self::MODE_BRANCH, + self::MODE_DIFF, ]))); } $this->mode = $mode; } - /** - * Resolve original branch to handle detached repositories. - * - * Usually, repository become detached when a tag is checked out. - * - * @return string - * Branch or detachment source. - * - * @throws \Exception - * If neither branch nor detachment source is not found. - */ - protected function resolveOriginalBranch(): string { - $branch = $this->gitRepository->getCurrentBranchName(); - // Repository could be in detached state. If this the case - we need to - // capture the source of detachment, if it exists. - if (str_contains($branch, 'HEAD detached')) { - $branch = NULL; - $branchList = $this->gitRepository->getBranches(); - if ($branchList) { - $branchList = array_filter($branchList); - foreach ($branchList as $branch) { - if (preg_match('/\(.*detached .* ([^\)]+)\)/', $branch, $matches)) { - $branch = $matches[1]; - break; - } - } - } - if (empty($branch)) { - throw new \Exception('Unable to determine detachment source'); - } - } - - return $branch; - } - - /** - * Set the branch in the remote repository where commits will be pushed to. - * - * @param string $branch - * Branch in the remote repository. - */ - protected function setDstBranch(string $branch): void { - $branch = (string) $this->tokenProcess($branch); - - if (!ArtifactGitRepository::isValidBranchName($branch)) { - throw new \RuntimeException(sprintf('Incorrect value "%s" specified for git remote branch', $branch)); - } - $this->destinationBranch = $branch; - } - - /** - * Set commit message. - * - * @param string $message - * Commit message to set on the deployment commit. - */ - protected function setMessage(string $message): void { - $message = (string) $this->tokenProcess($message); - $this->message = $message; - } - - /** - * Set replacement gitignore file path location. - * - * @param string $path - * Path to the replacement .gitignore file. - * - * @throws \Exception - */ - protected function setGitignoreFile(string $path): void { - $path = $this->fsGetAbsolutePath($path); - $this->fsPathsExist($path); - $this->gitignoreFile = $path; - } - /** * Check that there all requirements are met in order to to run this command. */ protected function checkRequirements(): void { // @todo Refactor this into more generic implementation. $this->logNotice('Checking requirements'); - if (!$this->fsIsCommandAvailable('git')) { - throw new \RuntimeException('At least one of the script running requirements was not met'); - } - $this->logNotice('All requirements were met'); - } - - /** - * Replace gitignore file with provided file. - * - * @param string $filename - * Path to new gitignore to replace current file with. - */ - protected function replaceGitignoreInGitRepository(string $filename): void { - $path = $this->getSourcePathGitRepository(); - $this->logDebug(sprintf('Replacing .gitignore: %s with %s', $path . DIRECTORY_SEPARATOR . '.gitignore', $filename)); - $this->fs->copy($filename, $path . DIRECTORY_SEPARATOR . '.gitignore', TRUE); - $this->fs->remove($filename); - } - /** - * Helper to get a file name of the local exclude file. - * - * @param string $path - * Path to directory. - * - * @return string - * Exclude file name path. - */ - protected function getLocalExcludeFileName(string $path): string { - return $path . DIRECTORY_SEPARATOR . '.git' . DIRECTORY_SEPARATOR . 'info' . DIRECTORY_SEPARATOR . 'exclude'; - } - - /** - * Check if local exclude (.git/info/exclude) file exists. - * - * @param string $path - * Path to repository. - * - * @return bool - * True if exists, false otherwise. - */ - protected function localExcludeExists(string $path): bool { - return $this->fs->exists($this->getLocalExcludeFileName($path)); - } - - /** - * Check if local exclude (.git/info/exclude) file is empty. - * - * @param string $path - * Path to repository. - * @param bool $strict - * Flag to check if the file is empty. If false, comments and empty lines - * are considered as empty. - * - * @return bool - * - true, if $strict is true and file has no records. - * - false, if $strict is true and file has some records. - * - true, if $strict is false and file has only empty lines and comments. - * - false, if $strict is false and file lines other than empty lines or - * comments. - * - * @throws \Exception - */ - protected function localExcludeEmpty(string $path, bool $strict = FALSE): bool { - if (!$this->localExcludeExists($path)) { - throw new \Exception(sprintf('File "%s" does not exist', $path)); - } - - $filename = $this->getLocalExcludeFileName($path); - if ($strict) { - return empty(file_get_contents($filename)); - } - $lines = file($filename); - if ($lines) { - $lines = array_map(trim(...), $lines); - $lines = array_filter($lines, static function ($line): bool { - return strlen($line) > 0; - }); - $lines = array_filter($lines, static function ($line): bool { - return !str_starts_with(trim($line), '#'); - }); - } - - return empty($lines); - } - - /** - * Disable local exclude file (.git/info/exclude). - * - * @param string $path - * Path to repository. - */ - protected function disableLocalExclude(string $path): void { - $filename = $this->getLocalExcludeFileName($path); - $filenameDisabled = $filename . '.bak'; - if ($this->fs->exists($filename)) { - $this->logDebug('Disabling local exclude'); - $this->fs->rename($filename, $filenameDisabled); - } - } - - /** - * Restore previously disabled local exclude file. - * - * @param string $path - * Path to repository. - */ - protected function restoreLocalExclude(string $path): void { - $filename = $this->getLocalExcludeFileName($path); - $filenameDisabled = $filename . '.bak'; - if ($this->fs->exists($filenameDisabled)) { - $this->logDebug('Restoring local exclude'); - $this->fs->rename($filenameDisabled, $filename); - } - } - - /** - * Remove ignored files. - * - * @param string $location - * Path to repository. - * @param string|null $gitignorePath - * Gitignore file name. - * - * @throws \Exception - * If removal command finished with an error. - */ - protected function removeIgnoredFiles(string $location, ?string $gitignorePath = NULL): void { - $location = $this->getSourcePathGitRepository(); - $gitignorePath = $gitignorePath ?: $location . DIRECTORY_SEPARATOR . '.gitignore'; - - $gitignoreContent = file_get_contents($gitignorePath); - if (!$gitignoreContent) { - $this->logDebug('Unable to load ' . $gitignoreContent); - } - else { - $this->logDebug('-----.gitignore---------'); - $this->logDebug($gitignoreContent); - $this->logDebug('-----.gitignore---------'); - } - - $files = $this - ->gitRepository - ->listIgnoredFilesFromGitIgnoreFile($gitignorePath); - - if (!empty($files)) { - $files = array_filter($files); - foreach ($files as $file) { - $fileName = $location . DIRECTORY_SEPARATOR . $file; - $this->logDebug(sprintf('Removing excluded file %s', $fileName)); - if ($this->fs->exists($fileName)) { - $this->fs->remove($fileName); - } - } - } - } - - /** - * Remove 'other' files. - * - * 'Other' files are files that are neither staged nor tracked in git. - * - * @throws \Exception - * If removal command finished with an error. - */ - protected function removeOtherFilesInGitRepository(): void { - $files = $this->gitRepository->listOtherFiles(); - if (!empty($files)) { - $files = array_filter($files); - foreach ($files as $file) { - $fileName = $this->getSourcePathGitRepository() . DIRECTORY_SEPARATOR . $file; - $this->logDebug(sprintf('Removing other file %s', $fileName)); - $this->fs->remove($fileName); - } + if (!$this->fsIsCommandAvailable('git')) { + throw new \RuntimeException('Git command is not available'); } - } - /** - * Remove any repositories within current repository. - */ - protected function removeSubReposInGitRepository(): void { - $finder = new Finder(); - $dirs = $finder - ->directories() - ->name('.git') - ->ignoreDotFiles(FALSE) - ->ignoreVCS(FALSE) - ->depth('>0') - ->in($this->getSourcePathGitRepository()); - - $dirs = iterator_to_array($dirs->directories()); - - foreach ($dirs as $dir) { - if ($dir instanceof \SplFileInfo) { - $dir = $dir->getPathname(); - } - $this->fs->remove($dir); - $this->logDebug(sprintf('Removing sub-repository "%s"', (string) $dir)); - } + $this->logNotice('All requirements were met'); } /** @@ -899,30 +461,24 @@ protected function removeSubReposInGitRepository(): void { * @throws \Exception */ protected function getTokenBranch(): string { - return $this - ->gitRepository - ->getCurrentBranchName(); + return $this->repo->getCurrentBranchName(); } /** * Token callback to get tags. * * @param string|null $delimiter - * Token delimiter. Defaults to ', '. + * Token delimiter. Defaults to '-'. * * @return string * String of tags. * * @throws \Exception */ - protected function getTokenTags(?string $delimiter = NULL): string { - $delimiter = $delimiter ?: '-'; - // We just want to get all tags point to the HEAD. - $tags = $this - ->gitRepository - ->getTagsPointToHead(); - - return implode($delimiter, $tags); + protected function getTokenTags(?string $delimiter): string { + $delimiter = $delimiter ?? '-'; + + return implode($delimiter, $this->repo->listTagsPointingToHead()); } /** @@ -938,30 +494,4 @@ protected function getTokenTimestamp(string $format = 'Y-m-d_H-i-s'): string { return date($format, $this->now); } - /** - * Write output. - * - * @param string $text - * Text. - */ - protected function writeln(string $text): void { - $this->output->writeln($text); - } - - /** - * Setup remote for current repository. - * - * @throws \CzProject\GitPhp\GitException - * @throws \Exception - */ - protected function setupRemoteForRepository(): void { - $remoteName = $this->remoteName; - $remoteUrl = $this->remoteUrl; - if (!ArtifactGitRepository::isValidRemoteUrl($remoteUrl)) { - throw new \Exception(sprintf('Invalid remote URL: %s', $remoteUrl)); - } - - $this->gitRepository->addRemote($remoteName, $remoteUrl); - } - } diff --git a/src/Git/ArtifactGit.php b/src/Git/ArtifactGit.php deleted file mode 100644 index edba160..0000000 --- a/src/Git/ArtifactGit.php +++ /dev/null @@ -1,23 +0,0 @@ -runner); - } - -} diff --git a/src/Git/ArtifactGitRepository.php b/src/Git/ArtifactGitRepository.php index 01cb7b4..8707fda 100644 --- a/src/Git/ArtifactGitRepository.php +++ b/src/Git/ArtifactGitRepository.php @@ -5,9 +5,13 @@ namespace DrevOps\GitArtifact\Git; use CzProject\GitPhp\GitRepository; +use CzProject\GitPhp\IRunner; use CzProject\GitPhp\RunnerResult; +use DrevOps\GitArtifact\Traits\FilesystemTrait; +use DrevOps\GitArtifact\Traits\LogTrait; use Psr\Log\LoggerInterface; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; /** * Artifact git repository. @@ -16,117 +20,64 @@ */ class ArtifactGitRepository extends GitRepository { - /** - * Filesystem. - */ - protected Filesystem $fs; + use FilesystemTrait; + use LogTrait; /** - * Logger. + * The gitignore file path. */ - protected LoggerInterface $logger; + protected string $gitignoreFile; /** - * Force pushing. - * - * @param string $remote - * Remote name. - * @param string $refSpec - * Specify what destination ref to update with what source object. - * - * @return ArtifactGitRepository - * Git repo. - * - * @throws \CzProject\GitPhp\GitException + * {@inheritdoc} */ - public function pushForce(string $remote, string $refSpec): ArtifactGitRepository { - return parent::push([$remote, $refSpec], ['--force']); - } + public function __construct($repository, ?IRunner $runner = NULL, ?LoggerInterface $logger = NULL) { + parent::__construct($repository, $runner); - /** - * List ignored files from git ignore file. - * - * @param string $gitIgnoreFilePath - * Git ignore file path. - * - * @return string[] - * Files. - * - * @throws \CzProject\GitPhp\GitException - */ - public function listIgnoredFilesFromGitIgnoreFile(string $gitIgnoreFilePath): array { - $files = $this->extractFromCommand(['ls-files', '-i', '-c', '--exclude-from=' . $gitIgnoreFilePath]); + $this->fs = new Filesystem(); - if (!$files) { - return []; + if ($logger instanceof LoggerInterface) { + $this->logger = $logger; } - - return $files; } /** - * List 'Other' files. - * - * 'Other' files are files that are neither staged nor tracked in git. - * - * @return string[] - * Files. - * - * @throws \CzProject\GitPhp\GitException + * {@inheritdoc} */ - public function listOtherFiles(): array { - $files = $this->extractFromCommand(['ls-files', '--other', '--exclude-standard']); - if (!$files) { - return []; - } + public function run(...$args): RunnerResult { + $command = array_shift($args); + array_unshift($args, '--no-pager', $command); - return $files; + return parent::run(...$args); } /** - * Get commits. - * - * @param string $format - * Commit format. - * - * @return string[] - * Commits. - * - * @throws \CzProject\GitPhp\GitException + * Set gitignore file. */ - public function getCommits(string $format = '%s'): array { - $commits = $this->extractFromCommand(['log', '--format=' . $format]); - if (!$commits) { - return []; - } + public function setGitignoreFile(string $filename): static { + $this->gitignoreFile = $filename; - return $commits; + return $this; } /** - * Reset hard. - * - * @return $this - * Git repo. - * - * @throws \CzProject\GitPhp\GitException + * {@inheritdoc} */ - public function resetHard(): ArtifactGitRepository { - $this->run('reset', ['--hard']); + public function addRemote($name, $url, ?array $options = NULL): static { + if (!self::isValidRemote($url)) { + throw new \InvalidArgumentException(sprintf('Invalid remote URL provided: %s', $url)); + } - return $this; + return parent::addRemote($name, $url, $options); } /** - * Clean repo. - * - * @return $this - * Git repo. - * - * @throws \CzProject\GitPhp\GitException + * {@inheritdoc} */ - public function cleanForce(): ArtifactGitRepository { - $this->run('clean', ['-dfx']); + public function removeRemote($name): static { + if (in_array($name, $this->listRemotes())) { + $this->run('remote', 'remove', $name); + } return $this; } @@ -134,22 +85,20 @@ public function cleanForce(): ArtifactGitRepository { /** * Switch to new branch. * - * @param string $branchName + * @param string $branch * Branch name. - * @param bool $createNew + * @param bool $create_new * Optional flag to also create a branch before switching. Default false. * - * @return ArtifactGitRepository + * @return static * The git repository. - * - * @throws \CzProject\GitPhp\GitException */ - public function switchToBranch(string $branchName, bool $createNew = FALSE): ArtifactGitRepository { - if (!$createNew) { - return $this->checkout($branchName); + public function switchToBranch(string $branch, bool $create_new = FALSE): static { + if (!$create_new) { + return $this->checkout($branch); } - return $this->createBranch($branchName, TRUE); + return $this->createBranch($branch, TRUE); } /** @@ -160,20 +109,20 @@ public function switchToBranch(string $branchName, bool $createNew = FALSE): Art * @param bool $force * Force remove or not. * - * @return ArtifactGitRepository + * @return static * Git repository - * - * @throws \CzProject\GitPhp\GitException */ - public function removeBranch($name, bool $force = FALSE): ArtifactGitRepository { + public function removeBranch($name, bool $force = FALSE): static { if (empty($name)) { return $this; } $branches = $this->getBranches(); + if (empty($branches)) { return $this; } + if (!in_array($name, $branches)) { return $this; } @@ -195,51 +144,56 @@ public function removeBranch($name, bool $force = FALSE): ArtifactGitRepository * * @return array * The changes. - * - * @throws \CzProject\GitPhp\GitException */ public function commitAllChanges(string $message): array { $this->addAllChanges(); - // We do not use commit method because we need return the output. + // We do not use the commit method because we need return the output. return $this->execute('commit', '--allow-empty', [ '-m' => $message, ]); } /** - * List committed files. - * - * @return string[] - * Files. - * - * @throws \CzProject\GitPhp\GitException + * Disable local exclude file (.git/info/exclude). */ - public function listCommittedFiles(): array { - $files = $this->extractFromCommand(['ls-tree', '--name-only', '-r', 'HEAD']); - if (!$files) { - return []; + public function disableLocalExclude(): static { + $filename = $this->getRepositoryPath() . DIRECTORY_SEPARATOR . '.git' . DIRECTORY_SEPARATOR . 'info' . DIRECTORY_SEPARATOR . 'exclude'; + + if ($this->fs->exists($filename)) { + $this->logDebug('Disabling local exclude'); + $this->fs->rename($filename, $filename . '.bak'); } - return $files; + return $this; } /** - * Set config receive.denyCurrentBranch is ignored. - * - * @return $this - * Git repo. - * - * @throws \CzProject\GitPhp\GitException + * Restore previously disabled local exclude file. */ - public function setConfigReceiveDenyCurrentBranchIgnore(): ArtifactGitRepository { - $this->extractFromCommand(['config', ['receive.denyCurrentBranch', 'ignore']]); + public function restoreLocalExclude(): static { + $filename = $this->getRepositoryPath() . DIRECTORY_SEPARATOR . '.git' . DIRECTORY_SEPARATOR . 'info' . DIRECTORY_SEPARATOR . 'exclude'; + + if ($this->fs->exists($filename . '.bak')) { + $this->logDebug('Restoring local exclude'); + $this->fs->rename($filename . '.bak', $filename); + } return $this; } /** - * Get tag point to HEAD. + * List remotes. + * + * @return array + * Remotes. + */ + protected function listRemotes(): array { + return $this->extractFromCommand(['remote']) ?: []; + } + + /** + * Get tag pointing to HEAD. * * @return string[] * Array of tags from the latest commit. @@ -247,7 +201,7 @@ public function setConfigReceiveDenyCurrentBranchIgnore(): ArtifactGitRepository * @throws \Exception * If no tags found in the latest commit. */ - public function getTagsPointToHead(): array { + public function listTagsPointingToHead(): array { $tags = $this->extractFromCommand(['tag', ['--points-at', 'HEAD']]); if (empty($tags)) { @@ -258,171 +212,169 @@ public function getTagsPointToHead(): array { } /** - * Create an annotated tag. - * - * @param string $name - * Name. - * @param string $message - * Message. - * - * @return $this - * Git repo. - * - * @throws \CzProject\GitPhp\GitException - */ - public function createAnnotatedTag(string $name, string $message): ArtifactGitRepository { - $this->createTag($name, [ - '--message=' . $message, - '-a', - ]); - - return $this; - } - - /** - * Create an annotated tag. + * Ger original branch, accounting for detached repository state. * - * @param string $name - * Name. + * Usually, repository become detached when a tag is checked out. * - * @return $this - * Git repo. + * @return string + * Branch or detachment source. * - * @throws \CzProject\GitPhp\GitException + * @throws \Exception + * If neither branch nor detachment source is not found. */ - public function createLightweightTag(string $name): ArtifactGitRepository { - $this->createTag($name); + public function getOriginalBranch(): string { + $branch = $this->getCurrentBranchName(); + + // Repository could be in detached state. If this the case - we need to + // capture the source of detachment, if it exists. + if (str_contains($branch, 'HEAD detached')) { + $branch = NULL; + $branch_list = $this->getBranches(); + if ($branch_list) { + $branch_list = array_filter($branch_list); + foreach ($branch_list as $branch) { + if (preg_match('/\(.*detached .* ([^)]+)\)/', $branch, $matches)) { + $branch = $matches[1]; + break; + } + } + } + + if (empty($branch)) { + throw new \Exception('Unable to determine a detachment source'); + } + } - return $this; + return $branch; } /** - * Remove remote by name. - * - * We need override this method because parent method does not work. - * - * @param string $name - * Remote name. - * - * @return ArtifactGitRepository - * Git repo. - * - * @throws \CzProject\GitPhp\GitException + * Remove ignored files. */ - public function removeRemote($name): ArtifactGitRepository { - if ($this->isRemoteExists($name)) { - $this->run('remote', 'remove', $name); + public function removeIgnoredFiles(): static { + if (!empty($this->gitignoreFile)) { + $gitignore = $this->getRepositoryPath() . DIRECTORY_SEPARATOR . '.gitignore'; + $this->logDebug(sprintf('Copying custom .gitignore file from %s to %s', $this->gitignoreFile, $gitignore)); + $this->fs->copy($this->gitignoreFile, $gitignore, TRUE); + + // Remove custom .gitignore file if it is within the repository. + // @todo Review if this is "magic" and should be explicitly listed in + // the file itself. Alternatively, we could add a check if the custom + // gitignore is ignored within itself and add it if it is not. + if (str_starts_with($this->gitignoreFile, $this->getRepositoryPath())) { + $this->fs->remove($this->gitignoreFile); + } + + // Custom .gitignore may contain rules that will change the list of + // ignored files. We need to add these files as changes so that they + // could be reported as excluded by the command below. + $this->addAllChanges(); + + $files = $this->extractFromCommand(['ls-files', '-i', '-c', '--exclude-from=' . $gitignore]) ?: []; + $files = array_filter($files); + + foreach ($files as $file) { + $filename = $this->getRepositoryPath() . DIRECTORY_SEPARATOR . $file; + if ($this->fs->exists($filename)) { + $this->logDebug(sprintf('Removing ignored file %s', $filename)); + $this->fs->remove($filename); + } + } } return $this; } /** - * Get remote list. + * Remove 'other' files. * - * @return array - * Remotes. - * - * @throws \CzProject\GitPhp\GitException + * 'Other' files are files that are neither staged nor tracked in git. */ - public function getRemotes(): array { - $remotes = $this->extractFromCommand(['remote']); - if (!$remotes) { - return []; + public function removeOtherFiles(): static { + $files = $this->extractFromCommand(['ls-files', '--other', '--exclude-standard']) ?: []; + $files = array_filter($files); + + foreach ($files as $file) { + $filename = $this->getRepositoryPath() . DIRECTORY_SEPARATOR . $file; + if ($this->fs->exists($filename)) { + $this->logDebug(sprintf('Removing other file %s', $filename)); + $this->fs->remove($filename); + } } - return $remotes; + return $this; } /** - * Check remote is existing or not by remote name. - * - * @param string $remoteName - * Remote name to check. - * - * @return bool - * Exist or not. - * - * @throws \CzProject\GitPhp\GitException + * Remove any repositories within current repository. */ - public function isRemoteExists(string $remoteName): bool { - $remotes = $this->getRemotes(); - if (empty($remotes)) { - return FALSE; + public function removeSubRepositories(): static { + $finder = new Finder(); + $dirs = $finder + ->directories() + ->name('.git') + ->ignoreDotFiles(FALSE) + ->ignoreVCS(FALSE) + ->depth('>0') + ->in($this->getRepositoryPath()); + + $dirs = iterator_to_array($dirs->directories()); + + foreach ($dirs as $dir) { + if ($dir instanceof \SplFileInfo) { + $dir = $dir->getPathname(); + $this->fs->remove($dir); + $this->logDebug(sprintf('Removing sub-repository "%s"', $this->fsGetAbsolutePath((string) $dir))); + } } - return in_array($remoteName, $remotes); - } - - /** - * Override run method to add --no-pager option to all command. - * - * @param mixed ...$args - * Command args. - * - * @return \CzProject\GitPhp\RunnerResult - * Runner result. - * - * @throws \CzProject\GitPhp\GitException - */ - public function run(...$args): RunnerResult { - $command = array_shift($args); - array_unshift($args, '--no-pager', $command); - - return parent::run(...$args); - } + // After removing sub-repositories, the files that were previously tracked + // in those repositories are now become a part of the current repository. + // We need to add them as changes. + $this->addAllChanges(); - /** - * Check if provided location is a URI. - * - * @param string $location - * Location to check. - * - * @return bool - * TRUE if location is URI, FALSE otherwise. - */ - public static function isUri(string $location): bool { - return (bool) preg_match('/^(?:git|ssh|https?|[\d\w\.\-_]+@[\w\.\-]+):(?:\/\/)?[\w\.@:\/~_-]+\.git(?:\/?|\#[\d\w\.\-_]+?)$/', $location); + return $this; } /** - * Check if provided branch name can be used in git. + * Check if provided branch name can be used in Git. * - * @param string $branchName - * Branch to check. + * @param string $name + * Branch name to check. * * @return bool * TRUE if it is a valid Git branch, FALSE otherwise. */ - public static function isValidBranchName(string $branchName): bool { - return preg_match('/^(?!\/|.*(?:[\/\.]\.|\/\/|\\|@\{))[^\040\177\s\~\^\:\?\*\[]+(?exists($pathOrUri); - $isUri = self::isUri($pathOrUri); + + $is_local = $filesystem->exists($uri); + $is_external = (bool) preg_match('/^(?:git|ssh|https?|[\d\w\.\-_]+@[\w\.\-]+):(?:\/\/)?[\w\.@:\/~_-]+\.git(?:\/?|\#[\d\w\.\-_]+?)$/', $uri); return match ($type) { - 'any' => $isLocal || $isUri, - 'local' => $isLocal, - 'uri' => $isUri, + 'any' => $is_local || $is_external, + 'local' => $is_local, + 'external' => $is_external, default => throw new \InvalidArgumentException(sprintf('Invalid argument "%s" provided', $type)), }; } diff --git a/src/Traits/FilesystemTrait.php b/src/Traits/FilesystemTrait.php index 6d1c534..9887695 100644 --- a/src/Traits/FilesystemTrait.php +++ b/src/Traits/FilesystemTrait.php @@ -44,7 +44,7 @@ trait FilesystemTrait { */ protected function fsSetRootDir(?string $path = NULL): static { $path = empty($path) ? $this->fsGetRootDir() : $this->fsGetAbsolutePath($path); - $this->fsPathsExist($path); + $this->fsAssertPathsExist($path); $this->fsRootDir = $path; return $this; @@ -84,6 +84,7 @@ protected function fsGetRootDir(): string { protected function fsSetCwd(string $dir): static { chdir($dir); $this->fsOriginalCwdStack[] = $dir; + return $this; } @@ -163,8 +164,9 @@ protected function fsGetAbsolutePath(string $file, ?string $root = NULL): string * @throws \Exception * If at least one file does not exist. */ - protected function fsPathsExist($paths, bool $strict = TRUE): bool { + protected function fsAssertPathsExist($paths, bool $strict = TRUE): bool { $paths = is_array($paths) ? $paths : [$paths]; + if (!$this->fs->exists($paths)) { if ($strict) { throw new \Exception(sprintf('One of the files or directories does not exist: %s', implode(', ', $paths))); @@ -177,7 +179,7 @@ protected function fsPathsExist($paths, bool $strict = TRUE): bool { } /** - * Replacement for PHP's `fsRealpath` resolves non-existing paths. + * Replacement for PHP's `realpath` resolves non-existing paths. * * The main deference is that it does not return FALSE on non-existing * paths. @@ -208,8 +210,8 @@ protected function fsRealpath(string $path): string { // Resolve path parts (single dot, double dot and double delimiters). $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); - $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), static function ($part) : bool { - return strlen($part) > 0; + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), static function ($part): bool { + return strlen($part) > 0; }); $absolutes = []; @@ -239,7 +241,16 @@ protected function fsRealpath(string $path): string { // Put initial separator that could have been lost. $path = $unipath ? $path : '/' . $path; - return $unc ? '\\\\' . $path : $path; + $path = $unc ? '\\\\' . $path : $path; + + if (str_starts_with($path, sys_get_temp_dir())) { + $tmp_realpath = realpath(sys_get_temp_dir()); + if ($tmp_realpath) { + $path = str_replace(sys_get_temp_dir(), $tmp_realpath, $path); + } + } + + return $path; } } diff --git a/src/Traits/LogTrait.php b/src/Traits/LogTrait.php index 43497b4..5755e10 100644 --- a/src/Traits/LogTrait.php +++ b/src/Traits/LogTrait.php @@ -9,6 +9,7 @@ use Monolog\Logger; use Psr\Log\LoggerInterface; use Symfony\Bridge\Monolog\Handler\ConsoleHandler; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** @@ -22,25 +23,26 @@ trait LogTrait { protected LoggerInterface $logger; /** - * Create Logger. - * - * @param string $name - * Name. - * @param \Symfony\Component\Console\Output\OutputInterface $output - * Output. - * @param string $filepath - * Filepath to log file. - * - * @return \Psr\Log\LoggerInterface - * Logger. + * Path to the temporary log file. */ - public static function loggerCreate(string $name, OutputInterface $output, string $filepath): LoggerInterface { - $logger = new Logger($name); + protected string $logDumpFile = ''; + + /** + * Prepare logger. + */ + protected function logPrepare(string $name, InputInterface $input, OutputInterface $output): void { + if ($input->getOption('log')) { + $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + } + + $this->logDumpFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . time() . '-artifact-log.log'; + + $this->logger = new Logger($name); $handler = new ConsoleHandler($output); - $logger->pushHandler($handler); + $this->logger->pushHandler($handler); - if (!empty($filepath)) { + if (!empty($this->logDumpFile)) { $map = [ OutputInterface::VERBOSITY_QUIET => Level::Error, OutputInterface::VERBOSITY_NORMAL => Level::Warning, @@ -49,12 +51,12 @@ public static function loggerCreate(string $name, OutputInterface $output, strin OutputInterface::VERBOSITY_DEBUG => Level::Debug, ]; - $handler = new StreamHandler($filepath, $map[$output->getVerbosity()] ?? Level::Debug); + $handler = new StreamHandler($this->logDumpFile, $map[$output->getVerbosity()] ?? Level::Debug); - $logger->pushHandler($handler); + $this->logger->pushHandler($handler); } - return $logger; + $this->logDebug('Debug messages enabled'); } /** @@ -93,4 +95,14 @@ public function logError(string|\Stringable $message, array $context = []): void $this->logger->error($message, $context); } + /** + * Dump log to file. + */ + protected function logDump(string $filename): void { + if ($this->fs->exists($this->logDumpFile)) { + $this->fs->copy($this->logDumpFile, $filename); + $this->fs->remove($this->logDumpFile); + } + } + } diff --git a/src/Traits/TokenTrait.php b/src/Traits/TokenTrait.php index 68a6325..9e70683 100644 --- a/src/Traits/TokenTrait.php +++ b/src/Traits/TokenTrait.php @@ -15,29 +15,33 @@ trait TokenTrait { * @param string $string * String that may contain tokens surrounded by '[' and ']'. * - * @return string|null + * @return string * String with replaced tokens if replacements are available or * original string. */ - protected function tokenProcess(string $string): ?string { - return preg_replace_callback('/(?:\[([^\]]+)\])/', function (array $match): string { + protected function tokenProcess(string $string): string { + $processed = preg_replace_callback('/(?:\[([^\]]+)\])/', function (array $match): string { + $replacement = strval($match[0]); + if (!empty($match[1])) { $parts = explode(':', $match[1], 2); - $token = $parts[0] ?? NULL; + $token = $parts[0]; $argument = $parts[1] ?? NULL; - if ($token) { + if ($token !== '' && $token !== '0') { $method = 'getToken' . ucfirst($token); if (method_exists($this, $method) && is_callable([$this, $method])) { - $match[0] = (string) $this->$method($argument); + $replacement = (string) $this->$method($argument); } } } - return strval($match[0]); + return $replacement; }, $string); + + return $processed ?? $string; } /** diff --git a/tests/phpunit/AbstractTestCase.php b/tests/phpunit/AbstractTestCase.php deleted file mode 100644 index 0af80ea..0000000 --- a/tests/phpunit/AbstractTestCase.php +++ /dev/null @@ -1,73 +0,0 @@ -fs = new Filesystem(); - $this->git = new ArtifactGit(); - - $this->fixtureDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'git_artifact'; - $this->fs->mkdir($this->fixtureDir); - - $this->commandTraitSetUp( - $this->fixtureDir . DIRECTORY_SEPARATOR . 'git_src', - $this->fixtureDir . DIRECTORY_SEPARATOR . 'git_remote', - ); - } - - /** - * {@inheritdoc} - */ - protected function tearDown(): void { - $this->commandTraitTearDown(); - - if ($this->fs->exists($this->fixtureDir)) { - $this->fs->remove($this->fixtureDir); - } - } - -} diff --git a/tests/phpunit/Exception/ErrorException.php b/tests/phpunit/Exception/ErrorException.php deleted file mode 100644 index f4e7e43..0000000 --- a/tests/phpunit/Exception/ErrorException.php +++ /dev/null @@ -1,24 +0,0 @@ -file = $file; - $this->line = $line; - } - -} diff --git a/tests/phpunit/Functional/AbstractFunctionalTestCase.php b/tests/phpunit/Functional/AbstractFunctionalTestCase.php index a5c21b1..b434b44 100644 --- a/tests/phpunit/Functional/AbstractFunctionalTestCase.php +++ b/tests/phpunit/Functional/AbstractFunctionalTestCase.php @@ -4,12 +4,48 @@ namespace DrevOps\GitArtifact\Tests\Functional; -use DrevOps\GitArtifact\Tests\AbstractTestCase; +use DrevOps\GitArtifact\Commands\ArtifactCommand; +use DrevOps\GitArtifact\Tests\Traits\FixtureTrait; +use DrevOps\GitArtifact\Tests\Traits\GitTrait; +use DrevOps\GitArtifact\Tests\Unit\AbstractUnitTestCase; +use DrevOps\GitArtifact\Traits\FilesystemTrait; +use PHPUnit\Framework\AssertionFailedError; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Filesystem; -/** - * Class AbstractTestCase. - */ -abstract class AbstractFunctionalTestCase extends AbstractTestCase { +abstract class AbstractFunctionalTestCase extends AbstractUnitTestCase { + + use FilesystemTrait; + use FixtureTrait; + use GitTrait; + + /** + * Fixture source repository directory. + * + * @var string + */ + protected $src; + + /** + * Fixture remote repository directory. + * + * @var string + */ + protected $dst; + + /** + * Artifact command. + */ + protected ArtifactCommand $command; + + /** + * Fixture directory. + * + * @var string + */ + protected $fixtureDir; /** * Current branch. @@ -30,7 +66,7 @@ abstract class AbstractFunctionalTestCase extends AbstractTestCase { * * @var string */ - protected $remote; + protected $remoteName; /** * Mode in which the build will run. @@ -56,31 +92,55 @@ abstract class AbstractFunctionalTestCase extends AbstractTestCase { protected function setUp(): void { parent::setUp(); + $this->fs = new Filesystem(); + + $this->fixtureDir = $this->fsGetAbsolutePath(sys_get_temp_dir() . DIRECTORY_SEPARATOR . date('U') . DIRECTORY_SEPARATOR . 'git_artifact'); + + $this->src = $this->fsGetAbsolutePath($this->fixtureDir . DIRECTORY_SEPARATOR . 'src'); + $this->gitInitRepo($this->src); + + $this->dst = $this->fixtureDir . DIRECTORY_SEPARATOR . 'dst'; + $this->gitInitRepo($this->dst) + // Allow pushing into already checked out branch. We need this to + // avoid additional management of fixture repository. + ->run('config', ['receive.denyCurrentBranch', 'ignore']); + $this->now = time(); - $this->currentBranch = 'master'; - $this->artifactBranch = 'master-artifact'; - $this->remote = 'dst'; + $this->currentBranch = $this->gitGetGlobalDefaultBranch(); + $this->artifactBranch = $this->currentBranch . '-artifact'; + $this->remoteName = 'dst'; + } + + /** + * {@inheritdoc} + */ + protected function tearDown(): void { + if ($this->fs->exists($this->fixtureDir)) { + $this->fs->remove($this->fixtureDir); + } } /** * Build the artifact and assert success. * - * @param string $args - * Optional string of arguments to pass to the build. + * @param array $args + * Array of arguments to pass to the build. * @param string $branch - * Optional --branch value. Defaults to 'testbranch'. + * Expected branch name. * @param string $commit * Optional commit string. Defaults to 'Deployment commit'. * * @return string * Command output. */ - protected function assertBuildSuccess(string $args = '', string $branch = 'testbranch', string $commit = 'Deployment commit'): string { - $output = $this->runBuild(sprintf('--branch=%s %s', $branch, $args)); + protected function assertCommandSuccess(?array $args = [], string $branch = 'testbranch', string $commit = 'Deployment commit'): string { + $args += ['--branch' => 'testbranch']; + $output = $this->runCommand($args); + $this->assertStringNotContainsString('[error]', $output); $this->assertStringContainsString(sprintf('Pushed branch "%s" with commit message "%s"', $branch, $commit), $output); $this->assertStringContainsString('Deployment finished successfully.', $output); - $this->assertStringNotContainsString('Deployment failed.', $output); + $this->assertStringNotContainsString('Processing failed with an error:', $output); return $output; } @@ -88,21 +148,23 @@ protected function assertBuildSuccess(string $args = '', string $branch = 'testb /** * Build the artifact and assert failure. * - * @param string $args - * Optional string of arguments to pass to the build. - * @param string $branch - * Optional --branch value. Defaults to 'testbranch'. + * @param array $args + * * Array of arguments to pass to the build. + * * @param string $branch + * * Expected branch name. * @param string $commit * Optional commit string. Defaults to 'Deployment commit'. * * @return string * Command output. */ - protected function assertBuildFailure(string $args = '', string $branch = 'testbranch', string $commit = 'Deployment commit'): string { - $output = $this->runBuild(sprintf('--branch=%s %s', $branch, $args), TRUE); - $this->assertStringNotContainsString(sprintf('Pushed branch "%s" with commit message "%s"', $branch, $commit), $output); + protected function assertCommandFailure(?array $args = [], string $commit = 'Deployment commit'): string { + $args += ['--branch' => 'testbranch']; + $output = $this->runCommand($args, TRUE); + + $this->assertStringNotContainsString(sprintf('Pushed branch "%s" with commit message "%s"', $args['--branch'], $commit), $output); $this->assertStringNotContainsString('Deployment finished successfully.', $output); - $this->assertStringContainsString('Deployment failed.', $output); + $this->assertStringContainsString('Processing failed with an error:', $output); return $output; } @@ -110,72 +172,101 @@ protected function assertBuildFailure(string $args = '', string $branch = 'testb /** * Run artifact build. * - * @param string $args - * Additional arguments or options as a string. - * @param bool $expectFail + * @param array $args + * Additional arguments or options as an associative array. + * @param bool $expect_fail * Expect on fail. * * @return string * Output string. */ - protected function runBuild(string $args = '', bool $expectFail = FALSE): string { - if ($this->mode) { - $args .= ' --mode=' . $this->mode; - } + protected function runCommand(?array $args = [], bool $expect_fail = FALSE): string { + try { - $output = $this->runGitArtifactCommandTimestamped(sprintf('--src=%s %s %s', $this->src, $this->dst, $args), $expectFail); + if (is_null($args)) { + $input = []; + } + else { + $input = [ + '--root' => $this->fixtureDir, + '--now' => $this->now, + '--src' => $this->src, + 'remote' => $this->dst, + ]; - return implode(PHP_EOL, $output); - } + if ($this->mode) { + $input['--mode'] = $this->mode; + } - /** - * Run command with current timestamp attached to artifact commands. - * - * @param string $command - * Command string to run. - * @param bool $expectFail - * Flag to state that the command should fail. - * - * @return array - * Array of output lines. - */ - protected function runGitArtifactCommandTimestamped(string $command, bool $expectFail = FALSE): array { - // Add --now option to all 'artifact' commands. - $command .= ' --now=' . $this->now; + $input += $args; + } + + $this->runExecute(ArtifactCommand::class, $input); + $output = $this->commandTester->getDisplay(); + + if ($this->commandTester->getStatusCode() !== 0) { + throw new \Exception(sprintf("Command exited with non-zero code.\nThe output was:\n%s\nThe error output was:\n%s", $this->commandTester->getDisplay(), $this->commandTester->getErrorOutput())); + } - return $this->commandRunGitArtifactCommand($command, $expectFail); + if ($expect_fail) { + throw new AssertionFailedError(sprintf("Command exited successfully but should not.\nThe output was:\n%s\nThe error output was:\n%s", $this->commandTester->getDisplay(), $this->commandTester->getErrorOutput())); + } + + } + catch (\RuntimeException $exception) { + if (!$expect_fail) { + throw new AssertionFailedError('Command exited with an error:' . PHP_EOL . $exception->getMessage()); + } + $output = $exception->getMessage(); + } + catch (\Exception $exception) { + if (!$expect_fail) { + throw new AssertionFailedError('Command exited with an error:' . PHP_EOL . $exception->getMessage()); + } + } + + return $output; } /** - * Assert current git branch. + * CommandTester instance. * - * @param string $path - * Path to repository. - * @param string $branch - * Branch name to assert. + * @var \Symfony\Component\Console\Tester\CommandTester */ - protected function assertGitCurrentBranch(string $path, string $branch): void { - $currentBranch = $this->git->open($path)->getCurrentBranchName(); - - $this->assertStringContainsString($branch, $currentBranch, sprintf('Current branch is "%s"', $branch)); - } + protected $commandTester; /** - * Assert that there is no remote specified in git repository. + * Run main() with optional arguments. * - * @param string $path - * Path to repository. - * @param string $remote - * Remote name to assert. + * @param string|object $object_or_class + * Object or class name. + * @param array $input + * Optional array of input arguments. + * @param array $options + * Optional array of options. See CommandTester::execute() for details. */ - protected function assertGitNoRemote(string $path, string $remote): void { - $remotes = $this->git->open($path)->getRemotes(); - if (empty($remotes)) { - $this->assertEmpty($remotes); + protected function runExecute(string|object $object_or_class, array $input = [], array $options = []): void { + $application = new Application(); + /** @var \Symfony\Component\Console\Command\Command $instance */ + $instance = is_object($object_or_class) ? $object_or_class : new $object_or_class(); + $application->add($instance); + + $name = $instance->getName(); + if (empty($name)) { + /** @var string $name */ + $name = $this->getProtectedValue($instance, 'defaultName'); } - else { - $this->assertStringNotContainsString($remote, implode("\n", $remotes), sprintf('Remote "%s" is not present"', $remote)); + + $command = $application->find($name); + $this->commandTester = new CommandTester($command); + + $options['capture_stderr_separately'] = TRUE; + if (array_key_exists('-vvv', $input)) { + $options['verbosity'] = ConsoleOutput::VERBOSITY_DEBUG; + unset($input['-vvv']); } + + $this->commandTester->execute($input, $options); } } diff --git a/tests/phpunit/Functional/BranchModeTest.php b/tests/phpunit/Functional/BranchModeTest.php new file mode 100644 index 0000000..dbba544 --- /dev/null +++ b/tests/phpunit/Functional/BranchModeTest.php @@ -0,0 +1,151 @@ +mode = 'branch'; + parent::setUp(); + } + + public function testBuild(): void { + $this->gitCreateFixtureCommits(2); + + $output = $this->assertCommandSuccess(); + $this->assertStringContainsString('WARNING! Provided branch name does not have a token', $output); + $this->assertStringContainsString('Mode: branch', $output); + $this->assertStringContainsString('Will push: Yes', $output); + + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + } + + public function testBuildMoreCommitsSameBranch(): void { + $this->gitCreateFixtureCommits(2); + + $this->assertCommandSuccess(); + + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + + $this->gitCreateFixtureCommits(3, 2); + $this->assertCommandFailure(); + + // Make sure that broken artifact was not pushed. + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + } + + public function testBuildMoreCommits(): void { + $this->gitCreateFixtureCommits(2); + + $this->now = time() - rand(1, 10 * 60); + $branch1 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); + $output = $this->assertCommandSuccess(['--branch' => 'testbranch-[timestamp:Y-m-d_H-i-s]'], $branch1); + $this->assertStringContainsString('Remote branch: ' . $branch1, $output); + $this->assertStringNotContainsString('WARNING! Provided branch name does not have a token', $output); + + $this->gitAssertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); + + $this->gitCreateFixtureCommits(3, 2); + + $this->now = time() - rand(1, 10 * 60); + $branch2 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); + $output = $this->assertCommandSuccess(['--branch' => 'testbranch-[timestamp:Y-m-d_H-i-s]'], $branch2); + $this->assertStringContainsString('Remote branch: ' . $branch2, $output); + $this->gitAssertFixtureCommits(5, $this->dst, $branch2, ['Deployment commit']); + + // Also, check that no changes were done to branch1. + $this->gitAssertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); + } + + public function testCleanupAfterSuccess(): void { + $this->gitCreateFixtureCommits(2); + + $this->assertCommandSuccess(); + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + + $this->gitAssertCurrentBranch($this->src, $this->currentBranch); + $this->gitAssertRemoteNotExists($this->src, $this->remoteName); + } + + public function testCleanupAfterFailure(): void { + $this->gitCreateFixtureCommits(2); + + $this->assertCommandSuccess(); + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + + $this->gitCreateFixtureCommits(3, 2); + // Trigger erroneous build by pushing to the same branch. + $this->assertCommandFailure(); + + $this->gitAssertCurrentBranch($this->src, $this->currentBranch); + $this->gitAssertRemoteNotExists($this->src, $this->remoteName); + } + + public function testGitignore(): void { + $this->fixtureCreateFile($this->src, '.gitignore', 'f3'); + $this->gitCreateFixtureCommits(2); + $this->fixtureCreateFile($this->src, 'f3'); + + $this->now = time() - rand(1, 10 * 60); + $branch1 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); + $this->assertCommandSuccess(['--branch' => 'testbranch-[timestamp:Y-m-d_H-i-s]'], $branch1); + + $this->gitAssertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); + $this->assertFileDoesNotExist($this->dst . DIRECTORY_SEPARATOR . 'f3'); + + // Now, remove the .gitignore and push again. + $this->fixtureRemoveFile($this->src, '.gitignore'); + $this->gitCommitAll($this->src, 'Commit number 3'); + $this->now = time() - rand(1, 10 * 60); + $branch2 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); + $this->assertCommandSuccess(['--branch' => 'testbranch-[timestamp:Y-m-d_H-i-s]'], $branch2); + + $this->gitAssertFixtureCommits(3, $this->dst, $branch2, ['Deployment commit']); + + // Assert that branch from previous deployment was not affected. + $this->gitAssertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); + $this->assertFileDoesNotExist($this->dst . DIRECTORY_SEPARATOR . 'f3'); + } + + public function testGitignoreCustom(): void { + $this->fixtureCreateFile($this->src, 'mygitignore', 'f3'); + $this->gitCreateFixtureCommits(2); + $this->fixtureCreateFile($this->src, 'f3'); + + $this->now = time() - rand(1, 10 * 60); + $branch1 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); + $this->assertCommandSuccess([ + '--branch' => 'testbranch-[timestamp:Y-m-d_H-i-s]', + '--gitignore' => $this->src . DIRECTORY_SEPARATOR . 'mygitignore', + ], $branch1); + + $this->gitAssertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); + $this->assertFileDoesNotExist($this->dst . DIRECTORY_SEPARATOR . 'f3'); + + // Now, remove the .gitignore and push again. + $this->fixtureCreateFile($this->src, 'f3'); + $this->fixtureRemoveFile($this->src, 'mygitignore'); + $this->gitCommitAll($this->src, 'Commit number 3'); + $this->now = time() - rand(1, 10 * 60); + $branch2 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); + $this->assertCommandSuccess(['--branch' => 'testbranch-[timestamp:Y-m-d_H-i-s]'], $branch2); + + $this->gitAssertFixtureCommits(3, $this->dst, $branch2, ['Deployment commit']); + + // Assert that branch from previous deployment was not affected. + $this->gitAssertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); + $this->assertFileDoesNotExist($this->dst . DIRECTORY_SEPARATOR . 'f3'); + } + +} diff --git a/tests/phpunit/Functional/BranchTest.php b/tests/phpunit/Functional/BranchTest.php deleted file mode 100644 index ffca788..0000000 --- a/tests/phpunit/Functional/BranchTest.php +++ /dev/null @@ -1,150 +0,0 @@ -mode = 'branch'; - parent::setUp(); - } - - public function testBuild(): void { - $this->gitCreateFixtureCommits(2); - - $output = $this->assertBuildSuccess(); - $this->assertStringContainsString('WARNING! Provided branch name does not have a token', $output); - $this->assertStringContainsString('Mode: branch', $output); - $this->assertStringContainsString('Will push: Yes', $output); - - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - } - - public function testBuildMoreCommitsSameBranch(): void { - $this->gitCreateFixtureCommits(2); - - $this->assertBuildSuccess(); - - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - - $this->gitCreateFixtureCommits(3, 2); - $this->assertBuildFailure(); - - // Make sure that broken artifact was not pushed. - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - } - - public function testBuildMoreCommits(): void { - $this->gitCreateFixtureCommits(2); - - $this->now = time() - rand(1, 10 * 60); - $branch1 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); - $output = $this->assertBuildSuccess('--branch=testbranch-[timestamp:Y-m-d_H-i-s]', $branch1); - $this->assertStringContainsString('Remote branch: ' . $branch1, $output); - $this->assertStringNotContainsString('WARNING! Provided branch name does not have a token', $output); - - $this->assertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); - - $this->gitCreateFixtureCommits(3, 2); - - $this->now = time() - rand(1, 10 * 60); - $branch2 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); - $output = $this->assertBuildSuccess('--branch=testbranch-[timestamp:Y-m-d_H-i-s]', $branch2); - $this->assertStringContainsString('Remote branch: ' . $branch2, $output); - $this->assertFixtureCommits(5, $this->dst, $branch2, ['Deployment commit']); - - // Also, check that no changes were done to branch1. - $this->assertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); - } - - public function testCleanupAfterSuccess(): void { - $this->gitCreateFixtureCommits(2); - - $this->assertBuildSuccess(); - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - - $this->assertGitCurrentBranch($this->src, $this->currentBranch); - $this->assertGitNoRemote($this->src, $this->remote); - } - - public function testCleanupAfterFailure(): void { - $this->gitCreateFixtureCommits(2); - - $this->assertBuildSuccess('', 'testbranch'); - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - - $this->gitCreateFixtureCommits(3, 2); - // Trigger erroneous build by pushing to the same branch. - $this->assertBuildFailure('', 'testbranch'); - - $this->assertGitCurrentBranch($this->src, $this->currentBranch); - $this->assertGitNoRemote($this->src, $this->remote); - } - - public function testGitignore(): void { - $this->gitCreateFixtureFile($this->src, '.gitignore', 'f3'); - $this->gitCreateFixtureCommits(2); - $this->gitCreateFixtureFile($this->src, 'f3'); - - $this->now = time() - rand(1, 10 * 60); - $branch1 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); - $this->assertBuildSuccess('--branch=testbranch-[timestamp:Y-m-d_H-i-s]', $branch1); - - $this->assertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); - $this->gitAssertFilesNotExist($this->dst, 'f3'); - - // Now, remove the .gitignore and push again. - $this->gitRemoveFixtureFile($this->src, '.gitignore'); - $this->gitCommitAll($this->src, 'Commit number 3'); - $this->now = time() - rand(1, 10 * 60); - $branch2 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); - $this->assertBuildSuccess('--branch=testbranch-[timestamp:Y-m-d_H-i-s]', $branch2); - - $this->assertFixtureCommits(3, $this->dst, $branch2, ['Deployment commit']); - - // Assert that branch from previous deployment was not affected. - $this->assertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); - $this->gitAssertFilesNotExist($this->dst, 'f3'); - } - - public function testGitignoreCustom(): void { - $this->gitCreateFixtureFile($this->src, 'mygitignore', 'f3'); - $this->gitCreateFixtureCommits(2); - $this->gitCreateFixtureFile($this->src, 'f3'); - - $this->now = time() - rand(1, 10 * 60); - $branch1 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); - $this->assertBuildSuccess('--branch=testbranch-[timestamp:Y-m-d_H-i-s] --gitignore=' . $this->src . DIRECTORY_SEPARATOR . 'mygitignore', $branch1); - - $this->assertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); - $this->gitAssertFilesNotExist($this->dst, 'f3'); - - // Now, remove the .gitignore and push again. - $this->gitCreateFixtureFile($this->src, 'f3'); - $this->gitRemoveFixtureFile($this->src, 'mygitignore'); - $this->gitCommitAll($this->src, 'Commit number 3'); - $this->now = time() - rand(1, 10 * 60); - $branch2 = 'testbranch-' . date('Y-m-d_H-i-s', $this->now); - $this->assertBuildSuccess('--branch=testbranch-[timestamp:Y-m-d_H-i-s]', $branch2); - - $this->assertFixtureCommits(3, $this->dst, $branch2, ['Deployment commit']); - - // Assert that branch from previous deployment was not affected. - $this->assertFixtureCommits(2, $this->dst, $branch1, ['Deployment commit']); - $this->gitAssertFilesNotExist($this->dst, 'f3'); - } - -} diff --git a/tests/phpunit/Functional/ForcePushModeTest.php b/tests/phpunit/Functional/ForcePushModeTest.php new file mode 100644 index 0000000..b7e5a77 --- /dev/null +++ b/tests/phpunit/Functional/ForcePushModeTest.php @@ -0,0 +1,311 @@ +mode = 'force-push'; + parent::setUp(); + } + + public function testBuild(): void { + $this->gitCreateFixtureCommits(2); + + $output = $this->assertCommandSuccess(); + $this->assertStringContainsString('Mode: force-push', $output); + $this->assertStringContainsString('Will push: Yes', $output); + + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + } + + public function testBuildMoreCommits(): void { + $this->gitCreateFixtureCommits(2); + + $this->assertCommandSuccess(); + + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + + $this->gitCreateFixtureCommits(3, 2); + $this->assertCommandSuccess(); + + $this->gitAssertFixtureCommits(5, $this->dst, 'testbranch', ['Deployment commit']); + } + + public function testIdempotence(): void { + $this->gitCreateFixtureCommits(2); + + $this->assertCommandSuccess(); + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + + $this->assertCommandSuccess(); + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + } + + public function testSubRepos(): void { + $this->gitCreateFixtureCommits(2); + + $this->fixtureCreateFile($this->src, 'c'); + $this->gitCommitAll($this->src, 'Commit number 3'); + + $this->gitInitRepo($this->src . DIRECTORY_SEPARATOR . 'r1/'); + $this->fixtureCreateFile($this->src, 'r1/c'); + + $this->gitInitRepo($this->src . DIRECTORY_SEPARATOR . 'r2/r21'); + $this->fixtureCreateFile($this->src, 'r2/r21/c'); + + $this->gitInitRepo($this->src . DIRECTORY_SEPARATOR . 'r3/r31/r311'); + $this->fixtureCreateFile($this->src, 'r3/r31/r311/c'); + + $this->gitAssertFilesExist($this->src, ['r1/c']); + $this->gitAssertFilesNotExist($this->src, ['r1/.git/index']); + $this->gitAssertFilesNotExist($this->src, ['r2/r21.git/index']); + $this->gitAssertFilesNotExist($this->src, ['r3/r31/r311/.git/index']); + + $output = $this->assertCommandSuccess(['-vvv' => TRUE]); + $this->assertStringContainsString(sprintf('Removing sub-repository "%s"', $this->fsGetAbsolutePath($this->src . DIRECTORY_SEPARATOR . 'r1/.git')), $output); + $this->assertStringContainsString(sprintf('Removing sub-repository "%s"', $this->fsGetAbsolutePath($this->src . DIRECTORY_SEPARATOR . 'r2/r21/.git')), $output); + $this->assertStringContainsString(sprintf('Removing sub-repository "%s"', $this->fsGetAbsolutePath($this->src . DIRECTORY_SEPARATOR . 'r3/r31/r311/.git')), $output); + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Commit number 3', 'Deployment commit']); + + $this->gitAssertFilesExist($this->dst, ['r1/c']); + $this->gitAssertFilesExist($this->dst, ['r2/r21/c']); + $this->gitAssertFilesExist($this->dst, ['r3/r31/r311/c']); + $this->gitAssertFilesNotExist($this->dst, ['r1/.git/index']); + $this->gitAssertFilesNotExist($this->dst, ['r1/.git']); + $this->gitAssertFilesNotExist($this->dst, ['r2/r21/.git/index']); + $this->gitAssertFilesNotExist($this->dst, ['r2/r21/.git']); + $this->gitAssertFilesNotExist($this->dst, ['r3/r31/311/.git/index']); + $this->gitAssertFilesNotExist($this->dst, ['r3/r31/311/.git']); + } + + public function testCleanupAfterSuccess(): void { + $this->gitCreateFixtureCommits(2); + + $this->assertCommandSuccess(); + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + + $this->gitAssertCurrentBranch($this->src, $this->currentBranch); + $this->gitAssertRemoteNotExists($this->src, $this->remoteName); + } + + public function testCleanupAfterFailure(): void { + $this->gitCreateFixtureCommits(1); + + $output = $this->assertCommandFailure(['--branch' => '*invalid']); + + $this->assertStringContainsString('Incorrect value "*invalid" specified for git remote branch', $output); + $this->gitAssertCurrentBranch($this->src, $this->currentBranch); + $this->gitAssertRemoteNotExists($this->src, $this->remoteName); + } + + public function testGitignore(): void { + $this->fixtureCreateFile($this->src, '.gitignore', 'f3'); + $this->gitCreateFixtureCommits(2); + $this->fixtureCreateFile($this->src, 'f3'); + + $this->assertCommandSuccess(); + + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + $this->gitAssertFilesNotExist($this->dst, 'f3'); + + // Now, remove the .gitignore and push again. + $this->fixtureRemoveFile($this->src, '.gitignore'); + $this->gitCommitAll($this->src, 'Commit number 3'); + $this->assertCommandSuccess(); + $this->gitAssertFixtureCommits(3, $this->dst, 'testbranch', ['Deployment commit']); + } + + public function testGitignoreCustom(): void { + $this->gitCreateFixtureCommits(2); + $this->fixtureCreateFile($this->src, 'uic'); + $this->fixtureCreateFile($this->src, 'uc'); + + $this->fixtureCreateFile($this->src, 'mygitignore', 'uic'); + + $this->assertCommandSuccess(['--gitignore' => $this->src . DIRECTORY_SEPARATOR . 'mygitignore']); + + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + $this->gitAssertFilesNotExist($this->dst, 'uic'); + $this->gitAssertFilesExist($this->dst, 'uc'); + + // Now, remove the .gitignore and push again. + // We have to create 'uic' file since it was rightfully + // removed during previous build run and the source repo branch was not + // reset (uncommitted files would be removed, unless they are excluded + // in .gitignore). + $this->fixtureCreateFile($this->src, 'uic'); + $this->fixtureRemoveFile($this->src, 'mygitignore'); + $this->gitCommitAll($this->src, 'Commit number 3'); + $this->assertCommandSuccess(); + + $this->gitAssertFixtureCommits(3, $this->dst, 'testbranch', ['Deployment commit'], FALSE); + $this->gitAssertFilesCommitted($this->dst, ['f1', 'f2', 'uic'], 'testbranch'); + $this->gitAssertFilesExist($this->dst, ['f1', 'f2', 'uic'], 'testbranch'); + $this->gitAssertFilesNotCommitted($this->dst, ['uc'], 'testbranch'); + } + + public function testGitignoreCustomRemoveCommittedFiles(): void { + $this->fixtureCreateFile($this->src, '.gitignore', ['ii', 'ic']); + + $this->fixtureCreateFile($this->src, 'ii'); + $this->fixtureCreateFile($this->src, 'ic'); + $this->fixtureCreateFile($this->src, 'd/cc'); + $this->fixtureCreateFile($this->src, 'd/ci'); + $this->gitCreateFixtureCommits(2); + $this->gitCommitAll($this->src, 'Custom third commit'); + $this->fixtureCreateFile($this->src, 'ui'); + $this->fixtureCreateFile($this->src, 'uc'); + $this->gitAssertFilesCommitted($this->src, ['.gitignore', 'f1', 'f2', 'd/cc', 'd/ci']); + $this->gitAssertFilesNotCommitted($this->src, ['ii', 'ic', 'ui', 'uc']); + + $this->fixtureCreateFile($this->src, 'mygitignore', ['f1', 'ii', 'ci', 'ui']); + + $this->assertCommandSuccess(['--gitignore' => $this->src . DIRECTORY_SEPARATOR . 'mygitignore']); + + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Custom third commit', 'Deployment commit'], FALSE); + $this->gitAssertFilesCommitted($this->dst, ['.gitignore', 'f2', 'ic', 'd/cc', 'uc'], 'testbranch'); + $this->gitAssertFilesNotCommitted($this->dst, ['f1', 'ii', 'd/ci', 'ui'], 'testbranch'); + $this->gitAssertFilesExist($this->dst, ['f2', 'ic', 'd/cc', 'uc'], 'testbranch'); + $this->gitAssertFilesNotExist($this->dst, ['f1', 'ii', 'd/ci', 'ui'], 'testbranch'); + } + + public function testGitignoreCustomAllowlisting(): void { + $this->fixtureCreateFile($this->src, '.gitignore', ['ii', 'ic', 'd_ic', 'd_ii', '/vendor']); + + $this->fixtureCreateFile($this->src, 'ii'); + $this->fixtureCreateFile($this->src, 'ic'); + $this->fixtureCreateFile($this->src, 'cc'); + $this->fixtureCreateFile($this->src, 'ci'); + + $this->fixtureCreateFile($this->src, 'd_cc/sub_cc'); + $this->fixtureCreateFile($this->src, 'd_ci/sub_ci'); + $this->fixtureCreateFile($this->src, 'd_ic/sub_ic'); + $this->fixtureCreateFile($this->src, 'd_ii/sub_ii'); + + $this->fixtureCreateFile($this->src, 'vendor/ve_ii'); + $this->fixtureCreateFile($this->src, 'vendor_cc'); + $this->fixtureCreateFile($this->src, 'vendor_com with space com.txt'); + $this->fixtureCreateFile($this->src, 'dir_other/vendor/ve_cc'); + + $this->gitCreateFixtureCommits(2); + + $this->gitCommitAll($this->src, 'Custom third commit'); + + $this->gitAssertFilesCommitted($this->src, [ + '.gitignore', 'f1', 'f2', + 'cc', 'ci', + 'd_cc/sub_cc', 'd_ci/sub_ci', + 'vendor_cc', 'dir_other/vendor/ve_cc', 'vendor_com with space com.txt', + ]); + + $this->gitAssertFilesNotCommitted($this->src, [ + 'ii', 'ic', 'ui', 'uc', 'ud', + 'd_ic/sub_ic', 'd_ii/sub_ii', + 'vendor/ve_ii', + ]); + + $this->fixtureCreateFile($this->src, 'ui'); + $this->fixtureCreateFile($this->src, 'uc'); + $this->fixtureCreateFile($this->src, 'ud'); + $this->fixtureCreateFile($this->src, 'd_ui/sub_ui'); + $this->fixtureCreateFile($this->src, 'd_uc/sub_uc'); + $this->fixtureCreateFile($this->src, 'd_ud/sub_ud'); + + // Now, create a custom .gitignore and add non-ignored files + // (allowlisting). + $this->fixtureCreateFile($this->src, 'mygitignore', [ + '/*', + '!f2', '!ic', '!cc', '!uc', + '!d_cc', '!d_ic', '!d_uc', + '!vendor', + ]); + + // Run the build. + $this->assertCommandSuccess([ + '-vvv' => TRUE, + '--gitignore' => $this->src . DIRECTORY_SEPARATOR . 'mygitignore', + ]); + + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Custom third commit', 'Deployment commit'], FALSE); + + $this->gitAssertFilesCommitted($this->dst, [ + 'f2', 'ic', 'cc', 'uc', + 'd_cc/sub_cc', 'd_ic/sub_ic', 'd_uc/sub_uc', + 'vendor/ve_ii', + ], 'testbranch'); + + $this->gitAssertFilesNotCommitted($this->dst, [ + 'f1', 'ii', 'ci', 'ui', 'ud', + 'd_ci/sub_ci', 'd_ii/sub_ii', 'd_ui/sub_ui', 'd_ud/sub_ud', + 'vendor_cc', 'dir_other/vendor/ve_cc', 'vendor_com with space com.txt', + ], 'testbranch'); + + $this->gitAssertFilesExist($this->dst, [ + 'f2', 'ic', 'cc', 'uc', + 'd_cc/sub_cc', 'd_ic/sub_ic', 'd_uc/sub_uc', + 'vendor/ve_ii', + ], 'testbranch'); + $this->gitAssertFilesNotExist($this->dst, [ + 'f1', 'ii', 'ci', 'ui', 'ud', + 'd_ci/sub_ci', + 'd_ii/sub_ii', 'd_ui/sub_ui', 'd_ud/sub_ud', + 'vendor_cc', 'dir_other/vendor/ve_cc', 'vendor_com with space com.txt', + ], 'testbranch'); + } + + public function testBuildTag(): void { + $this->gitCreateFixtureCommits(2); + $this->gitAddTag($this->src, 'tag1'); + + $this->assertCommandSuccess(['--branch' => '[tags]'], 'tag1'); + + $this->gitAssertFixtureCommits(2, $this->dst, 'tag1', ['Deployment commit']); + } + + public function testBuildMultipleTags(): void { + $this->gitCreateFixtureCommits(2); + $this->gitAddTag($this->src, 'tag1'); + $this->gitAddTag($this->src, 'tag2'); + + $this->assertCommandSuccess(['--branch' => '[tags]'], 'tag1-tag2'); + $this->gitAssertFixtureCommits(2, $this->dst, 'tag1-tag2', ['Deployment commit']); + + $this->gitCreateFixtureCommit(3); + $this->gitAddTag($this->src, 'tag3'); + $this->assertCommandSuccess(['--branch' => '[tags]'], 'tag3'); + $this->gitAssertFixtureCommits(3, $this->dst, 'tag3', ['Deployment commit']); + } + + public function testBuildMultipleTagsMissingTags(): void { + $this->gitCreateFixtureCommits(2); + $this->gitAddTag($this->src, 'tag1'); + $this->gitCreateFixtureCommit(3); + + $this->assertCommandFailure(['--branch' => '[tags]']); + } + + public function testBuildMultipleTagsDelimiter(): void { + $this->gitCreateFixtureCommits(2); + $this->gitAddTag($this->src, 'tag1'); + $this->gitAddTag($this->src, 'tag2'); + + $this->assertCommandSuccess(['--branch' => '[tags:__]'], 'tag1__tag2'); + + $this->gitAssertFixtureCommits(2, $this->dst, 'tag1__tag2', ['Deployment commit']); + } + +} diff --git a/tests/phpunit/Functional/ForcePushTest.php b/tests/phpunit/Functional/ForcePushTest.php deleted file mode 100644 index 3ea5eeb..0000000 --- a/tests/phpunit/Functional/ForcePushTest.php +++ /dev/null @@ -1,306 +0,0 @@ -mode = 'force-push'; - parent::setUp(); - } - - public function testBuild(): void { - $this->gitCreateFixtureCommits(2); - - $output = $this->assertBuildSuccess(); - $this->assertStringContainsString('Mode: force-push', $output); - $this->assertStringContainsString('Will push: Yes', $output); - - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - } - - public function testBuildMoreCommits(): void { - $this->gitCreateFixtureCommits(2); - - $this->assertBuildSuccess(); - - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - - $this->gitCreateFixtureCommits(3, 2); - $this->assertBuildSuccess(); - - $this->assertFixtureCommits(5, $this->dst, 'testbranch', ['Deployment commit']); - } - - public function testIdempotence(): void { - $this->gitCreateFixtureCommits(2); - - $this->assertBuildSuccess(); - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - - $this->assertBuildSuccess(); - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - } - - public function testSubRepos(): void { - $this->gitCreateFixtureCommits(2); - - $this->gitCreateFixtureFile($this->src, 'c'); - $this->gitCommitAll($this->src, 'Commit number 3'); - - $this->gitInitRepo($this->src . DIRECTORY_SEPARATOR . 'r1/'); - $this->gitCreateFixtureFile($this->src, 'r1/c'); - - $this->gitInitRepo($this->src . DIRECTORY_SEPARATOR . 'r2/r21'); - $this->gitCreateFixtureFile($this->src, 'r2/r21/c'); - - $this->gitInitRepo($this->src . DIRECTORY_SEPARATOR . 'r3/r31/r311'); - $this->gitCreateFixtureFile($this->src, 'r3/r31/r311/c'); - - $this->gitAssertFilesExist($this->src, ['r1/c']); - $this->gitAssertFilesNotExist($this->src, ['r1/.git/index']); - $this->gitAssertFilesNotExist($this->src, ['r2/r21.git/index']); - $this->gitAssertFilesNotExist($this->src, ['r3/r31/r311/.git/index']); - - $output = $this->assertBuildSuccess('-vvv'); - $this->assertStringContainsString(sprintf('Removing sub-repository "%s"', $this->src . DIRECTORY_SEPARATOR . 'r1/.git'), $output); - $this->assertStringContainsString(sprintf('Removing sub-repository "%s"', $this->src . DIRECTORY_SEPARATOR . 'r2/r21/.git'), $output); - $this->assertStringContainsString(sprintf('Removing sub-repository "%s"', $this->src . DIRECTORY_SEPARATOR . 'r3/r31/r311/.git'), $output); - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Commit number 3', 'Deployment commit']); - - $this->gitAssertFilesExist($this->dst, ['r1/c']); - $this->gitAssertFilesExist($this->dst, ['r2/r21/c']); - $this->gitAssertFilesExist($this->dst, ['r3/r31/r311/c']); - $this->gitAssertFilesNotExist($this->dst, ['r1/.git/index']); - $this->gitAssertFilesNotExist($this->dst, ['r1/.git']); - $this->gitAssertFilesNotExist($this->dst, ['r2/r21/.git/index']); - $this->gitAssertFilesNotExist($this->dst, ['r2/r21/.git']); - $this->gitAssertFilesNotExist($this->dst, ['r3/r31/311/.git/index']); - $this->gitAssertFilesNotExist($this->dst, ['r3/r31/311/.git']); - } - - public function testCleanupAfterSuccess(): void { - $this->gitCreateFixtureCommits(2); - - $this->assertBuildSuccess(); - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - - $this->assertGitCurrentBranch($this->src, $this->currentBranch); - $this->assertGitNoRemote($this->src, $this->remote); - } - - public function testCleanupAfterFailure(): void { - $this->gitCreateFixtureCommits(1); - - $output = $this->assertBuildFailure('--branch=*invalid'); - - $this->assertStringContainsString('Incorrect value "*invalid" specified for git remote branch', $output); - $this->assertGitCurrentBranch($this->src, $this->currentBranch); - $this->assertGitNoRemote($this->src, $this->remote); - } - - public function testGitignore(): void { - $this->gitCreateFixtureFile($this->src, '.gitignore', 'f3'); - $this->gitCreateFixtureCommits(2); - $this->gitCreateFixtureFile($this->src, 'f3'); - - $this->assertBuildSuccess(); - - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - $this->gitAssertFilesNotExist($this->dst, 'f3'); - - // Now, remove the .gitignore and push again. - $this->gitRemoveFixtureFile($this->src, '.gitignore'); - $this->gitCommitAll($this->src, 'Commit number 3'); - $this->assertBuildSuccess(); - $this->assertFixtureCommits(3, $this->dst, 'testbranch', ['Deployment commit']); - } - - public function testGitignoreCustom(): void { - $this->gitCreateFixtureCommits(2); - $this->gitCreateFixtureFile($this->src, 'uic'); - $this->gitCreateFixtureFile($this->src, 'uc'); - - $this->gitCreateFixtureFile($this->src, 'mygitignore', 'uic'); - - $this->assertBuildSuccess('--gitignore=' . $this->src . DIRECTORY_SEPARATOR . 'mygitignore'); - - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - $this->gitAssertFilesNotExist($this->dst, 'uic'); - $this->gitAssertFilesExist($this->dst, 'uc'); - - // Now, remove the .gitignore and push again. - // We have to create 'uic' file since it was rightfully - // removed during previous build run and the source repo branch was not - // reset (uncommitted files would be removed, unless they are excluded - // in .gitignore). - $this->gitCreateFixtureFile($this->src, 'uic'); - $this->gitRemoveFixtureFile($this->src, 'mygitignore'); - $this->gitCommitAll($this->src, 'Commit number 3'); - $this->assertBuildSuccess(); - - $this->assertFixtureCommits(3, $this->dst, 'testbranch', ['Deployment commit'], FALSE); - $this->gitAssertFilesCommitted($this->dst, ['f1', 'f2', 'uic'], 'testbranch'); - $this->gitAssertFilesExist($this->dst, ['f1', 'f2', 'uic'], 'testbranch'); - $this->gitAssertNoFilesCommitted($this->dst, ['uc'], 'testbranch'); - } - - public function testGitignoreCustomRemoveCommittedFiles(): void { - $this->gitCreateFixtureFile($this->src, '.gitignore', ['ii', 'ic']); - - $this->gitCreateFixtureFile($this->src, 'ii'); - $this->gitCreateFixtureFile($this->src, 'ic'); - $this->gitCreateFixtureFile($this->src, 'd/cc'); - $this->gitCreateFixtureFile($this->src, 'd/ci'); - $this->gitCreateFixtureCommits(2); - $this->gitCommitAll($this->src, 'Custom third commit'); - $this->gitCreateFixtureFile($this->src, 'ui'); - $this->gitCreateFixtureFile($this->src, 'uc'); - $this->gitAssertFilesCommitted($this->src, ['.gitignore', 'f1', 'f2', 'd/cc', 'd/ci']); - $this->gitAssertNoFilesCommitted($this->src, ['ii', 'ic', 'ui', 'uc']); - - $this->gitCreateFixtureFile($this->src, 'mygitignore', ['f1', 'ii', 'ci', 'ui']); - - $this->assertBuildSuccess('--gitignore=' . $this->src . DIRECTORY_SEPARATOR . 'mygitignore'); - - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Custom third commit', 'Deployment commit'], FALSE); - $this->gitAssertFilesCommitted($this->dst, ['.gitignore', 'f2', 'ic', 'd/cc', 'uc'], 'testbranch'); - $this->gitAssertNoFilesCommitted($this->dst, ['f1', 'ii', 'd/ci', 'ui'], 'testbranch'); - $this->gitAssertFilesExist($this->dst, ['f2', 'ic', 'd/cc', 'uc'], 'testbranch'); - $this->gitAssertFilesNotExist($this->dst, ['f1', 'ii', 'd/ci', 'ui'], 'testbranch'); - } - - public function testGitignoreCustomWhitelisting(): void { - $this->gitCreateFixtureFile($this->src, '.gitignore', ['ii', 'ic', 'd_ic', 'd_ii', '/vendor']); - - $this->gitCreateFixtureFile($this->src, 'ii'); - $this->gitCreateFixtureFile($this->src, 'ic'); - $this->gitCreateFixtureFile($this->src, 'cc'); - $this->gitCreateFixtureFile($this->src, 'ci'); - - $this->gitCreateFixtureFile($this->src, 'd_cc/sub_cc'); - $this->gitCreateFixtureFile($this->src, 'd_ci/sub_ci'); - $this->gitCreateFixtureFile($this->src, 'd_ic/sub_ic'); - $this->gitCreateFixtureFile($this->src, 'd_ii/sub_ii'); - - $this->gitCreateFixtureFile($this->src, 'vendor/ve_ii'); - $this->gitCreateFixtureFile($this->src, 'vendor_cc'); - $this->gitCreateFixtureFile($this->src, 'vendor_com with space com.txt'); - $this->gitCreateFixtureFile($this->src, 'dir_other/vendor/ve_cc'); - - $this->gitCreateFixtureCommits(2); - - $this->gitCommitAll($this->src, 'Custom third commit'); - - $this->gitAssertFilesCommitted($this->src, [ - '.gitignore', 'f1', 'f2', - 'cc', 'ci', - 'd_cc/sub_cc', 'd_ci/sub_ci', - 'vendor_cc', 'dir_other/vendor/ve_cc', 'vendor_com with space com.txt', - ]); - - $this->gitAssertNoFilesCommitted($this->src, [ - 'ii', 'ic', 'ui', 'uc', 'ud', - 'd_ic/sub_ic', 'd_ii/sub_ii', - 'vendor/ve_ii', - ]); - - $this->gitCreateFixtureFile($this->src, 'ui'); - $this->gitCreateFixtureFile($this->src, 'uc'); - $this->gitCreateFixtureFile($this->src, 'ud'); - $this->gitCreateFixtureFile($this->src, 'd_ui/sub_ui'); - $this->gitCreateFixtureFile($this->src, 'd_uc/sub_uc'); - $this->gitCreateFixtureFile($this->src, 'd_ud/sub_ud'); - - // Now, create a custom .gitignore and add non-ignored files - // (whitelisting). - $this->gitCreateFixtureFile($this->src, 'mygitignore', [ - '/*', '!f2', '!ic', '!cc', '!uc', - '!d_cc', '!d_ic', '!d_uc', - '!vendor', - ]); - - // Run the build. - $this->assertBuildSuccess('-vvv --gitignore=' . $this->src . DIRECTORY_SEPARATOR . 'mygitignore'); - - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Custom third commit', 'Deployment commit'], FALSE); - - $this->gitAssertFilesCommitted($this->dst, [ - 'f2', 'ic', 'cc', 'uc', - 'd_cc/sub_cc', 'd_ic/sub_ic', 'd_uc/sub_uc', - 'vendor/ve_ii', - ], 'testbranch'); - - $this->gitAssertNoFilesCommitted($this->dst, [ - 'f1', 'ii', 'ci', 'ui', 'ud', - 'd_ci/sub_ci', 'd_ii/sub_ii', 'd_ui/sub_ui', 'd_ud/sub_ud', - 'vendor_cc', 'dir_other/vendor/ve_cc', 'vendor_com with space com.txt', - ], 'testbranch'); - - $this->gitAssertFilesExist($this->dst, [ - 'f2', 'ic', 'cc', 'uc', - 'd_cc/sub_cc', 'd_ic/sub_ic', 'd_uc/sub_uc', - 'vendor/ve_ii', - ], 'testbranch'); - $this->gitAssertFilesNotExist($this->dst, [ - 'f1', 'ii', 'ci', 'ui', 'ud', - 'd_ci/sub_ci', - 'd_ii/sub_ii', 'd_ui/sub_ui', 'd_ud/sub_ud', - 'vendor_cc', 'dir_other/vendor/ve_cc', 'vendor_com with space com.txt', - ], 'testbranch'); - } - - public function testBuildTag(): void { - $this->gitCreateFixtureCommits(2); - $this->gitAddTag($this->src, 'tag1'); - - $this->assertBuildSuccess('--branch=[tags]', 'tag1'); - - $this->assertFixtureCommits(2, $this->dst, 'tag1', ['Deployment commit']); - } - - public function testBuildMultipleTags(): void { - $this->gitCreateFixtureCommits(2); - $this->gitAddTag($this->src, 'tag1'); - $this->gitAddTag($this->src, 'tag2'); - - $this->assertBuildSuccess('--branch=[tags]', 'tag1-tag2'); - $this->assertFixtureCommits(2, $this->dst, 'tag1-tag2', ['Deployment commit']); - - $this->gitCreateFixtureCommit(3); - $this->gitAddTag($this->src, 'tag3'); - $this->assertBuildSuccess('--branch=[tags]', 'tag3'); - $this->assertFixtureCommits(3, $this->dst, 'tag3', ['Deployment commit']); - } - - public function testBuildMultipleTagsMissingTags(): void { - $this->gitCreateFixtureCommits(2); - $this->gitAddTag($this->src, 'tag1'); - $this->gitCreateFixtureCommit(3); - - $this->assertBuildFailure('--branch=[tags]'); - } - - public function testBuildMultipleTagsDelimiter(): void { - $this->gitCreateFixtureCommits(2); - $this->gitAddTag($this->src, 'tag1'); - $this->gitAddTag($this->src, 'tag2'); - - $this->assertBuildSuccess('--branch=[tags:__]', 'tag1__tag2'); - - $this->assertFixtureCommits(2, $this->dst, 'tag1__tag2', ['Deployment commit']); - } - -} diff --git a/tests/phpunit/Functional/GeneralTest.php b/tests/phpunit/Functional/GeneralTest.php index fb86fbb..ba9e46e 100644 --- a/tests/phpunit/Functional/GeneralTest.php +++ b/tests/phpunit/Functional/GeneralTest.php @@ -4,31 +4,24 @@ namespace DrevOps\GitArtifact\Tests\Functional; -/** - * Class GeneralTest. - * - * @group integration - * - * @covers \DrevOps\GitArtifact\Commands\ArtifactCommand - * @covers \DrevOps\GitArtifact\Traits\FilesystemTrait - */ -class GeneralTest extends AbstractFunctionalTestCase { +use DrevOps\GitArtifact\Commands\ArtifactCommand; +use DrevOps\GitArtifact\Git\ArtifactGitRepository; +use PHPUnit\Framework\Attributes\CoversClass; - public function testHelp(): void { - $output = $this->runGitArtifactCommand('--help'); - $this->assertStringContainsString('artifact [options] [--] ', implode(PHP_EOL, $output)); - $this->assertStringContainsString('Assemble a code artifact from your codebase, remove unnecessary files, and push it into a separate Git repository.', implode(PHP_EOL, $output)); - } +#[CoversClass(ArtifactCommand::class)] +#[CoversClass(ArtifactGitRepository::class)] +class GeneralTest extends AbstractFunctionalTestCase { public function testCompulsoryParameter(): void { - $output = $this->runGitArtifactCommand('', TRUE); + $this->dst = ''; + $output = $this->runCommand(['remote' => ' '], TRUE); - $this->assertStringContainsString('Not enough arguments (missing: "remote")', implode(PHP_EOL, $output)); + $this->assertStringContainsString('Remote argument must be a non-empty string', $output); } public function testInfo(): void { $this->gitCreateFixtureCommits(1); - $output = $this->runBuild('--dry-run'); + $output = $this->runCommand(['--dry-run' => TRUE]); $this->assertStringContainsString('Artifact information', $output); $this->assertStringContainsString('Mode: force-push', $output); $this->assertStringContainsString('Source repository: ' . $this->src, $output); @@ -45,7 +38,10 @@ public function testInfo(): void { public function testShowChanges(): void { $this->gitCreateFixtureCommits(1); - $output = $this->runBuild('--show-changes --dry-run'); + $output = $this->runCommand([ + '--show-changes' => TRUE, + '--dry-run' => TRUE, + ]); $this->assertStringContainsString('Added changes:', $output); @@ -55,16 +51,22 @@ public function testShowChanges(): void { public function testNoCleanup(): void { $this->gitCreateFixtureCommits(1); - $output = $this->runBuild('--no-cleanup --dry-run'); + $output = $this->runCommand([ + '--no-cleanup' => TRUE, + '--dry-run' => TRUE, + ]); - $this->assertGitCurrentBranch($this->src, $this->artifactBranch); + $this->gitAssertCurrentBranch($this->src, $this->artifactBranch); $this->assertStringContainsString('Cowardly refusing to push to remote. Use without --dry-run to perform an actual push.', $output); $this->gitAssertFilesNotExist($this->dst, 'f1', $this->currentBranch); } public function testDebug(): void { $this->gitCreateFixtureCommits(1); - $output = $this->runBuild('-vvv --dry-run'); + $output = $this->runCommand([ + '-vvv' => TRUE, + '--dry-run' => TRUE, + ]); $this->assertStringContainsString('Debug messages enabled', $output); $this->assertStringContainsString('Artifact information', $output); @@ -90,7 +92,10 @@ public function testDebugLogFile(): void { $report = $this->src . DIRECTORY_SEPARATOR . 'report.txt'; $this->gitCreateFixtureCommits(1); - $commandOutput = $this->runBuild(sprintf('--dry-run --log=%s', $report)); + $commandOutput = $this->runCommand([ + '--dry-run' => TRUE, + '--log' => $report, + ]); $this->assertStringContainsString('Debug messages enabled', $commandOutput); $this->assertStringContainsString('Artifact information', $commandOutput); @@ -131,7 +136,7 @@ public function testDebugLogFile(): void { public function testDebugDisabled(): void { $this->gitCreateFixtureCommits(1); - $output = $this->runBuild('--dry-run'); + $output = $this->runCommand(['--dry-run' => TRUE]); $this->assertStringNotContainsString('Debug messages enabled', $output); diff --git a/tests/phpunit/Functional/README.md b/tests/phpunit/Functional/README.md index e223d10..86dc40a 100644 --- a/tests/phpunit/Functional/README.md +++ b/tests/phpunit/Functional/README.md @@ -6,12 +6,12 @@ - `u` - uncommitted - `d` - deleted - `d_` - directory -- `sub_` - sub directory or file; used for testing wildcard name matching (i.e. `d_1/f1` vs `d_1/sub_f1`) +- `sub_` - sub-directory or file; used for testing wildcard name matching (i.e. `d_1/f1` vs `d_1/sub_f1`) ## Examples - `f1`, `f2` - files `f1` and `f2` - `d_1` - directory `d_1` - `d_1/f1` - file `f1` in directory `d_1`. -- `d_ui/sub_ui` - file `sub_ui` is uncommitted and ignored and is located -in uncommitted and ignored directory `d_ui`. +- `d_ui/sub_ui` - file `sub_ui` is uncommitted and ignored and is located +in uncommitted and ignored directory `d_ui`. diff --git a/tests/phpunit/Functional/TagTest.php b/tests/phpunit/Functional/TagTest.php index d7498e3..7f09366 100644 --- a/tests/phpunit/Functional/TagTest.php +++ b/tests/phpunit/Functional/TagTest.php @@ -4,14 +4,13 @@ namespace DrevOps\GitArtifact\Tests\Functional; -/** - * Class TagTest. - * - * @group integration - * - * @covers \DrevOps\GitArtifact\Commands\ArtifactCommand - * @covers \DrevOps\GitArtifact\Traits\FilesystemTrait - */ +use CzProject\GitPhp\Git; +use DrevOps\GitArtifact\Commands\ArtifactCommand; +use DrevOps\GitArtifact\Git\ArtifactGitRepository; +use PHPUnit\Framework\Attributes\CoversClass; + +#[CoversClass(ArtifactCommand::class)] +#[CoversClass(ArtifactGitRepository::class)] class TagTest extends AbstractFunctionalTestCase { /** @@ -26,15 +25,16 @@ public function testDetachedTag(): void { $this->gitCreateFixtureCommits(2); $this->gitAddTag($this->src, 'tag1'); $this->gitCheckout($this->src, 'tag1'); - $gitRepo = $this->git->open($this->src); - $srcBranches = $gitRepo->getBranches(); - $output = $this->assertBuildSuccess(); + $repo = (new Git())->open($this->src); + $branches = $repo->getBranches(); + + $output = $this->assertCommandSuccess(); $this->assertStringContainsString('Mode: force-push', $output); $this->assertStringContainsString('Will push: Yes', $output); - $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - $this->assertEquals($srcBranches, $gitRepo->getBranches(), 'Cleanup has correctly returned to the previous branch.'); + $this->gitAssertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); + $this->assertEquals($branches, $repo->getBranches(), 'Cleanup has correctly returned to the previous branch.'); } } diff --git a/tests/phpunit/Functional/TokenTest.php b/tests/phpunit/Functional/TokenTest.php deleted file mode 100644 index 08b78be..0000000 --- a/tests/phpunit/Functional/TokenTest.php +++ /dev/null @@ -1,103 +0,0 @@ -prepareMock(TokenTrait::class, [ - 'getToken' . ucfirst($name) => static function (?string $prop) use ($replacement): string { - return empty($prop) ? $replacement : $replacement . ' with property ' . $prop; - }, - ]); - - $actual = $this->callProtectedMethod($mock, 'tokenProcess', [$string]); - $this->assertEquals($expectedString, $actual); - } - - /** - * @return array> - * Data provider. - */ - public static function dataProviderTokenProcess(): array { - return [ - [ - '', - '', - '', - '', - ], - [ - '', - 'sometoken', - 'somevalue', - '', - ], - [ - 'string without a token', - 'sometoken', - 'somevalue', - 'string without a token', - ], - [ - 'string with sometoken without delimiters', - 'sometoken', - 'somevalue', - 'string with sometoken without delimiters', - ], - [ - 'string with [sometoken broken delimiters', - 'sometoken', - 'somevalue', - 'string with [sometoken broken delimiters', - ], - [ - 'string with sometoken] broken delimiters', - 'sometoken', - 'somevalue', - 'string with sometoken] broken delimiters', - ], - // Proper token. - [ - '[sometoken]', - 'sometoken', - 'somevalue', - 'somevalue', - ], - [ - 'string with [sometoken] present', - 'sometoken', - 'somevalue', - 'string with somevalue present', - ], - // Token with properties. - [ - 'string with [sometoken:prop] present', - 'sometoken', - 'somevalue', - 'string with somevalue with property prop present', - ], - [ - 'string with [sometoken:prop:otherprop] present', - 'sometoken', - 'somevalue', - 'string with somevalue with property prop:otherprop present', - ], - ]; - } - -} diff --git a/tests/phpunit/Traits/CommandTrait.php b/tests/phpunit/Traits/CommandTrait.php deleted file mode 100644 index 2063818..0000000 --- a/tests/phpunit/Traits/CommandTrait.php +++ /dev/null @@ -1,538 +0,0 @@ -fs = new Filesystem(); - $this->src = $src; - $this->gitInitRepo($this->src); - $this->dst = $remote; - $remoteRepo = $this->gitInitRepo($this->dst); - // Allow pushing into already checked out branch. We need this to - // avoid additional management of fixture repository. - $remoteRepo->setConfigReceiveDenyCurrentBranchIgnore(); - } - - /** - * Tear down test. - * - * To be called by test's tearDown() method. - */ - protected function tearDown(): void { - if ($this->fs->exists($this->src)) { - $this->fs->remove($this->src); - } - if ($this->fs->exists($this->dst)) { - $this->fs->remove($this->dst); - } - } - - /** - * Init git repository. - * - * @param string $path - * Path to the repository directory. - */ - protected function gitInitRepo(string $path): ArtifactGitRepository { - if ($this->fs->exists($path)) { - $this->fs->remove($path); - } - $this->fs->mkdir($path); - /** @var \DrevOps\GitArtifact\Git\ArtifactGitRepository $repo */ - $repo = $this->git->init($path, ['-b' => 'master']); - - return $repo; - } - - /** - * Get all commit hashes in the repository. - * - * @param string $path - * Path to the repository directory. - * @param string $format - * Format of commits. - * - * @return array - * Array of commit hashes, sorted from the earliest to the latest commit. - * - * @throws \Exception - */ - protected function gitGetAllCommits(string $path, string $format = '%s'): array { - $commits = []; - try { - $commits = $this->git->open($path)->getCommits($format); - } - catch (\Exception $exception) { - $output = ($exception->getPrevious() instanceof \Throwable) ? $exception->getPrevious()->getMessage() : ''; - $output = trim($output); - // Different versions of Git may produce these expected messages. - $expectedErrorMessages = [ - "fatal: bad default revision 'HEAD'", - "fatal: your current branch 'master' does not have any commits yet", - ]; - if (!in_array($output, $expectedErrorMessages)) { - throw $exception; - } - } - - return array_reverse(array_filter($commits)); - } - - /** - * Get a range of commits. - * - * @param array $range - * Array of commit indexes, stating from 1. - * @param string $path - * Path to the repository directory. - * - * @return array - * Array of commit hashes, ordered by keys in the $range. - * - * @throws \Exception - */ - protected function gitGetCommitsHashesFromRange(array $range, string $path): array { - $commits = $this->gitGetAllCommits($path); - - array_walk($range, static function (&$v) : void { - --$v; - }); - - $ret = []; - foreach ($range as $key) { - $ret[] = $commits[$key]; - } - - return $ret; - } - - /** - * Get all committed files. - * - * @param string $path - * Path to the repository directory. - * - * @return array - * Array of commit committed files. - */ - protected function gitGetCommittedFiles(string $path): array { - return $this - ->git - ->open($path) - ->listCommittedFiles(); - } - - /** - * Create multiple fixture commits. - * - * @param int $count - * Number of commits to create. - * @param int $offset - * Number of commit indices to offset. - * @param string|null $path - * Optional path to the repository directory. If not provided, fixture - * directory is used. - */ - protected function gitCreateFixtureCommits(int $count, int $offset = 0, ?string $path = NULL): void { - $path = $path ? $path : $this->src; - for ($i = $offset; $i < $count + $offset; $i++) { - $this->gitCreateFixtureCommit($i + 1, $path); - } - } - - /** - * Create fixture commit with specified index. - * - * @param int $index - * Index of the commit to be used in the message. - * @param string|null $path - * Optional path to the repository directory. If not provided, fixture - * directory is used. - * - * @return string - * Hash of created commit. - */ - protected function gitCreateFixtureCommit(int $index, ?string $path = NULL): string { - $path = $path ? $path : $this->src; - $filename = 'f' . $index; - $this->gitCreateFixtureFile($path, $filename); - $repo = $this->git->open($path); - $repo->addFile($filename); - $message = 'Commit number ' . $index; - $repo->commitAllChanges($message); - $lastCommit = $repo->getLastCommit(); - - return $lastCommit->getId()->toString(); - } - - /** - * Commit all uncommitted files. - * - * @param string $path - * Path to repository. - * @param string $message - * Commit message. - */ - protected function gitCommitAll(string $path, string $message): void { - $repo = $this->git->open($path); - $repo->commitAllChanges($message); - } - - /** - * Checkout branch. - * - * @param string $path - * Path to repository. - * @param string $branch - * Branch name. - */ - protected function gitCheckout(string $path, string $branch): void { - try { - $repo = $this->git->open($path); - $repo->checkout($branch); - } - catch (GitException $gitException) { - $allowedFails = [ - sprintf("error: pathspec '%s' did not match any file(s) known to git", $branch), - ]; - - if ($gitException->getRunnerResult()) { - $output = $gitException->getRunnerResult()->getErrorOutput(); - } - - // Re-throw exception if it is not one of the allowed ones. - if (!isset($output) || empty(array_intersect($output, $allowedFails))) { - throw $gitException; - } - } - } - - /** - * Reset git repo at path. - * - * @param string $path - * Path to the repo. - */ - protected function gitReset($path): void { - $repo = $this->git->open($path); - $repo->resetHard(); - $repo->cleanForce(); - } - - /** - * Create fixture file at provided path. - * - * @param string $path - * File path. - * @param string $name - * Optional file name. - * @param string|array $content - * Optional file content. - * - * @return string - * Created file name. - */ - protected function gitCreateFixtureFile(string $path, string $name = '', $content = ''): string { - $name = $name !== '' && $name !== '0' ? $name : 'tmp' . rand(1000, 100000); - $path = $path . DIRECTORY_SEPARATOR . $name; - $dir = dirname($path); - if (!empty($dir)) { - $this->fs->mkdir($dir); - } - $this->fs->touch($path); - if (!empty($content)) { - $content = is_array($content) ? implode(PHP_EOL, $content) : $content; - $this->fs->dumpFile($path, $content); - } - - return $path; - } - - /** - * Remove fixture file at provided path. - * - * @param string $path - * File path. - * @param string $name - * File name. - */ - protected function gitRemoveFixtureFile(string $path, string $name): void { - $path = $path . DIRECTORY_SEPARATOR . $name; - $this->fs->remove($path); - } - - /** - * Create fixture tag with specified name and optional annotation. - * - * Annotated tags and lightweight tags have a different object - * representation in git, therefore may need to be created explicitly for - * some tests. - * - * @param string $path - * Optional path to the repository directory. - * @param string $name - * Tag name. - * @param bool $annotate - * Optional flag to add random annotation to the tag. Defaults to FALSE. - */ - protected function gitAddTag(string $path, string $name, bool $annotate = FALSE): void { - $repo = $this->git->open($path); - if ($annotate) { - $message = 'Annotation for tag ' . $name; - $repo->createAnnotatedTag($name, $message); - } - else { - $repo->createLightweightTag($name); - } - } - - /** - * Assert that files exist in repository in specified branch. - * - * @param string $path - * Repository location. - * @param array|string $files - * File or array of files. - * @param string|null $branch - * Optional branch. If set, will be checked out before assertion. - * - * @todo Update arguments order and add assertion message. - */ - protected function gitAssertFilesExist(string $path, $files, ?string $branch = NULL): void { - $files = is_array($files) ? $files : [$files]; - if ($branch) { - $this->gitCheckout($path, $branch); - } - foreach ($files as $file) { - $this->assertFileExists($path . DIRECTORY_SEPARATOR . $file); - } - } - - /** - * Assert that files do not exist in repository in specified branch. - * - * @param string $path - * Repository location. - * @param array|string $files - * File or array of files. - * @param string|null $branch - * Optional branch. If set, will be checked out before assertion. - */ - protected function gitAssertFilesNotExist(string $path, $files, ?string $branch = NULL): void { - $files = is_array($files) ? $files : [$files]; - if ($branch) { - $this->gitCheckout($path, $branch); - } - foreach ($files as $file) { - $this->assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . $file); - } - } - - /** - * Assert git files are present and were committed. - * - * @param string $path - * Path to repo. - * @param array|string $expectedFiles - * Array of files or a single file. - * @param string $branch - * Optional branch name. - */ - protected function gitAssertFilesCommitted(string $path, $expectedFiles, string $branch = ''): void { - if ($branch !== '' && $branch !== '0') { - $this->gitCheckout($path, $branch); - } - $expectedFiles = is_array($expectedFiles) ? $expectedFiles : [$expectedFiles]; - $committedFiles = $this->gitGetCommittedFiles($path); - $this->assertArraySimilar($expectedFiles, $committedFiles); - } - - /** - * Assert git files were not committed. - * - * @param string $path - * Path to repo. - * @param array|string $expectedFiles - * Array of files or a single file. - * @param string $branch - * Optional branch name. - */ - protected function gitAssertNoFilesCommitted(string $path, $expectedFiles, string $branch = ''): void { - if ($branch !== '' && $branch !== '0') { - $this->gitCheckout($path, $branch); - } - $expectedFiles = is_array($expectedFiles) ? $expectedFiles : [$expectedFiles]; - $committedFiles = $this->gitGetCommittedFiles($path); - $intersectedFiles = array_intersect($committedFiles, $expectedFiles); - $this->assertArraySimilar([], $intersectedFiles); - } - - /** - * Assert which git commits are present. - * - * @param int $count - * Number of commits. - * @param string $path - * Path to the repo. - * @param string $branch - * Branch name. - * @param array $additionalCommits - * Array of additional commits. - * @param bool $assertFiles - * Assert files or not. - * - * @throws \Exception - */ - protected function assertFixtureCommits(int $count, string $path, string $branch, array $additionalCommits = [], bool $assertFiles = TRUE): void { - $this->gitCheckout($path, $branch); - $this->gitReset($path); - - $expectedCommits = []; - $expectedFiles = []; - for ($i = 1; $i <= $count; $i++) { - $expectedCommits[] = sprintf('Commit number %s', $i); - $expectedFiles[] = sprintf('f%s', $i); - } - $expectedCommits = array_merge($expectedCommits, $additionalCommits); - - $commits = $this->gitGetAllCommits($path); - $this->assertEquals($expectedCommits, $commits, 'All fixture commits are present'); - - if ($assertFiles) { - $this->gitAssertFilesExist($this->dst, $expectedFiles, $branch); - } - } - - /** - * Run command. - * - * @param string $argsAndOptions - * Args and options. - * @param bool $expectFail - * Flag to state that the command should fail. - * @param string $gitArtifactBin - * Git artifact bin. - * - * @return array - * Array of output lines. - */ - public function runGitArtifactCommand(string $argsAndOptions, bool $expectFail = FALSE, string $gitArtifactBin = './git-artifact'): array { - if (!file_exists($gitArtifactBin)) { - throw new \RuntimeException(sprintf('git-artifact binary is not available at path "%s"', $gitArtifactBin)); - } - - try { - $output = $this->runCliCommand($gitArtifactBin . ' ' . $argsAndOptions); - if ($expectFail) { - throw new AssertionFailedError('Command exited successfully but should not'); - } - } - catch (ErrorException $errorException) { - if (!$expectFail) { - throw $errorException; - } - $output = explode(PHP_EOL, ($errorException->getPrevious() instanceof \Throwable) ? $errorException->getPrevious()->getMessage() : ''); - } - - return $output; - } - - /** - * Run CLI command. - * - * @param string $command - * Command string to run. - * - * @return array - * Array of output lines. - * - * @throws \DrevOps\GitArtifact\Tests\Exception\ErrorException - * If commands exists with non-zero status. - */ - protected function runCliCommand(string $command): array { - exec($command . ' 2>&1', $output, $code); - - if ($code !== 0) { - throw new ErrorException(sprintf('Command "%s" exited with non-zero status', $command), $code, '', -1, new ErrorException(implode(PHP_EOL, $output), $code, '', -1)); - } - - return $output; - } - - /** - * Asserts that two associative arrays are similar. - * - * Both arrays must have the same indexes with identical values - * without respect to key ordering. - * - * @param array $expected - * Expected assert. - * @param array $array - * The array want to assert. - * - * @phpstan-ignore-next-line - */ - protected function assertArraySimilar(array $expected, array $array): void { - $this->assertEquals([], array_diff($array, $expected)); - $this->assertEquals([], array_diff_key($array, $expected)); - foreach ($expected as $key => $value) { - if (is_array($value)) { - $this->assertArraySimilar($value, $array[$key]); - } - else { - $this->assertContains($value, $array); - } - } - } - -} diff --git a/tests/phpunit/Traits/FixtureTrait.php b/tests/phpunit/Traits/FixtureTrait.php new file mode 100644 index 0000000..201008c --- /dev/null +++ b/tests/phpunit/Traits/FixtureTrait.php @@ -0,0 +1,61 @@ + $content + * Optional file content. + * + * @return string + * Created file name. + */ + protected function fixtureCreateFile(string $path, string $name = '', string|array $content = ''): string { + $fs = new Filesystem(); + + $name = $name !== '' && $name !== '0' ? $name : 'tmp' . rand(1000, 100000); + $path = $path . DIRECTORY_SEPARATOR . $name; + + $dir = dirname($path); + if (!empty($dir)) { + $fs->mkdir($dir); + } + + $fs->touch($path); + if (!empty($content)) { + $content = is_array($content) ? implode(PHP_EOL, $content) : $content; + $fs->dumpFile($path, $content); + } + + return $path; + } + + /** + * Remove fixture file at provided path. + * + * @param string $path + * File path. + * @param string $name + * File name. + */ + protected function fixtureRemoveFile(string $path, string $name): void { + (new Filesystem())->remove($path . DIRECTORY_SEPARATOR . $name); + } + +} diff --git a/tests/phpunit/Traits/GitTrait.php b/tests/phpunit/Traits/GitTrait.php new file mode 100644 index 0000000..c583147 --- /dev/null +++ b/tests/phpunit/Traits/GitTrait.php @@ -0,0 +1,384 @@ +mkdir($path); + + return (new Git())->init($path); + } + + /** + * Checkout branch. + * + * @param string $path + * Path to repository. + * @param string $branch + * Branch name. + */ + protected function gitCheckout(string $path, string $branch): void { + try { + (new Git())->open($path)->checkout($branch); + } + catch (GitException $exception) { + $allowed_fails = [ + sprintf("error: pathspec '%s' did not match any file(s) known to git", $branch), + ]; + + if ($exception->getRunnerResult()) { + $output = $exception->getRunnerResult()->getErrorOutput(); + } + + // Re-throw exception if it is not one of the allowed ones. + if (!isset($output) || empty(array_intersect($output, $allowed_fails))) { + throw $exception; + } + } + } + + /** + * Reset git repo at path. + * + * @param string $path + * Path to the repo. + */ + protected function gitReset($path): void { + $repo = (new Git())->open($path); + $repo->run('reset', ['--hard']); + $repo->run('clean', ['-dfx']); + } + + /** + * Get all commit hashes in the repository. + * + * @param string $path + * Path to the repository directory. + * @param string $format + * Format of commits. + * + * @return array + * Array of commit hashes, sorted from the earliest to the latest commit. + * + * @throws \Exception + */ + protected function gitGetAllCommits(string $path, string $format = '%s'): array { + $commits = []; + + try { + $commits = (new Git())->open($path)->run(['log', '--format=' . $format])->getOutput(); + } + catch (\Exception $exception) { + // Different versions of Git may produce these expected messages. + $expected_error_messages = [ + "fatal: bad default revision 'HEAD'", + "fatal: your current branch 'master' does not have any commits yet", + ]; + + if (!in_array(trim($exception->getMessage()), $expected_error_messages)) { + throw $exception; + } + } + + return array_reverse(array_filter($commits)); + } + + /** + * Get a range of commits. + * + * @param array $range + * Array of commit indexes, stating from 1. + * @param string $path + * Path to the repository directory. + * + * @return array + * Array of commit hashes, ordered by keys in the $range. + * + * @throws \Exception + */ + protected function gitGetCommitsRange(array $range, string $path): array { + $ret = []; + + $commits = $this->gitGetAllCommits($path); + + array_walk($range, static function (&$v): void { + --$v; + }); + + foreach ($range as $key) { + $ret[] = $commits[$key]; + } + + return $ret; + } + + /** + * Create fixture tag with specified name and optional annotation. + * + * Annotated tags and lightweight tags have a different object + * representation in git, therefore may need to be created explicitly for + * some tests. + * + * @param string $path + * Optional path to the repository directory. + * @param string $name + * Tag name. + * @param bool $annotate + * Optional flag to add random annotation to the tag. Defaults to FALSE. + */ + protected function gitAddTag(string $path, string $name, bool $annotate = FALSE): void { + $repo = (new Git())->open($path); + + if ($annotate) { + $repo->createTag($name, ['--message="Annotation for tag ' . $name . '"', '-a']); + } + else { + $repo->createTag($name); + } + } + + /** + * Assert current Git branch. + * + * @param string $path + * Path to repository. + * @param string $branch + * Branch name to assert. + */ + protected function gitAssertCurrentBranch(string $path, string $branch): void { + $current = (new Git())->open($path)->getCurrentBranchName(); + $this->assertStringContainsString($branch, $current, sprintf('Current branch is "%s"', $branch)); + } + + /** + * Create multiple fixture commits. + * + * @param int $count + * Number of commits to create. + * @param int $offset + * Number of commit indices to offset. + * @param string|null $path + * Optional path to the repository directory. If not provided, fixture + * directory is used. + */ + protected function gitCreateFixtureCommits(int $count, int $offset = 0, ?string $path = NULL): void { + $path = $path ? $path : $this->src; + + for ($i = $offset; $i < $count + $offset; $i++) { + $this->gitCreateFixtureCommit($i + 1, $path); + } + } + + /** + * Create fixture commit with specified index. + * + * @param int $index + * Index of the commit to be used in the message. + * @param string|null $path + * Optional path to the repository directory. If not provided, fixture + * directory is used. + * + * @return string + * Hash of created commit. + */ + protected function gitCreateFixtureCommit(int $index, ?string $path = NULL): string { + $path = $path ? $path : $this->src; + + $filename = 'f' . $index; + + $fs = new Filesystem(); + $filepath = $path . DIRECTORY_SEPARATOR . $filename; + $fs->mkdir(dirname($path)); + $fs->touch($filepath); + + return (new Git())->open($path) + ->addFile($filename) + ->commit('Commit number ' . $index) + ->getLastCommit()->getId()->toString(); + } + + /** + * Commit all uncommitted files. + * + * @param string $path + * Path to repository. + * @param string $message + * Commit message. + */ + protected function gitCommitAll(string $path, string $message): void { + (new Git())->open($path) + ->addAllChanges() + ->commit($message); + } + + /** + * Assert that Git remote specified by name does not exist. + * + * @param string $path + * Path to repository. + * @param string $remote + * Remote name to assert. + */ + protected function gitAssertRemoteNotExists(string $path, string $remote): void { + $remotes = (new Git())->open($path)->run(['remote'])->getErrorOutputAsString() ?: ''; + $this->assertStringNotContainsString($remote, $remotes, sprintf('Remote "%s" is not present"', $remote)); + } + + /** + * Assert which git commits are present. + * + * @param int $count + * Number of commits. + * @param string $path + * Path to the repo. + * @param string $branch + * Branch name. + * @param array $additional_commits + * Array of additional commits. + * @param bool $should_assert_files + * Should assert if files are present. + * + * @throws \Exception + */ + protected function gitAssertFixtureCommits(int $count, string $path, string $branch, array $additional_commits = [], bool $should_assert_files = TRUE): void { + $this->gitCheckout($path, $branch); + $this->gitReset($path); + + $expected_commits = []; + $expected_files = []; + for ($i = 1; $i <= $count; $i++) { + $expected_commits[] = sprintf('Commit number %s', $i); + $expected_files[] = sprintf('f%s', $i); + } + $expected_commits = array_merge($expected_commits, $additional_commits); + + $commits = $this->gitGetAllCommits($path); + $this->assertEquals($expected_commits, $commits, 'All fixture commits are present'); + + if ($should_assert_files) { + $this->gitAssertFilesExist($this->dst, $expected_files, $branch); + } + } + + /** + * Assert that files exist in repository in specified branch. + * + * @param string $path + * Repository location. + * @param array|string $files + * File or array of files. + * @param string|null $branch + * Optional branch. If set, will be checked out before assertion. + */ + protected function gitAssertFilesExist(string $path, array|string $files, ?string $branch = NULL): void { + $files = is_array($files) ? $files : [$files]; + + if ($branch) { + $this->gitCheckout($path, $branch); + } + + foreach ($files as $file) { + $this->assertFileExists($path . DIRECTORY_SEPARATOR . $file); + } + } + + /** + * Assert that files do not exist in repository in specified branch. + * + * @param string $path + * Repository location. + * @param array|string $files + * File or array of files. + * @param string|null $branch + * Optional branch. If set, will be checked out before assertion. + */ + protected function gitAssertFilesNotExist(string $path, array|string $files, ?string $branch = NULL): void { + $files = is_array($files) ? $files : [$files]; + + if ($branch) { + $this->gitCheckout($path, $branch); + } + + foreach ($files as $file) { + $this->assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . $file); + } + } + + /** + * Assert git files are present and were committed. + * + * @param string $path + * Path to repo. + * @param array|string $expected_files + * Array of files or a single file. + * @param string $branch + * Optional branch name. + */ + protected function gitAssertFilesCommitted(string $path, array|string $expected_files, ?string $branch = NULL): void { + if ($branch) { + $this->gitCheckout($path, $branch); + } + + $expected_files = is_array($expected_files) ? $expected_files : [$expected_files]; + + $files = (new Git())->open($path)->run(['ls-tree', '--name-only', '-r', 'HEAD'])->getOutput(); + $files = array_filter($files); + + $this->assertArraySimilar($expected_files, $files); + } + + /** + * Assert git files were not committed. + * + * @param string $path + * Path to repo. + * @param array|string $expected_files + * Array of files or a single file. + * @param string $branch + * Optional branch name. + */ + protected function gitAssertFilesNotCommitted(string $path, array|string $expected_files, ?string $branch = NULL): void { + if ($branch) { + $this->gitCheckout($path, $branch); + } + + $expected_files = is_array($expected_files) ? $expected_files : [$expected_files]; + + $files = (new Git())->open($path)->run(['ls-tree', '--name-only', '-r', 'HEAD'])->getOutput(); + $files = array_filter($files); + + $intersected_files = array_intersect($files, $expected_files); + + $this->assertArraySimilar([], $intersected_files); + } + + /** + * Get global default branch. + * + * @return string + * Default branch name. + */ + protected function gitGetGlobalDefaultBranch(): string { + return trim(shell_exec('git config --global init.defaultBranch') ?: 'master'); + } + +} diff --git a/tests/phpunit/Traits/MockTrait.php b/tests/phpunit/Traits/MockTrait.php index 0dddfe4..67df8d0 100644 --- a/tests/phpunit/Traits/MockTrait.php +++ b/tests/phpunit/Traits/MockTrait.php @@ -4,6 +4,8 @@ namespace DrevOps\GitArtifact\Tests\Traits; +use PHPUnit\Framework\MockObject\MockObject; + /** * Trait MockTrait. * @@ -16,61 +18,50 @@ trait MockTrait { * * @param class-string $class * Class or trait name to generate the mock. - * @param array $methodsMap + * @param array $methods * Optional array of methods and values, keyed by method name. Array * elements can be return values, callbacks created with - * $this->returnCallback(), or closures. - * @param array $args - * Optional array of constructor arguments. If omitted, a constructor - * will not be called. + * $this->willReturnCallback(), or closures. + * @param bool|array $args + * Optional array of constructor arguments or FALSE to disable the original + * constructor. If omitted, an original constructor will be called. * - * @return object + * @return \PHPUnit\Framework\MockObject\MockObject * Mocked class. * * @throws \ReflectionException + * + * @SuppressWarnings(CyclomaticComplexity) */ - protected function prepareMock(string $class, array $methodsMap = [], array $args = []) { - $methods = array_keys($methodsMap); + protected function prepareMock(string $class, array $methods = [], array|bool $args = []): MockObject { + $methods = array_filter($methods, fn($value, $key): bool => is_string($key), ARRAY_FILTER_USE_BOTH); - $reflectionClass = new \ReflectionClass($class); + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('Class %s does not exist', $class)); + } - if ($reflectionClass->isAbstract()) { - $mock = $this->getMockForAbstractClass($class, $args, '', !empty($args), TRUE, TRUE, $methods); + $builder = $this->getMockBuilder($class); + + if (is_array($args) && !empty($args)) { + $builder->enableOriginalConstructor()->setConstructorArgs($args); } - elseif ($reflectionClass->isTrait()) { - $mock = $this->getMockForTrait($class, [], '', TRUE, TRUE, TRUE, array_keys($methodsMap)); - } - else { - $mockBuilder = $this->getMockBuilder($class); - if (!empty($args)) { - $mockBuilder = $mockBuilder->enableOriginalConstructor() - ->setConstructorArgs($args); - } - else { - $mockBuilder = $mockBuilder->disableOriginalConstructor(); - } - /* @todo setMethods method is not found on MockBuilder */ - /* @phpstan-ignore-next-line */ - $mock = $mockBuilder->setMethods($methods) - ->getMock(); + elseif ($args === FALSE) { + $builder->disableOriginalConstructor(); } - foreach ($methodsMap as $method => $value) { - // Handle callback values differently. + $method_names = array_filter(array_keys($methods), fn($method): bool => is_string($method) && !empty($method)); + $mock = $builder->onlyMethods($method_names)->getMock(); + + foreach ($methods as $method => $value) { + // Handle callback value differently based on its type. if (is_object($value) && str_contains($value::class, 'Callback')) { - $mock->expects($this->any()) - ->method($method) - ->will($value); + $mock->expects($this->any())->method($method)->willReturnCallback($value); } elseif (is_object($value) && str_contains($value::class, 'Closure')) { - $mock->expects($this->any()) - ->method($method) - ->will($this->returnCallback($value)); + $mock->expects($this->any())->method($method)->willReturnCallback($value); } else { - $mock->expects($this->any()) - ->method($method) - ->willReturn($value); + $mock->expects($this->any())->method($method)->willReturn($value); } } diff --git a/tests/phpunit/Unit/AbstractUnitTestCase.php b/tests/phpunit/Unit/AbstractUnitTestCase.php index fdbd87a..05c4e70 100644 --- a/tests/phpunit/Unit/AbstractUnitTestCase.php +++ b/tests/phpunit/Unit/AbstractUnitTestCase.php @@ -4,24 +4,38 @@ namespace DrevOps\GitArtifact\Tests\Unit; -use DrevOps\GitArtifact\Commands\ArtifactCommand; -use DrevOps\GitArtifact\Tests\AbstractTestCase; +use DrevOps\GitArtifact\Tests\Traits\MockTrait; +use DrevOps\GitArtifact\Tests\Traits\ReflectionTrait; +use PHPUnit\Framework\TestCase; -/** - * Class AbstractUnitTestCase. - */ -abstract class AbstractUnitTestCase extends AbstractTestCase { +abstract class AbstractUnitTestCase extends TestCase { + + use MockTrait; + use ReflectionTrait; /** - * Artifact command. + * Asserts that two associative arrays are similar. + * + * Both arrays must have the same indexes with identical values + * without respect to key ordering. + * + * @param array $expected + * Expected assert. + * @param array $array + * The array want to assert. */ - protected ArtifactCommand $command; - - protected function setUp(): void { - parent::setUp(); - - $this->command = new ArtifactCommand(); - $this->callProtectedMethod($this->command, 'fsSetRootDir', [$this->fixtureDir]); + protected function assertArraySimilar(array $expected, array $array): void { + $this->assertEquals([], array_diff($array, $expected)); + $this->assertEquals([], array_diff_key($array, $expected)); + + foreach ($expected as $key => $value) { + if (is_array($value)) { + $this->assertArraySimilar($value, $array[$key]); + } + else { + $this->assertContains($value, $array); + } + } } } diff --git a/tests/phpunit/Unit/ArtifactGitRepositoryTest.php b/tests/phpunit/Unit/ArtifactGitRepositoryTest.php index 376cf56..6e5073b 100644 --- a/tests/phpunit/Unit/ArtifactGitRepositoryTest.php +++ b/tests/phpunit/Unit/ArtifactGitRepositoryTest.php @@ -4,298 +4,121 @@ namespace DrevOps\GitArtifact\Tests\Unit; -use CzProject\GitPhp\GitException; use DrevOps\GitArtifact\Git\ArtifactGitRepository; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\Component\Filesystem\Filesystem; -/** - * Test ArtifactGitRepository class. - * - * @SuppressWarnings(PHPMD.TooManyPublicMethods) - */ #[CoversClass(ArtifactGitRepository::class)] class ArtifactGitRepositoryTest extends AbstractUnitTestCase { - /** - * Test push force. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testPushForce(): void { - $sourceRepo = $this->git->open($this->src); - $sourceRepo->commit('Source commit 1', ['--allow-empty']); - - $destinationRepo = $this->git->open($this->dst); - $destinationRepo->commit('Destination commit 1', ['--allow-empty']); - $lastCommit = $destinationRepo->getLastCommit(); - $this->assertEquals('Destination commit 1', $lastCommit->getSubject()); - - $sourceRepo->addRemote('dst', $this->dst); - $sourceRepo->pushForce('dst', 'refs/heads/master:refs/heads/master'); - $lastCommit = $destinationRepo->getLastCommit(); - $this->assertEquals('Source commit 1', $lastCommit->getSubject()); - } - - /** - * Test list files. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testListFiles(): void { - $sourceRepo = $this->git->open($this->src); - // Test list ignored files. - $gitIgnoreFile = $this->src . DIRECTORY_SEPARATOR . '.gitignore'; - file_put_contents($gitIgnoreFile, ''); - $this->assertFileExists($gitIgnoreFile); - $files = $sourceRepo->listIgnoredFilesFromGitIgnoreFile($gitIgnoreFile); - $this->assertEquals([], $files); - - $this->gitCreateFixtureFile($this->src, 'test-ignore-1'); - $this->gitCreateFixtureFile($this->src, 'test-ignore-2'); - $sourceRepo->commitAllChanges('Test list ignored files.'); - - file_put_contents($gitIgnoreFile, "test-ignore-1\ntest-ignore-2"); - $files = $sourceRepo->listIgnoredFilesFromGitIgnoreFile($gitIgnoreFile); - $this->assertEquals(['test-ignore-1', 'test-ignore-2'], $files); - - // Test list other files. - $otherFiles = $sourceRepo->listOtherFiles(); - $this->assertEquals([], $otherFiles); - $this->gitCreateFixtureFile($this->src, 'other-file-1'); - $this->gitCreateFixtureFile($this->src, 'other-file-2'); - $otherFiles = $sourceRepo->listOtherFiles(); - $this->assertEquals(['other-file-1', 'other-file-2'], $otherFiles); - } - - /** - * Test get commits. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testGetCommits(): void { - $sourceRepo = $this->git->open($this->src); - - $this->gitCreateFixtureFile($this->src, 'test-commit-file1'); - $sourceRepo->commitAllChanges('Add file 1'); - $commits = $sourceRepo->getCommits(); - - $this->assertEquals(['Add file 1'], $commits); - - $this->gitCreateFixtureFile($this->src, 'test-commit-file2'); - $sourceRepo->commitAllChanges('Add file 2'); - $commits = $sourceRepo->getCommits(); - - $this->assertEquals(['Add file 2', 'Add file 1'], $commits); - } - - /** - * Test reset hard command. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testResetHard(): void { - $sourceRepo = $this->git->open($this->src); - $file = $this->gitCreateFixtureFile($this->src, 'test-file1'); - file_put_contents($file, 'Content example'); - $sourceRepo->commitAllChanges('Add file 1'); - $this->assertEquals('Content example', file_get_contents($file)); - - file_put_contents($file, 'New content'); - $this->assertEquals('New content', file_get_contents($file)); - - $sourceRepo->resetHard(); - $this->assertEquals('Content example', file_get_contents($file)); - } - - /** - * Test clean force command. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testCleanForce(): void { - $sourceRepo = $this->git->open($this->src); - $this->gitCreateFixtureFile($this->src, 'test-file1'); - $sourceRepo->commitAllChanges('Add file 1'); - $file = $this->gitCreateFixtureFile($this->src, 'test-file2'); - $this->assertFileExists($file); - - $sourceRepo->cleanForce(); - $this->assertFileDoesNotExist($file); - } - - /** - * Test branch command. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testBranch(): void { - $sourceRepo = $this->git->open($this->src); - $this->gitCreateFixtureFile($this->src, 'test-file1'); - $sourceRepo->commitAllChanges('Add file 1'); - // Test switch. - $sourceRepo->switchToBranch('branch1', TRUE); - $this->assertEquals('branch1', $sourceRepo->getCurrentBranchName()); - $sourceRepo->switchToBranch('branch2', TRUE); - $this->assertEquals('branch2', $sourceRepo->getCurrentBranchName()); - $sourceRepo->switchToBranch('branch1'); - $this->assertEquals('branch1', $sourceRepo->getCurrentBranchName()); - // Test remove branch. - $this->assertEquals(['branch1', 'branch2', 'master'], $sourceRepo->getBranches()); - $sourceRepo->removeBranch('master'); - $this->assertEquals(['branch1', 'branch2'], $sourceRepo->getBranches()); - $sourceRepo->removeBranch('branch2', TRUE); - $this->assertEquals(['branch1'], $sourceRepo->getBranches()); - - $sourceRepo->removeBranch('', TRUE); - $this->assertEquals(['branch1'], $sourceRepo->getBranches()); - } - - /** - * Test commit all changes. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testCommitAllChanges(): void { - $sourceRepo = $this->git->open($this->src); - $file = $this->gitCreateFixtureFile($this->src, 'test-file1'); - $sourceRepo->addFile($file); - $sourceRepo->commit('Add file 1'); - $this->assertEquals(['Add file 1'], $sourceRepo->getCommits()); - - $this->gitCreateFixtureFile($this->src, 'test-file2'); - $sourceRepo->commitAllChanges('Commit all changes.'); - $this->assertEquals(['Commit all changes.', 'Add file 1'], $sourceRepo->getCommits()); - } - - /** - * Test list commited files. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testListCommittedFiles(): void { - $sourceRepo = $this->git->open($this->src); - $sourceRepo->commit('Commit 1', ['--allow-empty']); - $this->assertEquals([], $sourceRepo->listCommittedFiles()); - - $file = $this->gitCreateFixtureFile($this->src, 'file-1'); - $this->assertEquals([], $sourceRepo->listCommittedFiles()); - - $sourceRepo->addFile($file); - $sourceRepo->commit('Add file 1'); - $this->assertEquals(['file-1'], $sourceRepo->listCommittedFiles()); - } - - /** - * Test set config. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testSetConfigReceiveDenyCurrentBranchIgnore(): void { - $sourceRepo = $this->git->open($this->src); - try { - $receiveDenyCurrentBranch = $sourceRepo->execute('config', 'receive.denyCurrentBranch'); - } - catch (GitException) { - $receiveDenyCurrentBranch = ''; + #[DataProvider('dataProviderIsValidRemote')] + public function testIsValidRemote(string $url, string $type, bool $expect_exception, bool $expected): void { + if ($expect_exception) { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Invalid argument "%s" provided', $type)); } - $this->assertEquals('', $receiveDenyCurrentBranch); - $sourceRepo->setConfigReceiveDenyCurrentBranchIgnore(); - $receiveDenyCurrentBranch = $sourceRepo->execute('config', 'receive.denyCurrentBranch'); - $this->assertEquals(['ignore'], $receiveDenyCurrentBranch); - } - - /** - * Test create tag commands. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testCreateTag(): void { - $sourceRepo = $this->git->open($this->src); - $sourceRepo->commit('Commit 1', ['--allow-empty']); - $this->assertEquals(NULL, $sourceRepo->getTags()); - - $sourceRepo->createAnnotatedTag('tag1', 'Hello tag 1'); - $this->assertEquals(['tag1'], $sourceRepo->getTags()); - - $sourceRepo->createLightweightTag('tag2'); - $this->assertEquals(['tag1', 'tag2'], $sourceRepo->getTags()); - } - - /** - * Test remote commands. - * - * @throws \CzProject\GitPhp\GitException - */ - public function testRemote(): void { - $sourceRepo = $this->git->open($this->src); - $this->assertEquals([], $sourceRepo->getRemotes()); - $sourceRepo->addRemote('dst', $this->dst); - $this->assertEquals(['dst'], $sourceRepo->getRemotes()); - $this->assertTrue($sourceRepo->isRemoteExists('dst')); - $sourceRepo->removeRemote('dst'); - $this->assertEquals([], $sourceRepo->getRemotes()); - $this->assertFalse($sourceRepo->isRemoteExists('dst')); + $url = $url === '' ? (new Filesystem())->tempnam(sys_get_temp_dir(), 'test') : $url; - $sourceRepo->removeRemote('dummy'); - $this->assertEquals([], $sourceRepo->getRemotes()); - } - - /** - * Test is valid remote url. - * - * @throws \Exception - */ - #[DataProvider('dataProviderIsValidRemoteUrl')] - public function testIsValidRemoteUrl(?bool $expected, string $pathOrUri, string $type, bool $pass): void { - if (!$pass) { - $this->expectException(\InvalidArgumentException::class); - ArtifactGitRepository::isValidRemoteUrl($pathOrUri, $type); - } - else { - $this->assertEquals($expected, ArtifactGitRepository::isValidRemoteUrl($pathOrUri, $type)); - } + $actual = ArtifactGitRepository::isValidRemote($url, $type); + $this->assertEquals($expected, $actual); } - /** - * Data provider. - * - * @return array - * Data provider. - */ - public static function dataProviderIsValidRemoteUrl(): array { + public static function dataProviderIsValidRemote(): array { return [ - [TRUE, 'git@github.com:foo/git-foo.git', 'uri', TRUE], - [FALSE, 'git@github.com:foo/git-foo.git', 'local', TRUE], - [TRUE, 'git@github.com:foo/git-foo.git', 'any', TRUE], - [FALSE, '/no-existing/path', 'any', TRUE], - [FALSE, '/no-existing/path', 'local', TRUE], - [NULL, '/no-existing/path', 'custom', FALSE], + ['', 'any', FALSE, TRUE], + ['', 'local', FALSE, TRUE], + ['', 'external', FALSE, FALSE], + ['', 'custom_type', TRUE, FALSE], + // Negative tests. + ['/path/non-existing', 'any', FALSE, FALSE], + ['/path/non-existing', 'local', FALSE, FALSE], + ['/path/non-existing', 'external', FALSE, FALSE], + ['/path/non-existing', 'custom_type', TRUE, FALSE], + + ['git@github.com:user/repo.git', 'any', FALSE, TRUE], + ['git@github.com:user/repo.git', 'external', FALSE, TRUE], + ['git@github.com:user/repo.git', 'local', FALSE, FALSE], + ['git@github.com:user/repo.git', 'custom_type', TRUE, FALSE], + // Negative tests. + ['git@github.com:user/repo', 'any', FALSE, FALSE], + ['git@github.com:user/repo', 'external', FALSE, FALSE], + ['git@github.com:user/repo', 'local', FALSE, FALSE], + ['git@github.com:user/repo', 'custom_type', TRUE, FALSE], + + ['https://github.com/user/repo.git', 'any', FALSE, TRUE], + ['https://github.com/user/repo.git', 'external', FALSE, TRUE], + ['https://github.com/user/repo.git', 'local', FALSE, FALSE], + ['https://github.com/user/repo.git', 'custom_type', TRUE, FALSE], + // Negative tests. + ['https://github.com/user/repo', 'any', FALSE, FALSE], + ['https://github.com/user/repo', 'external', FALSE, FALSE], + ['https://github.com/user/repo', 'local', FALSE, FALSE], + ['https://github.com/user/repo', 'custom_type', TRUE, FALSE], + + ['http://github.com/user/repo.git', 'any', FALSE, TRUE], + ['http://github.com/user/repo.git', 'external', FALSE, TRUE], + ['http://github.com/user/repo.git', 'local', FALSE, FALSE], + ['http://github.com/user/repo.git', 'custom_type', TRUE, FALSE], + // Negative tests. + ['http://github.com/user/repo', 'any', FALSE, FALSE], + ['http://github.com/user/repo', 'external', FALSE, FALSE], + ['http://github.com/user/repo', 'local', FALSE, FALSE], + ['http://github.com/user/repo', 'custom_type', TRUE, FALSE], + + ['git://user/repo.git', 'any', FALSE, TRUE], + ['git://user/repo.git', 'external', FALSE, TRUE], + ['git://user/repo.git', 'local', FALSE, FALSE], + ['git://user/repo.git', 'custom_type', TRUE, FALSE], + // Negative tests. + ['git://user/repo', 'any', FALSE, FALSE], + ['git://user/repo', 'external', FALSE, FALSE], + ['git://user/repo', 'local', FALSE, FALSE], + ['git://user/repo', 'custom_type', TRUE, FALSE], + + ['ssh://git@github.com/user/repo.git', 'any', FALSE, TRUE], + ['ssh://git@github.com/user/repo.git', 'external', FALSE, TRUE], + ['ssh://git@github.com/user/repo.git', 'local', FALSE, FALSE], + ['ssh://git@github.com/user/repo.git', 'custom_type', TRUE, FALSE], + // Negative tests. + ['ssh://git@github.com/user/repo', 'any', FALSE, FALSE], + ['ssh://git@github.com/user/repo', 'external', FALSE, FALSE], + ['ssh://git@github.com/user/repo', 'local', FALSE, FALSE], + ['ssh://git@github.com/user/repo', 'custom_type', TRUE, FALSE], ]; } - /** - * Test is valid remote url. - * - * @throws \Exception - */ #[DataProvider('dataProviderIsValidBranchName')] - public function testIsValidBranchName(bool $expected, string $branchName): void { - $this->assertEquals($expected, ArtifactGitRepository::isValidBranchName($branchName)); + public function testIsValidBranchName(string $name, bool $expected): void { + $this->assertEquals($expected, ArtifactGitRepository::isValidBranchName($name)); } - /** - * Data provider. - * - * @return array - * Data provider. - */ public static function dataProviderIsValidBranchName(): array { return [ - [TRUE, 'branch'], - [FALSE, '*/branch'], - [FALSE, '*.branch'], + ['', FALSE], + [' ', FALSE], + ["\n", FALSE], + ['branch', TRUE], + ['branch/sub', TRUE], + ['branch/sub/subsub', TRUE], + ['branch/*', FALSE], + ['branch/sub/*', FALSE], + ['branch/sub/subsub/*', FALSE], + ['*/branch', FALSE], + ['*.branch', FALSE], + [':branch', FALSE], + ['~branch', FALSE], + ['?branch', FALSE], + ['branch?', FALSE], + ['branch/?', FALSE], + ['branch//', FALSE], + ['/branch', FALSE], + ['//branch', FALSE], + // Long branch names. + [str_repeat('a', 254), TRUE], + [str_repeat('a', 255), FALSE], + ['branch' . str_repeat('/sub', 255), FALSE], ]; } diff --git a/tests/phpunit/Unit/ArtifactGitTest.php b/tests/phpunit/Unit/ArtifactGitTest.php deleted file mode 100644 index 3a39a7a..0000000 --- a/tests/phpunit/Unit/ArtifactGitTest.php +++ /dev/null @@ -1,27 +0,0 @@ -git->open($this->src); - $this->assertEquals(ArtifactGitRepository::class, $repo::class); - } - -} diff --git a/tests/phpunit/Unit/ExcludeTest.php b/tests/phpunit/Unit/ExcludeTest.php deleted file mode 100644 index dc6f174..0000000 --- a/tests/phpunit/Unit/ExcludeTest.php +++ /dev/null @@ -1,182 +0,0 @@ -createFixtureExcludeFile(); - - $actual = $this->callProtectedMethod($this->command, 'localExcludeExists', [$this->fixtureDir]); - - $this->assertTrue($actual); - } - - /** - * @param array $lines - * Lines. - * @param bool $strict - * Strict. - * @param bool $expected - * Expected. - * - * - * @dataProvider dataProviderExcludeEmpty - * - * @throws \ReflectionException - */ - public function testExcludeEmpty(array $lines, bool $strict, bool $expected): void { - $this->createFixtureExcludeFile(implode(PHP_EOL, $lines)); - - $actual = $this->callProtectedMethod($this->command, 'localExcludeEmpty', [$this->fixtureDir, $strict]); - - $this->assertEquals($expected, $actual); - } - - /** - * @return array - * Data provider. - * - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public static function dataProviderExcludeEmpty(): array { - return [ - // Empty file. - [ - [], TRUE, TRUE, - ], - [ - [], FALSE, TRUE, - ], - - // Spaces single line. - [ - [ - ' ', - ], TRUE, FALSE, - ], - - [ - [ - ' ', - ], FALSE, TRUE, - ], - - // Spaces. - [ - [ - ' ', - ' ', - ], TRUE, FALSE, - ], - - [ - [ - ' ', - ' ', - ], FALSE, TRUE, - ], - - // Spaces, comments. - [ - [ - ' ', - '#comment ', - ' ', - ], TRUE, FALSE, - ], - - [ - [ - ' ', - '#comment ', - ' ', - ], FALSE, TRUE, - ], - - // Spaces, padded comments. - [ - [ - ' ', - ' #comment ', - ' ', - ], TRUE, FALSE, - ], - - [ - [ - ' ', - ' #comment ', - ' ', - ], FALSE, TRUE, - ], - - // Spaces, comments and valid content. - [ - [ - ' ', - '#comment ', - 'valid', - ' ', - ], TRUE, FALSE, - ], - - [ - [ - ' ', - '#comment ', - 'valid', - ' ', - ], FALSE, FALSE, - ], - - // Spaces, inline comments and valid content. - [ - [ - ' ', - '#comment ', - 'valid', - 'valid # other comment', - ' ', - ], TRUE, FALSE, - ], - - [ - [ - ' ', - '#comment ', - 'valid', - 'valid # other comment', - ' ', - ], FALSE, FALSE, - ], - - ]; - } - - /** - * Helper to create an exclude file. - * - * @param string $contents - * Optional file contents. - * - * @return string - * Created file name. - */ - protected function createFixtureExcludeFile(string $contents = ''): string { - return $this->gitCreateFixtureFile($this->fixtureDir . DIRECTORY_SEPARATOR . '.git' . DIRECTORY_SEPARATOR . 'info', 'exclude', $contents); - } - -} diff --git a/tests/phpunit/Unit/TokenTest.php b/tests/phpunit/Unit/TokenTest.php index 21be4e9..ff391d7 100644 --- a/tests/phpunit/Unit/TokenTest.php +++ b/tests/phpunit/Unit/TokenTest.php @@ -5,30 +5,86 @@ namespace DrevOps\GitArtifact\Tests\Unit; use DrevOps\GitArtifact\Traits\TokenTrait; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; -/** - * Class ForcePushTest. - * - * @group integration - * - * @covers \DrevOps\GitArtifact\Traits\TokenTrait - */ +#[CoversClass(TokenTrait::class)] class TokenTest extends AbstractUnitTestCase { - /** - * @dataProvider dataProviderTokenExists - */ + #[DataProvider('dataProviderTokenProcess')] + public function testTokenProcess(string $string, string $expected): void { + $class = new class() { + + use TokenTrait; + + public function getTokenSomeToken(?string $prop = NULL): string { + return empty($prop) ? 'somevalue' : 'somevalue with property ' . $prop; + } + + }; + + $actual = $this->callProtectedMethod($class, 'tokenProcess', [$string]); + $this->assertEquals($expected, $actual); + } + + public static function dataProviderTokenProcess(): array { + return [ + [ + '', + '', + ], + [ + '', + '', + ], + [ + 'string without a token', + 'string without a token', + ], + [ + 'string with sometoken without delimiters', + 'string with sometoken without delimiters', + ], + [ + 'string with [sometoken broken delimiters', + 'string with [sometoken broken delimiters', + ], + [ + 'string with sometoken] broken delimiters', + 'string with sometoken] broken delimiters', + ], + // Proper token. + [ + '[sometoken]', + 'somevalue', + ], + [ + 'string with [sometoken] present', + 'string with somevalue present', + ], + // Token with properties. + [ + 'string with [sometoken:prop] present', + 'string with somevalue with property prop present', + ], + [ + 'string with [sometoken:prop:otherprop] present', + 'string with somevalue with property prop:otherprop present', + ], + ]; + } + + #[DataProvider('dataProviderTokenExists')] public function testTokenExists(string $string, bool $expected): void { - $mock = $this->prepareMock(TokenTrait::class); + $class = new class() { + + use TokenTrait; + }; - $actual = $this->callProtectedMethod($mock, 'tokenExists', [$string]); + $actual = $this->callProtectedMethod($class, 'tokenExists', [$string]); $this->assertEquals($expected, $actual); } - /** - * @return array - * Data provider. - */ public static function dataProviderTokenExists(): array { return [ ['notoken', FALSE],