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