diff --git a/src/Composer/Command/Helper/DialogHelper.php b/src/Composer/Command/Helper/DialogHelper.php new file mode 100644 index 000000000..674b03800 --- /dev/null +++ b/src/Composer/Command/Helper/DialogHelper.php @@ -0,0 +1,37 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command\Helper; + +use Symfony\Component\Console\Helper\DialogHelper as BaseDialogHelper; +use Symfony\Component\Console\Output\OutputInterface; + +class DialogHelper extends BaseDialogHelper +{ + /** + * Build text for asking a question. For example: + * + * "Do you want to continue [yes]:" + * + * @param string $question The question you want to ask + * @param mixed $default Default value to add to message, if false no default will be shown + * @param string $sep Separation char for between message and user input + * + * @return string + */ + public function getQuestion($question, $default, $sep = ':') + { + return $default ? + sprintf('%s [%s]%s ', $question, $default, $sep) : + sprintf('%s%s ', $question, $sep); + } +} \ No newline at end of file diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php new file mode 100644 index 000000000..93023d2ad --- /dev/null +++ b/src/Composer/Command/InitCommand.php @@ -0,0 +1,370 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command; + +use Composer\Json\JsonFile; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; +use Symfony\Component\Process\ExecutableFinder; + +/** + * @author Justin Rainbow + */ +class InitCommand extends Command +{ + private $gitConfig; + + public function parseAuthorString($author) + { + if (preg_match('/^(?P[- \.,a-z0-9]+) <(?P.+?)>$/i', $author, $match)) { + if ($match['email'] === filter_var($match['email'], FILTER_VALIDATE_EMAIL)) { + return array( + 'name' => trim($match['name']), + 'email' => $match['email'] + ); + } + } + + throw new \InvalidArgumentException( + 'Invalid author string. Must be in the format:'. + ' John Smith ' + ); + } + + protected function configure() + { + $this + ->setName('init') + ->setDescription('Creates a basic composer.json file in current directory.') + ->setDefinition(array( + new InputOption('name', null, InputOption::VALUE_NONE, 'Name of the package'), + new InputOption('description', null, InputOption::VALUE_NONE, 'Description of package'), + new InputOption('author', null, InputOption::VALUE_NONE, 'Author name of package'), + // new InputOption('version', null, InputOption::VALUE_NONE, 'Version of package'), + new InputOption('homepage', null, InputOption::VALUE_NONE, 'Homepage of package'), + new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'An array required packages'), + )) + ->setHelp(<<init command creates a basic composer.json file +in the current directory. + +php composer.phar init + +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $dialog = $this->getHelperSet()->get('dialog'); + + $whitelist = array('name', 'description', 'author', 'require'); + + $options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist))); + + if (isset($options['author'])) { + $options['authors'] = $this->formatAuthors($options['author']); + unset($options['author']); + } + + $options['require'] = isset($options['require']) ? + $this->formatRequirements($options['require']) : + new \stdClass; + + $file = new JsonFile('composer.json'); + + $json = $file->encode($options); + + if ($input->isInteractive()) { + $output->writeln(array( + '', + $json, + '' + )); + if (!$dialog->askConfirmation($output, $dialog->getQuestion('Do you confirm generation', 'yes', '?'), true)) { + $output->writeln('Command aborted'); + + return 1; + } + } + + $file->write($options); + + if ($input->isInteractive()) { + $ignoreFile = realpath('.gitignore'); + + if (false === $ignoreFile) { + $ignoreFile = realpath('.') . '/.gitignore'; + } + + if (!$this->hasVendorIgnore($ignoreFile)) { + $question = 'Would you like the vendor directory added to your .gitignore [yes]?'; + + if ($dialog->askConfirmation($output, $question, true)) { + $this->addVendorIgnore($ignoreFile); + } + } + } + } + + protected function interact(InputInterface $input, OutputInterface $output) + { + $git = $this->getGitConfig(); + + $dialog = $this->getHelperSet()->get('dialog'); + $formatter = $this->getHelperSet()->get('formatter'); + $output->writeln(array( + '', + $formatter->formatBlock('Welcome to the Composer config generator', 'bg=blue;fg=white', true), + '' + )); + + // namespace + $output->writeln(array( + '', + 'This command will guide you through creating your composer.json config.', + '', + )); + + $cwd = realpath("."); + + if (false === $name = $input->getOption('name')) { + $name = basename($cwd); + if (isset($git['github.user'])) { + $name = $git['github.user'] . '/' . $name; + } else { + // package names must be in the format foo/bar + $name = $name . '/' . $name; + } + } + + $name = $dialog->askAndValidate( + $output, + $dialog->getQuestion('Package name', $name), + function ($value) use ($name) { + if (null === $value) { + return $name; + } + + if (!preg_match('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}i', $value)) { + throw new \InvalidArgumentException( + 'The package name '.$value.' is invalid, it should have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+' + ); + } + + return $value; + } + ); + $input->setOption('name', $name); + + $description = $input->getOption('description') ?: false; + $description = $dialog->ask( + $output, + $dialog->getQuestion('Description', $description) + ); + $input->setOption('description', $description); + + if (false === $author = $input->getOption('author')) { + if (isset($git['user.name']) && isset($git['user.email'])) { + $author = sprintf('%s <%s>', $git['user.name'], $git['user.email']); + } + } + + $self = $this; + $author = $dialog->askAndValidate( + $output, + $dialog->getQuestion('Author', $author), + function ($value) use ($self, $author) { + if (null === $value) { + return $author; + } + + $author = $self->parseAuthorString($value); + + return sprintf('%s <%s>', $author['name'], $author['email']); + } + ); + $input->setOption('author', $author); + + $output->writeln(array( + '', + 'Define your dependencies.', + '' + )); + + $requirements = array(); + if ($dialog->askConfirmation($output, $dialog->getQuestion('Would you like to define your dependencies interactively', 'yes', '?'), true)) { + $requirements = $this->determineRequirements($input, $output); + } + $input->setOption('require', $requirements); + } + + protected function findPackages($name) + { + $composer = $this->getComposer(); + + $packages = array(); + + // create local repo, this contains all packages that are installed in the local project + $localRepo = $composer->getRepositoryManager()->getLocalRepository(); + + $token = strtolower($name); + foreach ($composer->getRepositoryManager()->getRepositories() as $repository) { + foreach ($repository->getPackages() as $package) { + if (false === ($pos = strpos($package->getName(), $token))) { + continue; + } + + $packages[] = $package; + } + } + + return $packages; + } + + protected function determineRequirements(InputInterface $input, OutputInterface $output) + { + $dialog = $this->getHelperSet()->get('dialog'); + $prompt = $dialog->getQuestion('Search for a package', false, ':'); + + $requires = $input->getOption('require') ?: array(); + + while (null !== $package = $dialog->ask($output, $prompt)) { + $matches = $this->findPackages($package); + + if (count($matches)) { + $output->writeln(array( + '', + sprintf('Found %s packages matching %s', count($matches), $package), + '' + )); + + foreach ($matches as $position => $package) { + $output->writeln(sprintf(' %5s %s %s', "[$position]", $package->getPrettyName(), $package->getPrettyVersion())); + } + + $output->writeln(''); + + $validator = function ($selection) use ($matches) { + if ('' === $selection) { + return false; + } + + if (!isset($matches[(int) $selection])) { + throw new \Exception('Not a valid selection'); + } + + return $matches[(int) $selection]; + }; + + $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add', false, ':'), $validator, 3); + + if (false !== $package) { + $requires[] = sprintf('%s %s', $package->getName(), $package->getPrettyVersion()); + } + } + } + + return $requires; + } + + protected function formatAuthors($author) + { + return array($this->parseAuthorString($author)); + } + + protected function formatRequirements(array $requirements) + { + $requires = array(); + foreach ($requirements as $requirement) { + list($packageName, $packageVersion) = explode(" ", $requirement); + + $requires[$packageName] = $packageVersion; + } + + return empty($requires) ? new \stdClass : $requires; + } + + protected function getGitConfig() + { + if (null !== $this->gitConfig) { + return $this->gitConfig; + } + + $finder = new ExecutableFinder(); + $gitBin = $finder->find('git'); + + $cmd = new Process(sprintf('%s config -l', $gitBin)); + $cmd->run(); + + if ($cmd->isSuccessful()) { + return $this->gitConfig = parse_ini_string($cmd->getOutput(), false, INI_SCANNER_RAW); + } + + return $this->gitConfig = array(); + } + + /** + * Checks the local .gitignore file for the Composer vendor directory. + * + * Tested patterns include: + * "/$vendor" + * "$vendor" + * "$vendor/" + * "/$vendor/" + * "/$vendor/*" + * "$vendor/*" + * + * @param string $ignoreFile + * @param string $vendor + * + * @return Boolean + */ + protected function hasVendorIgnore($ignoreFile, $vendor = 'vendor') + { + if (!file_exists($ignoreFile)) { + return false; + } + + $pattern = sprintf( + '~^/?%s(/|/\*)?$~', + preg_quote($vendor, '~') + ); + + $lines = file($ignoreFile, FILE_IGNORE_NEW_LINES); + foreach ($lines as $line) { + if (preg_match($pattern, $line)) { + return true; + } + } + + return false; + } + + protected function addVendorIgnore($ignoreFile, $vendor = 'vendor') + { + $contents = ""; + if (file_exists($ignoreFile)) { + $contents = file_get_contents($ignoreFile); + + if ("\n" !== substr($contents, 0, -1)) { + $contents .= "\n"; + } + } + + file_put_contents($ignoreFile, $contents . $vendor); + } +} \ No newline at end of file diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 6d1d6ba0b..fe6b6cb13 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -20,6 +20,7 @@ use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Finder\Finder; use Composer\Command; +use Composer\Command\Helper\DialogHelper; use Composer\Composer; use Composer\Factory; use Composer\IO\IOInterface; @@ -104,6 +105,7 @@ class Application extends BaseApplication { $this->add(new Command\AboutCommand()); $this->add(new Command\DependsCommand()); + $this->add(new Command\InitCommand()); $this->add(new Command\InstallCommand()); $this->add(new Command\UpdateCommand()); $this->add(new Command\SearchCommand()); @@ -114,4 +116,16 @@ class Application extends BaseApplication $this->add(new Command\SelfUpdateCommand()); } } + + /** + * {@inheritDoc} + */ + protected function getDefaultHelperSet() + { + $helperSet = parent::getDefaultHelperSet(); + + $helperSet->set(new DialogHelper()); + + return $helperSet; + } }