diff --git a/bin/splitsh-lite-mac b/bin/splitsh-lite-mac new file mode 100755 index 0000000..71a4d89 Binary files /dev/null and b/bin/splitsh-lite-mac differ diff --git a/bin/splitsh-lite-unix b/bin/splitsh-lite-unix new file mode 100755 index 0000000..ddefe95 Binary files /dev/null and b/bin/splitsh-lite-unix differ diff --git a/box.json b/box.json index d86c0ff..4a754e3 100644 --- a/box.json +++ b/box.json @@ -4,7 +4,7 @@ "banner": [ "Winter CMS CLI helper.", "", - "(c) 2020 Ben Thomson", + "(c) 2020 Ben Thomson / Winter CMS", "", "This source file is subject to the MIT license that is bundled", "with this source code in the file LICENSE." @@ -17,5 +17,32 @@ "version": "##version##", "datetime": "##datetime##" }, - "algorithm": "SHA256" + "algorithm": "SHA256", + "files": [ + "index.php" + ], + "directories": [ + "src" + ], + "directories-bin": [ + "bin" + ], + "finder": [ + { + "notName": "/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/", + "exclude": [ + "doc", + "test", + "test_old", + "tests", + "Tests", + "vendor-bin" + ], + "in": "vendor" + }, + { + "name": "composer.json", + "in": "." + } + ] } diff --git a/composer.json b/composer.json index 2c6c1ec..9fea9c4 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "require": { "php": ">=7.2", "symfony/console": "^4.4.13", + "symfony/process": "^4.4.13", "php-http/guzzle6-adapter": "^2.0", "knplabs/github-api": "^2.15" }, diff --git a/composer.lock b/composer.lock index 4d210f8..0ef4d08 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "141308f659660236e0d3685e96438d1e", + "content-hash": "d36910874f85e4ef069178222a4327e4", "packages": [ { "name": "clue/stream-filter", @@ -1884,6 +1884,67 @@ ], "time": "2021-01-07T16:49:33+00:00" }, + { + "name": "symfony/process", + "version": "v4.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/7e950b6366d4da90292c2e7fa820b3c1842b965a", + "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v4.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-27T09:09:26+00:00" + }, { "name": "symfony/service-contracts", "version": "v2.2.0", diff --git a/src/BaseCommand.php b/src/BaseCommand.php index 326ee80..89ce35e 100644 --- a/src/BaseCommand.php +++ b/src/BaseCommand.php @@ -15,6 +15,9 @@ */ class BaseCommand extends SymfonyCommand { + /** @var InputInterface Input interface */ + protected $input; + /** @var OutputInterface Output interface */ protected $output; @@ -23,6 +26,7 @@ class BaseCommand extends SymfonyCommand */ public function run(InputInterface $input, OutputInterface $output) { + $this->input = $input; $this->output = $output; // Add success style @@ -141,4 +145,18 @@ protected function error(string $text, int $verbosity = OutputInterface::VERBOSI { $this->output->writeln('' . $text . '', $verbosity); } + + /** + * Gets the base directory for the application. + * + * @param string $path A path to append to the base directory. + * @return string + */ + protected function getBaseDir(string $path = '') + { + return dirname(__DIR__) . ((!empty($path)) + ? DIRECTORY_SEPARATOR . trim($path, DIRECTORY_SEPARATOR) + : '' + ); + } } diff --git a/src/Commands/Split/Command.php b/src/Commands/Split/Command.php new file mode 100644 index 0000000..386a17d --- /dev/null +++ b/src/Commands/Split/Command.php @@ -0,0 +1,772 @@ + [ + 'prefix' => 'modules/system', + 'url' => 'https://%s@github.com/wintercms/wn-system-module.git', + ], + 'backend' => [ + 'prefix' => 'modules/backend', + 'url' => 'https://%s@github.com/wintercms/wn-backend-module.git', + ], + 'cms' => [ + 'prefix' => 'modules/cms', + 'url' => 'https://%s@github.com/wintercms/wn-cms-module.git', + ], + ]; + + /** + * @inheritDoc + */ + protected function configure() + { + $this + // the short description shown while running "php bin/console list" + ->setDescription('Runs a subsplit to publish the Winter CMS modules in their own repositories.') + + // the full command description shown when running the command with + // the "--help" option + ->setHelp( + 'This is used by the maintainers to push changes from the main repository to the module subsplit' + . ' repositories.' + ) + + // hide this command from normal usage + ->setHidden(true) + + // options + ->addOption( + 'branch', + 'b', + InputOption::VALUE_REQUIRED, + 'Publishes a branch in the subsplit repositories.' + ) + ->addOption( + 'git', + 'g', + InputOption::VALUE_REQUIRED, + 'The path to the "git" binary. If this is not provided, it will be found automatically.' + ) + ->addOption( + 'remove-branch', + null, + InputOption::VALUE_REQUIRED, + 'Removes a branch in the subsplit repositories.' + ) + ->addOption( + 'remove-tag', + null, + InputOption::VALUE_REQUIRED, + 'Removes a tag in the subsplit repositories.' + ) + ->addOption( + 'sync', + 's', + InputOption::VALUE_NONE, + 'Fully synchronises all branches with the subsplit repositories.' + ) + ->addOption( + 'tag', + 'a', + InputOption::VALUE_REQUIRED, + 'Publishes a tag in the subsplit repositories.' + ) + ->addOption( + 'work-repo', + 'w', + InputOption::VALUE_REQUIRED, + 'Defines a custom location for the working repository.' + ) + ; + } + + /** + * @inheritDoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + if (PHP_OS_FAMILY === 'Windows') { + $this->error('Windows is not supported for subsplitting.'); + return; + } + + $this->token = (new Token())->read(); + + if ($input->getOption('work-repo')) { + $this->setGitRepoPath($input->getOption('work-repo')); + } + + // Determine action being used + $action = null; + + foreach (['branch', 'remove-branch', 'remove-tag', 'tag', 'sync'] as $option) { + if ($input->getOption($option)) { + $action = $option; + break; + } + } + + if (is_null($action)) { + $this->error( + 'You must specify an action to take: one of --branch, --remove-branch, --remove-tag, --tag or --sync' + ); + return; + } + + // Set up the work repository + $this->comment('Setting up work repository...'); + + if (!$this->repositoryExists()) { + $this->line(' - Creating work repository.'); + $this->createWorkRepo(); + } else { + $this->line(' - Work repository already exists.'); + $this->line(' - Updating work repository.'); + $this->updateWorkRepo(); + } + + // Execute action + switch ($action) { + case 'sync': + default: + $this->doSync(); + break; + case 'branch': + $this->doBranchSync(); + break; + case 'remove-branch': + $this->doRemoveBranch(); + break; + case 'tag': + $this->doTagSync(); + break; + case 'remove-tag': + $this->doRemoveTag(); + break; + } + + $this->comment('Complete'); + $this->line(''); + } + + /** + * Executes a full synchronisation (all branches) of the subsplit repositories. + * + * This function will remove any branches on subsplits that no longer exist on origin. + * + * @return void + */ + protected function doSync() + { + $this->comment('Performing full branch sync of subsplits...'); + + $this->line(' - Finding branches.'); + + $this->line(' - Synchronising branches.'); + + $branches = $this->getBranches(); + + // Create progress bar + $progress = new ProgressBar($this->output); + + foreach ($progress->iterate($branches) as $branch) { + $progress->clear(); + $this->line(' - Syncing "' . $branch . '" branch.'); + $progress->display(); + + $this->syncBranch($branch); + } + + $progress->clear(); + + $this->line(' - Cleaning orphaned branches.'); + + // Create progress bar + $progress = new ProgressBar($this->output); + + foreach ($progress->iterate(array_keys($this->remotes)) as $remote) { + $remoteBranches = $this->getBranches($remote); + $removedBranches = array_diff($remoteBranches, $branches); + + if (count($removedBranches)) { + foreach ($removedBranches as $removedBranch) { + $progress->clear(); + $this->line(' - Removing branch "' . $removedBranch . '" from remote "' . $remote . '".'); + $progress->display(); + + $this->deleteBranch($remote, $removedBranch); + } + } + } + + $progress->clear(); + } + + /** + * Executes a synchronisation of a branch to the subsplit repositories. + * + * @return void + */ + protected function doBranchSync() + { + $branch = $this->input->getOption('branch'); + + $this->comment('Performing sync of "' . $branch . '" to subsplits...'); + + $this->line(' - Syncing "' . $branch . '" branch.'); + + $this->syncBranch($branch); + } + + /** + * Executes a synchronisation of a tag to the subsplit repositories. + * + * @return void + */ + protected function doTagSync() + { + $tag = $this->input->getOption('tag'); + + $this->comment('Performing sync of "' . $tag . '" to subsplits...'); + + $this->line(' - Syncing "' . $tag . '" tag.'); + + $this->syncTag($tag); + } + + /** + * Executes a deletion of a branch from the subsplit repositories. + * + * @return void + */ + protected function doRemoveBranch() + { + $branch = $this->input->getOption('remove-branch'); + + $this->comment('Deleting branch "' . $branch . '" from subsplits...'); + + // Create progress bar + $progress = new ProgressBar($this->output); + + foreach ($progress->iterate(array_keys($this->remotes)) as $remote) { + $progress->clear(); + $this->line(' - Removing branch "' . $branch . '" from "' . $remote . '".'); + $progress->display(); + + if ($this->branchExists($remote, $branch)) { + $this->deleteBranch($remote, $branch); + $progress->clear(); + $this->line(' - Removed from "' . $remote . '".'); + $progress->display(); + } else { + $progress->clear(); + $this->line(' - Branch doesn\'t exist on "' . $remote . '". Skipping.'); + $progress->display(); + } + } + + $progress->clear(); + } + + /** + * Executes a deletion of a tag from the subsplit repositories. + * + * @return void + */ + protected function doRemoveTag() + { + $tag = $this->input->getOption('remove-tag'); + + $this->comment('Deleting tag "' . $tag . '" from subsplits...'); + + // Create progress bar + $progress = new ProgressBar($this->output); + + foreach ($progress->iterate(array_keys($this->remotes)) as $remote) { + $progress->clear(); + $this->line(' - Removing tag "' . $tag . '" from "' . $remote . '".'); + $progress->display(); + + if ($this->tagExists($remote, $tag)) { + $this->deleteTag($remote, $tag); + $progress->clear(); + $this->line(' - Removed from "' . $remote . '".'); + $progress->display(); + } else { + $progress->clear(); + $this->line(' - Tag doesn\'t exist on "' . $remote . '". Skipping.'); + $progress->display(); + } + } + + $progress->clear(); + } + + /** + * Get branches from a repository. + * + * By default, this will get the origin branches, but you may specify an optional remote to get branches from the + * remote. + * + * @return array + */ + protected function getBranches($remote = null) + { + if (is_null($remote)) { + $command = [ + 'branch', + '-l' + ]; + } elseif (in_array($remote, array_keys($this->remotes))) { + $command = [ + 'branch', + '-la' + ]; + } else { + throw new Exception('Invalid remote "' . $remote . '" specified.'); + } + + $process = $this->runGitCommand($command); + + if (!$process->isSuccessful()) { + $this->error('Unable to determine available branches.'); + return; + } + + $branches = array_map( + function ($item) use ($remote) { + $branch = trim(str_replace('* ', '', $item)); + + if (!is_null($remote)) { + $branch = str_ireplace('remotes/' . $remote . '/', '', $branch); + } + + return $branch; + }, + array_filter( + preg_split('/[\n\r]+/', trim($process->getOutput()), -1, PREG_SPLIT_NO_EMPTY), + function ($item) use ($remote) { + if (is_null($remote)) { + return true; + } + + return preg_match('/^ +remotes\\/' . preg_quote($remote, '/') . '/i', $item); + } + ) + ); + + return $branches; + } + + /** + * Determines if a branch exists on a given remote. + * + * @param string $remote + * @param string $branch + * @return bool + */ + protected function branchExists($remote, $branch) + { + if (!in_array($remote, array_keys($this->remotes))) { + throw new Exception('Invalid remote "' . $remote . '" specified.'); + } + + $branches = $this->getBranches($remote); + + return in_array($branch, $branches); + } + + /** + * Determines if a tag exists on a given remote. + * + * @param string $remote + * @param string $tag + * @return bool + */ + protected function tagExists($remote, $tag) + { + if (!in_array($remote, array_keys($this->remotes))) { + throw new Exception('Invalid remote "' . $remote . '" specified.'); + } + + $process = $this->runGitCommand([ + 'ls-remote', + $remote, + 'refs/tags/' . $tag + ]); + + if (!$process->isSuccessful()) { + $this->error('Unable to determine available tags.'); + return; + } + + $output = trim($process->getOutput()); + + return !empty($output); + } + + /** + * Deletes a branch on a remote. + * + * @param string $remote + * @param string $branch + * @return void + */ + protected function deleteBranch($remote, $branch) + { + if (!in_array($remote, array_keys($this->remotes))) { + throw new Exception('Invalid remote "' . $remote . '" specified.'); + } + + $process = $this->runGitCommand([ + 'push', + '--delete', + $remote, + $branch + ]); + + if (!$process->isSuccessful()) { + $this->error('Unable to delete branch "' . $branch . '" on remote "' . $remote . '"'); + return; + } + } + + /** + * Deletes a tag on a remote. + * + * @param string $remote + * @param string $branch + * @return void + */ + protected function deleteTag($remote, $tag) + { + if (!in_array($remote, array_keys($this->remotes))) { + throw new Exception('Invalid remote "' . $remote . '" specified.'); + } + + $process = $this->runGitCommand([ + 'push', + '--delete', + $remote, + $tag + ]); + + if (!$process->isSuccessful()) { + $this->error('Unable to delete tag "' . $tag . '" on remote "' . $remote . '"'); + return; + } + } + + /** + * Synchronises a branch with all subsplits. + * + * @param string $branch + * @return void + */ + protected function syncBranch($branch) + { + foreach ($this->remotes as $remote => $split) { + // Process subsplit through "splitsh" utility for given remote and module + $process = new Process([ + $this->getSplitshPath(), + '--origin=heads/' . $branch, + '--target=heads/' . $remote . '-' . $branch, + '--prefix=' . $split['prefix'], + '--path=' . $this->getGitRepoPath() + ]); + $this->line('Running command: ' . $process->getCommandLine(), OutputInterface::VERBOSITY_DEBUG); + $process->run(); + + if (!$process->isSuccessful()) { + throw new Exception( + 'Unable to create a subsplit of the ' . $remote . ' module from "' . $split['prefix'] . '". ' + . $process->getErrorOutput() + ); + } + + // Push to the remote + $process = $this->runGitCommand([ + 'push', + '-f', + $remote, + 'heads/' . $remote . '-' . $branch . ':refs/heads/' . $branch + ]); + $process->run(); + + if (!$process->isSuccessful()) { + throw new Exception( + 'Unable to push a subsplit of the ' . $remote . ' module from "' . $split['prefix'] . '". ' + . $process->getErrorOutput() + ); + } + } + } + + /** + * Synchronises a tag with all subsplits. + * + * @param string $branch + * @return void + */ + protected function syncTag($tag) + { + foreach ($this->remotes as $remote => $split) { + // Process subsplit through "splitsh" utility for given remote and module + $process = new Process([ + $this->getSplitshPath(), + '--origin=tags/' . $tag, + '--target=tags/' . $remote . '-' . $tag, + '--prefix=' . $split['prefix'], + '--path=' . $this->getGitRepoPath() + ]); + $this->line('Running command: ' . $process->getCommandLine(), OutputInterface::VERBOSITY_DEBUG); + $process->run(); + + if (!$process->isSuccessful()) { + throw new Exception( + 'Unable to create a subsplit of the tag"' . $tag . '" to the "' . $remote . '" module from "' + . $split['prefix'] . '". ' + . $process->getErrorOutput() + ); + } + + // Push to the remote + $process = $this->runGitCommand([ + 'push', + '-f', + $remote, + 'tags/' . $remote . '-' . $tag . ':refs/tags/' . $tag + ]); + $process->run(); + + if (!$process->isSuccessful()) { + throw new Exception( + 'Unable to push tag"' . $tag . '" to the "' . $remote . '" module from "' . $split['prefix'] . '". ' + . $process->getErrorOutput() + ); + } + } + } + + /** + * Creates a working bare Git repository for subsplitting the modules and determining commits. + * + * By default, this will be stored in the "storage/temp/split-repo" directory relative to the base path, but it can + * be modified by the "--work-repo" option in the command line. + * + * @return void + */ + protected function createWorkRepo() + { + $this->clearPath($this->getGitRepoPath()); + + if (!is_dir($this->getGitRepoPath()) && !mkdir($this->getGitRepoPath(), 0755, true)) { + throw new Exception( + 'Unable to create a work repository in path "' . $this->getGitRepoPath() . '". Please check your' + . ' permissions.' + ); + } + + $command = [ + 'clone', + '--bare', + sprintf($this->origin, $this->token), + $this->getGitRepoPath() + ]; + + $process = $this->runGitCommand($command, false); + if (!$process->isSuccessful()) { + $this->error( + 'Unable to create work repository in path "' . $this->getGitRepoPath() . '". ' + . $process->getErrorOutput() + ); + } else { + $this->line(' - Created and checked out bare repository.'); + } + + $this->updateWorkRepo(); + } + + /** + * Fetches all recent changes to origin in the working bare repository. + * + * @return void + */ + protected function updateWorkRepo() + { + $process = $this->runGitCommand([ + 'fetch', + 'origin', + 'refs/heads/*:refs/heads/*' + ]); + + if (!$process->isSuccessful()) { + $this->error( + 'Unable to update work repository in path "' . $this->getGitRepoPath() . '". ' + . $process->getErrorOutput() + ); + } + + $process = $this->runGitCommand([ + 'fetch', + 'origin', + 'refs/tags/*:refs/tags/*' + ]); + + if (!$process->isSuccessful()) { + $this->error( + 'Unable to update work repository in path "' . $this->getGitRepoPath() . '". ' + . $process->getErrorOutput() + ); + } else { + $this->line(' - Updated work repository.'); + } + + $this->setRemotes(); + } + + /** + * Sets the remotes for the working repository to point to subsplit repositories. + * + * @return void + */ + protected function setRemotes() + { + $process = $this->runGitCommand(['remote']); + $remotes = preg_split('/[\n\r]+/', trim($process->getOutput()), -1, PREG_SPLIT_NO_EMPTY); + + foreach ($this->remotes as $remote => $split) { + $process = $this->runGitCommand([ + 'remote', + (in_array($remote, $remotes)) ? 'set-url' : 'add', + $remote, + sprintf($split['url'], $this->token) + ]); + if (!$process->isSuccessful()) { + $this->error( + ' - Unable to set remote repository for "' . $remote . '" module. ' + . $process->getErrorOutput() + ); + } else { + $this->line(' - Set remote repository for "' . $remote . '" module.'); + } + + $process = $this->runGitCommand([ + 'fetch', + $remote + ]); + if (!$process->isSuccessful()) { + $this->error( + ' - Unable to fetch repository for "' . $remote . '" module. ' . $process->getErrorOutput() + ); + } else { + $this->line(' - Fetched repository for "' . $remote . '" module.'); + } + + $process = $this->runGitCommand([ + 'remote', + 'prune', + $remote + ]); + if (!$process->isSuccessful()) { + $this->error( + ' - Unable to prune branches from "' . $remote . '" module. ' . $process->getErrorOutput() + ); + } else { + $this->line(' - Pruned branches for "' . $remote . '" module.'); + } + } + } + + /** + * Get path to the "splitsh" utility for the current OS, bundled with the app. + * + * @return string + */ + protected function getSplitshPath() + { + $phar = Phar::running(true); + + if (empty($phar)) { + if (PHP_OS_FAMILY === 'Darwin') { + return $this->getBaseDir('bin/splitsh-lite-mac'); + } + + return $this->getBaseDir('bin/splitsh-lite-unix'); + } else { + $dataDir = new DataDir(); + + if (PHP_OS_FAMILY === 'Darwin') { + if (!$dataDir->exists('bin/splitsh-lite-mac')) { + $dataDir->put( + 'bin/splitsh-lite-mac', + file_get_contents($phar . '/bin/splitsh-lite-mac') + ); + } + + $dataDir->chmod('bin/splitsh-lite-mac', 0755); + + return $dataDir->path('bin/splitsh-lite-mac'); + } + + if (!$dataDir->exists('bin/splitsh-lite-unix')) { + $dataDir->put( + 'bin/splitsh-lite-unix', + file_get_contents($phar . '/bin/splitsh-lite-unix') + ); + } + + $dataDir->chmod('bin/splitsh-lite-unix', 0755); + + return $dataDir->path('bin/splitsh-lite-unix'); + } + } +} diff --git a/src/Filesystem/DataDir.php b/src/Filesystem/DataDir.php index 0df03cd..211c1cd 100644 --- a/src/Filesystem/DataDir.php +++ b/src/Filesystem/DataDir.php @@ -37,6 +37,59 @@ public function __construct() $this->fallbackPath = $_SERVER['HOME'] . DIRECTORY_SEPARATOR . '.winter-cli'; } + /** + * Gets a file into the data directory. + * + * If the file does not exist, returns `false`. + * + * @param string $path + * @return string|bool + */ + public function get(string $path) + { + $path = $this->resolvePath($path); + + if (!$path || !is_readable($path)) { + throw new Exception('Unable to get file "' . $path . '", please check permissions.'); + } + + return file_get_contents($path); + } + + /** + * Gets a path to a file or directory in the data directory. + * + * If the file does not exist, returns `false`. + * + * @param string $path + * @return string|bool + */ + public function path(string $path) + { + if (!$this->exists($path)) { + return false; + } + + return $this->resolvePath($path); + } + + /** + * Finds if a file exists within the data directory. + * + * @param string $path + * @return bool + */ + public function exists(string $path) + { + $path = $this->resolvePath($path); + + if (!$path || !is_readable($path)) { + return false; + } + + return true; + } + /** * Puts a file into the data directory. * @@ -74,22 +127,69 @@ public function put(string $path, string $content) } /** - * Gets a file into the data directory. + * Creates a directory within the data directory. * - * If the file does not exist, returns `false`. + * If successful, will return the path written to. * * @param string $path - * @return string|bool + * @param string $content + * @return string + * @throws Exception If the directory cannot be written. */ - public function get(string $path) + public function mkdir(string $path) { $path = $this->resolvePath($path); - if (!$path || !is_readable($path)) { - throw new Exception('Unable to get file "' . $path . '", please check permissions.'); + if (is_dir($path)) { + return $path; } - return file_get_contents($path); + if (is_file($path)) { + throw new Exception('A file already exists as path "' . $path . '"'); + } + + try { + $dir = dirname($path); + + if (is_dir($dir) && !is_writeable($dir)) { + throw new Exception('Path not writable'); + } + + if (!is_dir($dir) && !mkdir($dir, 0755, true)) { + throw new Exception('Directory not writable'); + } + + mkdir($path, 0755); + } catch (Throwable $e) { + throw new Exception('Unable to make directory "' . $path . '", please check permissions.'); + } + + return $path; + } + + /** + * Changes file permissions on a path in the data directory + * + * If the file does not exist, returns `false`. + * + * @param string $path + * @param octal $chmod + * @return bool + * @throws Exception If the permissions cannot be written. + */ + public function chmod(string $path, $chmod) + { + if (!$this->exists($path)) { + return false; + } + + $path = $this->resolvePath($path); + + try { + chmod($path, $chmod); + } catch (Throwable $e) { + throw new Exception('Unable to chmod "' . $path . '", please check permissions.'); + } } /** diff --git a/src/GitHub/Token.php b/src/GitHub/Token.php index a019a79..71c55f7 100644 --- a/src/GitHub/Token.php +++ b/src/GitHub/Token.php @@ -53,7 +53,7 @@ public function read(): string } $dataDir = new DataDir(); - $token = $dataDir->get($this->tokenFile); + $token = $dataDir->exists($this->tokenFile); if (!$token) { throw new Exception( @@ -64,6 +64,6 @@ public function read(): string ); } - return $this->token; + return $dataDir->get($this->tokenFile); } } diff --git a/src/Traits/InteractsWithFiles.php b/src/Traits/InteractsWithFiles.php new file mode 100644 index 0000000..60e618f --- /dev/null +++ b/src/Traits/InteractsWithFiles.php @@ -0,0 +1,42 @@ +run(); + + if (!$process->isSuccessful()) { + throw new Exception('Unable to clear path "' . $path . '"'); + } + } +} diff --git a/src/Traits/InteractsWithGit.php b/src/Traits/InteractsWithGit.php new file mode 100644 index 0000000..fa56db2 --- /dev/null +++ b/src/Traits/InteractsWithGit.php @@ -0,0 +1,132 @@ +getGitPath())) { + return; + } + + if ($includeGitRepoPath) { + array_unshift($command, '--git-dir=' . $this->getGitRepoPath() . ''); + } + array_unshift($command, $this->getGitPath()); + + $process = new Process($command); + $this->line('Running Git command: ' . implode(' ', $command), OutputInterface::VERBOSITY_DEBUG); + $process->run(); + + return $process; + } + + /** + * Determines the path to the "git" binary. + * + * @return string + */ + protected function getGitPath() + { + if (!empty($this->gitPath)) { + return $this->gitPath; + } + + if (PHP_OS_FAMILY == 'Windows') { + $command = ['where.exe', 'git.exe']; + } else { + $command = ['which', 'git']; + } + + $process = new Process($command); + $process->run(); + + if (!$process->isSuccessful()) { + $this->error( + 'Unable to determine the correct path for the "git" binary. Please make sure it is installed.' + ); + return; + } + + $path = $process->getOutput(); + + if (empty($path)) { + $this->error( + 'Unable to determine the correct path for the "git" binary. Please make sure it is installed.' + ); + return; + } + + return $this->gitPath = trim($path); + } + + /** + * Returns the Git repository path. + * + * If a Git repository path has not been previously defined, it will be created within the data directory. + * + * @return string + */ + protected function getGitRepoPath() + { + if (isset($this->gitRepoPath)) { + return $this->gitRepoPath; + } + + return (new DataDir)->mkdir($this->repoName ?? 'repo'); + } + + /** + * Sets the path to the Git repository. + * + * @param string $gitRepoPath + * @return void + */ + protected function setGitRepoPath(string $gitRepoPath) + { + if (!is_dir($gitRepoPath)) { + throw new Exception('Path to Git repository "' . $gitRepoPath . '" not found.'); + } + + $this->gitRepoPath = rtrim($gitRepoPath, DIRECTORY_SEPARATOR); + } + + /** + * Determines if the repository exists. + * + * @return bool + */ + protected function repositoryExists() + { + return is_dir($this->getGitRepoPath()) + && file_exists($this->getGitRepoPath() . '/HEAD') + && is_dir($this->getGitRepoPath() . '/refs'); + } +}