31
0
mirror of https://github.com/joomla-extensions/patchtester.git synced 2024-06-07 07:50:48 +00:00

Implement custom GitHub API connector (Fix #155)

This commit is contained in:
Michael Babker 2016-06-25 11:34:48 -05:00
parent a876a3593a
commit 0056aa216b
6 changed files with 385 additions and 37 deletions

View File

@ -8,6 +8,7 @@
namespace PatchTester\Controller;
use PatchTester\GitHub\Exception\UnexpectedResponse;
use PatchTester\Helper;
use PatchTester\Model\PullsModel;
use PatchTester\Model\TestsModel;
@ -47,13 +48,12 @@ class StartfetchController extends AbstractController
}
// Make sure we can fetch the data from GitHub - throw an error on < 10 available requests
$github = Helper::initializeGithub();
try
{
$rate = $github->authorization->getRateLimit();
$rateResponse = Helper::initializeGithub()->getRateLimit();
$rate = json_decode($rateResponse->body);
}
catch (\Exception $e)
catch (UnexpectedResponse $e)
{
$response = new \JResponseJson(
new \Exception(

View File

@ -0,0 +1,54 @@
<?php
/**
* Patch testing component for the Joomla! CMS
*
* @copyright Copyright (C) 2011 - 2012 Ian MacLennan, Copyright (C) 2013 - 2016 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later
*/
namespace PatchTester\GitHub\Exception;
/**
* Exception representing an unexpected response
*
* @since __DEPLOY_VERSION__
*/
class UnexpectedResponse extends \DomainException
{
/**
* The Response object.
*
* @var \JHttpResponse
* @since __DEPLOY_VERSION__
*/
private $response;
/**
* Constructor
*
* @param \JHttpResponse $response The Response object.
* @param string $message The Exception message to throw.
* @param integer $code The Exception code.
* @param \Exception $previous The previous exception used for the exception chaining.
*
* @since __DEPLOY_VERSION__
*/
public function __construct(\JHttpResponse $response, $message = '', $code = 0, \Exception $previous = null)
{
parent::__construct($message, $code, $previous);
$this->response = $response;
}
/**
* Get the Response object.
*
* @return \JHttpResponse
*
* @since __DEPLOY_VERSION__
*/
public function getResponse()
{
return $this->response;
}
}

View File

@ -0,0 +1,296 @@
<?php
/**
* Patch testing component for the Joomla! CMS
*
* @copyright Copyright (C) 2011 - 2012 Ian MacLennan, Copyright (C) 2013 - 2016 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later
*/
namespace PatchTester\GitHub;
use Joomla\Registry\Registry;
/**
* Helper class for interacting with the GitHub API.
*
* @since __DEPLOY_VERSION__
*/
class GitHub
{
/**
* Options for the connector.
*
* @var Registry
* @since __DEPLOY_VERSION__
*/
protected $options;
/**
* The HTTP client object to use in sending HTTP requests.
*
* @var \JHttp
* @since __DEPLOY_VERSION__
*/
protected $client;
/**
* Constructor.
*
* @param Registry $options Connector options.
* @param \JHttp $client The HTTP client object.
*
* @since __DEPLOY_VERSION__
*/
public function __construct(Registry $options = null, \JHttp $client = null)
{
$this->options = $options ?: new Registry;
$this->client = $client ?: \JHttpFactory::getHttp($options);
}
/**
* Build and return a full request URL.
*
* This method will add appropriate pagination details and basic authentication credentials if necessary
* and also prepend the API url to have a complete URL for the request.
*
* @param string $path URL to inflect
* @param integer $page Page to request
* @param integer $limit Number of results to return per page
*
* @return string The request URL.
*
* @since __DEPLOY_VERSION__
*/
protected function fetchUrl($path, $page = 0, $limit = 0)
{
// Get a new JUri object fousing the api url and given path.
$uri = new \JUri($this->options->get('api.url') . $path);
// Only apply basic authentication if an access token is not set
if ($this->options->get('gh.token', false) === false)
{
// Use basic authentication
if ($this->options->get('api.username', false))
{
$username = $this->options->get('api.username');
$username = str_replace('@', '%40', $username);
$uri->setUser($username);
}
if ($this->options->get('api.password', false))
{
$password = $this->options->get('api.password');
$password = str_replace('@', '%40', $password);
$uri->setPass($password);
}
}
// If we have a defined page number add it to the JUri object.
if ($page > 0)
{
$uri->setVar('page', (int) $page);
}
// If we have a defined items per page add it to the JUri object.
if ($limit > 0)
{
$uri->setVar('per_page', (int) $limit);
}
return (string) $uri;
}
/**
* Get the HTTP client for this connector.
*
* @return \JHttp
*
* @since __DEPLOY_VERSION__
*/
public function getClient()
{
return $this->client;
}
/**
* Get the diff for a pull request.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
*
* @return \JHttpResponse
*
* @since __DEPLOY_VERSION__
*/
public function getDiffForPullRequest($user, $repo, $pullId)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId;
// Build the request headers.
$headers = array('Accept' => 'application/vnd.github.diff');
$prepared = $this->prepareRequest($path, 0, 0, $headers);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
/**
* Get the list of modified files for a pull request.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
*
* @return \JHttpResponse
*
* @since __DEPLOY_VERSION__
*/
public function getFilesForPullRequest($user, $repo, $pullId)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId . '/files';
$prepared = $this->prepareRequest($path, 0, 0, $headers);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
/**
* Get a list of the open issues for a repository.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $page The page number from which to get items.
* @param integer $limit The number of items on a page.
*
* @return \JHttpResponse
*
* @since __DEPLOY_VERSION__
*/
public function getOpenIssues($user, $repo, $page = 0, $limit = 0)
{
$prepared = $this->prepareRequest("/repos/$user/$repo/issues", $page, $limit);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
/**
* Get an option from the connector.
*
* @param string $key The name of the option to get.
* @param mixed $default The default value if the option is not set.
*
* @return mixed The option value.
*
* @since __DEPLOY_VERSION__
*/
public function getOption($key, $default = null)
{
return $this->options->get($key, $default);
}
/**
* Get a single pull request.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
*
* @return \JHttpResponse
*
* @since __DEPLOY_VERSION__
*/
public function getPullRequest($user, $repo, $pullId)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId;
$prepared = $this->prepareRequest($path);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
/**
* Get the rate limit for the authenticated user.
*
* @return \JHttpResponse
*
* @since __DEPLOY_VERSION__
*/
public function getRateLimit()
{
$prepared = $this->prepareRequest('/rate_limit');
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
/**
* Process the response and return it.
*
* @param \JHttpResponse $response The response.
* @param integer $expectedCode The expected response code.
*
* @return \JHttpResponse
*
* @since __DEPLOY_VERSION__
* @throws Exception\UnexpectedResponse
*/
protected function processResponse(\JHttpResponse $response, $expectedCode = 200)
{
// Validate the response code.
if ($response->code != $expectedCode)
{
// Decode the error response and throw an exception.
$body = json_decode($response->body);
throw new Exception\UnexpectedResponse($response, $body->error, $response->code);
}
return $response;
}
/**
* Method to build and return a full request URL for the request.
*
* This method will add appropriate pagination details if necessary and also prepend the API url to have a complete URL for the request.
*
* @param string $path Path to process
* @param integer $page Page to request
* @param integer $limit Number of results to return per page
* @param array $headers The headers to send with the request
*
* @return array Associative array containing the prepared URL and request headers
*
* @since __DEPLOY_VERSION__
*/
protected function prepareRequest($path, $page = 0, $limit = 0, array $headers = array())
{
$url = $this->fetchUrl($path, $page, $limit);
if ($token = $this->options->get('gh.token', false))
{
$headers['Authorization'] = "token $token";
}
return array('url' => $url, 'headers' => $headers);
}
/**
* Set an option for the connector.
*
* @param string $key The name of the option to set.
* @param mixed $value The option value to set.
*
* @return $this
*
* @since __DEPLOY_VERSION__
*/
public function setOption($key, $value)
{
$this->options->set($key, $value);
return $this;
}
}

View File

@ -9,6 +9,7 @@
namespace PatchTester;
use Joomla\Registry\Registry;
use PatchTester\GitHub\GitHub;
/**
* Helper class for the patch tester component
@ -18,9 +19,9 @@ use Joomla\Registry\Registry;
abstract class Helper
{
/**
* Initializes the JGithub object
* Initializes the GitHub object
*
* @return \JGithub
* @return GitHub
*
* @since 2.0
*/
@ -30,6 +31,15 @@ abstract class Helper
$options = new Registry;
// Set a user agent for the request
$options->set('userAgent', 'PatchTester/3.0');
// Set the default timeout to 120 seconds
$options->set('timeout', 120);
// Set the API URL
$options->set('api.url', 'https://api.github.com');
// If an API token is set in the params, use it for authentication
if ($params->get('gh_token', ''))
{
@ -47,6 +57,6 @@ abstract class Helper
\JFactory::getApplication()->enqueueMessage(\JText::_('COM_PATCHTESTER_NO_CREDENTIALS'), 'notice');
}
return new \JGithub($options);
return new GitHub($options);
}
}

View File

@ -10,6 +10,7 @@ namespace PatchTester\Model;
use Joomla\Registry\Registry;
use PatchTester\GitHub\Exception\UnexpectedResponse;
use PatchTester\Helper;
/**
@ -160,9 +161,10 @@ class PullModel extends \JModelDatabase
try
{
$rate = $github->authorization->getRateLimit();
$rateResponse = $github->getRateLimit();
$rate = json_decode($rateResponse->body);
}
catch (\Exception $e)
catch (UnexpectedResponse $e)
{
throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e);
}
@ -177,9 +179,10 @@ class PullModel extends \JModelDatabase
try
{
$pull = $github->pulls->get($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $id);
$pullResponse = $github->getPullRequest($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $id);
$pull = json_decode($pullResponse->body);
}
catch (\Exception $e)
catch (UnexpectedResponse $e)
{
throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e);
}
@ -189,17 +192,12 @@ class PullModel extends \JModelDatabase
throw new \RuntimeException(\JText::_('COM_PATCHTESTER_REPO_IS_GONE'));
}
// Set up the JHttp object
$options = new Registry;
$options->set('userAgent', 'JPatchTester/2.0');
$options->set('timeout', 120);
try
{
$transport = \JHttpFactory::getHttp($options);
$patch = $transport->get($pull->diff_url)->body;
$patchResponse = $github->getDiffForPullRequest($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $id);
$patch = json_decode($patchResponse->body);
}
catch (\Exception $e)
catch (UnexpectedResponse $e)
{
throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e);
}
@ -236,7 +234,7 @@ class PullModel extends \JModelDatabase
try
{
$file->body = $transport->get($url)->body;
$file->body = $github->getClient()->get($url)->body;
}
catch (\Exception $e)
{
@ -246,6 +244,7 @@ class PullModel extends \JModelDatabase
}
jimport('joomla.filesystem.file');
jimport('joomla.filesystem.path');
// At this point, we have ensured that we have all the new files and there are no conflicts
foreach ($files as $file)

View File

@ -10,6 +10,7 @@ namespace PatchTester\Model;
use Joomla\Registry\Registry;
use PatchTester\GitHub\Exception\UnexpectedResponse;
use PatchTester\Helper;
/**
@ -283,9 +284,6 @@ class PullsModel extends \JModelDatabase
*/
public function requestFromGithub($page)
{
// Get the Github object
$github = Helper::initializeGithub();
// If on page 1, dump the old data
if ($page === 1)
{
@ -295,22 +293,13 @@ class PullsModel extends \JModelDatabase
try
{
// TODO - Option to configure the batch size
$pulls = $github->issues->getListByRepository(
$this->getState()->get('github_user'),
$this->getState()->get('github_repo'),
null,
'open',
null,
null,
null,
null,
null,
null,
$page,
100
$pullsResponse = Helper::initializeGithub()->getOpenIssues(
$this->getState()->get('github_user'), $this->getState()->get('github_repo'), $page, 100
);
$pulls = json_decode($pullsResponse->body);
}
catch (\DomainException $e)
catch (UnexpectedResponse $e)
{
throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_GITHUB_FETCH', $e->getMessage()), $e->getCode(), $e);
}