diff --git a/nix/buildkit-update.sh b/nix/buildkit-update.sh new file mode 100755 index 0000000..6b5a43c --- /dev/null +++ b/nix/buildkit-update.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +{ # https://stackoverflow.com/a/21100710 + + ## Re-generate the buildkit.nix file - with the current 'master' branch. + + set -e + + if [ ! -f "nix/buildkit.nix" ]; then + echo >&2 "Must run in project root" + exit 1 + fi + + now=$( date -u '+%Y-%m-%d %H:%M %Z' ) + commit=$( git ls-remote https://github.com/civicrm/civicrm-buildkit.git | awk '/refs\/heads\/master$/ { print $1 }' ) + url="https://github.com/civicrm/civicrm-buildkit/archive/${commit}.tar.gz" + hash=$( nix-prefetch-url "$url" --type sha256 --unpack ) + + function render_file() { + echo "{ pkgs ? import {} }:" + echo "" + echo "## Get civicrm-buildkit from github." + echo "## Based on \"master\" branch circa $now" + echo "import (pkgs.fetchzip {" + echo " url = \"$url\";" + echo " sha256 = \"$hash\";" + echo "})" + echo + echo "## Get a local copy of civicrm-buildkit. (Useful for developing patches.)" + echo "# import ((builtins.getEnv \"HOME\") + \"/buildkit/default.nix\")" + echo "# import ((builtins.getEnv \"HOME\") + \"/bknix/default.nix\")" + } + render_file > nix/buildkit.nix + + exit +} diff --git a/nix/buildkit.nix b/nix/buildkit.nix new file mode 100644 index 0000000..8c2067e --- /dev/null +++ b/nix/buildkit.nix @@ -0,0 +1,12 @@ +{ pkgs ? import {} }: + +## Get civicrm-buildkit from github. +## Based on "master" branch circa 2024-02-26 04:30 UTC +import (pkgs.fetchzip { + url = "https://github.com/civicrm/civicrm-buildkit/archive/d6f6b8dd2d5944c35cd78cb319fef21673214b35.tar.gz"; + sha256 = "02p2yzdfgv66a2zf8i36h6pjfi78wnp92m3klij7fqbfd9mpvi5a"; +}) + +## Get a local copy of civicrm-buildkit. (Useful for developing patches.) +# import ((builtins.getEnv "HOME") + "/buildkit/default.nix") +# import ((builtins.getEnv "HOME") + "/bknix/default.nix") diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..63b8f14 --- /dev/null +++ b/shell.nix @@ -0,0 +1,33 @@ +/** + * This shell is suitable for compiling PHAR executables.... and not much else. + * + * Ex: `nix-shell --run ./scripts/build.sh` + */ + +{ pkgs ? import {} }: + +let + + buildkit = (import ./nix/buildkit.nix) { inherit pkgs; }; + +in + + pkgs.mkShell { + nativeBuildInputs = buildkit.profiles.base ++ [ + + (buildkit.pins.v2305.php82.buildEnv { + extraConfig = '' + memory_limit=-1 + ''; + }) + + buildkit.pkgs.box + buildkit.pkgs.composer + buildkit.pkgs.phpunit9 + + pkgs.bash-completion + ]; + shellHook = '' + source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh + ''; + } diff --git a/src/GitScan/Command/AutoMergeCommand.php b/src/GitScan/Command/AutoMergeCommand.php index 4a9e5db..8ef198d 100644 --- a/src/GitScan/Command/AutoMergeCommand.php +++ b/src/GitScan/Command/AutoMergeCommand.php @@ -58,6 +58,7 @@ protected function configure() { ->addOption('keep', 'K', InputOption::VALUE_NONE, 'When applying patches, keep the current branch. Preserve local changes.') ->addOption('new', 'N', InputOption::VALUE_NONE, 'When applying patches, create a new merge branch.') ->addOption('path', NULL, InputOption::VALUE_REQUIRED, 'The local base path to search', getcwd()) + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addOption('url-split', NULL, InputOption::VALUE_REQUIRED, 'If listing multiple URLs in one argument, use the given delimiter', '|') // The preflight check is optional because 'git apply --check' can be too picky sometimes (e.g. commit A adds a file; commit B renames the file) ->addOption('check', NULL, InputOption::VALUE_NONE, 'Before applying patches, do a preflight check.') @@ -79,7 +80,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getOption('path')); + $gitRepos = $scanner->scan($input->getOption('path'), $input->getOption('max-depth')); // array(string $absDir => TRUE) $checkouts = array(); diff --git a/src/GitScan/Command/BranchCommand.php b/src/GitScan/Command/BranchCommand.php index f7486eb..1a4eee6 100644 --- a/src/GitScan/Command/BranchCommand.php +++ b/src/GitScan/Command/BranchCommand.php @@ -31,6 +31,7 @@ protected function configure() { ->setName('branch') ->setDescription('Create branches across repos') ->addOption('path', NULL, InputOption::VALUE_REQUIRED, 'The local base path to search', getcwd()) + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addOption('prefix', 'p', InputOption::VALUE_NONE, 'Autodetect prefixed variations') ->addOption('delete', 'd', InputOption::VALUE_NONE, 'Delete fully merged branches') ->addOption('force-delete', 'D', InputOption::VALUE_NONE, 'Delete branch (even if not merged)') @@ -60,7 +61,7 @@ protected function executeCreate(InputInterface $input, OutputInterface $output) $helper = $this->getHelper('question'); $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getOption('path')); + $gitRepos = $scanner->scan($input->getOption('path'), $input->getOption('max-depth')); $batch = new ProcessBatch('Creating branch(es)...'); $self = $this; @@ -103,7 +104,7 @@ function (GitRepo $gitRepo, $oldBranch, $newBranch) use ($input, $output, $helpe protected function executeDelete(InputInterface $input, OutputInterface $output): int { $helper = $this->getHelper('question'); $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getOption('path')); + $gitRepos = $scanner->scan($input->getOption('path'), $input->getOption('max-depth')); $batch = new ProcessBatch('Deleting branch(es)...'); $branchName = $input->getArgument('branchName'); diff --git a/src/GitScan/Command/DiffCommand.php b/src/GitScan/Command/DiffCommand.php index 6546238..e0070ab 100644 --- a/src/GitScan/Command/DiffCommand.php +++ b/src/GitScan/Command/DiffCommand.php @@ -34,6 +34,7 @@ protected function configure() { ->setHelp('Compare the commits/revisions in different source trees') ->addArgument('from', InputArgument::REQUIRED, 'Path to the project folder or JSON export') ->addArgument('to', InputArgument::REQUIRED, 'Path to the project folder or JSON export') + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addOption('format', NULL, InputOption::VALUE_REQUIRED, 'Output format (text|html|json)', 'text'); } @@ -46,8 +47,8 @@ protected function initialize(InputInterface $input, OutputInterface $output) { } protected function execute(InputInterface $input, OutputInterface $output): int { - $fromDoc = $this->getCheckoutDocument($input->getArgument('from')); - $toDoc = $this->getCheckoutDocument($input->getArgument('to')); + $fromDoc = $this->getCheckoutDocument($input->getArgument('from'), $input->getOption('max-depth')); + $toDoc = $this->getCheckoutDocument($input->getArgument('to'), $input->getOption('max-depth')); $report = new DiffReport( $fromDoc, @@ -87,12 +88,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * @param string $path path to a directory or JSON file + * @param int $maxDepth * @return \GitScan\CheckoutDocument */ - protected function getCheckoutDocument($path) { + protected function getCheckoutDocument($path, $maxDepth = -1) { if (is_dir($path)) { $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($path); + $gitRepos = $scanner->scan($path, $maxDepth); return CheckoutDocument::create($path) ->importRepos($gitRepos); diff --git a/src/GitScan/Command/ExportCommand.php b/src/GitScan/Command/ExportCommand.php index 244a339..437c319 100644 --- a/src/GitScan/Command/ExportCommand.php +++ b/src/GitScan/Command/ExportCommand.php @@ -4,6 +4,7 @@ use GitScan\Util\Filesystem; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ExportCommand extends BaseCommand { @@ -26,6 +27,7 @@ protected function configure() { ->setName('export') ->setDescription('Show the status of any nested git repositories') ->setHelp("Export the current checkout information to JSON format") + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addArgument('path', InputArgument::IS_ARRAY, 'The local base path to search', array(getcwd())); } @@ -42,7 +44,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $gitRepos = $scanner->scan($paths); + $gitRepos = $scanner->scan($paths, $input->getOption('max-depth')); $output->writeln( \GitScan\CheckoutDocument::create($paths[0]) ->importRepos($gitRepos) diff --git a/src/GitScan/Command/ForeachCommand.php b/src/GitScan/Command/ForeachCommand.php index 8ee1809..0cf80d7 100644 --- a/src/GitScan/Command/ForeachCommand.php +++ b/src/GitScan/Command/ForeachCommand.php @@ -42,6 +42,7 @@ protected function configure() { . "Important: The example uses single-quotes to escape the $'s\n" ) ->addArgument('path', InputArgument::IS_ARRAY, 'The local base path to search', array(getcwd())) + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addOption('command', 'c', InputOption::VALUE_REQUIRED, 'The command to execute') ->addOption('status', NULL, InputOption::VALUE_REQUIRED, 'Filter table output by repo statuses ("all","novel","boring")', 'all'); } @@ -70,7 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("[[ Finding repositories ]]"); } $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getArgument('path')); + $gitRepos = $scanner->scan($input->getArgument('path'), $input->getOption('max-depth')); foreach ($gitRepos as $gitRepo) { /** @var \GitScan\GitRepo $gitRepo */ diff --git a/src/GitScan/Command/HashCommand.php b/src/GitScan/Command/HashCommand.php index a978354..2e47afb 100644 --- a/src/GitScan/Command/HashCommand.php +++ b/src/GitScan/Command/HashCommand.php @@ -4,6 +4,7 @@ use GitScan\Util\Filesystem; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class HashCommand extends BaseCommand { @@ -26,6 +27,7 @@ protected function configure() { ->setName('hash') ->setDescription('Generate a hash') ->setHelp("Generate a cumulative hash code for the current checkouts") + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addArgument('path', InputArgument::IS_ARRAY, 'The local base path to search', array(getcwd())); } @@ -42,7 +44,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $output->writeln($scanner->hash($paths[0])); + $output->writeln($scanner->hash($paths[0], $input->getOption('max-depth'))); return 0; } diff --git a/src/GitScan/Command/LsCommand.php b/src/GitScan/Command/LsCommand.php index bd9d549..ace8329 100644 --- a/src/GitScan/Command/LsCommand.php +++ b/src/GitScan/Command/LsCommand.php @@ -31,6 +31,7 @@ protected function configure() { Example: git scan ls | while read dir; do ls -la $dir ; done ') ->addOption('absolute', 'A', InputOption::VALUE_NONE, 'Output absolute paths') + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addArgument('path', InputArgument::IS_ARRAY, 'The local base path to search', array(getcwd())); } @@ -47,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $gitRepos = $scanner->scan($paths); + $gitRepos = $scanner->scan($paths, $input->getOption('max-depth')); foreach ($gitRepos as $gitRepo) { /** @var \GitScan\GitRepo $gitRepo */ $path = $input->getOption('absolute') diff --git a/src/GitScan/Command/PushCommand.php b/src/GitScan/Command/PushCommand.php index 0a987c6..26d7660 100644 --- a/src/GitScan/Command/PushCommand.php +++ b/src/GitScan/Command/PushCommand.php @@ -28,6 +28,7 @@ protected function configure() { ->setName('push') ->setDescription('Push tags or branches on all repos') ->addOption('path', NULL, InputOption::VALUE_REQUIRED, 'The local base path to search', getcwd()) + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addOption('prefix', 'p', InputOption::VALUE_NONE, 'Autodetect prefixed variations') ->addOption('dry-run', 'T', InputOption::VALUE_NONE, 'Display what would be done') ->addOption('set-upstream', 'u', InputOption::VALUE_NONE, 'Set remote branch as upstream for local branch') @@ -42,7 +43,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) { protected function execute(InputInterface $input, OutputInterface $output): int { $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getOption('path')); + $gitRepos = $scanner->scan($input->getOption('path'), $input->getOption('max-depth')); $remote = $input->getArgument('remote'); $batch = new ProcessBatch('Pushing...'); diff --git a/src/GitScan/Command/StatusCommand.php b/src/GitScan/Command/StatusCommand.php index d0ca61d..f83da9a 100644 --- a/src/GitScan/Command/StatusCommand.php +++ b/src/GitScan/Command/StatusCommand.php @@ -35,6 +35,7 @@ protected function configure() { ->setDescription('Show the status of any nested git repositories') ->setHelp("Show the status of any nested git repositories.\n\nNote: This will fetch upstream repositories to help determine the status (unless you specify --offline mode).") ->addArgument('path', InputArgument::IS_ARRAY, 'The local base path to search', array(getcwd())) + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addOption('status', NULL, InputOption::VALUE_REQUIRED, 'Filter table output by repo statuses ("all","novel","boring","auto")', 'auto') ->addOption('fetch', NULL, InputOption::VALUE_NONE, 'Fetch latest data about remote repositories. (Slower but more accurate statuses.)'); //->addOption('scan', 's', InputOption::VALUE_NONE, 'Force an immediate scan for new git repositories before doing anything') @@ -48,7 +49,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) { protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln("[[ Finding repositories ]]"); $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getArgument('path')); + $gitRepos = $scanner->scan($input->getArgument('path'), $input->getOption('max-depth')); if ($input->getOption('status') == 'auto') { $input->setOption('status', count($gitRepos) > self::DISPLAY_ALL_THRESHOLD ? 'novel' : 'all'); diff --git a/src/GitScan/Command/TagCommand.php b/src/GitScan/Command/TagCommand.php index 1cc942a..038805a 100644 --- a/src/GitScan/Command/TagCommand.php +++ b/src/GitScan/Command/TagCommand.php @@ -31,6 +31,7 @@ protected function configure() { ->setName('tag') ->setDescription('Create tags across repos') ->addOption('path', NULL, InputOption::VALUE_REQUIRED, 'The local base path to search', getcwd()) + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addOption('prefix', 'p', InputOption::VALUE_NONE, 'Autodetect prefixed variations') ->addOption('delete', 'd', InputOption::VALUE_NONE, 'Delete fully merged branches') ->addOption('dry-run', 'T', InputOption::VALUE_NONE, 'Display what would be done') @@ -59,7 +60,7 @@ protected function executeCreate(InputInterface $input, OutputInterface $output) $helper = $this->getHelper('question'); $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getOption('path')); + $gitRepos = $scanner->scan($input->getOption('path'), $input->getOption('max-depth')); $batch = new ProcessBatch('Creating tag(s)...'); $self = $this; @@ -101,7 +102,7 @@ function (GitRepo $gitRepo, $oldBranch, $newTag) use ($input, $output, $helper, protected function executeDelete(InputInterface $input, OutputInterface $output): int { $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getOption('path')); + $gitRepos = $scanner->scan($input->getOption('path'), $input->getOption('max-depth')); $batch = new ProcessBatch('Deleting branch(es)...'); $tagName = $input->getArgument('tagName'); diff --git a/src/GitScan/Command/UpdateCommand.php b/src/GitScan/Command/UpdateCommand.php index 3a6a5d1..7415b65 100644 --- a/src/GitScan/Command/UpdateCommand.php +++ b/src/GitScan/Command/UpdateCommand.php @@ -4,6 +4,7 @@ use GitScan\Util\Filesystem; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class UpdateCommand extends BaseCommand { @@ -27,6 +28,7 @@ protected function configure() { ->setAliases(array('up')) ->setDescription('Execute fast-forward merges on all nested repositories') ->setHelp('Execute fast-forward merges on all nested repositories (which are already amenable to fast-forwarding)') + ->addOption('max-depth', NULL, InputOption::VALUE_REQUIRED, 'Limit the depth of the search', -1) ->addArgument('path', InputArgument::IS_ARRAY, 'The local base path to search', array(getcwd())); } @@ -40,7 +42,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("[[ Finding repositories ]]"); $scanner = new \GitScan\GitRepoScanner(); - $gitRepos = $scanner->scan($input->getArgument('path')); + $gitRepos = $scanner->scan($input->getArgument('path'), $input->getOption('max-depth')); $output->writeln("[[ Fast-forwarding ]]"); foreach ($gitRepos as $gitRepo) { diff --git a/src/GitScan/GitRepoScanner.php b/src/GitScan/GitRepoScanner.php index 5e48dbf..62b71ab 100644 --- a/src/GitScan/GitRepoScanner.php +++ b/src/GitScan/GitRepoScanner.php @@ -30,11 +30,17 @@ public function __construct($fs = NULL, ?\GitScan\Config $config = NULL) { * given base dir. * * @param string|array $basedir + * @param int $maxDepth + * Maximum number of directory-levels to traverse. + * Use -1 for unlimited. * @return array of GitRepo */ - public function scan($basedir) { + public function scan($basedir, $maxDepth = -1) { $gitRepos = array(); $finder = new Finder(); + if ($maxDepth >= 0) { + $finder->depth('<= ' . $maxDepth); + } $finder->in($basedir) ->ignoreUnreadableDirs() // Specifically looking for .git files! @@ -55,10 +61,11 @@ public function scan($basedir) { * within a given base dir. * * @param string $basedir + * @param int $maxDepth * @return string */ - public function hash($basedir) { - $gitRepos = $this->scan($basedir); + public function hash($basedir, $maxDepth = -1) { + $gitRepos = $this->scan($basedir, $maxDepth); $buf = ''; foreach ($gitRepos as $gitRepo) { $path = rtrim($this->fs->makePathRelative($gitRepo->getPath(), $basedir), '/'); diff --git a/tests/GitScan/Command/LsCommandTest.php b/tests/GitScan/Command/LsCommandTest.php index 09455be..09208e0 100644 --- a/tests/GitScan/Command/LsCommandTest.php +++ b/tests/GitScan/Command/LsCommandTest.php @@ -71,4 +71,64 @@ public function testLs_output_absolute() { $this->assertEquals($expectPaths, $actualPaths); } + public function testLsMaxDepth() { + $this->createExampleRepo($this->fixturePath . '/depth1'); + $this->createExampleRepo($this->fixturePath . '/depth1/depth2'); + $this->createExampleRepo($this->fixturePath . '/depth1/depth2/depth3'); + + // Test with max-depth=0 + $commandTester = $this->createCommandTester(array( + 'command' => 'ls', + '--max-depth' => 0, + 'path' => array($this->fixturePath), + )); + $actualPaths = explode("\n", trim($commandTester->getDisplay(FALSE))); + $this->assertEquals(array(''), $actualPaths); + + // Test with max-depth=1 + $commandTester = $this->createCommandTester(array( + 'command' => 'ls', + '--max-depth' => 1, + 'path' => array($this->fixturePath), + )); + $actualPaths = explode("\n", trim($commandTester->getDisplay(FALSE))); + $expectPaths = array( + 'depth1', + ); + sort($actualPaths); + sort($expectPaths); + $this->assertEquals($expectPaths, $actualPaths); + + // Test with max-depth=2 + $commandTester = $this->createCommandTester(array( + 'command' => 'ls', + '--max-depth' => 2, + 'path' => array($this->fixturePath), + )); + $actualPaths = explode("\n", trim($commandTester->getDisplay(FALSE))); + $expectPaths = array( + 'depth1', + 'depth1/depth2', + ); + sort($actualPaths); + sort($expectPaths); + $this->assertEquals($expectPaths, $actualPaths); + + // Test with max-depth=3 + $commandTester = $this->createCommandTester(array( + 'command' => 'ls', + '--max-depth' => 3, + 'path' => array($this->fixturePath), + )); + $actualPaths = explode("\n", trim($commandTester->getDisplay(FALSE))); + $expectPaths = array( + 'depth1', + 'depth1/depth2', + 'depth1/depth2/depth3', + ); + sort($actualPaths); + sort($expectPaths); + $this->assertEquals($expectPaths, $actualPaths); + } + }