PageRenderTime 48ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Composer/Repository/Vcs/GitHubDriver.php

https://github.com/ramondelafuente/composer
PHP | 405 lines | 274 code | 58 blank | 73 comment | 46 complexity | d016d98bf87121f2c95b6149d413802d MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Repository\Vcs;
  12. use Composer\Downloader\TransportException;
  13. use Composer\Json\JsonFile;
  14. use Composer\Cache;
  15. use Composer\IO\IOInterface;
  16. use Composer\Util\RemoteFilesystem;
  17. use Composer\Util\GitHub;
  18. /**
  19. * @author Jordi Boggiano <j.boggiano@seld.be>
  20. */
  21. class GitHubDriver extends VcsDriver
  22. {
  23. protected $cache;
  24. protected $owner;
  25. protected $repository;
  26. protected $tags;
  27. protected $branches;
  28. protected $rootIdentifier;
  29. protected $hasIssues;
  30. protected $infoCache = array();
  31. protected $isPrivate = false;
  32. /**
  33. * Git Driver
  34. *
  35. * @var GitDriver
  36. */
  37. protected $gitDriver;
  38. /**
  39. * {@inheritDoc}
  40. */
  41. public function initialize()
  42. {
  43. preg_match('#^(?:(?:https?|git)://github\.com/|git@github\.com:)([^/]+)/(.+?)(?:\.git)?$#', $this->url, $match);
  44. $this->owner = $match[1];
  45. $this->repository = $match[2];
  46. $this->originUrl = 'github.com';
  47. $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository);
  48. $this->fetchRootIdentifier();
  49. }
  50. /**
  51. * {@inheritDoc}
  52. */
  53. public function getRootIdentifier()
  54. {
  55. if ($this->gitDriver) {
  56. return $this->gitDriver->getRootIdentifier();
  57. }
  58. return $this->rootIdentifier;
  59. }
  60. /**
  61. * {@inheritDoc}
  62. */
  63. public function getUrl()
  64. {
  65. if ($this->gitDriver) {
  66. return $this->gitDriver->getUrl();
  67. }
  68. return 'https://github.com/'.$this->owner.'/'.$this->repository.'.git';
  69. }
  70. /**
  71. * {@inheritDoc}
  72. */
  73. public function getSource($identifier)
  74. {
  75. if ($this->gitDriver) {
  76. return $this->gitDriver->getSource($identifier);
  77. }
  78. if ($this->isPrivate) {
  79. // Private GitHub repositories should be accessed using the
  80. // SSH version of the URL.
  81. $url = $this->generateSshUrl();
  82. } else {
  83. $url = $this->getUrl();
  84. }
  85. return array('type' => 'git', 'url' => $url, 'reference' => $identifier);
  86. }
  87. /**
  88. * {@inheritDoc}
  89. */
  90. public function getDist($identifier)
  91. {
  92. if ($this->gitDriver) {
  93. return $this->gitDriver->getDist($identifier);
  94. }
  95. $url = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/zipball/'.$identifier;
  96. return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => '');
  97. }
  98. /**
  99. * {@inheritDoc}
  100. */
  101. public function getComposerInformation($identifier)
  102. {
  103. if ($this->gitDriver) {
  104. return $this->gitDriver->getComposerInformation($identifier);
  105. }
  106. if (preg_match('{[a-f0-9]{40}}i', $identifier) && $res = $this->cache->read($identifier)) {
  107. $this->infoCache[$identifier] = JsonFile::parseJson($res);
  108. }
  109. if (!isset($this->infoCache[$identifier])) {
  110. $notFoundRetries = 2;
  111. while ($notFoundRetries) {
  112. try {
  113. $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/contents/composer.json?ref='.urlencode($identifier);
  114. $composer = JsonFile::parseJson($this->getContents($resource));
  115. if (empty($composer['content']) || $composer['encoding'] !== 'base64' || !($composer = base64_decode($composer['content']))) {
  116. throw new \RuntimeException('Could not retrieve composer.json from '.$resource);
  117. }
  118. break;
  119. } catch (TransportException $e) {
  120. if (404 !== $e->getCode()) {
  121. throw $e;
  122. }
  123. // TODO should be removed when possible
  124. // retry fetching if github returns a 404 since they happen randomly
  125. $notFoundRetries--;
  126. $composer = false;
  127. }
  128. }
  129. if ($composer) {
  130. $composer = JsonFile::parseJson($composer, $resource);
  131. if (!isset($composer['time'])) {
  132. $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier);
  133. $commit = JsonFile::parseJson($this->getContents($resource), $resource);
  134. $composer['time'] = $commit['commit']['committer']['date'];
  135. }
  136. if (!isset($composer['support']['source'])) {
  137. $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier;
  138. $composer['support']['source'] = sprintf('https://github.com/%s/%s/tree/%s', $this->owner, $this->repository, $label);
  139. }
  140. if (!isset($composer['support']['issues']) && $this->hasIssues) {
  141. $composer['support']['issues'] = sprintf('https://github.com/%s/%s/issues', $this->owner, $this->repository);
  142. }
  143. }
  144. if (preg_match('{[a-f0-9]{40}}i', $identifier)) {
  145. $this->cache->write($identifier, json_encode($composer));
  146. }
  147. $this->infoCache[$identifier] = $composer;
  148. }
  149. return $this->infoCache[$identifier];
  150. }
  151. /**
  152. * {@inheritDoc}
  153. */
  154. public function getTags()
  155. {
  156. if ($this->gitDriver) {
  157. return $this->gitDriver->getTags();
  158. }
  159. if (null === $this->tags) {
  160. $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/tags';
  161. $tagsData = JsonFile::parseJson($this->getContents($resource), $resource);
  162. $this->tags = array();
  163. foreach ($tagsData as $tag) {
  164. $this->tags[$tag['name']] = $tag['commit']['sha'];
  165. }
  166. }
  167. return $this->tags;
  168. }
  169. /**
  170. * {@inheritDoc}
  171. */
  172. public function getBranches()
  173. {
  174. if ($this->gitDriver) {
  175. return $this->gitDriver->getBranches();
  176. }
  177. if (null === $this->branches) {
  178. $resource = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads';
  179. $branchData = JsonFile::parseJson($this->getContents($resource), $resource);
  180. $this->branches = array();
  181. foreach ($branchData as $branch) {
  182. $name = substr($branch['ref'], 11);
  183. $this->branches[$name] = $branch['object']['sha'];
  184. }
  185. }
  186. return $this->branches;
  187. }
  188. /**
  189. * {@inheritDoc}
  190. */
  191. public static function supports(IOInterface $io, $url, $deep = false)
  192. {
  193. if (!preg_match('#^((?:https?|git)://github\.com/|git@github\.com:)([^/]+)/(.+?)(?:\.git)?$#', $url)) {
  194. return false;
  195. }
  196. if (!extension_loaded('openssl')) {
  197. if ($io->isVerbose()) {
  198. $io->write('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.');
  199. }
  200. return false;
  201. }
  202. return true;
  203. }
  204. /**
  205. * Generate an SSH URL
  206. *
  207. * @return string
  208. */
  209. protected function generateSshUrl()
  210. {
  211. return 'git@github.com:'.$this->owner.'/'.$this->repository.'.git';
  212. }
  213. /**
  214. * {@inheritDoc}
  215. */
  216. protected function getContents($url, $fetchingRepoData = false)
  217. {
  218. try {
  219. return parent::getContents($url);
  220. } catch (TransportException $e) {
  221. $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->remoteFilesystem);
  222. switch ($e->getCode()) {
  223. case 401:
  224. case 404:
  225. // try to authorize only if we are fetching the main /repos/foo/bar data, otherwise it must be a real 404
  226. if (!$fetchingRepoData) {
  227. throw $e;
  228. }
  229. if ($gitHubUtil->authorizeOAuth($this->originUrl)) {
  230. return parent::getContents($url);
  231. }
  232. if (!$this->io->isInteractive()) {
  233. return $this->attemptCloneFallback();
  234. }
  235. $gitHubUtil->authorizeOAuthInteractively($this->originUrl, 'Your GitHub credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>)');
  236. return parent::getContents($url);
  237. case 403:
  238. if (!$this->io->hasAuthentication($this->originUrl) && $gitHubUtil->authorizeOAuth($this->originUrl)) {
  239. return parent::getContents($url);
  240. }
  241. if (!$this->io->isInteractive() && $fetchingRepoData) {
  242. return $this->attemptCloneFallback();
  243. }
  244. $rateLimited = false;
  245. foreach ($e->getHeaders() as $header) {
  246. if (preg_match('{^X-RateLimit-Remaining: *0$}i', trim($header))) {
  247. $rateLimited = true;
  248. }
  249. }
  250. if (!$this->io->hasAuthentication($this->originUrl)) {
  251. if (!$this->io->isInteractive()) {
  252. $this->io->write('<error>GitHub API limit exhausted. Failed to get metadata for the '.$this->url.' repository, try running in interactive mode so that you can enter your GitHub credentials to increase the API limit</error>');
  253. throw $e;
  254. }
  255. $gitHubUtil->authorizeOAuthInteractively($this->originUrl, 'API limit exhausted. Enter your GitHub credentials to get a larger API limit (<info>'.$this->url.'</info>)');
  256. return parent::getContents($url);
  257. }
  258. if ($rateLimited) {
  259. $rateLimit = $this->getRateLimit($e->getHeaders());
  260. $this->io->write(sprintf(
  261. '<error>GitHub API limit (%d calls/hr) is exhausted. You are already authorized so you have to wait until %s before doing more requests</error>',
  262. $rateLimit['limit'],
  263. $rateLimit['reset']
  264. ));
  265. }
  266. throw $e;
  267. default:
  268. throw $e;
  269. }
  270. }
  271. }
  272. /**
  273. * Extract ratelimit from response.
  274. *
  275. * @param array $headers Headers from Composer\Downloader\TransportException.
  276. *
  277. * @return array Associative array with the keys limit and reset.
  278. */
  279. protected function getRateLimit(array $headers)
  280. {
  281. $rateLimit = array(
  282. 'limit' => '?',
  283. 'reset' => '?',
  284. );
  285. foreach ($headers as $header) {
  286. $header = trim($header);
  287. if (false === strpos($header, 'X-RateLimit-')) {
  288. continue;
  289. }
  290. list($type, $value) = explode(':', $header, 2);
  291. switch ($type) {
  292. case 'X-RateLimit-Limit':
  293. $rateLimit['limit'] = (int) trim($value);
  294. break;
  295. case 'X-RateLimit-Reset':
  296. $rateLimit['reset'] = date('Y-m-d H:i:s', (int) trim($value));
  297. break;
  298. }
  299. }
  300. return $rateLimit;
  301. }
  302. /**
  303. * Fetch root identifier from GitHub
  304. *
  305. * @throws TransportException
  306. */
  307. protected function fetchRootIdentifier()
  308. {
  309. $repoDataUrl = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository;
  310. $repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl);
  311. if (null === $repoData && null !== $this->gitDriver) {
  312. return;
  313. }
  314. $this->isPrivate = !empty($repoData['private']);
  315. if (isset($repoData['default_branch'])) {
  316. $this->rootIdentifier = $repoData['default_branch'];
  317. } elseif (isset($repoData['master_branch'])) {
  318. $this->rootIdentifier = $repoData['master_branch'];
  319. } else {
  320. $this->rootIdentifier = 'master';
  321. }
  322. $this->hasIssues = !empty($repoData['has_issues']);
  323. }
  324. protected function attemptCloneFallback()
  325. {
  326. $this->isPrivate = true;
  327. try {
  328. // If this repository may be private (hard to say for sure,
  329. // GitHub returns 404 for private repositories) and we
  330. // cannot ask for authentication credentials (because we
  331. // are not interactive) then we fallback to GitDriver.
  332. $this->gitDriver = new GitDriver(
  333. array('url' => $this->generateSshUrl()),
  334. $this->io,
  335. $this->config,
  336. $this->process,
  337. $this->remoteFilesystem
  338. );
  339. $this->gitDriver->initialize();
  340. return;
  341. } catch (\RuntimeException $e) {
  342. $this->gitDriver = null;
  343. $this->io->write('<error>Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your GitHub credentials</error>');
  344. throw $e;
  345. }
  346. }
  347. }