From 22c667c537906776ca6b965d79f894f38c0c6101 Mon Sep 17 00:00:00 2001 From: Tan Nguyen Date: Tue, 19 Mar 2024 13:41:03 +0700 Subject: [PATCH] [#76] Moved functionality from Artifact class to ArtifactCommand class. (#80) * Move functionality from Artifact class to ArtifactCommand class. * Update coverage test name. * Check branch exisiting before do remove. * Revert to standard app entry --- src/Artifact.php | 963 ------------------- src/Commands/ArtifactCommand.php | 980 +++++++++++++++++++- src/GitArtifactGitRepository.php | 8 + tests/phpunit/Functional/BranchTest.php | 2 +- tests/phpunit/Functional/ForcePushTest.php | 2 +- tests/phpunit/Functional/GeneralTest.php | 2 +- tests/phpunit/Functional/TagTest.php | 2 +- tests/phpunit/Unit/AbstractUnitTestCase.php | 21 +- tests/phpunit/Unit/ExcludeTest.php | 6 +- 9 files changed, 993 insertions(+), 993 deletions(-) delete mode 100644 src/Artifact.php diff --git a/src/Artifact.php b/src/Artifact.php deleted file mode 100644 index 796147a..0000000 --- a/src/Artifact.php +++ /dev/null @@ -1,963 +0,0 @@ -fsFileSystem = $fsFileSystem; - } - - /** - * Assemble a code artifact from your codebase. - * - * @param string $remote - * Path to the remote git repository. - * @param array $opts - * Options. - * - * @option $branch Destination branch with optional tokens. - * @option $debug Print debug information. - * @option $gitignore Path to gitignore file to replace current .gitignore. - * @option $message Commit message with optional tokens. - * @option $mode Mode of artifact build: branch, force-push or diff. - * Defaults to force-push. - * @option $now Internal value used to set internal time. - * @option $no-cleanup Do not cleanup after run. - * @option $push Push artifact to the remote repository. Defaults to FALSE. - * @option $report Path to the report file. - * @option $root Path to the root for file path resolution. If not - * specified, current directory is used. - * @option $show-changes Show changes made to the repo by the build in the - * output. - * @option $src Directory where source repository is located. If not - * specified, root directory is used. - * - * @throws \Exception - * - * @phpstan-ignore-next-line - */ - public function artifact(string $remote, array $opts = [ - 'branch' => '[branch]', - 'debug' => FALSE, - 'gitignore' => '', - 'message' => 'Deployment commit', - 'mode' => 'force-push', - 'no-cleanup' => FALSE, - 'now' => '', - 'push' => FALSE, - 'report' => '', - 'root' => '', - 'show-changes' => FALSE, - 'src' => '', - ]): void { - try { - $error = NULL; - $this->checkRequirements(); - $this->resolveOptions($remote, $opts); - - // Now we have all what we need. - // Let process artifact function. - $this->printDebug('Debug messages enabled'); - - $this->setupRemoteForRepository(); - $this->showInfo(); - $this->prepareArtifact(); - - if ($this->needsPush) { - $this->doPush(); - } - else { - $this->yell('Cowardly refusing to push to remote. Use --push option to perform an actual push.'); - } - $this->result = TRUE; - } - catch (\Exception $exception) { - // Capture message and allow to rollback. - $error = $exception->getMessage(); - } - - if (!empty($this->reportFile)) { - $this->dumpReport(); - } - - if ($this->needCleanup) { - $this->cleanup(); - } - - if ($this->result) { - $this->say('Deployment finished successfully.'); - } - else { - $this->say('Deployment failed.'); - 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->say(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()); - } - 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->sayOkay(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 - ); - } - } - - /** - * Resolve and validate CLI options values into internal values. - * - * @param string $remote - * Remote URL. - * @param array $options - * Array of CLI options. - * - * @throws \CzProject\GitPhp\GitException - * - * @phpstan-ignore-next-line - */ - protected function resolveOptions(string $remote, array $options): void { - // First handle root for filesystem. - $this->fsSetRootDir($options['root']); - - // Resolve some basic options into properties. - $this->showChanges = !empty($options['show-changes']); - $this->needCleanup = empty($options['no-cleanup']); - $this->needsPush = !empty($options['push']); - $this->reportFile = empty($options['report']) ? '' : $options['report']; - $this->now = empty($options['now']) ? time() : (int) $options['now']; - $this->debug = !empty($options['debug']); - $this->remoteName = self::GIT_REMOTE_NAME; - $this->remoteUrl = $remote; - $this->setMode($options['mode'], $options); - - // Handle some complex options. - $srcPath = empty($options['src']) ? $this->fsGetRootDir() : $this->fsGetAbsolutePath($options['src']); - $this->sourcePathGitRepository = $srcPath; - // Setup Git repository from source path. - $this->initGitRepository($srcPath); - // 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']); - // Set git ignore file path. - if (!empty($options['gitignore'])) { - $this->setGitignoreFile($options['gitignore']); - } - - } - - /** - * Setup git repository. - * - * @param string $sourcePath - * Source path. - * - * @return GitArtifactGitRepository - * Current git repository. - * - * @throws \CzProject\GitPhp\GitException - * @throws \Exception - */ - protected function initGitRepository(string $sourcePath): GitArtifactGitRepository { - $this->gitRepository = $this->git->open($sourcePath); - - return $this->gitRepository; - } - - /** - * Show artifact build information. - */ - protected function showInfo(): void { - $lines[] = ('----------------------------------------------------------------------'); - $lines[] = (' Artifact information'); - $lines[] = ('----------------------------------------------------------------------'); - $lines[] = (' Build timestamp: ' . date('Y/m/d H:i:s', $this->now)); - $lines[] = (' Mode: ' . $this->mode); - $lines[] = (' Source repository: ' . $this->getSourcePathGitRepository()); - $lines[] = (' Remote repository: ' . $this->remoteUrl); - $lines[] = (' Remote branch: ' . $this->destinationBranch); - $lines[] = (' Gitignore file: ' . ($this->gitignoreFile ?: 'No')); - $lines[] = (' Will push: ' . ($this->needsPush ? 'Yes' : 'No')); - $lines[] = ('----------------------------------------------------------------------'); - $this->output->writeln($lines); - } - - /** - * Dump artifact report to a file. - */ - protected function dumpReport(): 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[] = ' 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[] = '----------------------------------------------------------------------'; - - $this->fsFileSystem->dumpFile($this->reportFile, implode(PHP_EOL, $lines)); - } - - /** - * Set build mode. - * - * @param string $mode - * Mode to set. - * @param array $options - * Array of CLI options. - * - * @phpstan-ignore-next-line - */ - protected function setMode(string $mode, array $options): void { - $this->say(sprintf('Running in "%s" mode', $mode)); - - switch ($mode) { - case self::modeForcePush(): - // Intentionally empty. - break; - - case self::modeBranch(): - if (!$this->hasToken($options['branch'])) { - $this->say('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. - Consider adding tokens with unique values to the branch name.'); - } - break; - - case self::modeDiff(): - 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(), - ]))); - } - - $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 (!GitArtifactGitRepository::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->say('Checking requirements'); - if (!$this->fsIsCommandAvailable('git')) { - throw new \RuntimeException('At least one of the script running requirements was not met'); - } - $this->sayOkay('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->printDebug('Replacing .gitignore: %s with %s', $path . DIRECTORY_SEPARATOR . '.gitignore', $filename); - $this->fsFileSystem->copy($filename, $path . DIRECTORY_SEPARATOR . '.gitignore', TRUE); - $this->fsFileSystem->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->fsFileSystem->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->fsFileSystem->exists($filename)) { - $this->printDebug('Disabling local exclude'); - $this->fsFileSystem->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->fsFileSystem->exists($filenameDisabled)) { - $this->printDebug('Restoring local exclude'); - $this->fsFileSystem->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->printDebug('Unable to load ' . $gitignoreContent); - } - else { - $this->printDebug('-----.gitignore---------'); - $this->printDebug($gitignoreContent); - $this->printDebug('-----.gitignore---------'); - } - - $files = $this - ->gitRepository - ->listIgnoredFilesFromGitIgnoreFile($gitignorePath); - - if (!empty($files)) { - $files = array_filter($files); - foreach ($files as $file) { - $fileName = $location . DIRECTORY_SEPARATOR . $file; - $this->printDebug('Removing excluded file %s', $fileName); - if ($this->fsFileSystem->exists($fileName)) { - $this->fsFileSystem->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->printDebug('Removing other file %s', $fileName); - $this->fsFileSystem->remove($fileName); - } - } - } - - /** - * 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->fsFileSystem->remove($dir); - $this->printDebug('Removing sub-repository "%s"', (string) $dir); - } - } - - /** - * Token callback to get current branch. - * - * @return string - * Branch name. - * - * @throws \Exception - */ - protected function getTokenBranch(): string { - return $this - ->gitRepository - ->getCurrentBranchName(); - } - - /** - * Token callback to get tags. - * - * @param string|null $delimiter - * Token delimiter. Defaults to ', '. - * - * @return string - * String of tags. - * - * @throws \Exception - */ - protected function getTokenTags(string $delimiter = NULL): string { - $delimiter = $delimiter ?: '-'; - $tags = $this - ->gitRepository - ->getTags(); - - return implode($delimiter, $tags); - } - - /** - * Token callback to get current timestamp. - * - * @param string $format - * Date format suitable for date() function. - * - * @return string - * Date string. - */ - protected function getTokenTimestamp(string $format = 'Y-m-d_H-i-s'): string { - return date($format, $this->now); - } - - /** - * Check if running in debug mode. - * - * @return bool - * Check is debugging mode or not. - */ - protected function isDebug(): bool { - return $this->debug || $this->output->isDebug(); - } - - /** - * Write line as yell style. - * - * @param string $text - * Text yell. - */ - protected function yell(string $text): void { - $color = 'green'; - $char = $this->decorationCharacter('>', '➜'); - $format = sprintf('%%s %%s', $color, $color); - $this->writeln(sprintf($format, $char, $text)); - } - - /** - * Write line as say style. - * - * @param string $text - * Text. - */ - protected function say(string $text): void { - $char = $this->decorationCharacter('>', '➜'); - $this->writeln(sprintf('%s %s', $char, $text)); - } - - /** - * Print success message. - * - * Usually used to explicitly state that some action was successfully - * executed. - * - * @param string $text - * Message text. - */ - protected function sayOkay(string $text): void { - $color = 'green'; - $char = $this->decorationCharacter('V', '✔'); - $format = sprintf('%%s %%s', $color, $color); - $this->writeln(sprintf($format, $char, $text)); - } - - /** - * Print debug information. - * - * @param mixed ...$args - * The args. - */ - protected function printDebug(mixed ...$args): void { - if (!$this->isDebug()) { - return; - } - $message = array_shift($args); - /* @phpstan-ignore-next-line */ - $this->writeln(vsprintf($message, $args)); - } - - /** - * Write output. - * - * @param string $text - * Text. - */ - protected function writeln(string $text): void { - $this->output->writeln($text); - } - - /** - * Decoration character. - * - * @param string $nonDecorated - * Non decorated. - * @param string $decorated - * Decorated. - * - * @return string - * The decoration character. - */ - protected function decorationCharacter(string $nonDecorated, string $decorated): string { - if (!$this->output->isDecorated() || (strncasecmp(PHP_OS, 'WIN', 3) === 0)) { - return $nonDecorated; - } - - return $decorated; - } - - /** - * Setup remote for current repository. - * - * @throws \CzProject\GitPhp\GitException - * @throws \Exception - */ - protected function setupRemoteForRepository(): void { - $remoteName = $this->remoteName; - $remoteUrl = $this->remoteUrl; - if (!GitArtifactGitRepository::isValidRemoteUrl($remoteUrl)) { - throw new \Exception(sprintf('Invalid remote URL: %s', $remoteUrl)); - } - - $this->gitRepository->addRemote($remoteName, $remoteUrl); - } - -} diff --git a/src/Commands/ArtifactCommand.php b/src/Commands/ArtifactCommand.php index c822771..b59607a 100644 --- a/src/Commands/ArtifactCommand.php +++ b/src/Commands/ArtifactCommand.php @@ -4,20 +4,152 @@ namespace DrevOps\GitArtifact\Commands; -use DrevOps\GitArtifact\Artifact; +use DrevOps\GitArtifact\FilesystemTrait; use DrevOps\GitArtifact\GitArtifactGit; +use DrevOps\GitArtifact\GitArtifactGitRepository; +use DrevOps\GitArtifact\TokenTrait; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\Finder; /** * Artifact Command. + * + * @SuppressWarnings(PHPMD.TooManyMethods) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.ExcessiveClassLength) */ class ArtifactCommand extends Command { + use TokenTrait; + use FilesystemTrait; + + const GIT_REMOTE_NAME = 'dst'; + + /** + * Represent to current repository. + */ + protected GitArtifactGitRepository $gitRepository; + + /** + * Source path of git repository. + */ + protected string $sourcePathGitRepository = ''; + + /** + * Mode in which current build is going to run. + * + * Available modes: branch, force-push, diff. + */ + protected string $mode; + + /** + * Original branch in current repository. + */ + protected string $originalBranch = ''; + + /** + * Destination branch with optional tokens. + */ + protected string $destinationBranch = ''; + + /** + * Local branch where artifact will be built. + */ + protected string $artifactBranch = ''; + + /** + * Remote name. + */ + protected string $remoteName = ''; + + /** + * Remote URL includes uri or local path. + */ + protected string $remoteUrl = ''; + + /** + * Gitignore file to be used during artifact creation. + * + * If not set, the current `.gitignore` will be used, if any. + */ + protected ?string $gitignoreFile = NULL; + + /** + * Commit message with optional tokens. + */ + protected string $message = ''; + + /** + * Flag to specify if push is required or should be using dry run. + */ + protected bool $needsPush = FALSE; + + /** + * Flag to specify if cleanup is required to run after the build. + */ + protected bool $needCleanup = TRUE; + + /** + * Path to report file. + */ + protected string $reportFile = ''; + + /** + * Flag to show changes made to the repo by the build in the output. + */ + protected bool $showChanges = FALSE; + + /** + * Artifact build result. + */ + protected bool $result = FALSE; + + /** + * Flag to print debug information. + */ + protected bool $debug = FALSE; + + /** + * Internal option to set current timestamp. + */ + protected int $now; + + /** + * Output. + */ + protected OutputInterface $output; + + /** + * Git wrapper. + */ + protected GitArtifactGit $git; + + /** + * Artifact constructor. + * + * @param \DrevOps\GitArtifact\GitArtifactGit $gitWrapper + * Git wrapper. + * @param \Symfony\Component\Filesystem\Filesystem $fsFileSystem + * File system. + * @param string|null $name + * Command name. + */ + public function __construct( + GitArtifactGit $gitWrapper = NULL, + Filesystem $fsFileSystem = NULL, + ?string $name = NULL + ) { + parent::__construct($name); + $this->fsFileSystem = is_null($fsFileSystem) ? new Filesystem() : $fsFileSystem; + $this->git = is_null($gitWrapper) ? new GitArtifactGit() : $gitWrapper; + } + /** * Configure command. */ @@ -89,14 +221,848 @@ protected function configure(): void { * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { - $fileSystem = new Filesystem(); - $git = new GitArtifactGit(); - $artifact = new Artifact($git, $fileSystem, $output); - $remote = $input->getArgument('remote'); - // @phpstan-ignore-next-line - $artifact->artifact($remote, $input->getOptions()); + $this->output = $output; + try { + $this->checkRequirements(); + $remote = $input->getArgument('remote'); + // @phpstan-ignore-next-line + $this->processArtifact($remote, $input->getOptions()); + } + catch (\Exception $exception) { + $output->writeln('' . $exception->getMessage() . ''); + + return Command::FAILURE; + } return Command::SUCCESS; } + /** + * Assemble a code artifact from your codebase. + * + * @param string $remote + * Path to the remote git repository. + * @param array $opts + * Options. + * + * @option $branch Destination branch with optional tokens. + * @option $debug Print debug information. + * @option $gitignore Path to gitignore file to replace current .gitignore. + * @option $message Commit message with optional tokens. + * @option $mode Mode of artifact build: branch, force-push or diff. + * Defaults to force-push. + * @option $now Internal value used to set internal time. + * @option $no-cleanup Do not cleanup after run. + * @option $push Push artifact to the remote repository. Defaults to FALSE. + * @option $report Path to the report file. + * @option $root Path to the root for file path resolution. If not + * specified, current directory is used. + * @option $show-changes Show changes made to the repo by the build in the + * output. + * @option $src Directory where source repository is located. If not + * specified, root directory is used. + * + * @throws \Exception + * + * @phpstan-ignore-next-line + */ + protected function processArtifact(string $remote, array $opts = [ + 'branch' => '[branch]', + 'debug' => FALSE, + 'gitignore' => '', + 'message' => 'Deployment commit', + 'mode' => 'force-push', + 'no-cleanup' => FALSE, + 'now' => '', + 'push' => FALSE, + 'report' => '', + 'root' => '', + 'show-changes' => FALSE, + 'src' => '', + ]): void { + try { + $error = NULL; + $this->resolveOptions($remote, $opts); + + // Now we have all what we need. + // Let process artifact function. + $this->printDebug('Debug messages enabled'); + + $this->setupRemoteForRepository(); + $this->showInfo(); + $this->prepareArtifact(); + + if ($this->needsPush) { + $this->doPush(); + } + else { + $this->yell('Cowardly refusing to push to remote. Use --push option to perform an actual push.'); + } + $this->result = TRUE; + } + catch (\Exception $exception) { + // Capture message and allow to rollback. + $error = $exception->getMessage(); + } + + if (!empty($this->reportFile)) { + $this->dumpReport(); + } + + if ($this->needCleanup) { + $this->cleanup(); + } + + if ($this->result) { + $this->say('Deployment finished successfully.'); + } + else { + $this->say('Deployment failed.'); + 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->say(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()); + } + 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->sayOkay(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 + ); + } + } + + /** + * Resolve and validate CLI options values into internal values. + * + * @param string $remote + * Remote URL. + * @param array $options + * Array of CLI options. + * + * @throws \CzProject\GitPhp\GitException + * @throws \Exception + * + * @phpstan-ignore-next-line + */ + protected function resolveOptions(string $remote, array $options): void { + // First handle root for filesystem. + $this->fsSetRootDir($options['root']); + + // Resolve some basic options into properties. + $this->showChanges = !empty($options['show-changes']); + $this->needCleanup = empty($options['no-cleanup']); + $this->needsPush = !empty($options['push']); + $this->reportFile = empty($options['report']) ? '' : $options['report']; + $this->now = empty($options['now']) ? time() : (int) $options['now']; + $this->debug = !empty($options['debug']); + $this->remoteName = self::GIT_REMOTE_NAME; + $this->remoteUrl = $remote; + $this->setMode($options['mode'], $options); + + // Handle some complex options. + $srcPath = empty($options['src']) ? $this->fsGetRootDir() : $this->fsGetAbsolutePath($options['src']); + $this->sourcePathGitRepository = $srcPath; + // Setup Git repository from source path. + $this->initGitRepository($srcPath); + // 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']); + // Set git ignore file path. + if (!empty($options['gitignore'])) { + $this->setGitignoreFile($options['gitignore']); + } + + } + + /** + * Setup git repository. + * + * @param string $sourcePath + * Source path. + * + * @return \DrevOps\GitArtifact\GitArtifactGitRepository + * Current git repository. + * + * @throws \CzProject\GitPhp\GitException + * @throws \Exception + */ + protected function initGitRepository(string $sourcePath): GitArtifactGitRepository { + $this->gitRepository = $this->git->open($sourcePath); + + return $this->gitRepository; + } + + /** + * Show artifact build information. + */ + protected function showInfo(): void { + $lines[] = ('----------------------------------------------------------------------'); + $lines[] = (' Artifact information'); + $lines[] = ('----------------------------------------------------------------------'); + $lines[] = (' Build timestamp: ' . date('Y/m/d H:i:s', $this->now)); + $lines[] = (' Mode: ' . $this->mode); + $lines[] = (' Source repository: ' . $this->getSourcePathGitRepository()); + $lines[] = (' Remote repository: ' . $this->remoteUrl); + $lines[] = (' Remote branch: ' . $this->destinationBranch); + $lines[] = (' Gitignore file: ' . ($this->gitignoreFile ?: 'No')); + $lines[] = (' Will push: ' . ($this->needsPush ? 'Yes' : 'No')); + $lines[] = ('----------------------------------------------------------------------'); + $this->output->writeln($lines); + } + + /** + * Dump artifact report to a file. + */ + protected function dumpReport(): 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[] = ' 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[] = '----------------------------------------------------------------------'; + + $this->fsFileSystem->dumpFile($this->reportFile, implode(PHP_EOL, $lines)); + } + + /** + * Set build mode. + * + * @param string $mode + * Mode to set. + * @param array $options + * Array of CLI options. + * + * @phpstan-ignore-next-line + */ + protected function setMode(string $mode, array $options): void { + $this->say(sprintf('Running in "%s" mode', $mode)); + + switch ($mode) { + case self::modeForcePush(): + // Intentionally empty. + break; + + case self::modeBranch(): + if (!$this->hasToken($options['branch'])) { + $this->say('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. + Consider adding tokens with unique values to the branch name.'); + } + break; + + case self::modeDiff(): + 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(), + ]))); + } + + $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 (!GitArtifactGitRepository::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->say('Checking requirements'); + if (!$this->fsIsCommandAvailable('git')) { + throw new \RuntimeException('At least one of the script running requirements was not met'); + } + $this->sayOkay('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->printDebug('Replacing .gitignore: %s with %s', $path . DIRECTORY_SEPARATOR . '.gitignore', $filename); + $this->fsFileSystem->copy($filename, $path . DIRECTORY_SEPARATOR . '.gitignore', TRUE); + $this->fsFileSystem->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->fsFileSystem->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->fsFileSystem->exists($filename)) { + $this->printDebug('Disabling local exclude'); + $this->fsFileSystem->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->fsFileSystem->exists($filenameDisabled)) { + $this->printDebug('Restoring local exclude'); + $this->fsFileSystem->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->printDebug('Unable to load ' . $gitignoreContent); + } + else { + $this->printDebug('-----.gitignore---------'); + $this->printDebug($gitignoreContent); + $this->printDebug('-----.gitignore---------'); + } + + $files = $this + ->gitRepository + ->listIgnoredFilesFromGitIgnoreFile($gitignorePath); + + if (!empty($files)) { + $files = array_filter($files); + foreach ($files as $file) { + $fileName = $location . DIRECTORY_SEPARATOR . $file; + $this->printDebug('Removing excluded file %s', $fileName); + if ($this->fsFileSystem->exists($fileName)) { + $this->fsFileSystem->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->printDebug('Removing other file %s', $fileName); + $this->fsFileSystem->remove($fileName); + } + } + } + + /** + * 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->fsFileSystem->remove($dir); + $this->printDebug('Removing sub-repository "%s"', (string) $dir); + } + } + + /** + * Token callback to get current branch. + * + * @return string + * Branch name. + * + * @throws \Exception + */ + protected function getTokenBranch(): string { + return $this + ->gitRepository + ->getCurrentBranchName(); + } + + /** + * Token callback to get tags. + * + * @param string|null $delimiter + * Token delimiter. Defaults to ', '. + * + * @return string + * String of tags. + * + * @throws \Exception + */ + protected function getTokenTags(string $delimiter = NULL): string { + $delimiter = $delimiter ?: '-'; + $tags = $this + ->gitRepository + ->getTags(); + + return implode($delimiter, $tags); + } + + /** + * Token callback to get current timestamp. + * + * @param string $format + * Date format suitable for date() function. + * + * @return string + * Date string. + */ + protected function getTokenTimestamp(string $format = 'Y-m-d_H-i-s'): string { + return date($format, $this->now); + } + + /** + * Check if running in debug mode. + * + * @return bool + * Check is debugging mode or not. + */ + protected function isDebug(): bool { + return $this->debug || $this->output->isDebug(); + } + + /** + * Write line as yell style. + * + * @param string $text + * Text yell. + */ + protected function yell(string $text): void { + $color = 'green'; + $char = $this->decorationCharacter('>', '➜'); + $format = sprintf('%%s %%s', $color, $color); + $this->writeln(sprintf($format, $char, $text)); + } + + /** + * Write line as say style. + * + * @param string $text + * Text. + */ + protected function say(string $text): void { + $char = $this->decorationCharacter('>', '➜'); + $this->writeln(sprintf('%s %s', $char, $text)); + } + + /** + * Print success message. + * + * Usually used to explicitly state that some action was successfully + * executed. + * + * @param string $text + * Message text. + */ + protected function sayOkay(string $text): void { + $color = 'green'; + $char = $this->decorationCharacter('V', '✔'); + $format = sprintf('%%s %%s', $color, $color); + $this->writeln(sprintf($format, $char, $text)); + } + + /** + * Print debug information. + * + * @param mixed ...$args + * The args. + */ + protected function printDebug(mixed ...$args): void { + if (!$this->isDebug()) { + return; + } + $message = array_shift($args); + /* @phpstan-ignore-next-line */ + $this->writeln(vsprintf($message, $args)); + } + + /** + * Write output. + * + * @param string $text + * Text. + */ + protected function writeln(string $text): void { + $this->output->writeln($text); + } + + /** + * Decoration character. + * + * @param string $nonDecorated + * Non decorated. + * @param string $decorated + * Decorated. + * + * @return string + * The decoration character. + */ + protected function decorationCharacter(string $nonDecorated, string $decorated): string { + if (!$this->output->isDecorated() || (strncasecmp(PHP_OS, 'WIN', 3) === 0)) { + return $nonDecorated; + } + + return $decorated; + } + + /** + * Setup remote for current repository. + * + * @throws \CzProject\GitPhp\GitException + * @throws \Exception + */ + protected function setupRemoteForRepository(): void { + $remoteName = $this->remoteName; + $remoteUrl = $this->remoteUrl; + if (!GitArtifactGitRepository::isValidRemoteUrl($remoteUrl)) { + throw new \Exception(sprintf('Invalid remote URL: %s', $remoteUrl)); + } + + $this->gitRepository->addRemote($remoteName, $remoteUrl); + } + } diff --git a/src/GitArtifactGitRepository.php b/src/GitArtifactGitRepository.php index c104ae7..b768a4e 100644 --- a/src/GitArtifactGitRepository.php +++ b/src/GitArtifactGitRepository.php @@ -169,6 +169,14 @@ public function removeBranch($name, bool $force = FALSE): GitArtifactGitReposito return $this; } + $branches = $this->getBranches(); + if (empty($branches)) { + return $this; + } + if (!in_array($name, $branches)) { + return $this; + } + if (!$force) { return parent::removeBranch($name); } diff --git a/tests/phpunit/Functional/BranchTest.php b/tests/phpunit/Functional/BranchTest.php index bc9c7d8..8c5f188 100644 --- a/tests/phpunit/Functional/BranchTest.php +++ b/tests/phpunit/Functional/BranchTest.php @@ -10,7 +10,7 @@ * @group integration * * @covers \DrevOps\GitArtifact\GitTrait - * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\Commands\ArtifactCommand * @covers \DrevOps\GitArtifact\FilesystemTrait */ class BranchTest extends AbstractFunctionalTestCase { diff --git a/tests/phpunit/Functional/ForcePushTest.php b/tests/phpunit/Functional/ForcePushTest.php index 4a6b8bd..79d5e7c 100644 --- a/tests/phpunit/Functional/ForcePushTest.php +++ b/tests/phpunit/Functional/ForcePushTest.php @@ -12,7 +12,7 @@ * @SuppressWarnings(PHPMD.TooManyPublicMethods) * * @covers \DrevOps\GitArtifact\GitTrait - * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\Commands\ArtifactCommand * @covers \DrevOps\GitArtifact\FilesystemTrait */ class ForcePushTest extends AbstractFunctionalTestCase { diff --git a/tests/phpunit/Functional/GeneralTest.php b/tests/phpunit/Functional/GeneralTest.php index f4de3ba..7fdac32 100644 --- a/tests/phpunit/Functional/GeneralTest.php +++ b/tests/phpunit/Functional/GeneralTest.php @@ -9,7 +9,7 @@ * * @group integration * - * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\Commands\ArtifactCommand * @covers \DrevOps\GitArtifact\FilesystemTrait */ class GeneralTest extends AbstractFunctionalTestCase { diff --git a/tests/phpunit/Functional/TagTest.php b/tests/phpunit/Functional/TagTest.php index 4179e7d..82777be 100644 --- a/tests/phpunit/Functional/TagTest.php +++ b/tests/phpunit/Functional/TagTest.php @@ -10,7 +10,7 @@ * @group integration * * @covers \DrevOps\GitArtifact\GitTrait - * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\Commands\ArtifactCommand * @covers \DrevOps\GitArtifact\FilesystemTrait */ class TagTest extends AbstractFunctionalTestCase { diff --git a/tests/phpunit/Unit/AbstractUnitTestCase.php b/tests/phpunit/Unit/AbstractUnitTestCase.php index a29d95d..330cfd3 100644 --- a/tests/phpunit/Unit/AbstractUnitTestCase.php +++ b/tests/phpunit/Unit/AbstractUnitTestCase.php @@ -2,11 +2,8 @@ namespace DrevOps\GitArtifact\Tests\Unit; -use DrevOps\GitArtifact\Artifact; -use DrevOps\GitArtifact\GitArtifactGit; +use DrevOps\GitArtifact\Commands\ArtifactCommand; use DrevOps\GitArtifact\Tests\AbstractTestCase; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Filesystem\Filesystem; /** * Class AbstractUnitTestCase. @@ -14,23 +11,15 @@ abstract class AbstractUnitTestCase extends AbstractTestCase { /** - * Mock of the class. - * - * @var \PHPUnit\Framework\MockObject\MockObject + * Artifact command. */ - protected $mock; + protected ArtifactCommand $command; protected function setUp(): void { parent::setUp(); - $mockBuilder = $this->getMockBuilder(Artifact::class); - $fileSystem = new Filesystem(); - $gitWrapper = new GitArtifactGit(); - $output = new ConsoleOutput(); - - $mockBuilder->setConstructorArgs([$gitWrapper, $fileSystem, $output]); - $this->mock = $mockBuilder->getMock(); - $this->callProtectedMethod($this->mock, 'fsSetRootDir', [$this->fixtureDir]); + $this->command = new ArtifactCommand(); + $this->callProtectedMethod($this->command, 'fsSetRootDir', [$this->fixtureDir]); } } diff --git a/tests/phpunit/Unit/ExcludeTest.php b/tests/phpunit/Unit/ExcludeTest.php index fc026df..dc6f174 100644 --- a/tests/phpunit/Unit/ExcludeTest.php +++ b/tests/phpunit/Unit/ExcludeTest.php @@ -9,7 +9,7 @@ * * @group unit * - * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\Commands\ArtifactCommand */ class ExcludeTest extends AbstractUnitTestCase { @@ -19,7 +19,7 @@ class ExcludeTest extends AbstractUnitTestCase { public function testExcludeExists(): void { $this->createFixtureExcludeFile(); - $actual = $this->callProtectedMethod($this->mock, 'localExcludeExists', [$this->fixtureDir]); + $actual = $this->callProtectedMethod($this->command, 'localExcludeExists', [$this->fixtureDir]); $this->assertTrue($actual); } @@ -40,7 +40,7 @@ public function testExcludeExists(): void { public function testExcludeEmpty(array $lines, bool $strict, bool $expected): void { $this->createFixtureExcludeFile(implode(PHP_EOL, $lines)); - $actual = $this->callProtectedMethod($this->mock, 'localExcludeEmpty', [$this->fixtureDir, $strict]); + $actual = $this->callProtectedMethod($this->command, 'localExcludeEmpty', [$this->fixtureDir, $strict]); $this->assertEquals($expected, $actual); }