diff --git a/bin/composer b/bin/composer index b188aa337..15b9a907c 100755 --- a/bin/composer +++ b/bin/composer @@ -29,6 +29,7 @@ $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); // initialize download manager $dm = new Downloader\DownloadManager(); $dm->setDownloader('git', new Downloader\GitDownloader()); +$dm->setDownloader('hg', new Downloader\HgDownloader()); $dm->setDownloader('pear', new Downloader\PearDownloader()); $dm->setDownloader('zip', new Downloader\ZipDownloader()); diff --git a/src/Composer/Downloader/HgDownloader.php b/src/Composer/Downloader/HgDownloader.php new file mode 100644 index 000000000..32d23b770 --- /dev/null +++ b/src/Composer/Downloader/HgDownloader.php @@ -0,0 +1,74 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader; + +use Composer\Package\PackageInterface; + +/** + * @author Per Bernhardt + */ +class HgDownloader implements DownloaderInterface +{ + /** + * {@inheritDoc} + */ + public function getInstallationSource() + { + return 'source'; + } + + /** + * {@inheritDoc} + */ + public function download(PackageInterface $package, $path) + { + if (!$package->getSourceReference()) { + throw new \InvalidArgumentException('The given package is missing reference information'); + } + + $url = escapeshellarg($package->getSourceUrl()); + $ref = escapeshellarg($package->getSourceReference()); + system(sprintf('hg clone %s %s && cd %2$s && hg up %s', $url, $path, $ref)); + } + + /** + * {@inheritDoc} + */ + public function update(PackageInterface $initial, PackageInterface $target, $path) + { + if (!$target->getSourceReference()) { + throw new \InvalidArgumentException('The given package is missing reference information'); + } + + $this->enforceCleanDirectory($path); + system(sprintf('cd %s && hg pull && hg up %s', $path, escapeshellarg($target->getSourceReference()))); + } + + /** + * {@inheritDoc} + */ + public function remove(PackageInterface $package, $path) + { + $this->enforceCleanDirectory($path); + $fs = new Util\Filesystem(); + $fs->remove($path); + } + + private function enforceCleanDirectory($path) + { + exec(sprintf('cd %s && hg st', $path), $output); + if (implode('', $output)) { + throw new \RuntimeException('Source directory has uncommitted changes'); + } + } +} diff --git a/src/Composer/Repository/Vcs/GitBitbucketDriver.php b/src/Composer/Repository/Vcs/GitBitbucketDriver.php new file mode 100644 index 000000000..1ac4f85f3 --- /dev/null +++ b/src/Composer/Repository/Vcs/GitBitbucketDriver.php @@ -0,0 +1,163 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository\Vcs; + +use Composer\Json\JsonFile; + +/** + * @author Per Bernhardt + */ +class GitBitbucketDriver implements VcsDriverInterface +{ + protected $url; + protected $owner; + protected $repository; + protected $tags; + protected $branches; + protected $rootIdentifier; + protected $infoCache = array(); + + public function __construct($url) + { + $this->url = $url; + preg_match('#^https://bitbucket\.org/([^/]+)/(.+?)\.git$#', $url, $match); + $this->owner = $match[1]; + $this->repository = $match[2]; + } + + /** + * {@inheritDoc} + */ + public function initialize() + { + } + + /** + * {@inheritDoc} + */ + public function getRootIdentifier() + { + if (null === $this->rootIdentifier) { + $repoData = json_decode(file_get_contents('https://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository), true); + $this->rootIdentifier = $repoData['main_branch'] ?: 'master'; + } + + return $this->rootIdentifier; + } + + /** + * {@inheritDoc} + */ + public function getUrl() + { + return $this->url; + } + + /** + * {@inheritDoc} + */ + public function getSource($identifier) + { + $label = array_search($identifier, $this->getTags()) ?: $identifier; + + return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $label); + } + + /** + * {@inheritDoc} + */ + public function getDist($identifier) + { + $label = array_search($identifier, $this->getTags()) ?: $identifier; + $url = 'https://bitbucket.org/'.$this->owner.'/'.$this->repository.'/get/'.$label.'.zip'; + + return array('type' => 'zip', 'url' => $url, 'reference' => $label, 'shasum' => ''); + } + + /** + * {@inheritDoc} + */ + public function getComposerInformation($identifier) + { + if (!isset($this->infoCache[$identifier])) { + $composer = @file_get_contents('https://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json'); + if (!$composer) { + throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl()); + } + + $composer = JsonFile::parseJson($composer); + + if (!isset($composer['time'])) { + $changeset = json_decode(file_get_contents('https://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier), true); + $composer['time'] = $changeset['timestamp']; + } + $this->infoCache[$identifier] = $composer; + } + + return $this->infoCache[$identifier]; + } + + /** + * {@inheritDoc} + */ + public function getTags() + { + if (null === $this->tags) { + $tagsData = json_decode(file_get_contents('https://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'), true); + $this->tags = array(); + foreach ($tagsData as $tag => $data) { + $this->tags[$tag] = $data['raw_node']; + } + } + + return $this->tags; + } + + /** + * {@inheritDoc} + */ + public function getBranches() + { + if (null === $this->branches) { + $branchData = json_decode(file_get_contents('https://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'), true); + $this->branches = array(); + foreach ($branchData as $branch => $data) { + $this->branches[$branch] = $data['raw_node']; + } + } + + return $this->branches; + } + + /** + * {@inheritDoc} + */ + public function hasComposerFile($identifier) + { + try { + $this->getComposerInformation($identifier); + return true; + } catch (\Exception $e) { + } + + return false; + } + + /** + * {@inheritDoc} + */ + public static function supports($url, $deep = false) + { + return preg_match('#^https://bitbucket\.org/([^/]+)/(.+?)\.git$#', $url, $match); + } +} diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index b74f2e87e..3661fe046 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -46,7 +46,7 @@ class GitDriver implements VcsDriverInterface if (null === $this->rootIdentifier) { $this->rootIdentifier = 'master'; exec(sprintf('cd %s && git branch --no-color -r', escapeshellarg($this->tmpDir)), $output); - foreach ($output as $key => $branch) { + foreach ($output as $branch) { if ($branch && preg_match('{/HEAD +-> +[^/]+/(\S+)}', $branch, $match)) { $this->rootIdentifier = $match[1]; break; @@ -132,7 +132,7 @@ class GitDriver implements VcsDriverInterface $branches = array(); exec(sprintf('cd %s && git branch --no-color -rv', escapeshellarg($this->tmpDir)), $output); - foreach ($output as $key => $branch) { + foreach ($output as $branch) { if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) { preg_match('{^ *[^/]+/(\S+) *([a-f0-9]+) .*$}', $branch, $match); $branches[$match[1]] = $match[2]; diff --git a/src/Composer/Repository/Vcs/HgBitbucketDriver.php b/src/Composer/Repository/Vcs/HgBitbucketDriver.php new file mode 100644 index 000000000..9c1c82b3b --- /dev/null +++ b/src/Composer/Repository/Vcs/HgBitbucketDriver.php @@ -0,0 +1,164 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository\Vcs; + +use Composer\Json\JsonFile; + +/** + * @author Per Bernhardt + */ +class HgBitbucketDriver implements VcsDriverInterface +{ + protected $url; + protected $owner; + protected $repository; + protected $tags; + protected $branches; + protected $rootIdentifier; + protected $infoCache = array(); + + public function __construct($url) + { + $this->url = $url; + preg_match('#^https://bitbucket\.org/([^/]+)/([^/]+)/?$#', $url, $match); + $this->owner = $match[1]; + $this->repository = $match[2]; + } + + /** + * {@inheritDoc} + */ + public function initialize() + { + } + + /** + * {@inheritDoc} + */ + public function getRootIdentifier() + { + if (null === $this->rootIdentifier) { + $repoData = json_decode(file_get_contents('https://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'), true); + $this->rootIdentifier = $repoData['tip']['raw_node']; + } + + return $this->rootIdentifier; + } + + /** + * {@inheritDoc} + */ + public function getUrl() + { + return $this->url; + } + + /** + * {@inheritDoc} + */ + public function getSource($identifier) + { + $label = array_search($identifier, $this->getTags()) ?: $identifier; + + return array('type' => 'hg', 'url' => $this->getUrl(), 'reference' => $label); + } + + /** + * {@inheritDoc} + */ + public function getDist($identifier) + { + $label = array_search($identifier, $this->getTags()) ?: $identifier; + $url = 'https://bitbucket.org/'.$this->owner.'/'.$this->repository.'/get/'.$label.'.zip'; + + return array('type' => 'zip', 'url' => $url, 'reference' => $label, 'shasum' => ''); + } + + /** + * {@inheritDoc} + */ + public function getComposerInformation($identifier) + { + if (!isset($this->infoCache[$identifier])) { + $composer = @file_get_contents('https://bitbucket.org/'.$this->owner.'/'.$this->repository.'/raw/'.$identifier.'/composer.json'); + if (!$composer) { + throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl()); + } + + $composer = JsonFile::parseJson($composer); + + if (!isset($composer['time'])) { + $changeset = json_decode(file_get_contents('https://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/changesets/'.$identifier), true); + $composer['time'] = $changeset['timestamp']; + } + $this->infoCache[$identifier] = $composer; + } + + return $this->infoCache[$identifier]; + } + + /** + * {@inheritDoc} + */ + public function getTags() + { + if (null === $this->tags) { + $tagsData = json_decode(file_get_contents('https://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/tags'), true); + $this->tags = array(); + foreach ($tagsData as $tag => $data) { + $this->tags[$tag] = $data['raw_node']; + } + unset($this->tags['tip']); + } + + return $this->tags; + } + + /** + * {@inheritDoc} + */ + public function getBranches() + { + if (null === $this->branches) { + $branchData = json_decode(file_get_contents('https://api.bitbucket.org/1.0/repositories/'.$this->owner.'/'.$this->repository.'/branches'), true); + $this->branches = array(); + foreach ($branchData as $branch => $data) { + $this->branches[$branch] = $data['raw_node']; + } + } + + return $this->branches; + } + + /** + * {@inheritDoc} + */ + public function hasComposerFile($identifier) + { + try { + $this->getComposerInformation($identifier); + return true; + } catch (\Exception $e) { + } + + return false; + } + + /** + * {@inheritDoc} + */ + public static function supports($url, $deep = false) + { + return preg_match('#^https://bitbucket\.org/([^/]+)/([^/]+)/?$#', $url, $match); + } +} diff --git a/src/Composer/Repository/Vcs/HgDriver.php b/src/Composer/Repository/Vcs/HgDriver.php new file mode 100644 index 000000000..29af3199b --- /dev/null +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -0,0 +1,187 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository\Vcs; + +use Composer\Json\JsonFile; + +/** + * @author Per Bernhardt + */ +class HgDriver implements VcsDriverInterface +{ + protected $url; + protected $tags; + protected $branches; + protected $rootIdentifier; + protected $infoCache = array(); + + public function __construct($url) + { + $this->url = $url; + $this->tmpDir = sys_get_temp_dir() . '/composer-' . preg_replace('{[^a-z0-9]}i', '-', $url) . '/'; + } + + /** + * {@inheritDoc} + */ + public function initialize() + { + $url = escapeshellarg($this->url); + $tmpDir = escapeshellarg($this->tmpDir); + if (is_dir($this->tmpDir)) { + exec(sprintf('cd %s && hg pull -u', $tmpDir), $output); + } else { + exec(sprintf('hg clone %s %s', $url, $tmpDir), $output); + } + + $this->getTags(); + $this->getBranches(); + } + + /** + * {@inheritDoc} + */ + public function getRootIdentifier() + { + $tmpDir = escapeshellarg($this->tmpDir); + if (null === $this->rootIdentifier) { + exec(sprintf('cd %s && hg tip --template "{rev}:{node|short}" --color never', $tmpDir), $output); + $this->rootIdentifier = $output[0]; + } + + return $this->rootIdentifier; + } + + /** + * {@inheritDoc} + */ + public function getUrl() + { + return $this->url; + } + + /** + * {@inheritDoc} + */ + public function getSource($identifier) + { + $label = array_search($identifier, (array)$this->tags) ? : $identifier; + + return array('type' => 'hg', 'url' => $this->getUrl(), 'reference' => $label); + } + + /** + * {@inheritDoc} + */ + public function getDist($identifier) + { + return null; + } + + /** + * {@inheritDoc} + */ + public function getComposerInformation($identifier) + { + if (!isset($this->infoCache[$identifier])) { + exec(sprintf('cd %s && hg cat --color never -r %s composer.json', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $output); + $composer = implode("\n", $output); + unset($output); + + if (!$composer) { + throw new \UnexpectedValueException('Failed to retrieve composer information for identifier ' . $identifier . ' in ' . $this->getUrl()); + } + + $composer = JsonFile::parseJson($composer); + + if (!isset($composer['time'])) { + exec(sprintf('cd %s && hg log --template "{date|rfc822date}" -r %s', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $output); + $date = new \DateTime($output[0]); + $composer['time'] = $date->format('Y-m-d H:i:s'); + } + $this->infoCache[$identifier] = $composer; + } + + return $this->infoCache[$identifier]; + } + + /** + * {@inheritDoc} + */ + public function getTags() + { + if (null === $this->tags) { + exec(sprintf('cd %s && hg tags --color never', escapeshellarg($this->tmpDir)), $output); + foreach ($output as $tag) { + preg_match('(^([^\s]+)[\s]+[\d+]:(.*)$)', $tag, $match); + $tags[$match[1]] = $match[2]; + } + unset($tags['tip']); + $this->tags = $tags; + } + + return $this->tags; + } + + /** + * {@inheritDoc} + */ + public function getBranches() + { + if (null === $this->branches) { + $branches = array(); + + exec(sprintf('cd %s && hg branches --color never', escapeshellarg($this->tmpDir)), $output); + foreach ($output as $branch) { + preg_match('(^([^\s]+)[\s]+[\d+]:(.*)$)', $branch, $match); + $branches[$match[1]] = $match[2]; + } + + $this->branches = $branches; + } + + return $this->branches; + } + + /** + * {@inheritDoc} + */ + public function hasComposerFile($identifier) + { + try { + $this->getComposerInformation($identifier); + return true; + } catch (\Exception $e) { + } + + return false; + } + + /** + * {@inheritDoc} + */ + public static function supports($url, $deep = false) + { + if (preg_match('#(^(?:https?|ssh)://(?:[^@]@)?bitbucket.org|https://(?:.*?)\.kilnhg.com)#i', $url)) { + return true; + } + + if (!$deep) { + return false; + } + + exec(sprintf('hg identify %s', escapeshellarg($url)), $output); + + return (boolean)$output; + } +} diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 5b5c4a8d4..5e13ac151 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -31,8 +31,10 @@ class VcsRepository extends ArrayRepository $drivers = array( 'Composer\Repository\Vcs\GitHubDriver', + 'Composer\Repository\Vcs\GitBitbucketDriver', 'Composer\Repository\Vcs\GitDriver', - 'Composer\Repository\Vcs\SvnDriver', + 'Composer\Repository\Vcs\HgBitbucketDriver', + 'Composer\Repository\Vcs\HgDriver', ); foreach ($drivers as $driver) {