diff --git a/composer.json b/composer.json index d6b6fd6..a0d8c68 100644 --- a/composer.json +++ b/composer.json @@ -16,11 +16,12 @@ }, "require": { "php": ">=8.1", - "cpliakas/git-wrapper": "^3.1", "monolog/monolog": "^3.5", - "symfony/console": "^6.3 || ^7", - "symfony/filesystem": "^6.3 || ^7", - "symfony/finder": "^6.3 || ^7" + "symfony/console": "^6", + "symfony/filesystem": "^6", + "symfony/finder": "^6", + "czproject/git-php": "^4.2", + "symfony/process": "^6" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8", diff --git a/src/Artifact.php b/src/Artifact.php index 81f5369..796147a 100644 --- a/src/Artifact.php +++ b/src/Artifact.php @@ -4,7 +4,6 @@ namespace DrevOps\GitArtifact; -use GitWrapper\GitWrapper; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; @@ -14,117 +13,108 @@ * * @SuppressWarnings(PHPMD.TooManyMethods) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.TooManyFields) */ class Artifact { - use GitTrait; + 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. - * - * @var string */ - protected $mode; + protected string $mode; /** * Original branch in current repository. - * - * @var string|null */ - protected $originalBranch; + protected string $originalBranch = ''; /** * Destination branch with optional tokens. - * - * @var string */ - protected $dstBranch; + protected string $destinationBranch = ''; /** * Local branch where artifact will be built. - * - * @var string */ - protected $artifactBranch; + protected string $artifactBranch = ''; /** * Remote name. - * - * @var string */ - protected $remoteName; + 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. - * - * @var string */ - protected $gitignoreFile; + protected ?string $gitignoreFile = NULL; /** * Commit message with optional tokens. - * - * @var string */ - protected $message; + protected string $message = ''; /** * Flag to specify if push is required or should be using dry run. - * - * @var bool */ - protected $needsPush; + protected bool $needsPush = FALSE; /** * Flag to specify if cleanup is required to run after the build. - * - * @var bool */ - protected $needCleanup; + protected bool $needCleanup = TRUE; /** * Path to report file. - * - * @var string */ - protected $report; + protected string $reportFile = ''; /** * Flag to show changes made to the repo by the build in the output. - * - * @var bool */ - protected $showChanges; + protected bool $showChanges = FALSE; /** * Artifact build result. - * - * @var bool */ - protected $result = FALSE; + protected bool $result = FALSE; /** * Flag to print debug information. - * - * @var bool */ - protected $debug = FALSE; + protected bool $debug = FALSE; /** * Internal option to set current timestamp. - * - * @var int */ - protected $now; + protected int $now; /** * Artifact constructor. * - * @param \GitWrapper\GitWrapper $gitWrapper + * @param GitArtifactGit $git * Git wrapper. * @param \Symfony\Component\Filesystem\Filesystem $fsFileSystem * File system. @@ -132,12 +122,14 @@ class Artifact { * Output. */ public function __construct( - GitWrapper $gitWrapper, + /** + * Git runner. + */ + protected GitArtifactGit $git, Filesystem $fsFileSystem, protected OutputInterface $output, ) { $this->fsFileSystem = $fsFileSystem; - $this->gitWrapper = $gitWrapper; } /** @@ -185,14 +177,14 @@ public function artifact(string $remote, array $opts = [ ]): void { try { $error = NULL; - $this->checkRequirements(); - $this->resolveOptions($opts); + $this->resolveOptions($remote, $opts); + // Now we have all what we need. + // Let process artifact function. $this->printDebug('Debug messages enabled'); - $this->gitSetDst($remote); - + $this->setupRemoteForRepository(); $this->showInfo(); $this->prepareArtifact(); @@ -209,7 +201,7 @@ public function artifact(string $remote, array $opts = [ $error = $exception->getMessage(); } - if ($this->report) { + if (!empty($this->reportFile)) { $this->dumpReport(); } @@ -226,6 +218,16 @@ public function artifact(string $remote, array $opts = [ } } + /** + * Get source path git repository. + * + * @return string + * Source path. + */ + public function getSourcePathGitRepository(): string { + return $this->sourcePathGitRepository; + } + /** * Branch mode. * @@ -262,26 +264,64 @@ public static function modeDiff(): string { * @throws \Exception */ protected function prepareArtifact(): void { - $this->gitSwitchToBranch($this->src, $this->artifactBranch, TRUE); + // 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))); + } + } - $this->removeSubRepos($this->src); - $this->disableLocalExclude($this->src); + /** + * 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->replaceGitignore($this->gitignoreFile, $this->src); - $this->gitAddAll($this->src); - $this->removeIgnoredFiles($this->src); + $this->replaceGitignoreInGitRepository($this->gitignoreFile); + $this->gitRepository->addAllChanges(); + $this->removeIgnoredFiles($this->getSourcePathGitRepository()); } else { - $this->gitAddAll($this->src); - } - - $this->removeOtherFiles($this->src); - - $result = $this->gitCommit($this->src, $this->message); - - if ($this->showChanges) { - $this->say(sprintf('Added changes: %s', $result)); + $this->gitRepository->addAllChanges(); } } @@ -291,10 +331,20 @@ protected function prepareArtifact(): void { * @throws \Exception */ protected function cleanup(): void { - $this->restoreLocalExclude($this->src); - $this->gitSwitchToBranch($this->src, (string) $this->originalBranch); - $this->gitRemoveBranch($this->src, $this->artifactBranch); - $this->gitRemoveRemote($this->src, $this->remoteName); + $this + ->restoreLocalExclude($this->getSourcePathGitRepository()); + + $this + ->gitRepository + ->switchToBranch($this->originalBranch); + + $this + ->gitRepository + ->removeBranch($this->artifactBranch, TRUE); + + $this + ->gitRepository + ->removeRemote($this->remoteName); } /** @@ -303,26 +353,25 @@ protected function cleanup(): void { * @throws \Exception */ protected function doPush(): void { - if (!$this->gitRemoteExists($this->src, $this->remoteName)) { - $this->gitAddRemote($this->src, $this->remoteName, $this->dst); - } - try { - $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)); + $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->dstBranch, + $this->destinationBranch, $this->message ), $exception->getCode(), @@ -334,45 +383,64 @@ protected function doPush(): void { /** * Resolve and validate CLI options values into internal values. * + * @param string $remote + * Remote URL. * @param array $options * Array of CLI options. * - * @throws \Exception + * @throws \CzProject\GitPhp\GitException * * @phpstan-ignore-next-line */ - protected function resolveOptions(array $options): void { - $this->now = empty($options['now']) ? time() : (int) $options['now']; + 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); - $this->remoteName = 'dst'; - - $this->fsSetRootDir($options['root']); - - // Default source to the root directory. + // Handle some complex options. $srcPath = empty($options['src']) ? $this->fsGetRootDir() : $this->fsGetAbsolutePath($options['src']); - $this->gitSetSrcRepo($srcPath); - - $this->originalBranch = $this->resolveOriginalBranch($this->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->dstBranch . '-artifact'; - + $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']); } - $this->showChanges = !empty($options['show-changes']); - - $this->needCleanup = empty($options['no-cleanup']); - - $this->needsPush = !empty($options['push']); + } - $this->report = empty($options['report']) ? NULL : $options['report']; + /** + * 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); - $this->setMode($options['mode'], $options); + return $this->gitRepository; } /** @@ -384,10 +452,10 @@ protected function showInfo(): void { $lines[] = ('----------------------------------------------------------------------'); $lines[] = (' Build timestamp: ' . date('Y/m/d H:i:s', $this->now)); $lines[] = (' Mode: ' . $this->mode); - $lines[] = (' Source repository: ' . $this->src); - $lines[] = (' Remote repository: ' . $this->dst); - $lines[] = (' Remote branch: ' . $this->dstBranch); - $lines[] = (' Gitignore file: ' . ($this->gitignoreFile ? $this->gitignoreFile : 'No')); + $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); @@ -402,15 +470,15 @@ protected function dumpReport(): void { $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[] = ' 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->report, implode(PHP_EOL, $lines)); + $this->fsFileSystem->dumpFile($this->reportFile, implode(PHP_EOL, $lines)); } /** @@ -458,28 +526,23 @@ protected function setMode(string $mode, array $options): void { * * Usually, repository become detached when a tag is checked out. * - * @param string $location - * Path to repository. - * - * @return null|string + * @return string * Branch or detachment source. * * @throws \Exception * If neither branch nor detachment source is not found. */ - protected function resolveOriginalBranch(string $location): ?string { - $branch = $this->gitGetCurrentBranch($location); - + 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 exist. - if ($branch === 'HEAD') { + // capture the source of detachment, if it exists. + if (str_contains($branch, 'HEAD detached')) { $branch = NULL; - $result = $this->gitCommandRun($location, 'branch'); - $branchList = preg_split('/\R/', $result); + $branchList = $this->gitRepository->getBranches(); if ($branchList) { $branchList = array_filter($branchList); foreach ($branchList as $branch) { - if (preg_match('/\* \(.*detached .* ([^\)]+)\)/', $branch, $matches)) { + if (preg_match('/\(.*detached .* ([^\)]+)\)/', $branch, $matches)) { $branch = $matches[1]; break; } @@ -502,10 +565,10 @@ protected function resolveOriginalBranch(string $location): ?string { protected function setDstBranch(string $branch): void { $branch = (string) $this->tokenProcess($branch); - if (!self::gitIsValidBranch($branch)) { + if (!GitArtifactGitRepository::isValidBranchName($branch)) { throw new \RuntimeException(sprintf('Incorrect value "%s" specified for git remote branch', $branch)); } - $this->dstBranch = $branch; + $this->destinationBranch = $branch; } /** @@ -550,10 +613,9 @@ protected function checkRequirements(): void { * * @param string $filename * Path to new gitignore to replace current file with. - * @param string $path - * Path to repository. */ - protected function replaceGitignore(string $filename, string $path): void { + 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); @@ -656,47 +718,6 @@ protected function restoreLocalExclude(string $path): void { } } - /** - * Update index for all files. - * - * @param string $location - * Path to repository. - * - * @throws \Exception - */ - protected function gitAddAll(string $location): void { - $result = $this->gitCommandRun( - $location, - 'add -A', - ); - - $this->printDebug(sprintf("Added all files:\n%s", $result)); - } - - /** - * Update index for all files. - * - * @param string $location - * Path to repository. - * - * @throws \Exception - */ - protected function gitUpdateIndex(string $location): void { - $finder = new Finder(); - $files = $finder - ->in($location) - ->ignoreDotFiles(FALSE) - ->files(); - - foreach ($files as $file) { - $this->gitCommandRun( - $location, - sprintf('update-index --info-only --add "%s"', $file), - ); - $this->printDebug(sprintf('Updated index for file "%s"', $file)); - } - } - /** * Remove ignored files. * @@ -709,6 +730,7 @@ protected function gitUpdateIndex(string $location): void { * 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); @@ -720,13 +742,11 @@ 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', - ); - $files = preg_split('/\R/', $result); + + $files = $this + ->gitRepository + ->listIgnoredFilesFromGitIgnoreFile($gitignorePath); + if (!empty($files)) { $files = array_filter($files); foreach ($files as $file) { @@ -744,24 +764,15 @@ protected function removeIgnoredFiles(string $location, string $gitignorePath = * * 'Other' files are files that are neither staged nor tracked in git. * - * @param string $location - * Path to repository. - * * @throws \Exception * If removal command finished with an error. */ - protected function removeOtherFiles(string $location): void { - $command = 'ls-files --others --exclude-standard'; - $result = $this->gitCommandRun( - $location, - $command, - 'Unable to remove other files', - ); - $files = preg_split('/\R/', $result); + protected function removeOtherFilesInGitRepository(): void { + $files = $this->gitRepository->listOtherFiles(); if (!empty($files)) { $files = array_filter($files); foreach ($files as $file) { - $fileName = $location . DIRECTORY_SEPARATOR . $file; + $fileName = $this->getSourcePathGitRepository() . DIRECTORY_SEPARATOR . $file; $this->printDebug('Removing other file %s', $fileName); $this->fsFileSystem->remove($fileName); } @@ -770,11 +781,8 @@ protected function removeOtherFiles(string $location): void { /** * Remove any repositories within current repository. - * - * @param string $path - * Path to current repository. */ - protected function removeSubRepos(string $path): void { + protected function removeSubReposInGitRepository(): void { $finder = new Finder(); $dirs = $finder ->directories() @@ -782,7 +790,7 @@ protected function removeSubRepos(string $path): void { ->ignoreDotFiles(FALSE) ->ignoreVCS(FALSE) ->depth('>0') - ->in($path); + ->in($this->getSourcePathGitRepository()); $dirs = iterator_to_array($dirs->directories()); @@ -804,7 +812,9 @@ protected function removeSubRepos(string $path): void { * @throws \Exception */ protected function getTokenBranch(): string { - return $this->gitGetCurrentBranch($this->src); + return $this + ->gitRepository + ->getCurrentBranchName(); } /** @@ -819,8 +829,10 @@ protected function getTokenBranch(): string { * @throws \Exception */ protected function getTokenTags(string $delimiter = NULL): string { - $delimiter = $delimiter ? $delimiter : '-'; - $tags = $this->gitGetTags($this->src); + $delimiter = $delimiter ?: '-'; + $tags = $this + ->gitRepository + ->getTags(); return implode($delimiter, $tags); } @@ -932,4 +944,20 @@ protected function decorationCharacter(string $nonDecorated, string $decorated): 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 fee46dc..c822771 100644 --- a/src/Commands/ArtifactCommand.php +++ b/src/Commands/ArtifactCommand.php @@ -5,11 +5,7 @@ namespace DrevOps\GitArtifact\Commands; use DrevOps\GitArtifact\Artifact; -use GitWrapper\EventSubscriber\GitLoggerEventSubscriber; -use GitWrapper\GitWrapper; -use Monolog\Handler\StreamHandler; -use Monolog\Level; -use Monolog\Logger; +use DrevOps\GitArtifact\GitArtifactGit; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -93,20 +89,10 @@ protected function configure(): void { * @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); + $git = new GitArtifactGit(); + $artifact = new Artifact($git, $fileSystem, $output); $remote = $input->getArgument('remote'); - // @phpstan-ignore-next-line $artifact->artifact($remote, $input->getOptions()); diff --git a/src/FilesystemTrait.php b/src/FilesystemTrait.php index 338b4b2..7df9a7e 100644 --- a/src/FilesystemTrait.php +++ b/src/FilesystemTrait.php @@ -14,10 +14,8 @@ trait FilesystemTrait { /** * Current directory where call originated. - * - * @var string */ - protected $fsRootDir; + protected string $fsRootDir; /** * File system for custom commands. @@ -33,7 +31,7 @@ trait FilesystemTrait { * * @var array */ - protected $fsOriginalCwdStack = []; + protected array $fsOriginalCwdStack = []; /** * Set root directory path. @@ -57,7 +55,7 @@ protected function fsSetRootDir(string $path = NULL): void { * script was started from or current working directory. */ protected function fsGetRootDir(): string { - if ($this->fsRootDir) { + if (isset($this->fsRootDir)) { return $this->fsRootDir; } diff --git a/src/GitArtifactGit.php b/src/GitArtifactGit.php new file mode 100644 index 0000000..63f9f9c --- /dev/null +++ b/src/GitArtifactGit.php @@ -0,0 +1,23 @@ +runner); + } + +} diff --git a/src/GitArtifactGitRepository.php b/src/GitArtifactGitRepository.php new file mode 100644 index 0000000..c104ae7 --- /dev/null +++ b/src/GitArtifactGitRepository.php @@ -0,0 +1,402 @@ +extractFromCommand(['ls-files', '-i', '-c', '--exclude-from=' . $gitIgnoreFilePath]); + if (!$files) { + return []; + } + + return $files; + } + + /** + * List 'Other' files. + * + * 'Other' files are files that are neither staged nor tracked in git. + * + * @return string[] + * Files. + * + * @throws \CzProject\GitPhp\GitException + */ + public function listOtherFiles(): array { + $files = $this->extractFromCommand(['ls-files', '--other', '--exclude-standard']); + if (!$files) { + return []; + } + + return $files; + } + + /** + * Get commits. + * + * @param string $format + * Commit format. + * + * @return string[] + * Commits. + * + * @throws \CzProject\GitPhp\GitException + */ + public function getCommits(string $format = '%s'): array { + $commits = $this->extractFromCommand(['log', '--format=' . $format]); + if (!$commits) { + return []; + } + + return $commits; + } + + /** + * Reset hard. + * + * @return $this + * Git repo. + * + * @throws \CzProject\GitPhp\GitException + */ + public function resetHard(): GitArtifactGitRepository { + $this->run('reset', ['--hard']); + + return $this; + } + + /** + * Clean repo. + * + * @return $this + * Git repo. + * + * @throws \CzProject\GitPhp\GitException + */ + public function cleanForce(): GitArtifactGitRepository { + $this->run('clean', ['-dfx']); + + return $this; + } + + /** + * Switch to new branch. + * + * @param string $branchName + * Branch name. + * @param bool $createNew + * Optional flag to also create a branch before switching. Default false. + * + * @return GitArtifactGitRepository + * The git repository. + * + * @throws \CzProject\GitPhp\GitException + */ + public function switchToBranch(string $branchName, bool $createNew = FALSE): GitArtifactGitRepository { + if (!$createNew) { + return $this->checkout($branchName); + } + + return $this->createBranch($branchName, TRUE); + } + + /** + * Remove branch. + * + * @param string $name + * Branch name. + * @param bool $force + * Force remove or not. + * + * @return GitArtifactGitRepository + * Git repository + * + * @throws \CzProject\GitPhp\GitException + */ + public function removeBranch($name, bool $force = FALSE): GitArtifactGitRepository { + if (empty($name)) { + return $this; + } + + if (!$force) { + return parent::removeBranch($name); + } + + $this->run('branch', ['-D' => $name]); + + return $this; + } + + /** + * Commit all files to git repo. + * + * @param string $message + * The commit message. + * + * @return array + * The changes. + * + * @throws \CzProject\GitPhp\GitException + */ + public function commitAllChanges(string $message): array { + $this->addAllChanges(); + + // We do not use commit method because we need return the output. + return $this->execute('commit', '--allow-empty', [ + '-m' => $message, + ]); + } + + /** + * List committed files. + * + * @return string[] + * Files. + * + * @throws \CzProject\GitPhp\GitException + */ + public function listCommittedFiles(): array { + $files = $this->extractFromCommand(['ls-tree', '--name-only', '-r', 'HEAD']); + if (!$files) { + return []; + } + + return $files; + } + + /** + * Set config receive.denyCurrentBranch is ignored. + * + * @return $this + * Git repo. + * + * @throws \CzProject\GitPhp\GitException + */ + public function setConfigReceiveDenyCurrentBranchIgnore(): GitArtifactGitRepository { + $this->extractFromCommand(['config', ['receive.denyCurrentBranch', 'ignore']]); + + return $this; + } + + /** + * Create an annotated tag. + * + * @param string $name + * Name. + * @param string $message + * Message. + * + * @return $this + * Git repo. + * + * @throws \CzProject\GitPhp\GitException + */ + public function createAnnotatedTag(string $name, string $message): GitArtifactGitRepository { + $this->createTag($name, [ + '--message=' . $message, + '-a', + ]); + + return $this; + } + + /** + * Create an annotated tag. + * + * @param string $name + * Name. + * + * @return $this + * Git repo. + * + * @throws \CzProject\GitPhp\GitException + */ + public function createLightweightTag(string $name): GitArtifactGitRepository { + $this->createTag($name); + + return $this; + } + + /** + * Remove remote by name. + * + * We need override this method because parent method does not work. + * + * @param string $name + * Remote name. + * + * @return GitArtifactGitRepository + * Git repo. + * + * @throws \CzProject\GitPhp\GitException + */ + public function removeRemote($name): GitArtifactGitRepository { + if ($this->isRemoteExists($name)) { + $this->run('remote', 'remove', $name); + } + + return $this; + } + + /** + * Get remote list. + * + * @return array + * Remotes. + * + * @throws \CzProject\GitPhp\GitException + */ + public function getRemotes(): array { + $remotes = $this->extractFromCommand(['remote']); + if (!$remotes) { + return []; + } + + return $remotes; + } + + /** + * Check remote is existing or not by remote name. + * + * @param string $remoteName + * Remote name to check. + * + * @return bool + * Exist or not. + * + * @throws \CzProject\GitPhp\GitException + */ + public function isRemoteExists(string $remoteName): bool { + $remotes = $this->getRemotes(); + if (empty($remotes)) { + return FALSE; + } + + return in_array($remoteName, $remotes); + } + + /** + * Override run method to add --no-pager option to all command. + * + * @param mixed ...$args + * Command args. + * + * @return \CzProject\GitPhp\RunnerResult + * Runner result. + * + * @throws \CzProject\GitPhp\GitException + */ + protected function run(...$args): RunnerResult { + $command = array_shift($args); + array_unshift($args, '--no-pager', $command); + + return parent::run(...$args); + } + + /** + * Check if provided location is a URI. + * + * @param string $location + * Location to check. + * + * @return bool + * TRUE if location is URI, FALSE otherwise. + */ + public static function isUri(string $location): bool { + return (bool) preg_match('/^(?:git|ssh|https?|[\d\w\.\-_]+@[\w\.\-]+):(?:\/\/)?[\w\.@:\/~_-]+\.git(?:\/?|\#[\d\w\.\-_]+?)$/', $location); + } + + /** + * Check if provided branch name can be used in git. + * + * @param string $branchName + * Branch to check. + * + * @return bool + * TRUE if it is a valid Git branch, FALSE otherwise. + */ + public static function isValidBranchName(string $branchName): bool { + return preg_match('/^(?!\/|.*(?:[\/\.]\.|\/\/|\\|@\{))[^\040\177\s\~\^\:\?\*\[]+(?exists($pathOrUri); + $isUri = self::isUri($pathOrUri); + + return match ($type) { + 'any' => $isLocal || $isUri, + 'local' => $isLocal, + 'uri' => $isUri, + default => throw new \InvalidArgumentException(sprintf('Invalid argument "%s" provided', $type)), + }; + } + +} diff --git a/tests/phpunit/AbstractTestCase.php b/tests/phpunit/AbstractTestCase.php index e5cc538..e48e6d6 100644 --- a/tests/phpunit/AbstractTestCase.php +++ b/tests/phpunit/AbstractTestCase.php @@ -4,6 +4,7 @@ namespace DrevOps\GitArtifact\Tests; +use DrevOps\GitArtifact\GitArtifactGit; use DrevOps\GitArtifact\Tests\Traits\CommandTrait; use DrevOps\GitArtifact\Tests\Traits\MockTrait; use DrevOps\GitArtifact\Tests\Traits\ReflectionTrait; @@ -47,6 +48,7 @@ protected function setUp(): void { parent::setUp(); $this->fs = new Filesystem(); + $this->git = new GitArtifactGit(); $this->fixtureDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'git_artifact'; $this->fs->mkdir($this->fixtureDir); diff --git a/tests/phpunit/Functional/AbstractFunctionalTestCase.php b/tests/phpunit/Functional/AbstractFunctionalTestCase.php index 5215d3e..d09c6d0 100644 --- a/tests/phpunit/Functional/AbstractFunctionalTestCase.php +++ b/tests/phpunit/Functional/AbstractFunctionalTestCase.php @@ -161,9 +161,9 @@ protected function runGitArtifactCommandTimestamped(string $command, bool $expec * Branch name to assert. */ protected function assertGitCurrentBranch(string $path, string $branch): void { - $currentBranch = $this->runGitCommand('rev-parse --abbrev-ref HEAD', $path); + $currentBranch = $this->git->open($path)->getCurrentBranchName(); - $this->assertStringContainsString($branch, implode('', $currentBranch), sprintf('Current branch is "%s"', $branch)); + $this->assertStringContainsString($branch, $currentBranch, sprintf('Current branch is "%s"', $branch)); } /** @@ -175,9 +175,13 @@ protected function assertGitCurrentBranch(string $path, string $branch): void { * Remote name to assert. */ protected function assertGitNoRemote(string $path, string $remote): void { - $remotes = $this->runGitCommand('remote', $path); - - $this->assertStringNotContainsString($remote, implode('', $remotes), sprintf('Remote "%s" is not present"', $remote)); + $remotes = $this->git->open($path)->getRemotes(); + if (empty($remotes)) { + $this->assertEmpty($remotes); + } + else { + $this->assertStringNotContainsString($remote, implode("\n", $remotes), sprintf('Remote "%s" is not present"', $remote)); + } } } diff --git a/tests/phpunit/Functional/GeneralTest.php b/tests/phpunit/Functional/GeneralTest.php index c6da843..f4de3ba 100644 --- a/tests/phpunit/Functional/GeneralTest.php +++ b/tests/phpunit/Functional/GeneralTest.php @@ -29,7 +29,6 @@ public function testCompulsoryParameter(): void { public function testInfo(): void { $this->gitCreateFixtureCommits(1); $output = $this->runBuild(); - $this->assertStringContainsString('Artifact information', $output); $this->assertStringContainsString('Mode: force-push', $output); $this->assertStringContainsString('Source repository: ' . $this->src, $output); @@ -59,7 +58,6 @@ public function testNoCleanup(): void { $output = $this->runBuild('--no-cleanup'); $this->assertGitCurrentBranch($this->src, $this->artifactBranch); - $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/phpunit/Functional/TagTest.php b/tests/phpunit/Functional/TagTest.php index 6a1c151..4179e7d 100644 --- a/tests/phpunit/Functional/TagTest.php +++ b/tests/phpunit/Functional/TagTest.php @@ -27,14 +27,15 @@ public function testDetachedTag(): void { $this->gitCreateFixtureCommits(2); $this->gitAddTag($this->src, 'tag1'); $this->gitCheckout($this->src, 'tag1'); - $srcBranches = $this->runGitCommand('branch'); + $gitRepo = $this->git->open($this->src); + $srcBranches = $gitRepo->getBranches(); $output = $this->assertBuildSuccess(); $this->assertStringContainsString('Mode: force-push', $output); $this->assertStringContainsString('Will push: Yes', $output); $this->assertFixtureCommits(2, $this->dst, 'testbranch', ['Deployment commit']); - $this->assertEquals($srcBranches, $this->runGitCommand('branch'), 'Cleanup has correctly returned to the previous branch.'); + $this->assertEquals($srcBranches, $gitRepo->getBranches(), 'Cleanup has correctly returned to the previous branch.'); } } diff --git a/tests/phpunit/Traits/CommandTrait.php b/tests/phpunit/Traits/CommandTrait.php index dbd4301..2efbba8 100644 --- a/tests/phpunit/Traits/CommandTrait.php +++ b/tests/phpunit/Traits/CommandTrait.php @@ -4,6 +4,9 @@ namespace DrevOps\GitArtifact\Tests\Traits; +use CzProject\GitPhp\GitException; +use DrevOps\GitArtifact\GitArtifactGit; +use DrevOps\GitArtifact\GitArtifactGitRepository; use DrevOps\GitArtifact\Tests\Exception\ErrorException; use PHPUnit\Framework\AssertionFailedError; use Symfony\Component\Filesystem\Filesystem; @@ -41,6 +44,11 @@ trait CommandTrait { */ protected $printDebug; + /** + * Artifact git. + */ + protected GitArtifactGit $git; + /** * Setup test. * @@ -60,10 +68,10 @@ protected function setUp(string $src, string $remote, bool $printDebug = FALSE): $this->src = $src; $this->gitInitRepo($this->src); $this->dst = $remote; - $this->gitInitRepo($this->dst); + $remoteRepo = $this->gitInitRepo($this->dst); // Allow pushing into already checked out branch. We need this to // avoid additional management of fixture repository. - $this->runGitCommand('config receive.denyCurrentBranch ignore', $this->dst); + $remoteRepo->setConfigReceiveDenyCurrentBranchIgnore(); } /** @@ -86,21 +94,22 @@ protected function tearDown(): void { * @param string $path * Path to the repository directory. */ - protected function gitInitRepo(string $path): void { + protected function gitInitRepo(string $path): GitArtifactGitRepository { if ($this->fs->exists($path)) { $this->fs->remove($path); } $this->fs->mkdir($path); + /** @var \DrevOps\GitArtifact\GitArtifactGitRepository $repo */ + $repo = $this->git->init($path, ['-b' => 'master']); - $this->runGitCommand('init -b master', $path); + return $repo; } /** * Get all commit hashes in the repository. * - * @param string|null $path - * Optional path to the repository directory. If not provided, fixture - * directory is used. + * @param string $path + * Path to the repository directory. * @param string $format * Format of commits. * @@ -109,10 +118,10 @@ protected function gitInitRepo(string $path): void { * * @throws \Exception */ - protected function gitGetAllCommits(string $path = NULL, string $format = '%s'): array { + protected function gitGetAllCommits(string $path, string $format = '%s'): array { $commits = []; try { - $commits = $this->runGitCommand('log --format="' . $format . '"', $path); + $commits = $this->git->open($path)->getCommits($format); } catch (\Exception $exception) { $output = ($exception->getPrevious() instanceof \Throwable) ? $exception->getPrevious()->getMessage() : ''; @@ -135,16 +144,15 @@ protected function gitGetAllCommits(string $path = NULL, string $format = '%s'): * * @param array $range * Array of commit indexes, stating from 1. - * @param string|null $path - * Optional path to the repository directory. If not provided, fixture - * directory is used. + * @param string $path + * Path to the repository directory. * * @return array * Array of commit hashes, ordered by keys in the $range. * * @throws \Exception */ - protected function gitGetCommitsHashesFromRange(array $range, string $path = NULL): array { + protected function gitGetCommitsHashesFromRange(array $range, string $path): array { $commits = $this->gitGetAllCommits($path); array_walk($range, static function (&$v) : void { @@ -162,15 +170,17 @@ protected function gitGetCommitsHashesFromRange(array $range, string $path = NUL /** * Get all committed files. * - * @param string|null $path - * Optional path to the repository directory. If not provided, fixture - * directory is used. + * @param string $path + * Path to the repository directory. * * @return array * Array of commit committed files. */ - protected function gitGetCommittedFiles(string $path = NULL): array { - return $this->runGitCommand('ls-tree --full-tree --name-only -r HEAD', $path); + protected function gitGetCommittedFiles(string $path): array { + return $this + ->git + ->open($path) + ->listCommittedFiles(); } /** @@ -205,13 +215,15 @@ protected function gitCreateFixtureCommits(int $count, int $offset = 0, string $ */ protected function gitCreateFixtureCommit(int $index, string $path = NULL): string { $path = $path ? $path : $this->src; - $this->gitCreateFixtureFile($path, 'f' . $index); - $this->runGitCommand(sprintf('add f%s', $index), $path); - $this->runGitCommand(sprintf('commit -am "Commit number %s"', $index), $path); - - $output = $this->runGitCommand('rev-parse HEAD', $path); - - return trim(implode(' ', $output)); + $filename = 'f' . $index; + $this->gitCreateFixtureFile($path, $filename); + $repo = $this->git->open($path); + $repo->addFile($filename); + $message = 'Commit number ' . $index; + $repo->commitAllChanges($message); + $lastCommit = $repo->getLastCommit(); + + return $lastCommit->getId()->toString(); } /** @@ -223,8 +235,8 @@ protected function gitCreateFixtureCommit(int $index, string $path = NULL): stri * Commit message. */ protected function gitCommitAll(string $path, string $message): void { - $this->runGitCommand('add .', $path); - $this->runGitCommand(sprintf('commit -am "%s"', $message), $path); + $repo = $this->git->open($path); + $repo->commitAllChanges($message); } /** @@ -237,17 +249,21 @@ protected function gitCommitAll(string $path, string $message): void { */ protected function gitCheckout(string $path, string $branch): void { try { - $this->runGitCommand(sprintf('checkout %s', $branch), $path); + $repo = $this->git->open($path); + $repo->checkout($branch); } - catch (ErrorException $errorException) { + catch (GitException $gitException) { $allowedFails = [ sprintf("error: pathspec '%s' did not match any file(s) known to git", $branch), ]; - $output = explode(PHP_EOL, ($errorException->getPrevious() instanceof \Throwable) ? $errorException->getPrevious()->getMessage() : ''); + if ($gitException->getRunnerResult()) { + $output = $gitException->getRunnerResult()->getErrorOutput(); + } + // Re-throw exception if it is not one of the allowed ones. - if (empty(array_intersect($output, $allowedFails))) { - throw $errorException; + if (!isset($output) || empty(array_intersect($output, $allowedFails))) { + throw $gitException; } } } @@ -259,8 +275,9 @@ protected function gitCheckout(string $path, string $branch): void { * Path to the repo. */ protected function gitReset($path): void { - $this->runGitCommand('reset --hard', $path); - $this->runGitCommand('clean -dfx', $path); + $repo = $this->git->open($path); + $repo->resetHard(); + $repo->cleanForce(); } /** @@ -320,11 +337,13 @@ protected function gitRemoveFixtureFile(string $path, string $name): void { * Optional flag to add random annotation to the tag. Defaults to FALSE. */ protected function gitAddTag(string $path, string $name, bool $annotate = FALSE): void { + $repo = $this->git->open($path); if ($annotate) { - $this->runGitCommand(sprintf('tag -a %s -m "%s"', $name, 'Annotation for tag ' . $name), $path); + $message = 'Annotation for tag ' . $name; + $repo->createAnnotatedTag($name, $message); } else { - $this->runGitCommand(sprintf('tag %s', $name), $path); + $repo->createLightweightTag($name); } } @@ -445,30 +464,6 @@ protected function assertFixtureCommits(int $count, string $path, string $branch } } - /** - * Run Git command. - * - * @param string $args - * CLI arguments. - * @param string|null $path - * Optional path to the repository. If not provided, fixture repository is - * used. - * - * @return array - * Array of output lines. - */ - protected function runGitCommand(string $args, string $path = NULL): array { - $path = $path ? $path : $this->src; - - $command = 'git --no-pager'; - if (!empty($path)) { - $command .= ' --git-dir=' . $path . '/.git'; - $command .= ' --work-tree=' . $path; - } - - return $this->runCliCommand($command . ' ' . trim($args)); - } - /** * Run command. * diff --git a/tests/phpunit/Unit/AbstractUnitTestCase.php b/tests/phpunit/Unit/AbstractUnitTestCase.php index 04ec16a..a29d95d 100644 --- a/tests/phpunit/Unit/AbstractUnitTestCase.php +++ b/tests/phpunit/Unit/AbstractUnitTestCase.php @@ -3,8 +3,8 @@ namespace DrevOps\GitArtifact\Tests\Unit; use DrevOps\GitArtifact\Artifact; +use DrevOps\GitArtifact\GitArtifactGit; use DrevOps\GitArtifact\Tests\AbstractTestCase; -use GitWrapper\GitWrapper; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Filesystem\Filesystem; @@ -25,7 +25,7 @@ protected function setUp(): void { $mockBuilder = $this->getMockBuilder(Artifact::class); $fileSystem = new Filesystem(); - $gitWrapper = new GitWrapper(); + $gitWrapper = new GitArtifactGit(); $output = new ConsoleOutput(); $mockBuilder->setConstructorArgs([$gitWrapper, $fileSystem, $output]); diff --git a/tests/phpunit/Unit/GitArtifactGitRepositoryTest.php b/tests/phpunit/Unit/GitArtifactGitRepositoryTest.php new file mode 100644 index 0000000..67907cb --- /dev/null +++ b/tests/phpunit/Unit/GitArtifactGitRepositoryTest.php @@ -0,0 +1,296 @@ +git->open($this->src); + $sourceRepo->commit('Source commit 1', ['--allow-empty']); + + $destinationRepo = $this->git->open($this->dst); + $destinationRepo->commit('Destination commit 1', ['--allow-empty']); + $lastCommit = $destinationRepo->getLastCommit(); + $this->assertEquals('Destination commit 1', $lastCommit->getSubject()); + + $sourceRepo->addRemote('dst', $this->dst); + $sourceRepo->pushForce('dst', 'refs/heads/master:refs/heads/master'); + $lastCommit = $destinationRepo->getLastCommit(); + $this->assertEquals('Source commit 1', $lastCommit->getSubject()); + } + + /** + * Test list files. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testListFiles(): void { + $sourceRepo = $this->git->open($this->src); + // Test list ignored files. + $gitIgnoreFile = $this->src . DIRECTORY_SEPARATOR . '.gitignore'; + file_put_contents($gitIgnoreFile, ''); + $this->assertFileExists($gitIgnoreFile); + $files = $sourceRepo->listIgnoredFilesFromGitIgnoreFile($gitIgnoreFile); + $this->assertEquals([], $files); + + $this->gitCreateFixtureFile($this->src, 'test-ignore-1'); + $this->gitCreateFixtureFile($this->src, 'test-ignore-2'); + $sourceRepo->commitAllChanges('Test list ignored files.'); + + file_put_contents($gitIgnoreFile, "test-ignore-1\ntest-ignore-2"); + $files = $sourceRepo->listIgnoredFilesFromGitIgnoreFile($gitIgnoreFile); + $this->assertEquals(['test-ignore-1', 'test-ignore-2'], $files); + + // Test list other files. + $otherFiles = $sourceRepo->listOtherFiles(); + $this->assertEquals([], $otherFiles); + $this->gitCreateFixtureFile($this->src, 'other-file-1'); + $this->gitCreateFixtureFile($this->src, 'other-file-2'); + $otherFiles = $sourceRepo->listOtherFiles(); + $this->assertEquals(['other-file-1', 'other-file-2'], $otherFiles); + } + + /** + * Test get commits. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testGetCommits(): void { + $sourceRepo = $this->git->open($this->src); + $this->gitCreateFixtureFile($this->src, 'test-commit-file1'); + $sourceRepo->commitAllChanges('Add file 1'); + $commits = $sourceRepo->getCommits(); + $this->assertEquals(['Add file 1'], $commits); + $this->gitCreateFixtureFile($this->src, 'test-commit-file2'); + $sourceRepo->commitAllChanges('Add file 2'); + $commits = $sourceRepo->getCommits(); + $this->assertEquals(['Add file 2', 'Add file 1'], $commits); + } + + /** + * Test reset hard command. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testResetHard(): void { + $sourceRepo = $this->git->open($this->src); + $file = $this->gitCreateFixtureFile($this->src, 'test-file1'); + file_put_contents($file, 'Content example'); + $sourceRepo->commitAllChanges('Add file 1'); + $this->assertEquals('Content example', file_get_contents($file)); + + file_put_contents($file, 'New content'); + $this->assertEquals('New content', file_get_contents($file)); + + $sourceRepo->resetHard(); + $this->assertEquals('Content example', file_get_contents($file)); + } + + /** + * Test clean force command. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testCleanForce(): void { + $sourceRepo = $this->git->open($this->src); + $this->gitCreateFixtureFile($this->src, 'test-file1'); + $sourceRepo->commitAllChanges('Add file 1'); + $file = $this->gitCreateFixtureFile($this->src, 'test-file2'); + $this->assertFileExists($file); + + $sourceRepo->cleanForce(); + $this->assertFileDoesNotExist($file); + } + + /** + * Test branch command. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testBranch(): void { + $sourceRepo = $this->git->open($this->src); + $this->gitCreateFixtureFile($this->src, 'test-file1'); + $sourceRepo->commitAllChanges('Add file 1'); + // Test switch. + $sourceRepo->switchToBranch('branch1', TRUE); + $this->assertEquals('branch1', $sourceRepo->getCurrentBranchName()); + $sourceRepo->switchToBranch('branch2', TRUE); + $this->assertEquals('branch2', $sourceRepo->getCurrentBranchName()); + $sourceRepo->switchToBranch('branch1'); + $this->assertEquals('branch1', $sourceRepo->getCurrentBranchName()); + // Test remove branch. + $this->assertEquals(['branch1', 'branch2', 'master'], $sourceRepo->getBranches()); + $sourceRepo->removeBranch('master'); + $this->assertEquals(['branch1', 'branch2'], $sourceRepo->getBranches()); + $sourceRepo->removeBranch('branch2', TRUE); + $this->assertEquals(['branch1'], $sourceRepo->getBranches()); + + $sourceRepo->removeBranch('', TRUE); + $this->assertEquals(['branch1'], $sourceRepo->getBranches()); + } + + /** + * Test commit all changes. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testCommitAllChanges(): void { + $sourceRepo = $this->git->open($this->src); + $file = $this->gitCreateFixtureFile($this->src, 'test-file1'); + $sourceRepo->addFile($file); + $sourceRepo->commit('Add file 1'); + $this->assertEquals(['Add file 1'], $sourceRepo->getCommits()); + + $this->gitCreateFixtureFile($this->src, 'test-file2'); + $sourceRepo->commitAllChanges('Commit all changes.'); + $this->assertEquals(['Commit all changes.', 'Add file 1'], $sourceRepo->getCommits()); + } + + /** + * Test list commited files. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testListCommittedFiles(): void { + $sourceRepo = $this->git->open($this->src); + $sourceRepo->commit('Commit 1', ['--allow-empty']); + $this->assertEquals([], $sourceRepo->listCommittedFiles()); + + $file = $this->gitCreateFixtureFile($this->src, 'file-1'); + $this->assertEquals([], $sourceRepo->listCommittedFiles()); + + $sourceRepo->addFile($file); + $sourceRepo->commit('Add file 1'); + $this->assertEquals(['file-1'], $sourceRepo->listCommittedFiles()); + } + + /** + * Test set config. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testSetConfigReceiveDenyCurrentBranchIgnore(): void { + $sourceRepo = $this->git->open($this->src); + try { + $receiveDenyCurrentBranch = $sourceRepo->execute('config', 'receive.denyCurrentBranch'); + } + catch (GitException) { + $receiveDenyCurrentBranch = ''; + } + $this->assertEquals('', $receiveDenyCurrentBranch); + $sourceRepo->setConfigReceiveDenyCurrentBranchIgnore(); + $receiveDenyCurrentBranch = $sourceRepo->execute('config', 'receive.denyCurrentBranch'); + $this->assertEquals(['ignore'], $receiveDenyCurrentBranch); + } + + /** + * Test create tag commands. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testCreateTag(): void { + $sourceRepo = $this->git->open($this->src); + $sourceRepo->commit('Commit 1', ['--allow-empty']); + $this->assertEquals(NULL, $sourceRepo->getTags()); + + $sourceRepo->createAnnotatedTag('tag1', 'Hello tag 1'); + $this->assertEquals(['tag1'], $sourceRepo->getTags()); + + $sourceRepo->createLightweightTag('tag2'); + $this->assertEquals(['tag1', 'tag2'], $sourceRepo->getTags()); + } + + /** + * Test remote commands. + * + * @throws \CzProject\GitPhp\GitException + */ + public function testRemote(): void { + $sourceRepo = $this->git->open($this->src); + $this->assertEquals([], $sourceRepo->getRemotes()); + + $sourceRepo->addRemote('dst', $this->dst); + $this->assertEquals(['dst'], $sourceRepo->getRemotes()); + $this->assertTrue($sourceRepo->isRemoteExists('dst')); + $sourceRepo->removeRemote('dst'); + $this->assertEquals([], $sourceRepo->getRemotes()); + $this->assertFalse($sourceRepo->isRemoteExists('dst')); + + $sourceRepo->removeRemote('dummy'); + $this->assertEquals([], $sourceRepo->getRemotes()); + } + + /** + * Test is valid remote url. + * + * @throws \Exception + */ + #[DataProvider('dataProviderIsValidRemoteUrl')] + public function testIsValidRemoteUrl(?bool $expected, string $pathOrUri, string $type, bool $pass): void { + if (!$pass) { + $this->expectException(\InvalidArgumentException::class); + GitArtifactGitRepository::isValidRemoteUrl($pathOrUri, $type); + } + else { + $this->assertEquals($expected, GitArtifactGitRepository::isValidRemoteUrl($pathOrUri, $type)); + } + } + + /** + * Data provider. + * + * @return array + * Data provider. + */ + public static function dataProviderIsValidRemoteUrl(): array { + return [ + [TRUE, 'git@github.com:foo/git-foo.git', 'uri', TRUE], + [FALSE, 'git@github.com:foo/git-foo.git', 'local', TRUE], + [TRUE, 'git@github.com:foo/git-foo.git', 'any', TRUE], + [FALSE, '/no-existing/path', 'any', TRUE], + [FALSE, '/no-existing/path', 'local', TRUE], + [NULL, '/no-existing/path', 'custom', FALSE], + ]; + } + + /** + * Test is valid remote url. + * + * @throws \Exception + */ + #[DataProvider('dataProviderIsValidBranchName')] + public function testIsValidBranchName(bool $expected, string $branchName): void { + $this->assertEquals($expected, GitArtifactGitRepository::isValidBranchName($branchName)); + } + + /** + * Data provider. + * + * @return array + * Data provider. + */ + public static function dataProviderIsValidBranchName(): array { + return [ + [TRUE, 'branch'], + [FALSE, '*/branch'], + [FALSE, '*.branch'], + ]; + } + +} diff --git a/tests/phpunit/Unit/GitArtifactGitTest.php b/tests/phpunit/Unit/GitArtifactGitTest.php new file mode 100644 index 0000000..d11350b --- /dev/null +++ b/tests/phpunit/Unit/GitArtifactGitTest.php @@ -0,0 +1,27 @@ +git->open($this->src); + $this->assertEquals(GitArtifactGitRepository::class, $repo::class); + } + +}