You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
5.9 KiB
201 lines
5.9 KiB
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Symfony\Flex;
|
|
|
|
use Composer\Util\HttpDownloader;
|
|
use Composer\Util\RemoteFilesystem;
|
|
|
|
class GithubApi
|
|
{
|
|
/** @var HttpDownloader|RemoteFilesystem */
|
|
private $downloader;
|
|
|
|
public function __construct($downloader)
|
|
{
|
|
$this->downloader = $downloader;
|
|
}
|
|
|
|
/**
|
|
* Attempts to find data about when the recipe was installed.
|
|
*
|
|
* Returns an array containing:
|
|
* commit: The git sha of the last commit of the recipe
|
|
* date: The date of the commit
|
|
* new_commits: An array of commit sha's in this recipe's directory+version since the commit
|
|
* The key is the sha & the value is the date
|
|
*/
|
|
public function findRecipeCommitDataFromTreeRef(string $package, string $repo, string $branch, string $version, string $lockRef): ?array
|
|
{
|
|
$repositoryName = $this->getRepositoryName($repo);
|
|
if (!$repositoryName) {
|
|
return null;
|
|
}
|
|
|
|
$recipePath = sprintf('%s/%s', $package, $version);
|
|
$commitsData = $this->requestGitHubApi(sprintf(
|
|
'https://api.github.com/repos/%s/commits?path=%s&sha=%s',
|
|
$repositoryName,
|
|
$recipePath,
|
|
$branch
|
|
));
|
|
|
|
$commitShas = [];
|
|
foreach ($commitsData as $commitData) {
|
|
$commitShas[$commitData['sha']] = $commitData['commit']['committer']['date'];
|
|
// go back the commits one-by-one
|
|
$treeUrl = $commitData['commit']['tree']['url'].'?recursive=true';
|
|
|
|
// fetch the full tree, then look for the tree for the package path
|
|
$treeData = $this->requestGitHubApi($treeUrl);
|
|
foreach ($treeData['tree'] as $treeItem) {
|
|
if ($treeItem['path'] !== $recipePath) {
|
|
continue;
|
|
}
|
|
|
|
if ($treeItem['sha'] === $lockRef) {
|
|
// remove *this* commit from the new commits list
|
|
array_pop($commitShas);
|
|
|
|
return [
|
|
// shorten for brevity
|
|
'commit' => substr($commitData['sha'], 0, 7),
|
|
'date' => $commitData['commit']['committer']['date'],
|
|
'new_commits' => $commitShas,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function getVersionsOfRecipe(string $repo, string $branch, string $recipePath): ?array
|
|
{
|
|
$repositoryName = $this->getRepositoryName($repo);
|
|
if (!$repositoryName) {
|
|
return null;
|
|
}
|
|
|
|
$url = sprintf(
|
|
'https://api.github.com/repos/%s/contents/%s?ref=%s',
|
|
$repositoryName,
|
|
$recipePath,
|
|
$branch
|
|
);
|
|
$contents = $this->requestGitHubApi($url);
|
|
$versions = [];
|
|
foreach ($contents as $fileData) {
|
|
if ('dir' !== $fileData['type']) {
|
|
continue;
|
|
}
|
|
|
|
$versions[] = $fileData['name'];
|
|
}
|
|
|
|
return $versions;
|
|
}
|
|
|
|
public function getCommitDataForPath(string $repo, string $path, string $branch): array
|
|
{
|
|
$repositoryName = $this->getRepositoryName($repo);
|
|
if (!$repositoryName) {
|
|
return [];
|
|
}
|
|
|
|
$commitsData = $this->requestGitHubApi(sprintf(
|
|
'https://api.github.com/repos/%s/commits?path=%s&sha=%s',
|
|
$repositoryName,
|
|
$path,
|
|
$branch
|
|
));
|
|
|
|
$data = [];
|
|
foreach ($commitsData as $commitData) {
|
|
$data[$commitData['sha']] = $commitData['commit']['committer']['date'];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
public function getPullRequestForCommit(string $commit, string $repo): ?array
|
|
{
|
|
$data = $this->requestGitHubApi('https://api.github.com/search/issues?q='.$commit.'+is:pull-request');
|
|
|
|
if (0 === \count($data['items'])) {
|
|
return null;
|
|
}
|
|
|
|
$repositoryName = $this->getRepositoryName($repo);
|
|
if (!$repositoryName) {
|
|
return null;
|
|
}
|
|
|
|
$bestItem = null;
|
|
foreach ($data['items'] as $item) {
|
|
// make sure the PR referenced isn't from a different repository
|
|
if (false === strpos($item['html_url'], sprintf('%s/pull', $repositoryName))) {
|
|
continue;
|
|
}
|
|
|
|
if (null === $bestItem) {
|
|
$bestItem = $item;
|
|
|
|
continue;
|
|
}
|
|
|
|
// find the first PR to reference - avoids rare cases where an invalid
|
|
// PR that references *many* commits is first
|
|
// e.g. https://api.github.com/search/issues?q=a1a70353f64f405cfbacfc4ce860af623442d6e5
|
|
if ($item['number'] < $bestItem['number']) {
|
|
$bestItem = $item;
|
|
}
|
|
}
|
|
|
|
if (!$bestItem) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'number' => $bestItem['number'],
|
|
'url' => $bestItem['html_url'],
|
|
'title' => $bestItem['title'],
|
|
];
|
|
}
|
|
|
|
private function requestGitHubApi(string $path)
|
|
{
|
|
$contents = $this->downloader->get($path)->getBody();
|
|
|
|
return json_decode($contents, true);
|
|
}
|
|
|
|
/**
|
|
* Converts the "repo" stored in symfony.lock to a repository name.
|
|
*
|
|
* For example: "github.com/symfony/recipes" => "symfony/recipes"
|
|
*/
|
|
private function getRepositoryName(string $repo): ?string
|
|
{
|
|
// only supports public repository placement
|
|
if (0 !== strpos($repo, 'github.com')) {
|
|
return null;
|
|
}
|
|
|
|
$parts = explode('/', $repo);
|
|
if (3 !== \count($parts)) {
|
|
return null;
|
|
}
|
|
|
|
return implode('/', [$parts[1], $parts[2]]);
|
|
}
|
|
}
|