From b5c19a7726caf8e8274025c8a96baa344b150739 Mon Sep 17 00:00:00 2001 From: Tan Nguyen Date: Sat, 9 Mar 2024 03:24:35 +0700 Subject: [PATCH] [#42] Converted to standalone Symfony Console CLI app (#59) --- .circleci/config.yml | 10 +- .gitattributes | 1 + .github/workflows/release-php.yml | 47 +++ .github/workflows/test-php.yml | 48 ++- .gitignore | 1 + README.md | 31 +- RoboFile.php | 28 -- box.json | 13 + composer.json | 19 +- git-artifact | 12 + phpcs.xml | 1 - phpstan.neon | 1 - src/{ArtifactTrait.php => Artifact.php} | 274 ++++++++++++------ src/Commands/ArtifactCommand.php | 111 +++++++ src/FilesystemTrait.php | 31 +- src/GitTrait.php | 170 +++++------ src/TokenTrait.php | 2 +- src/app.php | 15 + tests/AbstractTestCase.php | 12 +- tests/Exception/ErrorException.php | 2 +- .../Functional/AbstractFunctionalTestCase.php | 17 +- tests/Functional/BranchTest.php | 8 +- tests/Functional/ForcePushTest.php | 8 +- tests/Functional/GeneralTest.php | 19 +- tests/Functional/TagTest.php | 8 +- tests/Functional/TokenTest.php | 6 +- tests/Traits/CommandTrait.php | 41 ++- tests/Traits/MockTrait.php | 2 +- tests/Traits/ReflectionTrait.php | 2 +- tests/Unit/AbstractUnitTestCase.php | 17 +- tests/Unit/ExcludeTest.php | 4 +- 31 files changed, 598 insertions(+), 363 deletions(-) create mode 100644 .github/workflows/release-php.yml delete mode 100644 RoboFile.php create mode 100644 box.json create mode 100755 git-artifact rename src/{ArtifactTrait.php => Artifact.php} (82%) create mode 100644 src/Commands/ArtifactCommand.php create mode 100644 src/app.php diff --git a/.circleci/config.yml b/.circleci/config.yml index 28c75e9..295dc0a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -92,7 +92,7 @@ jobs: - run: name: Deployment 1 command: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--force-push--circleci--[branch] \ --mode=force-push \ @@ -117,7 +117,7 @@ jobs: - run: name: Deployment 2 command: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--force-push--circleci--[branch] \ --mode=force-push \ @@ -166,7 +166,7 @@ jobs: - run: name: Deployment 1 command: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--branch--circleci--[branch]--[timestamp:Y-m-d_H-i] \ --mode=branch \ @@ -183,7 +183,7 @@ jobs: - run: name: Deployment 2 - same branch command: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--branch--circleci--[branch]--[timestamp:Y-m-d_H-i] \ --mode=branch \ @@ -203,7 +203,7 @@ jobs: - run: name: Deployment 2 - new branch command: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--branch--circleci--[branch]--[timestamp:Y-m-d_H-i-s] \ --mode=branch \ diff --git a/.gitattributes b/.gitattributes index 7ec53ed..62a4897 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,7 @@ /.gitattributes export-ignore /.github export-ignore /.gitignore export-ignore +/box.json export-ignore /tests export-ignore /phpcs.xml export-ignore /phpmd.xml export-ignore diff --git a/.github/workflows/release-php.yml b/.github/workflows/release-php.yml new file mode 100644 index 0000000..3b904c3 --- /dev/null +++ b/.github/workflows/release-php.yml @@ -0,0 +1,47 @@ +name: Release PHP + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + release-php: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: /tmp/composer-cache + key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + + - name: Install dependencies + run: composer install + + - name: Build PHAR + run: composer build + + - name: Test PHAR + run: ./.build/git-artifact || exit 1 + + - name: Get tag name + id: get-version + run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + ./.build/git-artifact diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml index 5bf4e6a..3033597 100644 --- a/.github/workflows/test-php.yml +++ b/.github/workflows/test-php.yml @@ -66,9 +66,41 @@ jobs: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + build-php: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: /tmp/composer-cache + key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + + - name: Install dependencies + run: composer install + + - name: Check composer version + run: composer --version + + - name: Build PHAR + run: composer build + + - name: Test PHAR + run: ./.build/git-artifact --help || exit 1 + #;> PHP_PHAR + # Demonstration of deployment in 'force-push' mode. deploy-force-push: - needs: test-php + needs: + - test-php + - build-php runs-on: ubuntu-latest @@ -123,7 +155,7 @@ jobs: - name: Deployment 1 run: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--force-push--gha--[branch] \ --mode=force-push \ @@ -146,7 +178,7 @@ jobs: - name: Deployment 2 run: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--force-push--gha--[branch] \ --mode=force-push \ @@ -164,7 +196,9 @@ jobs: # of the second push. This is because the mode is intended to create a new # branch per artifact deployment. deploy-branch: - needs: test-php + needs: + - test-php + - build-php runs-on: ubuntu-latest @@ -223,7 +257,7 @@ jobs: - name: Deployment 1 run: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--branch--gha--[branch]--[timestamp:Y-m-d_H-i] \ --mode=branch \ @@ -246,7 +280,7 @@ jobs: - name: Deployment 2 - same branch run: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--branch--gha--[branch]--[timestamp:Y-m-d_H-i] \ --mode=branch \ @@ -257,7 +291,7 @@ jobs: - name: Deployment 2 - new branch run: | - vendor/bin/robo artifact \ + ./git-artifact \ git@github.com:drevops/git-artifact-destination.git \ --branch=mode--branch--gha--[branch]--[timestamp:Y-m-d_H-i-s] \ --mode=branch \ diff --git a/.gitignore b/.gitignore index 522ecdc..54d74e7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /cobertura.xml /composer.lock /vendor +/vendor-bin diff --git a/README.md b/README.md index bc06b9b..53c0ca1 100644 --- a/README.md +++ b/README.md @@ -93,34 +93,8 @@ composer require drevops/git-artifact ``` ## Usage - -Use provided [`RoboFile.php`](RoboFile.php) or create a custom `RoboFile.php` -in your repository with the following content: - -```php -__artifactConstruct(); - } -} -``` - -### Run ```shell -vendor/bin/robo artifact git@github.com:yourorg/your-repo-destination.git +./git-artifact git@github.com:yourorg/your-repo-destination.git ``` This will create an artifact from current directory and will send it to the @@ -136,8 +110,7 @@ See examples: Call from CI configuration or deployment script: ```shell export DEPLOY_BRANCH= -vendor/bin/robo --load-from "${HOME}/.composer/vendor/drevops/git-artifact/RoboFile.php" artifact \ - git@github.com:yourorg/your-repo-destination.git \ +./git-artifact git@github.com:yourorg/your-repo-destination.git \ --branch="${DEPLOY_BRANCH}" \ --push ``` diff --git a/RoboFile.php b/RoboFile.php deleted file mode 100644 index 5a50b09..0000000 --- a/RoboFile.php +++ /dev/null @@ -1,28 +0,0 @@ -__artifactConstruct(); - } -} diff --git a/box.json b/box.json new file mode 100644 index 0000000..b1e836e --- /dev/null +++ b/box.json @@ -0,0 +1,13 @@ +{ + "output": ".build/git-artifact", + "compression": "GZ", + "finder": [ + { + "in": "./", + "exclude": [ + "php-script", + "tests" + ] + } + ] +} diff --git a/composer.json b/composer.json index 3ffddba..b17cf2f 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "drevops/git-artifact", - "type": "robo-tasks", + "type": "package", "description": "Build artifact from your codebase in CI and push it to a separate git repo.", "license": "GPL-2.0-or-later", "authors": [ @@ -16,9 +16,14 @@ }, "require": { "php": ">=8.1", - "consolidation/robo": "^4" + "cpliakas/git-wrapper": "^3.1", + "symfony/console": "^6", + "symfony/finder": "^6", + "symfony/filesystem": "^6", + "monolog/monolog": "^3.5" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.8", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "escapestudios/symfony2-coding-standard": "^3", "phpmd/phpmd": "^2.15", @@ -29,16 +34,17 @@ }, "autoload": { "psr-4": { - "DrevOps\\Robo\\": "src" + "DrevOps\\GitArtifact\\": "src" } }, "autoload-dev": { "psr-4": { - "DrevOps\\Robo\\Tests\\": "tests" + "DrevOps\\GitArtifact\\Tests\\": "tests" } }, "config": { "allow-plugins": { + "bamarni/composer-bin-plugin": true, "dealerdirect/phpcodesniffer-composer-installer": true } }, @@ -59,5 +65,8 @@ "box validate", "box compile" ] - } + }, + "bin": [ + "git-artifact" + ] } diff --git a/git-artifact b/git-artifact new file mode 100755 index 0000000..5c8e545 --- /dev/null +++ b/git-artifact @@ -0,0 +1,12 @@ +#!/usr/bin/env php + src - RoboFile.php tests diff --git a/phpstan.neon b/phpstan.neon index 351833b..9444048 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,7 +9,6 @@ parameters: paths: - src - tests - - RoboFile.php excludePaths: - vendor/* diff --git a/src/ArtifactTrait.php b/src/Artifact.php similarity index 82% rename from src/ArtifactTrait.php rename to src/Artifact.php index b45ef2f..688cb84 100644 --- a/src/ArtifactTrait.php +++ b/src/Artifact.php @@ -1,26 +1,23 @@ __artifactFsConstruct(); + public function __construct( + GitWrapper $gitWrapper, + Filesystem $fsFileSystem, + protected OutputInterface $output, + ) { + $this->fsFileSystem = $fsFileSystem; + $this->gitWrapper = $gitWrapper; } /** @@ -159,7 +166,6 @@ public function __construct() * @option $src Directory where source repository is located. If not * specified, root directory is used. * - * @throws AbortTasksException * @throws \Exception * * @phpstan-ignore-next-line @@ -167,16 +173,16 @@ public function __construct() public function artifact(string $remote, array $opts = [ 'branch' => '[branch]', 'debug' => false, - 'gitignore' => InputOption::VALUE_REQUIRED, + 'gitignore' => '', 'message' => 'Deployment commit', 'mode' => 'force-push', 'no-cleanup' => false, - 'now' => InputOption::VALUE_REQUIRED, + 'now' => '', 'push' => false, - 'report' => InputOption::VALUE_REQUIRED, - 'root' => InputOption::VALUE_REQUIRED, + 'report' => '', + 'root' => '', 'show-changes' => false, - 'src' => InputOption::VALUE_REQUIRED, + 'src' => '', ]): void { try { @@ -215,10 +221,43 @@ public function artifact(string $remote, array $opts = [ $this->say('Deployment finished successfully.'); } else { $this->say('Deployment failed.'); - throw new AbortTasksException((string) $error); + throw new \Exception((string) $error); } } + /** + * 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. * @@ -244,7 +283,7 @@ protected function prepareArtifact(): void $result = $this->gitCommit($this->src, $this->message); if ($this->showChanges) { - $this->say(sprintf('Added changes: %s', $result->getMessage())); + $this->say(sprintf('Added changes: %s', $result)); } } @@ -273,19 +312,25 @@ protected function doPush(): void } try { - $result = $this->gitPush($this->src, $this->artifactBranch, $this->remoteName, $this->dstBranch, $this->mode === self::modeForcePush()); - $this->result = $result->wasSuccessful(); + $this->gitPush( + $this->src, + $this->artifactBranch, + $this->remoteName, + $this->dstBranch, + $this->mode === self::modeForcePush() + ); + $this->sayOkay(sprintf('Pushed branch "%s" with commit message "%s"', $this->dstBranch, $this->message)); } catch (\Exception $exception) { // Re-throw the message with additional context. - throw new \Exception(sprintf('Error occurred while pushing branch "%s": %s', $this->dstBranch, $exception->getMessage()), $exception->getCode(), $exception); - } - - if ($this->result) { - $this->sayOkay(sprintf('Pushed branch "%s" with commit message "%s"', $this->dstBranch, $this->message)); - } else { - // We should never reach this - any problems with git push should - // throw an exception, that we catching above. - throw new \Exception(sprintf('Error occurred while pushing branch "%s" with commit message "%s"', $this->dstBranch, $this->message)); + throw new \Exception( + sprintf( + 'Error occurred while pushing branch "%s" with commit message "%s"', + $this->dstBranch, + $this->message + ), + $exception->getCode(), + $exception + ); } } @@ -339,17 +384,18 @@ protected function resolveOptions(array $options): void */ protected function showInfo(): void { - $this->writeln('----------------------------------------------------------------------'); - $this->writeln(' Artifact information'); - $this->writeln('----------------------------------------------------------------------'); - $this->writeln(' Build timestamp: '.date('Y/m/d H:i:s', $this->now)); - $this->writeln(' Mode: '.$this->mode); - $this->writeln(' Source repository: '.$this->src); - $this->writeln(' Remote repository: '.$this->dst); - $this->writeln(' Remote branch: '.$this->dstBranch); - $this->writeln(' Gitignore file: '.($this->gitignoreFile ? $this->gitignoreFile : 'No')); - $this->writeln(' Will push: '.($this->needsPush ? 'Yes' : 'No')); - $this->writeln('----------------------------------------------------------------------'); + $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->src); + $lines[] = (' Remote repository: '.$this->dst); + $lines[] = (' Remote branch: '.$this->dstBranch); + $lines[] = (' Gitignore file: '.($this->gitignoreFile ? $this->gitignoreFile : 'No')); + $lines[] = (' Will push: '.($this->needsPush ? 'Yes' : 'No')); + $lines[] = ('----------------------------------------------------------------------'); + $this->output->writeln($lines); } /** @@ -394,7 +440,9 @@ protected function setMode(string $mode, array $options): void 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.'); + $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; @@ -412,39 +460,6 @@ protected function setMode(string $mode, array $options): void $this->mode = $mode; } - /** - * 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'; - } - /** * Resolve original branch to handle detached repositories. * @@ -468,7 +483,7 @@ protected function resolveOriginalBranch(string $location): ?string if ($branch === 'HEAD') { $branch = null; $result = $this->gitCommandRun($location, 'branch'); - $branchList = preg_split('/\R/', $result->getMessage()); + $branchList = preg_split('/\R/', $result); if ($branchList) { $branchList = array_filter($branchList); foreach ($branchList as $branch) { @@ -673,10 +688,10 @@ protected function gitAddAll(string $location): void { $result = $this->gitCommandRun( $location, - 'add -A' + 'add -A', ); - $this->printDebug(sprintf("Added all files:\n%s", $result->getMessage())); + $this->printDebug(sprintf("Added all files:\n%s", $result)); } /** @@ -698,7 +713,7 @@ protected function gitUpdateIndex(string $location): void foreach ($files as $file) { $this->gitCommandRun( $location, - sprintf('update-index --info-only --add "%s"', $file) + sprintf('update-index --info-only --add "%s"', $file), ); $this->printDebug(sprintf('Updated index for file "%s"', $file)); } @@ -717,7 +732,7 @@ protected function gitUpdateIndex(string $location): void */ protected function removeIgnoredFiles(string $location, string $gitignorePath = null): void { - $gitignorePath = $gitignorePath ? $gitignorePath : $location.DIRECTORY_SEPARATOR.'.gitignore'; + $gitignorePath = $gitignorePath ?: $location.DIRECTORY_SEPARATOR.'.gitignore'; $gitignoreContent = file_get_contents($gitignorePath); if (!$gitignoreContent) { @@ -727,10 +742,13 @@ protected function removeIgnoredFiles(string $location, string $gitignorePath = $this->printDebug($gitignoreContent); $this->printDebug('-----.gitignore---------'); } - $command = sprintf('ls-files --directory -i -c --exclude-from=%s %s', $gitignorePath, $location); - $result = $this->gitCommandRun($location, $command, 'Unable to remove ignored files', true); - $files = preg_split('/\R/', $result->getMessage()); + $result = $this->gitCommandRun( + $location, + $command, + 'Unable to remove ignored files', + ); + $files = preg_split('/\R/', $result); if (!empty($files)) { $files = array_filter($files); foreach ($files as $file) { @@ -757,8 +775,12 @@ protected function removeIgnoredFiles(string $location, string $gitignorePath = protected function removeOtherFiles(string $location): void { $command = 'ls-files --others --exclude-standard'; - $result = $this->gitCommandRun($location, $command, 'Unable to remove other files', true); - $files = preg_split('/\R/', $result->getMessage()); + $result = $this->gitCommandRun( + $location, + $command, + 'Unable to remove other files', + ); + $files = preg_split('/\R/', $result); if (!empty($files)) { $files = array_filter($files); foreach ($files as $file) { @@ -843,6 +865,43 @@ 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. * @@ -868,11 +927,42 @@ protected function sayOkay(string $text): void */ protected function printDebug(mixed ...$args): void { - if (!$this->debug) { + 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; + } } diff --git a/src/Commands/ArtifactCommand.php b/src/Commands/ArtifactCommand.php new file mode 100644 index 0000000..77276fd --- /dev/null +++ b/src/Commands/ArtifactCommand.php @@ -0,0 +1,111 @@ +setName('artifact'); + $this->setDescription('Push artifact of current repository to remote git repository.'); + $this->addArgument('remote', InputArgument::REQUIRED, 'Path to the remote git repository.'); + $this + ->addOption('branch', null, InputOption::VALUE_REQUIRED, 'Destination branch with optional tokens.', '[branch]') + ->addOption('debug', null, InputOption::VALUE_NONE, 'Print debug information.') + ->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('push', null, InputOption::VALUE_NONE, 'Push artifact to the remote repository') + ->addOption('report', null, InputOption::VALUE_REQUIRED, 'Path to the report 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.' + ); + } + + /** + * Perform actual command. + * + * @param InputInterface $input + * Input. + * @param OutputInterface $output + * Output. + * + * @return int + * Status code. + * + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $gitWrapper = new GitWrapper(); + $optionDebug = $input->getOption('debug'); + if (($optionDebug || $output->isDebug())) { + $logger = new Logger('git'); + $logger->pushHandler(new StreamHandler('php://stdout', Level::Debug)); + $gitWrapper->addLoggerEventSubscriber(new GitLoggerEventSubscriber($logger)); + } + $fileSystem = new Filesystem(); + $artifact = new Artifact($gitWrapper, $fileSystem, $output); + $remote = $input->getArgument('remote'); + // @phpstan-ignore-next-line + $artifact->artifact($remote, $input->getOptions()); + + return Command::SUCCESS; + } +} diff --git a/src/FilesystemTrait.php b/src/FilesystemTrait.php index 42f493e..5ccc820 100644 --- a/src/FilesystemTrait.php +++ b/src/FilesystemTrait.php @@ -2,12 +2,10 @@ declare(strict_types=1); -namespace DrevOps\Robo; +namespace DrevOps\GitArtifact; -use Robo\Contract\VerbosityThresholdInterface; -use Robo\Exception\TaskException; -use Robo\LoadAllTasks; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Process\Process; /** * Trait FilesystemTrait. @@ -15,8 +13,6 @@ trait FilesystemTrait { - use LoadAllTasks; - /** * Current directory where call originated. * @@ -26,10 +22,8 @@ trait FilesystemTrait /** * File system for custom commands. - * - * @var \Symfony\Component\Filesystem\Filesystem */ - protected $fsFileSystem; + protected Filesystem $fsFileSystem; /** * Stack of original current working directories. @@ -42,14 +36,6 @@ trait FilesystemTrait */ protected $fsOriginalCwdStack = []; - /** - * FilesystemTrait constructor. - */ - public function __construct() - { - $this->fsFileSystem = new Filesystem(); - } - /** * Set root directory path. * @@ -133,18 +119,13 @@ protected function fsCwdGet(): string * @return bool * TRUE if command is available, FALSE otherwise. * - * @throws TaskException */ protected function fsIsCommandAvailable(string $command): bool { - /* @phpstan-ignore-next-line */ - $result = $this->taskExecStack() - ->setVerbosityThreshold(VerbosityThresholdInterface::VERBOSITY_DEBUG) - ->printOutput(false) - ->exec('which '.$command) - ->run(); + $process = new Process(['which', $command]); + $process->run(); - return $result->wasSuccessful(); + return $process->isSuccessful(); } /** diff --git a/src/GitTrait.php b/src/GitTrait.php index b594729..857057c 100644 --- a/src/GitTrait.php +++ b/src/GitTrait.php @@ -2,12 +2,10 @@ declare(strict_types = 1); -namespace DrevOps\Robo; +namespace DrevOps\GitArtifact; -use Robo\Collection\CollectionBuilder; -use Robo\Contract\VerbosityThresholdInterface; -use Robo\Result; -use Robo\Task\Base\Exec; +use GitWrapper\GitCommand; +use GitWrapper\GitWrapper; /** * Trait GitTrait. @@ -15,6 +13,13 @@ trait GitTrait { + use FilesystemTrait; + + /** + * Git Wrapper. + */ + protected GitWrapper $gitWrapper; + /** * Path to the source repository. * @@ -69,13 +74,13 @@ protected function gitSetDst(string $path): void * @param string $remote * Path or remote URI of the repository to add remote for. * - * @return \Robo\Result - * Result object. + * @return string + * stdout. * * @throws \Exception * If unable to add a remote. */ - protected function gitAddRemote(string $location, string $name, string $remote): Result + protected function gitAddRemote(string $location, string $name, string $remote): string { return $this->gitCommandRun( $location, @@ -104,10 +109,9 @@ protected function gitRemoteExists(string $location, string $name): bool $location, 'remote', 'Unable to list remotes', - true ); - $lines = preg_split('/\R/', $result->getMessage()); + $lines = preg_split('/\R/', (string) $result); if (empty($lines)) { return false; @@ -127,16 +131,18 @@ protected function gitRemoteExists(string $location, string $name): bool * Optional flag to also create a branch before switching. Defaults to * false. * - * @return \Robo\Result - * Result object. + * @return string + * stdout. * * @throws \Exception */ - protected function gitSwitchToBranch(string $location, string $branch, bool $createNew = false): Result + protected function gitSwitchToBranch(string $location, string $branch, bool $createNew = false): string { + $command = $createNew ? sprintf('checkout -b %s', $branch) : sprintf('checkout %s', $branch); + return $this->gitCommandRun( $location, - sprintf('checkout %s %s', $createNew ? '-b' : '', $branch) + $command, ); } @@ -148,16 +154,16 @@ protected function gitSwitchToBranch(string $location, string $branch, bool $cre * @param string $branch * Branch name. * - * @return \Robo\Result - * Result object. + * @return string + * stdout. * * @throws \Exception */ - protected function gitRemoveBranch($location, $branch): Result + protected function gitRemoveBranch($location, $branch): string { return $this->gitCommandRun( $location, - sprintf('branch -D %s', $branch) + sprintf('branch -D %s', $branch), ); } @@ -176,7 +182,7 @@ protected function gitRemoveRemote(string $location, string $remote): void if ($this->gitRemoteExists($location, $remote)) { $this->gitCommandRun( $location, - sprintf('remote rm %s', $remote) + sprintf('remote rm %s', $remote), ); } } @@ -195,17 +201,33 @@ protected function gitRemoveRemote(string $location, string $remote): void * @param bool $force * Force push. * - * @return \Robo\Result - * Result object. + * @return string + * Stdout. * * @throws \Exception */ - protected function gitPush(string $location, string $localBranch, string $remoteName, string $remoteBranch, bool $force = false): Result - { + protected function gitPush( + string $location, + string $localBranch, + string $remoteName, + string $remoteBranch, + bool $force = false + ): string { return $this->gitCommandRun( $location, - sprintf('push %s refs/heads/%s:refs/heads/%s%s', $remoteName, $localBranch, $remoteBranch, $force ? ' --force' : ''), - sprintf('Unable to push local branch "%s" to "%s" remote branch "%s"', $localBranch, $remoteName, $remoteBranch) + sprintf( + 'push %s refs/heads/%s:refs/heads/%s%s', + $remoteName, + $localBranch, + $remoteBranch, + $force ? ' --force' : '' + ), + sprintf( + 'Unable to push local branch "%s" to "%s" remote branch "%s"', + $localBranch, + $remoteName, + $remoteBranch + ) ); } @@ -217,22 +239,24 @@ protected function gitPush(string $location, string $localBranch, string $remote * @param string $message * Commit message. * - * @return \Robo\Result - * Result object. + * @return string + * Stdout. * * @throws \Exception */ - protected function gitCommit(string $location, string $message): Result + protected function gitCommit(string $location, string $message): string { $this->gitCommandRun( $location, - 'add -A' + 'add -A', ); + $command = new GitCommand('commit', [ + 'allow-empty' => true, + 'm' => $message, + ]); + $command->setDirectory($location); - return $this->gitCommandRun( - $location, - sprintf('commit --allow-empty -m "%s"', $message) - ); + return $this->gitWrapper->run($command); } /** @@ -243,6 +267,7 @@ protected function gitCommit(string $location, string $message): Result * * @return string * Current branch. + * * @throws \Exception * If unable to get the branch. */ @@ -252,10 +277,9 @@ protected function gitGetCurrentBranch(string $location): string $location, 'rev-parse --abbrev-ref HEAD', 'Unable to get current repository branch', - true ); - return trim($result->getMessage()); + return trim((string) $result); } /** @@ -276,9 +300,8 @@ protected function gitGetTags(string $location): array $location, 'tag -l --points-at HEAD', 'Unable to retrieve tags', - true ); - $tags = preg_split('/\R/', $result->getMessage()); + $tags = preg_split('/\R/', (string) $result); if (empty($tags)) { return []; } @@ -289,75 +312,32 @@ protected function gitGetTags(string $location): array /** * Run git command. * - * We cannot use Robo's git stack here as it does not support specifying - * current git working dir. - * Instead, we are using Robo's exec stack and our own wrapper. - * * @param string $location * Repository location path or URI. * @param string $command * Command to run. - * @param string|null $errorMessage + * @param string $errorMessage * Optional error message. - * @param bool $noDebug - * Flag to enforce no-debug mode. Used by commands that use output for - * values. * - * @return \Robo\Result - * Result object. + * @return string + * Stdout. * * @throws \Exception If command did not finish successfully. */ - protected function gitCommandRun(string $location, string $command, string $errorMessage = null, bool $noDebug = false): Result - { - $git = $this->gitCommand($location, $noDebug); - /* @phpstan-ignore-next-line */ - $git->rawArg($command); - $result = $git->run(); - - if (!$result->wasSuccessful()) { - $message = $errorMessage ? sprintf('%s: %s', $errorMessage, $result->getMessage()) : $result->getMessage(); - throw new \Exception($message); - } - - return $result; - } - - /** - * Get unified git command. - * - * @param string|null $location - * Optional repository location. - * @param bool $noDebug - * Flag to enforce no-debug mode. Used by commands that use output for - * values. - * - * @return \Robo\Task\Base\Exec|\Robo\Collection\CollectionBuilder - * Exec task. - */ - protected function gitCommand(string $location = null, bool $noDebug = false): object - { - /* @phpstan-ignore-next-line */ - $git = $this->taskExec('git') - ->printOutput(false) - ->arg('--no-pager'); - - if ($this->debug) { - $git->env('GIT_SSH_COMMAND', 'ssh -vvv'); - if (!$noDebug) { - $git->printOutput(true); + protected function gitCommandRun( + string $location, + string $command, + string $errorMessage = '', + ): string { + $command = '--no-pager '.$command; + try { + return $this->gitWrapper->git($command, $location); + } catch (\Exception $exception) { + if ($errorMessage !== '') { + throw new \Exception($errorMessage, $exception->getCode(), $exception); } - $git->setVerbosityThreshold(VerbosityThresholdInterface::VERBOSITY_NORMAL); - } else { - $git->setVerbosityThreshold(VerbosityThresholdInterface::VERBOSITY_DEBUG); - } - - if (!empty($location)) { - $git->arg('--git-dir='.$location.'/.git'); - $git->arg('--work-tree='.$location); + throw $exception; } - - return $git; } /** diff --git a/src/TokenTrait.php b/src/TokenTrait.php index 88424d7..07ced0e 100644 --- a/src/TokenTrait.php +++ b/src/TokenTrait.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace DrevOps\Robo; +namespace DrevOps\GitArtifact; /** * Trait TokenTrait. diff --git a/src/app.php b/src/app.php new file mode 100644 index 0000000..e1eb894 --- /dev/null +++ b/src/app.php @@ -0,0 +1,15 @@ +add($command); +$application->setDefaultCommand((string) $command->getName(), true); +$application->run(); diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index f14403e..a7a784f 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -2,11 +2,11 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests; +namespace DrevOps\GitArtifact\Tests; -use DrevOps\Robo\Tests\Traits\CommandTrait; -use DrevOps\Robo\Tests\Traits\MockTrait; -use DrevOps\Robo\Tests\Traits\ReflectionTrait; +use DrevOps\GitArtifact\Tests\Traits\CommandTrait; +use DrevOps\GitArtifact\Tests\Traits\MockTrait; +use DrevOps\GitArtifact\Tests\Traits\ReflectionTrait; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; @@ -21,7 +21,7 @@ abstract class AbstractTestCase extends TestCase use CommandTrait { CommandTrait::setUp as protected commandTraitSetUp; CommandTrait::tearDown as protected commandTraitTearDown; - CommandTrait::runRoboCommand as public commandRunRoboCommand; + CommandTrait::runGitArtifactCommand as public commandRunGitArtifactCommand; } use ReflectionTrait; @@ -50,7 +50,7 @@ protected function setUp(): void $this->fs = new Filesystem(); - $this->fixtureDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'robo_git_artifact'; + $this->fixtureDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'git_artifact'; $this->fs->mkdir($this->fixtureDir); $this->commandTraitSetUp( diff --git a/tests/Exception/ErrorException.php b/tests/Exception/ErrorException.php index 537d37c..be199d1 100644 --- a/tests/Exception/ErrorException.php +++ b/tests/Exception/ErrorException.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Exception; +namespace DrevOps\GitArtifact\Tests\Exception; use PHPUnit\Framework\Exception; diff --git a/tests/Functional/AbstractFunctionalTestCase.php b/tests/Functional/AbstractFunctionalTestCase.php index 4a0adcb..cccbd97 100644 --- a/tests/Functional/AbstractFunctionalTestCase.php +++ b/tests/Functional/AbstractFunctionalTestCase.php @@ -2,9 +2,9 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Functional; +namespace DrevOps\GitArtifact\Tests\Functional; -use DrevOps\Robo\Tests\AbstractTestCase; +use DrevOps\GitArtifact\Tests\AbstractTestCase; /** * Class AbstractTestCase @@ -104,7 +104,6 @@ protected function assertBuildSuccess(string $args = '', string $branch = 'testb protected function assertBuildFailure(string $args = '', string $branch = 'testbranch', string $commit = 'Deployment commit'): string { $output = $this->runBuild(sprintf('--push --branch=%s %s', $branch, $args), true); - $this->assertStringContainsString('[error]', $output); $this->assertStringNotContainsString(sprintf('Pushed branch "%s" with commit message "%s"', $branch, $commit), $output); $this->assertStringNotContainsString('Deployment finished successfully.', $output); $this->assertStringContainsString('Deployment failed.', $output); @@ -129,7 +128,7 @@ protected function runBuild(string $args = '', bool $expectFail = false): string $args .= ' --mode='.$this->mode; } - $output = $this->runRoboCommandTimestamped(sprintf('artifact --src=%s %s %s', $this->src, $this->dst, $args), $expectFail); + $output = $this->runGitArtifactCommandTimestamped(sprintf('--src=%s %s %s', $this->src, $this->dst, $args), $expectFail); if ($this->isDebug()) { print str_pad('', 80, '+').PHP_EOL; @@ -141,7 +140,7 @@ protected function runBuild(string $args = '', bool $expectFail = false): string } /** - * Run Robo command with current timestamp attached to artifact commands. + * Run command with current timestamp attached to artifact commands. * * @param string $command * Command string to run. @@ -151,14 +150,12 @@ protected function runBuild(string $args = '', bool $expectFail = false): string * @return array * Array of output lines. */ - protected function runRoboCommandTimestamped(string $command, bool $expectFail = false): array + protected function runGitArtifactCommandTimestamped(string $command, bool $expectFail = false): array { // Add --now option to all 'artifact' commands. - if (str_starts_with($command, 'artifact')) { - $command .= ' --now='.$this->now; - } + $command .= ' --now='.$this->now; - return $this->commandRunRoboCommand($command, $expectFail); + return $this->commandRunGitArtifactCommand($command, $expectFail); } /** diff --git a/tests/Functional/BranchTest.php b/tests/Functional/BranchTest.php index ee7f725..4ea41c0 100644 --- a/tests/Functional/BranchTest.php +++ b/tests/Functional/BranchTest.php @@ -2,16 +2,16 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Functional; +namespace DrevOps\GitArtifact\Tests\Functional; /** * Class BranchTest. * * @group integration * - * @covers \DrevOps\Robo\GitTrait - * @covers \DrevOps\Robo\ArtifactTrait - * @covers \DrevOps\Robo\FilesystemTrait + * @covers \DrevOps\GitArtifact\GitTrait + * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\FilesystemTrait */ class BranchTest extends AbstractFunctionalTestCase { diff --git a/tests/Functional/ForcePushTest.php b/tests/Functional/ForcePushTest.php index c86f8ac..46bfd2c 100644 --- a/tests/Functional/ForcePushTest.php +++ b/tests/Functional/ForcePushTest.php @@ -2,7 +2,7 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Functional; +namespace DrevOps\GitArtifact\Tests\Functional; /** * Class ForcePushTest. @@ -11,9 +11,9 @@ * * @SuppressWarnings(PHPMD.TooManyPublicMethods) * - * @covers \DrevOps\Robo\GitTrait - * @covers \DrevOps\Robo\ArtifactTrait - * @covers \DrevOps\Robo\FilesystemTrait + * @covers \DrevOps\GitArtifact\GitTrait + * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\FilesystemTrait */ class ForcePushTest extends AbstractFunctionalTestCase { diff --git a/tests/Functional/GeneralTest.php b/tests/Functional/GeneralTest.php index c287ec0..5db1b1a 100644 --- a/tests/Functional/GeneralTest.php +++ b/tests/Functional/GeneralTest.php @@ -2,34 +2,29 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Functional; +namespace DrevOps\GitArtifact\Tests\Functional; /** * Class GeneralTest. * * @group integration * - * @covers \DrevOps\Robo\ArtifactTrait - * @covers \DrevOps\Robo\FilesystemTrait + * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\FilesystemTrait */ class GeneralTest extends AbstractFunctionalTestCase { - public function testPresence(): void - { - $output = $this->runRoboCommand('list'); - $this->assertStringContainsString('artifact', implode(PHP_EOL, $output)); - } - public function testHelp(): void { - $output = $this->runRoboCommand('--help artifact'); + $output = $this->runGitArtifactCommand('--help'); $this->assertStringContainsString('artifact [options] [--] ', implode(PHP_EOL, $output)); + $this->assertStringContainsString('Push artifact of current repository to remote git repository.', implode(PHP_EOL, $output)); } public function testCompulsoryParameter(): void { - $output = $this->runRoboCommand('artifact', true); + $output = $this->runGitArtifactCommand('', true); $this->assertStringContainsString('Not enough arguments (missing: "remote")', implode(PHP_EOL, $output)); } @@ -99,7 +94,6 @@ public function testDebug(): void $output = $this->runBuild('--debug'); $this->assertStringContainsString('Debug messages enabled', $output); - $this->assertStringContainsString('[Exec]', $output); $this->assertStringContainsString('Cowardly refusing to push to remote. Use --push option to perform an actual push.', $output); $this->gitAssertFilesNotExist($this->dst, 'f1', $this->currentBranch); @@ -111,7 +105,6 @@ public function testDebugDisabled(): void $output = $this->runBuild(); $this->assertStringNotContainsString('Debug messages enabled', $output); - $this->assertStringNotContainsString('[Exec]', $output); $this->assertStringContainsString('Cowardly refusing to push to remote. Use --push option to perform an actual push.', $output); $this->gitAssertFilesNotExist($this->dst, 'f1', $this->currentBranch); diff --git a/tests/Functional/TagTest.php b/tests/Functional/TagTest.php index ba80aeb..3779ec6 100644 --- a/tests/Functional/TagTest.php +++ b/tests/Functional/TagTest.php @@ -2,16 +2,16 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Functional; +namespace DrevOps\GitArtifact\Tests\Functional; /** * Class TagTest. * * @group integration * - * @covers \DrevOps\Robo\GitTrait - * @covers \DrevOps\Robo\ArtifactTrait - * @covers \DrevOps\Robo\FilesystemTrait + * @covers \DrevOps\GitArtifact\GitTrait + * @covers \DrevOps\GitArtifact\Artifact + * @covers \DrevOps\GitArtifact\FilesystemTrait */ class TagTest extends AbstractFunctionalTestCase { diff --git a/tests/Functional/TokenTest.php b/tests/Functional/TokenTest.php index 7268426..5187e72 100644 --- a/tests/Functional/TokenTest.php +++ b/tests/Functional/TokenTest.php @@ -2,16 +2,16 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Functional; +namespace DrevOps\GitArtifact\Tests\Functional; -use DrevOps\Robo\TokenTrait; +use DrevOps\GitArtifact\TokenTrait; /** * Class ForcePushTest. * * @group integration * - * @covers \DrevOps\Robo\TokenTrait + * @covers \DrevOps\GitArtifact\TokenTrait */ class TokenTest extends AbstractFunctionalTestCase { diff --git a/tests/Traits/CommandTrait.php b/tests/Traits/CommandTrait.php index 550b12a..3c434f3 100644 --- a/tests/Traits/CommandTrait.php +++ b/tests/Traits/CommandTrait.php @@ -2,11 +2,10 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Traits; +namespace DrevOps\GitArtifact\Tests\Traits; -use DrevOps\Robo\Tests\Exception\ErrorException; +use DrevOps\GitArtifact\Tests\Exception\ErrorException; use PHPUnit\Framework\AssertionFailedError; -use SebastianBergmann\GlobalState\RuntimeException; use Symfony\Component\Filesystem\Filesystem; /** @@ -488,27 +487,27 @@ protected function runGitCommand(string $args, string $path = null): array return $this->runCliCommand($command.' '.trim($args)); } - /** - * Run Robo command. - * - * @param string $command - * Command string to run. - * @param bool $expectFail - * Flag to state that the command should fail. - * @param string $roboBin - * Optional relative path to Robo binary. - * - * @return array - * Array of output lines. - */ - public function runRoboCommand(string $command, bool $expectFail = false, string $roboBin = 'vendor/bin/robo'): array + /** + * 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($roboBin)) { - throw new RuntimeException(sprintf('Robo binary is not available at path "%s"', $roboBin)); + if (!file_exists($gitArtifactBin)) { + throw new \RuntimeException(sprintf('git-artifact binary is not available at path "%s"', $gitArtifactBin)); } try { - $output = $this->runCliCommand($roboBin.' '.$command); + $output = $this->runCliCommand($gitArtifactBin.' '.$argsAndOptions); if ($expectFail) { throw new AssertionFailedError('Command exited successfully but should not'); } @@ -531,7 +530,7 @@ public function runRoboCommand(string $command, bool $expectFail = false, string * @return array * Array of output lines. * - * @throws \DrevOps\Robo\Tests\Exception\ErrorException + * @throws \DrevOps\GitArtifact\Tests\Exception\ErrorException * If commands exists with non-zero status. */ protected function runCliCommand(string $command): array diff --git a/tests/Traits/MockTrait.php b/tests/Traits/MockTrait.php index 54ead3a..ff0af70 100644 --- a/tests/Traits/MockTrait.php +++ b/tests/Traits/MockTrait.php @@ -1,6 +1,6 @@ mock = $this->getMockForTrait(ArtifactTrait::class); + $mockBuilder = $this->getMockBuilder(Artifact::class); + $fileSystem = new Filesystem(); + $gitWrapper = new GitWrapper(); + $output = new ConsoleOutput(); + + $mockBuilder->setConstructorArgs([$gitWrapper, $fileSystem, $output]); + $this->mock = $mockBuilder->getMock(); $this->callProtectedMethod($this->mock, 'fsSetRootDir', [$this->fixtureDir]); } } diff --git a/tests/Unit/ExcludeTest.php b/tests/Unit/ExcludeTest.php index 854ed75..cac2aed 100644 --- a/tests/Unit/ExcludeTest.php +++ b/tests/Unit/ExcludeTest.php @@ -2,14 +2,14 @@ declare(strict_types = 1); -namespace DrevOps\Robo\Tests\Unit; +namespace DrevOps\GitArtifact\Tests\Unit; /** * Class ExcludeTest. * * @group unit * - * @covers \DrevOps\Robo\ArtifactTrait + * @covers \DrevOps\GitArtifact\Artifact */ class ExcludeTest extends AbstractUnitTestCase {