From 0056aa216b963747533b0832100e6149adaa96ce Mon Sep 17 00:00:00 2001 From: Michael Babker Date: Sat, 25 Jun 2016 11:34:48 -0500 Subject: [PATCH] Implement custom GitHub API connector (Fix #155) --- .../Controller/StartfetchController.php | 8 +- .../GitHub/Exception/UnexpectedResponse.php | 54 ++++ .../PatchTester/GitHub/GitHub.php | 296 ++++++++++++++++++ .../com_patchtester/PatchTester/Helper.php | 16 +- .../PatchTester/Model/PullModel.php | 25 +- .../PatchTester/Model/PullsModel.php | 23 +- 6 files changed, 385 insertions(+), 37 deletions(-) create mode 100644 administrator/components/com_patchtester/PatchTester/GitHub/Exception/UnexpectedResponse.php create mode 100644 administrator/components/com_patchtester/PatchTester/GitHub/GitHub.php diff --git a/administrator/components/com_patchtester/PatchTester/Controller/StartfetchController.php b/administrator/components/com_patchtester/PatchTester/Controller/StartfetchController.php index 79bb7af..7349378 100644 --- a/administrator/components/com_patchtester/PatchTester/Controller/StartfetchController.php +++ b/administrator/components/com_patchtester/PatchTester/Controller/StartfetchController.php @@ -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( diff --git a/administrator/components/com_patchtester/PatchTester/GitHub/Exception/UnexpectedResponse.php b/administrator/components/com_patchtester/PatchTester/GitHub/Exception/UnexpectedResponse.php new file mode 100644 index 0000000..07492f9 --- /dev/null +++ b/administrator/components/com_patchtester/PatchTester/GitHub/Exception/UnexpectedResponse.php @@ -0,0 +1,54 @@ +response = $response; + } + + /** + * Get the Response object. + * + * @return \JHttpResponse + * + * @since __DEPLOY_VERSION__ + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/administrator/components/com_patchtester/PatchTester/GitHub/GitHub.php b/administrator/components/com_patchtester/PatchTester/GitHub/GitHub.php new file mode 100644 index 0000000..f37da81 --- /dev/null +++ b/administrator/components/com_patchtester/PatchTester/GitHub/GitHub.php @@ -0,0 +1,296 @@ +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; + } +} diff --git a/administrator/components/com_patchtester/PatchTester/Helper.php b/administrator/components/com_patchtester/PatchTester/Helper.php index 9c1abab..489a015 100644 --- a/administrator/components/com_patchtester/PatchTester/Helper.php +++ b/administrator/components/com_patchtester/PatchTester/Helper.php @@ -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); } } diff --git a/administrator/components/com_patchtester/PatchTester/Model/PullModel.php b/administrator/components/com_patchtester/PatchTester/Model/PullModel.php index 0d76b69..7fa5e41 100644 --- a/administrator/components/com_patchtester/PatchTester/Model/PullModel.php +++ b/administrator/components/com_patchtester/PatchTester/Model/PullModel.php @@ -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) diff --git a/administrator/components/com_patchtester/PatchTester/Model/PullsModel.php b/administrator/components/com_patchtester/PatchTester/Model/PullsModel.php index adb453d..67eeb9e 100644 --- a/administrator/components/com_patchtester/PatchTester/Model/PullsModel.php +++ b/administrator/components/com_patchtester/PatchTester/Model/PullsModel.php @@ -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); }