Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
PHP_CS_RULES=@Symfony,-global_namespace_import

.PHONY: test
test: check-style check-rules
test: check-style check-rules phpunit

.PHONY: phpunit
phpunit:
@echo "-- Running phpunit... See output/coverage/index.html "
XDEBUG_MODE=coverage vendor/bin/phpunit -c phpunit.xml.dist \
--log-junit output/junit-report.xml \
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
},
"require": {
"php": ">=8.3",
"guzzlehttp/guzzle": "~7.0"
"guzzlehttp/guzzle": "~7.0",
"psr/log": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^12",
Expand Down
69 changes: 43 additions & 26 deletions src/AbstractClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@
*/
abstract class AbstractClient implements ClientInterface
{
/**
* @var GuzzleHttpClient
*/
protected $httpClient;
private const DEFAULT_PER_PAGE = 50;
private const MAX_PAGES = 1000;

/**
* @var LoggerInterface
Expand All @@ -24,9 +22,12 @@ abstract class AbstractClient implements ClientInterface

/**
* Constructor with an httpClient ready to performs API requests.
*
* @param string $limitPerPageParam 'per_page' (GitLab, GitHub) or 'limit' (Gogs, Gitea)
*/
protected function __construct(
GuzzleHttpClient $httpClient,
protected GuzzleHttpClient $httpClient,
private string $limitPerPageParam,
?LoggerInterface $logger = null,
) {
$this->httpClient = $httpClient;
Expand All @@ -50,14 +51,19 @@ protected function getLogger(): LoggerInterface
*/
abstract protected function createProject(array $rawProject): ProjectInterface;

public function find(FindOptions $options): array
{
return iterator_to_array($this->getProjects($options), false);
}

/**
* Get projets for a given path with parameters.
*
* @param array<string,string|int> $params
*
* @return ProjectInterface[]
*/
protected function getProjects(
private function fetchProjects(
string $path,
array $params = [],
): array {
Expand All @@ -73,6 +79,37 @@ protected function getProjects(
return $projects;
}

/**
* Fetch all pages for a given path with query params.
*
* @param string $path ex : "/api/v4/projects"
* @param ProjectFilterInterface $projectFilter client side filtering
* @param array<string,string> $extraParams ex : ['affiliation' => 'owner']
*
* @return iterable<ProjectInterface>
*/
protected function fetchAllPages(
string $path,
ProjectFilterInterface $projectFilter,
array $extraParams = [],
): iterable {
for ($page = 1; $page <= self::MAX_PAGES; ++$page) {
$params = array_merge($extraParams, [
'page' => $page,
$this->limitPerPageParam => self::DEFAULT_PER_PAGE,
]);
$projects = $this->fetchProjects($path, $params);
if (empty($projects)) {
break;
}
foreach ($projects as $project) {
if ($projectFilter->isAccepted($project)) {
yield $project;
}
}
}
}

/**
* Implode params to performs HTTP request.
*
Expand All @@ -87,24 +124,4 @@ protected function implodeParams(array $params): string

return implode('&', $parts);
}

/**
* Helper to apply filter to a project list.
*
* @param ProjectInterface[] $projects
*
* @return ProjectInterface[]
*/
protected function filter(array $projects, ProjectFilterInterface $filter): array
{
$result = [];
foreach ($projects as $project) {
if (!$filter->isAccepted($project)) {
continue;
}
$result[] = $project;
}

return $result;
}
}
14 changes: 8 additions & 6 deletions src/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

use GuzzleHttp\Client as GuzzleHttpClient;
use MBO\RemoteGit\Exception\ClientNotFoundException;
use MBO\RemoteGit\Exception\ProtocolNotSupportedException;
use MBO\RemoteGit\Github\GithubClient;
use MBO\RemoteGit\Gitlab\GitlabClient;
use MBO\RemoteGit\Gogs\GogsClient;
use MBO\RemoteGit\Helper\ClientHelper;
use MBO\RemoteGit\Helper\LoggerHelper;
use MBO\RemoteGit\Http\TokenType;
use MBO\RemoteGit\Local\LocalClient;
use Psr\Log\LoggerInterface;

/**
Expand Down Expand Up @@ -39,7 +39,6 @@ private function __construct()
$this->register(GitlabClient::class);
$this->register(GithubClient::class);
$this->register(GogsClient::class);
$this->register(LocalClient::class);
}

/**
Expand Down Expand Up @@ -97,9 +96,9 @@ public function createGitClient(
throw new ClientNotFoundException($options->getType(), $this->getTypes());
}

/* Handle LocalClient */
if (LocalClient::TYPE === $options->getType()) {
return new LocalClient($options->getUrl(), $logger);
/* handle removed LocalClient */
if ('local' === $options->getType()) {
throw new ClientNotFoundException($options->getType(), $this->getTypes());
}

/* Force github API URL */
Expand Down Expand Up @@ -141,8 +140,11 @@ public function createGitClient(
public static function detectClientClass(string $url): string
{
$scheme = parse_url($url, PHP_URL_SCHEME);
if (!is_string($scheme)) {
$scheme = 'file';
}
if (!in_array($scheme, ['http', 'https'])) {
return LocalClient::class;
throw new ProtocolNotSupportedException($scheme, $url);
}

$hostname = parse_url($url, PHP_URL_HOST);
Expand Down
9 changes: 9 additions & 0 deletions src/ClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,18 @@
*/
interface ClientInterface
{
/**
* Find projects according to options.
*
* @return iterable<ProjectInterface>
*/
public function getProjects(FindOptions $options): iterable;

/**
* Find projects using API calls.
*
* @deprecated use getProjects instead
*
* @return ProjectInterface[]
*/
public function find(FindOptions $options): array;
Expand Down
17 changes: 17 additions & 0 deletions src/Exception/ProtocolNotSupportedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace MBO\RemoteGit\Exception;

/**
* Custom exception for non http/https URL.
*/
class ProtocolNotSupportedException extends \RuntimeException
{
public function __construct(string $scheme, string $url)
{
// handle removed LocalClient
$note = 'file' == $scheme ? 'LocalClient has been removed' : 'use HTTPS';
$message = sprintf("protocol '%s' for '%s' is not supported (%s)", $scheme, $url, $note);
parent::__construct($message);
}
}
74 changes: 20 additions & 54 deletions src/Github/GithubClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ class GithubClient extends AbstractClient
{
public const TYPE = 'github';
public const TOKEN_TYPE = TokenType::AUTHORIZATION_TOKEN;

public const DEFAULT_PER_PAGE = 100;
public const MAX_PAGES = 10000;
private const LIMIT_PER_PAGE_PARAM = 'per_page';

/**
* Constructor with an http client and a logger.
Expand All @@ -40,34 +38,31 @@ public function __construct(
GuzzleHttpClient $httpClient,
?LoggerInterface $logger = null,
) {
parent::__construct($httpClient, $logger);
parent::__construct($httpClient, self::LIMIT_PER_PAGE_PARAM, $logger);
}

protected function createProject(array $rawProject): GithubProject
{
return new GithubProject($rawProject);
}

public function find(FindOptions $options): array
public function getProjects(FindOptions $options): iterable
{
$result = [];
if (empty($options->getUsers()) && empty($options->getOrganizations())) {
throw new RequiredParameterException('[GithubClient]Define at least an org or a user to use find');
throw new RequiredParameterException('[GithubClient]Define at least an org or an user');
}
foreach ($options->getUsers() as $user) {
$result = array_merge($result, $this->findByUser(
yield from $this->findByUser(
$user,
$options->getFilter()
));
);
}
foreach ($options->getOrganizations() as $org) {
$result = array_merge($result, $this->findByOrg(
yield from $this->findByOrg(
$org,
$options->getFilter()
));
);
}

return $result;
}

/**
Expand All @@ -81,70 +76,41 @@ public function find(FindOptions $options): array
protected function findByUser(
string $user,
ProjectFilterInterface $projectFilter,
) {
): iterable {
/*
* Use /user/repos?affiliation=owner for special user _me_
*/
if ('_me_' == $user) {
return $this->fetchAllPages(
yield from $this->fetchAllPages(
'/user/repos',
$projectFilter,
[
extraParams: [
'affiliation' => 'owner',
]
);
} else {
yield from $this->fetchAllPages(
'/users/'.$user.'/repos',
$projectFilter
);
}

return $this->fetchAllPages(
'/users/'.$user.'/repos',
$projectFilter
);
}

/**
* Find projects by username.
* Find projects by org name.
*
* @return ProjectInterface[]
* @return iterable<ProjectInterface>
*/
protected function findByOrg(
string $org,
ProjectFilterInterface $projectFilter,
) {
return $this->fetchAllPages(
): iterable {
yield from $this->fetchAllPages(
'/orgs/'.$org.'/repos',
$projectFilter
);
}

/**
* Fetch all pages for a given URI.
*
* @param string $path such as '/orgs/IGNF/repos' or '/users/mborne/repos'
* @param array<string,string|int> $extraParams
*
* @return ProjectInterface[]
*/
private function fetchAllPages(
string $path,
ProjectFilterInterface $projectFilter,
array $extraParams = [],
): array {
$result = [];
for ($page = 1; $page <= self::MAX_PAGES; ++$page) {
$params = array_merge($extraParams, [
'page' => $page,
'per_page' => self::DEFAULT_PER_PAGE,
]);
$projects = $this->getProjects($path, $params);
if (empty($projects)) {
break;
}
$result = array_merge($result, $this->filter($projects, $projectFilter));
}

return $result;
}

public function getRawFile(
ProjectInterface $project,
$filePath,
Expand Down
Loading