1<?php 2 3/* 4 * This file is part of Composer. 5 * 6 * (c) Nils Adermann <naderman@naderman.de> 7 * Jordi Boggiano <j.boggiano@seld.be> 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13namespace Composer\Repository\Vcs; 14 15use Composer\Config; 16use Composer\Json\JsonFile; 17use Composer\Util\ProcessExecutor; 18use Composer\Util\Filesystem; 19use Composer\IO\IOInterface; 20 21/** 22 * @author Per Bernhardt <plb@webfactory.de> 23 */ 24class HgDriver extends VcsDriver 25{ 26 protected $tags; 27 protected $branches; 28 protected $rootIdentifier; 29 protected $repoDir; 30 protected $infoCache = array(); 31 32 /** 33 * {@inheritDoc} 34 */ 35 public function initialize() 36 { 37 if (Filesystem::isLocalPath($this->url)) { 38 $this->repoDir = $this->url; 39 } else { 40 $cacheDir = $this->config->get('cache-vcs-dir'); 41 $this->repoDir = $cacheDir . '/' . preg_replace('{[^a-z0-9]}i', '-', $this->url) . '/'; 42 43 $fs = new Filesystem(); 44 $fs->ensureDirectoryExists($cacheDir); 45 46 if (!is_writable(dirname($this->repoDir))) { 47 throw new \RuntimeException('Can not clone '.$this->url.' to access package information. The "'.$cacheDir.'" directory is not writable by the current user.'); 48 } 49 50 // Ensure we are allowed to use this URL by config 51 $this->config->prohibitUrlByConfig($this->url, $this->io); 52 53 // update the repo if it is a valid hg repository 54 if (is_dir($this->repoDir) && 0 === $this->process->execute('hg summary', $output, $this->repoDir)) { 55 if (0 !== $this->process->execute('hg pull', $output, $this->repoDir)) { 56 $this->io->writeError('<error>Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')</error>'); 57 } 58 } else { 59 // clean up directory and do a fresh clone into it 60 $fs->removeDirectory($this->repoDir); 61 62 if (0 !== $this->process->execute(sprintf('hg clone --noupdate %s %s', ProcessExecutor::escape($this->url), ProcessExecutor::escape($this->repoDir)), $output, $cacheDir)) { 63 $output = $this->process->getErrorOutput(); 64 65 if (0 !== $this->process->execute('hg --version', $ignoredOutput)) { 66 throw new \RuntimeException('Failed to clone '.$this->url.', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); 67 } 68 69 throw new \RuntimeException('Failed to clone '.$this->url.', could not read packages from it' . "\n\n" .$output); 70 } 71 } 72 } 73 74 $this->getTags(); 75 $this->getBranches(); 76 } 77 78 /** 79 * {@inheritDoc} 80 */ 81 public function getRootIdentifier() 82 { 83 if (null === $this->rootIdentifier) { 84 $this->process->execute(sprintf('hg tip --template "{node}"'), $output, $this->repoDir); 85 $output = $this->process->splitLines($output); 86 $this->rootIdentifier = $output[0]; 87 } 88 89 return $this->rootIdentifier; 90 } 91 92 /** 93 * {@inheritDoc} 94 */ 95 public function getUrl() 96 { 97 return $this->url; 98 } 99 100 /** 101 * {@inheritDoc} 102 */ 103 public function getSource($identifier) 104 { 105 return array('type' => 'hg', 'url' => $this->getUrl(), 'reference' => $identifier); 106 } 107 108 /** 109 * {@inheritDoc} 110 */ 111 public function getDist($identifier) 112 { 113 return null; 114 } 115 116 /** 117 * {@inheritDoc} 118 */ 119 public function getComposerInformation($identifier) 120 { 121 if (!isset($this->infoCache[$identifier])) { 122 $this->process->execute(sprintf('hg cat -r %s composer.json', ProcessExecutor::escape($identifier)), $composer, $this->repoDir); 123 124 if (!trim($composer)) { 125 return; 126 } 127 128 $composer = JsonFile::parseJson($composer, $identifier); 129 130 if (empty($composer['time'])) { 131 $this->process->execute(sprintf('hg log --template "{date|rfc3339date}" -r %s', ProcessExecutor::escape($identifier)), $output, $this->repoDir); 132 $date = new \DateTime(trim($output), new \DateTimeZone('UTC')); 133 $composer['time'] = $date->format('Y-m-d H:i:s'); 134 } 135 $this->infoCache[$identifier] = $composer; 136 } 137 138 return $this->infoCache[$identifier]; 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 public function getTags() 145 { 146 if (null === $this->tags) { 147 $tags = array(); 148 149 $this->process->execute('hg tags', $output, $this->repoDir); 150 foreach ($this->process->splitLines($output) as $tag) { 151 if ($tag && preg_match('(^([^\s]+)\s+\d+:(.*)$)', $tag, $match)) { 152 $tags[$match[1]] = $match[2]; 153 } 154 } 155 unset($tags['tip']); 156 157 $this->tags = $tags; 158 } 159 160 return $this->tags; 161 } 162 163 /** 164 * {@inheritDoc} 165 */ 166 public function getBranches() 167 { 168 if (null === $this->branches) { 169 $branches = array(); 170 $bookmarks = array(); 171 172 $this->process->execute('hg branches', $output, $this->repoDir); 173 foreach ($this->process->splitLines($output) as $branch) { 174 if ($branch && preg_match('(^([^\s]+)\s+\d+:([a-f0-9]+))', $branch, $match)) { 175 $branches[$match[1]] = $match[2]; 176 } 177 } 178 179 $this->process->execute('hg bookmarks', $output, $this->repoDir); 180 foreach ($this->process->splitLines($output) as $branch) { 181 if ($branch && preg_match('(^(?:[\s*]*)([^\s]+)\s+\d+:(.*)$)', $branch, $match)) { 182 $bookmarks[$match[1]] = $match[2]; 183 } 184 } 185 186 // Branches will have preference over bookmarks 187 $this->branches = array_merge($bookmarks, $branches); 188 } 189 190 return $this->branches; 191 } 192 193 /** 194 * {@inheritDoc} 195 */ 196 public static function supports(IOInterface $io, Config $config, $url, $deep = false) 197 { 198 if (preg_match('#(^(?:https?|ssh)://(?:[^@]+@)?bitbucket.org|https://(?:.*?)\.kilnhg.com)#i', $url)) { 199 return true; 200 } 201 202 // local filesystem 203 if (Filesystem::isLocalPath($url)) { 204 $url = Filesystem::getPlatformPath($url); 205 if (!is_dir($url)) { 206 return false; 207 } 208 209 $process = new ProcessExecutor(); 210 // check whether there is a hg repo in that path 211 if ($process->execute('hg summary', $output, $url) === 0) { 212 return true; 213 } 214 } 215 216 if (!$deep) { 217 return false; 218 } 219 220 $processExecutor = new ProcessExecutor(); 221 $exit = $processExecutor->execute(sprintf('hg identify %s', ProcessExecutor::escape($url)), $ignored); 222 223 return $exit === 0; 224 } 225} 226