diff --git a/src/Composer/Command/Helper/DialogHelper.php b/src/Composer/Command/Helper/DialogHelper.php new file mode 100644 index 000000000..d08ac1946 --- /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 = null, $sep = ':') + { + return $default !== null ? + 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..80c487935 --- /dev/null +++ b/src/Composer/Command/InitCommand.php @@ -0,0 +1,386 @@ + + * 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 Composer\Repository\CompositeRepository; +use Composer\Repository\PlatformRepository; +use Composer\Repository\ComposerRepository; +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 + * @author Jordi Boggiano + */ +class InitCommand extends Command +{ + private $gitConfig; + private $repos; + + 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; + } elseif (!empty($_SERVER['USERNAME'])) { + $name = $_SERVER['USERNAME'] . '/' . $name; + } elseif (get_current_user()) { + $name = get_current_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) + { + $packages = array(); + + // init repos + if (!$this->repos) { + $this->repos = new CompositeRepository(array( + new PlatformRepository, + new ComposerRepository(array('url' => 'http://packagist.org')) + )); + } + + $token = strtolower($name); + foreach ($this->repos->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 (!is_numeric($selection) && preg_match('{^\s*(\S+) +(\S.*)\s*}', $selection, $matches)) { + return $matches[1].' '.$matches[2]; + } + + if (!isset($matches[(int) $selection])) { + throw new \Exception('Not a valid selection'); + } + + $package = $matches[(int) $selection]; + + return sprintf('%s %s', $package->getName(), $package->getPrettyVersion()); + }; + + $package = $dialog->askAndValidate($output, $dialog->getQuestion('Enter package # to add, or a couple if it is not listed', false, ':'), $validator, 3); + + if (false !== $package) { + $requires[] = $package; + } + } + } + + 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, 2); + + $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. "\n"); + } +} \ No newline at end of file diff --git a/src/Composer/Command/SearchCommand.php b/src/Composer/Command/SearchCommand.php index 4a937539d..accaf853f 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -15,6 +15,9 @@ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; +use Composer\Repository\CompositeRepository; +use Composer\Repository\PlatformRepository; +use Composer\Repository\ComposerRepository; /** * @author Robert Schönthal @@ -40,27 +43,32 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - $composer = $this->getComposer(); - - // create local repo, this contains all packages that are installed in the local project - $localRepo = $composer->getRepositoryManager()->getLocalRepository(); + // init repos + $platformRepo = new PlatformRepository; + if ($composer = $this->getComposer(false)) { + $localRepo = $composer->getRepositoryManager()->getLocalRepository(); + $installedRepo = new CompositeRepository(array($localRepo, $platformRepo)); + $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories())); + } else { + $output->writeln('No composer.json found in the current directory, showing packages from packagist.org'); + $installedRepo = $platformRepo; + $repos = new CompositeRepository(array($installedRepo, new ComposerRepository(array('url' => 'http://packagist.org')))); + } $tokens = array_map('strtolower', $input->getArgument('tokens')); - foreach ($composer->getRepositoryManager()->getRepositories() as $repository) { - foreach ($repository->getPackages() as $package) { - foreach ($tokens as $token) { - if (false === ($pos = strpos($package->getName(), $token))) { - continue; - } - - $state = $localRepo->hasPackage($package) ? 'installed' : $state = 'available'; - - $name = substr($package->getPrettyName(), 0, $pos) - . '' . substr($package->getPrettyName(), $pos, strlen($token)) . '' - . substr($package->getPrettyName(), $pos + strlen($token)); - $output->writeln($state . ': ' . $name . ' ' . $package->getPrettyVersion() . ''); - continue 2; + foreach ($repos->getPackages() as $package) { + foreach ($tokens as $token) { + if (false === ($pos = strpos($package->getName(), $token))) { + continue; } + + $state = $localRepo->hasPackage($package) ? 'installed' : $state = 'available'; + + $name = substr($package->getPrettyName(), 0, $pos) + . '' . substr($package->getPrettyName(), $pos, strlen($token)) . '' + . substr($package->getPrettyName(), $pos + strlen($token)); + $output->writeln($state . ': ' . $name . ' ' . $package->getPrettyVersion() . ''); + continue 2; } } } diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index febc78295..426fc6882 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -42,7 +42,7 @@ EOT { $ctx = StreamContextFactory::getContext(); - $latest = trim(file_get_contents('http://getcomposer.org/version'), false, $ctx); + $latest = trim(file_get_contents('http://getcomposer.org/version', false, $ctx)); if (Composer::VERSION !== $latest) { $output->writeln(sprintf("Updating to version %s.", $latest)); 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; + } } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index ef9ae8e6e..b830986cd 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -98,6 +98,13 @@ abstract class FileDownloader implements DownloaderInterface $contentDir = glob($path . '/*'); if (1 === count($contentDir)) { $contentDir = $contentDir[0]; + + // Rename the content directory to avoid error when moving up + // a child folder with the same name + $temporaryName = md5(time().rand()); + rename($contentDir, $temporaryName); + $contentDir = $temporaryName; + foreach (array_merge(glob($contentDir . '/.*'), glob($contentDir . '/*')) as $file) { if (trim(basename($file), '.')) { rename($file, $path . '/' . basename($file)); diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index ecd286d78..307139a6f 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -16,6 +16,16 @@ use Composer\Repository\RepositoryManager; use Composer\Composer; use Composer\Util\StreamContextFactory; +if (!defined('JSON_UNESCAPED_SLASHES')) { + define('JSON_UNESCAPED_SLASHES', 64); +} +if (!defined('JSON_PRETTY_PRINT')) { + define('JSON_PRETTY_PRINT', 128); +} +if (!defined('JSON_UNESCAPED_UNICODE')) { + define('JSON_UNESCAPED_UNICODE', 256); +} + /** * Reads/writes json files. * @@ -77,10 +87,9 @@ class JsonFile * Writes json file. * * @param array $hash writes hash into json file - * @param Boolean $prettyPrint If true, output is pretty-printed - * @param Boolean $unescapeUnicode If true, unicode chars in output are unescaped + * @param int $options json_encode options */ - public function write(array $hash, $prettyPrint = true, $unescapeUnicode = true) + public function write(array $hash, $options = 448) { $dir = dirname($this->path); if (!is_dir($dir)) { @@ -95,7 +104,7 @@ class JsonFile ); } } - file_put_contents($this->path, static::encode($hash, $prettyPrint, $unescapeUnicode)); + file_put_contents($this->path, static::encode($hash, $options). ($options & JSON_PRETTY_PRINT ? "\n" : '')); } /** @@ -105,23 +114,20 @@ class JsonFile * http://recursive-design.com/blog/2008/03/11/format-json-with-php/ * * @param array $hash Data to encode into a formatted JSON string - * @param Boolean $prettyPrint If true, output is pretty-printed - * @param Boolean $unescapeUnicode If true, unicode chars in output are unescaped - * @return string Indented version of the original JSON string + * @param int $options json_encode options + * @return string Encoded json */ - static public function encode(array $hash, $prettyPrint = true, $unescapeUnicode = true) + static public function encode(array $hash, $options = 448) { if (version_compare(PHP_VERSION, '5.4', '>=')) { - $options = $prettyPrint ? JSON_PRETTY_PRINT : 0; - if ($unescapeUnicode) { - $options |= JSON_UNESCAPED_UNICODE; - } - return json_encode($hash, $options); } $json = json_encode($hash); + $prettyPrint = (Boolean) ($options & JSON_PRETTY_PRINT); + $unescapeUnicode = (Boolean) ($options & JSON_UNESCAPED_UNICODE); + if (!$prettyPrint && !$unescapeUnicode) { return $json; }