From 11407d0e2be2e50f667f5c30671da814bb355017 Mon Sep 17 00:00:00 2001 From: Damien Regad Date: Sun, 21 Sep 2025 17:57:32 +0200 Subject: [PATCH 01/11] Add language code to commit message Before this, the commit message was just `translation update`. now it includes the language code, i.e. `Translation update (xx)`. --- src/Services/Repository/Repository.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Services/Repository/Repository.php b/src/Services/Repository/Repository.php index a383854c..da5a7535 100644 --- a/src/Services/Repository/Repository.php +++ b/src/Services/Repository/Repository.php @@ -500,8 +500,9 @@ private function createAndSendPatchWithException(TranslationUpdateEntity $update // add files to local temporary git repository $this->applyChanges($tmpGit, $tmpDir, $update); // commit files to local temporary git repository + $message = 'Translation update (' . $update->getLanguage() . ')'; $author = $this->prepareAuthor($update); - $tmpGit->commit('translation update', $author); + $tmpGit->commit($message, $author); $this->behavior->sendChange($tmpGit, $update, $this->git); } From 92ea221c90ccac24af1bda92d7d8c0b7231d0c79 Mon Sep 17 00:00:00 2001 From: Damien Regad Date: Sun, 21 Sep 2025 22:40:43 +0200 Subject: [PATCH 02/11] New abstract class GitHostingProviderService Parent Class for Git Hosting Provider Services, factors out duplicated code from GitHubService and GitLabService classes. --- src/Services/GitHostingProviderService.php | 114 +++++++++++++++++++++ src/Services/GitHub/GitHubService.php | 74 ++++--------- src/Services/GitLab/GitLabService.php | 74 ++++--------- 3 files changed, 157 insertions(+), 105 deletions(-) create mode 100644 src/Services/GitHostingProviderService.php diff --git a/src/Services/GitHostingProviderService.php b/src/Services/GitHostingProviderService.php new file mode 100644 index 00000000..3105eb28 --- /dev/null +++ b/src/Services/GitHostingProviderService.php @@ -0,0 +1,114 @@ +url = $url; + } + + protected function getCachePool(string $dataFolder): FilesystemCachePool + { + // folders are relative to folder set here + $filesystemAdapter = new Local($dataFolder); + $filesystem = new Filesystem($filesystemAdapter); + + $pool = new FilesystemCachePool($filesystem); + $pool->setFolder('cache/' . strtolower($this->provider)); + + return $pool; + } + + /** + * Create fork in our Hosting Provider account + * + * @param string $url URL to create the fork from + * @return string Git URL of the fork + */ + abstract public function createFork(string $url): string; + + /** + * Delete fork from our Hosting Provider account + * + * @param string $remoteUrl Git url of the forked repository + */ + abstract public function deleteFork(string $remoteUrl): void; + + /** + * @param string $patchBranch name of branch with language update + * @param string $destinationBranch name of branch at remote + * @param string $languageCode + * @param string $url git url original upstream repository + * @param string $patchUrl remote url + */ + abstract public function createPullRequest(string $patchBranch, string $destinationBranch, string $languageCode, string $url, string $patchUrl): void; + + /** + * Get information about the open pull requests i.e. url and count + * + * @param string $urlUpstream original git clone url + * @param string $languageCode + * @return array{count: int, listURL: string, title: string} + */ + abstract public function getOpenPRListInfo(string $urlUpstream, string $languageCode): array; + + /** + * @param string $url git clone url + * @return array with user's account name, repository name + */ + protected function getUsernameAndRepositoryFromURL(string $url): array + { + $result = preg_replace($this::REGEX_REPO_USER, '$2', $url, 1, $counter); + if ($counter === 0) { + throw new $this->exceptionType('Invalid ' . $this->provider . ' clone URL: ' . $url); + } + return explode('/', $result); + } + +} diff --git a/src/Services/GitHub/GitHubService.php b/src/Services/GitHub/GitHubService.php index 612f8eff..4862a541 100644 --- a/src/Services/GitHub/GitHubService.php +++ b/src/Services/GitHub/GitHubService.php @@ -2,49 +2,45 @@ namespace App\Services\GitHub; -use Cache\Adapter\Filesystem\FilesystemCachePool; +use App\Services\GitHostingProviderService; use Exception; use Github\AuthMethod; use Github\Client; use Github\Exception\MissingArgumentException; use Github\Exception\RuntimeException; -use League\Flysystem\Adapter\Local; -use League\Flysystem\Filesystem; use Symfony\Component\HttpClient\HttplugClient; -class GitHubService +class GitHubService extends GitHostingProviderService { + const REGEX_REPO_USER = '#^(https://github.com/|git@.*?github.com:|git://github.com/)(.*)\.git$#'; - private Client $client; - private string $gitHubUrl; + protected string $provider = 'GitHub'; + protected string $exceptionType = GitHubServiceException::class; - public function __construct(string $gitHubApiToken, string $dataFolder, string $gitHubUrl, bool $autoStartup = true) + /** + * @var Client + */ + protected $client; + + + public function __construct(string $apiToken, string $dataFolder, string $url, bool $autoStartup = true) { - $this->gitHubUrl = $gitHubUrl; + parent::__construct($apiToken, $dataFolder, $url, $autoStartup); if (!$autoStartup) { return; } - $filesystemAdapter = new Local($dataFolder); // folders are relative to folder set here - $filesystem = new Filesystem($filesystemAdapter); - - $pool = new FilesystemCachePool($filesystem); - $pool->setFolder('cache/github'); - $this->client = Client::createWithHttpClient( new HttplugClient() ); - $this->client->addCache($pool); - $this->client->authenticate($gitHubApiToken, null, AuthMethod::ACCESS_TOKEN); + $this->client->addCache($this->getCachePool($dataFolder)); + $this->client->authenticate($apiToken, null, AuthMethod::ACCESS_TOKEN); } /** - * Create fork in our GitHub account - * - * @param string $url GitHub URL to create the fork from - * @return string Git URL of the fork + * @inheritDoc * * @throws GitHubForkException * @throws GitHubServiceException @@ -61,9 +57,7 @@ public function createFork(string $url): string } /** - * Delete fork from our GitHub account - * - * @param string $remoteUrl git url of the forked repository + * @inheritDoc * * @throws GitHubServiceException */ @@ -78,11 +72,7 @@ public function deleteFork(string $remoteUrl): void } /** - * @param string $patchBranch name of branch with language update - * @param string $destinationBranch name of branch at remote - * @param string $languageCode - * @param string $url git url original upstream repository - * @param string $patchUrl remote url + * @inheritDoc * * @throws GitHubCreatePullRequestException * @throws GitHubServiceException @@ -106,11 +96,7 @@ public function createPullRequest(string $patchBranch, string $destinationBranch } /** - * Get information about the open pull requests i.e. url and count - * - * @param string $url original git clone url - * @param string $languageCode - * @return array{count: int, listURL: string, title: string} + * @inheritDoc * * @throws GitHubServiceException * @throws Exception only if in 'test' environment @@ -144,33 +130,15 @@ public function getOpenPRListInfo(string $url, string $languageCode): array return $info; } - /** - * @param string $url git clone url - * @return array with user's account name, repository name - * - * @throws GitHubServiceException - */ - private function getUsernameAndRepositoryFromURL(string $url): array - { - $result = preg_replace( - '#^(https://github.com/|git@.*?github.com:|git://github.com/)(.*)\.git$#', - '$2', $url, 1, $counter - ); - if ($counter === 0) { - throw new GitHubServiceException('Invalid GitHub clone URL: ' . $url); - } - return explode('/', $result); - } - /** * @param string $url git clone url * @return string modified git clone url */ private function gitHubUrlHack(string $url): string { - if ($this->gitHubUrl === 'github.com') { + if ($this->url === 'github.com') { return $url; } - return str_replace('github.com', $this->gitHubUrl, $url); + return str_replace('github.com', $this->url, $url); } } diff --git a/src/Services/GitLab/GitLabService.php b/src/Services/GitLab/GitLabService.php index 2179416d..200c0a87 100644 --- a/src/Services/GitLab/GitLabService.php +++ b/src/Services/GitLab/GitLabService.php @@ -2,45 +2,44 @@ namespace App\Services\GitLab; -use Cache\Adapter\Filesystem\FilesystemCachePool; +use App\Services\GitHostingProviderService; use Exception; use Gitlab\Api\MergeRequests; use Gitlab\Client; use Gitlab\Exception\RuntimeException; use Gitlab\HttpClient\Builder; use Http\Client\Common\Plugin\LoggerPlugin; -use League\Flysystem\Adapter\Local; -use League\Flysystem\Filesystem; use Psr\Log\LoggerInterface; -class GitLabService +class GitLabService extends GitHostingProviderService { - private string $gitLabUrl; - private Client $client; + const REGEX_REPO_USER = '#^(https://gitlab.com/|git@.*?gitlab.com:|git://gitlab.com/)(.*)\.git$#'; + + protected string $provider = 'GitLab'; + protected string $exceptionType = GitLabServiceException::class; + + /** + * @var Client + */ + protected $client; + private string $projectIdFolder; - public function __construct(string $gitLabApiToken, string $dataFolder, string $gitLabUrl, LoggerInterface $httpLogger, bool $autoStartup = true) + public function __construct(string $apiToken, string $dataFolder, string $url, LoggerInterface $httpLogger, bool $autoStartup = true) { - $this->gitLabUrl = $gitLabUrl; + parent::__construct($apiToken, $dataFolder, $url, $autoStartup); if (!$autoStartup) { return; } - $filesystemAdapter = new Local($dataFolder); // folders are relative to folder set here - $filesystem = new Filesystem($filesystemAdapter); - - $pool = new FilesystemCachePool($filesystem); - $pool->setFolder('cache/gitlab'); - - $loggerPlugin = new LoggerPlugin($httpLogger); //==new Logger('http') $builder = new Builder(); - $builder->addCache($pool); + $builder->addCache($this->getCachePool($dataFolder)); $builder->addPlugin($loggerPlugin); $this->client = new Client($builder); - $this->client->authenticate($gitLabApiToken, Client::AUTH_HTTP_TOKEN); + $this->client->authenticate($apiToken, Client::AUTH_HTTP_TOKEN); } public function setProjectIdFolder(string $projectIdFolder): void @@ -69,10 +68,7 @@ private function getProjectIdOfUpstream(): int } /** - * Create fork in our GitLab account - * - * @param string $url GitLab URL to create the fork from - * @return string Git URL of the fork + * @inheritDoc * * @throws GitLabForkException * @throws GitLabServiceException @@ -91,9 +87,7 @@ public function createFork(string $url): string } /** - * Delete fork from our GitHub account - * - * @param string $remoteUrl git url of the forked repository + * @inheritDoc * * @throws GitLabServiceException */ @@ -112,11 +106,7 @@ public function deleteFork(string $remoteUrl): void /** - * @param string $patchBranch name of branch with language update - * @param string $destinationBranch name of branch at remote - * @param string $languageCode - * @param string $url git url original upstream repository - * @param string $patchUrl remote url + * @inheritDoc * * @throws GitLabCreateMergeRequestException * @throws GitLabServiceException @@ -145,11 +135,7 @@ public function createPullRequest(string $patchBranch, string $destinationBranch } /** - * Get information about the open pull requests i.e. url and count - * - * @param string $urlUpstream original git clone url - * @param string $languageCode - * @return array{count: int, listURL: string, title: string} + * @inheritDoc * * @throws GitLabServiceException * @throws Exception only if in 'test' environment @@ -187,31 +173,15 @@ public function getOpenPRListInfo(string $urlUpstream, string $languageCode): ar return $info; } - - /** - * @param string $url git clone url - * @return array with user's account name, repository name - * - * @throws GitLabServiceException - */ - private function getUsernameAndRepositoryFromURL(string $url): array - { - $result = preg_replace('#^(https://gitlab.com/|git@.*?gitlab.com:|git://gitlab.com/)(.*)\.git$#', '$2', $url, 1, $counter); - if ($counter === 0) { - throw new GitLabServiceException('Invalid GitLab clone URL: ' . $url); - } - return explode('/', $result); - } - /** * @param string $url git clone url * @return string modified git clone url */ private function gitLabUrlHack(string $url): string { - if ($this->gitLabUrl === 'gitlab.com') { + if ($this->url === 'gitlab.com') { return $url; } - return str_replace('gitlab.com', $this->gitLabUrl, $url); + return str_replace('gitlab.com', $this->url, $url); } } \ No newline at end of file From 43c26dea4173eba304e2d2e26808161fc1bcc233 Mon Sep 17 00:00:00 2001 From: Damien Regad Date: Sun, 21 Sep 2025 22:44:58 +0200 Subject: [PATCH 03/11] Rename parameter to match parent method signature --- src/Services/GitHub/GitHubService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Services/GitHub/GitHubService.php b/src/Services/GitHub/GitHubService.php index 4862a541..d82242b4 100644 --- a/src/Services/GitHub/GitHubService.php +++ b/src/Services/GitHub/GitHubService.php @@ -101,9 +101,9 @@ public function createPullRequest(string $patchBranch, string $destinationBranch * @throws GitHubServiceException * @throws Exception only if in 'test' environment */ - public function getOpenPRListInfo(string $url, string $languageCode): array + public function getOpenPRListInfo(string $urlUpstream, string $languageCode): array { - [$user, $repository] = $this->getUsernameAndRepositoryFromURL($url); + [$user, $repository] = $this->getUsernameAndRepositoryFromURL($urlUpstream); $info = [ 'listURL' => '', From d30b9e11a63c377f99471d82ca9f74bdfa72d09b Mon Sep 17 00:00:00 2001 From: Damien Regad Date: Sun, 21 Sep 2025 23:06:00 +0200 Subject: [PATCH 04/11] New abstract class GitHostingProviderStatusService Parent Class for Git Hosting Provider Status Services, factors out duplicated code from GitHubStatusService and GitLabStatusService classes. --- .../GitHostingProviderStatusService.php | 40 +++++++++++++++ src/Services/GitHub/GitHubStatusService.php | 41 +++------------- src/Services/GitLab/GitLabStatusService.php | 49 +++++-------------- 3 files changed, 60 insertions(+), 70 deletions(-) create mode 100644 src/Services/GitHostingProviderStatusService.php diff --git a/src/Services/GitHostingProviderStatusService.php b/src/Services/GitHostingProviderStatusService.php new file mode 100644 index 00000000..ca9058f7 --- /dev/null +++ b/src/Services/GitHostingProviderStatusService.php @@ -0,0 +1,40 @@ +status === null) { + $json = file_get_contents($this::STATUS_URL); + $this->status = $this->checkResponse($json); + } + return $this->status; + } + + /** + * Returns true if response status of API Requests is good, otherwise false + * + * @param string|false $content + * @return bool + */ + abstract protected function checkResponse($content): bool; + +} diff --git a/src/Services/GitHub/GitHubStatusService.php b/src/Services/GitHub/GitHubStatusService.php index c36451d8..9082ff51 100644 --- a/src/Services/GitHub/GitHubStatusService.php +++ b/src/Services/GitHub/GitHubStatusService.php @@ -2,46 +2,19 @@ namespace App\Services\GitHub; +use App\Services\GitHostingProviderStatusService; use JsonException; -class GitHubStatusService +class GitHubStatusService extends GitHostingProviderStatusService { - - private ?bool $status = null; - - /** - * Check if GitHub is functional - * - * @return bool true if status is good, otherwise false - */ - public function isFunctional(): bool - { - if ($this->status === null) { - $this->status = $this->checkFunctional(); - } - return $this->status; - } - /** - * Retrieve status and check if GitHub is functional - * - * @return bool true if status is good, otherwise false + * GitHub status API. + * @see https://www.githubstatus.com/api for more about the GitHub status api + * (same api as https://kctbh9vrtdwd.statuspage.io/api/v2/summary.json) */ - private function checkFunctional(): bool - { - // more about the GitHub status api, see: https://www.githubstatus.com/api - // (same api as https://kctbh9vrtdwd.statuspage.io/api/v2/summary.json) https://www.githubstatus.com/api/v2/summary.json - $content = file_get_contents('https://www.githubstatus.com/api/v2/summary.json'); + protected const STATUS_URL = 'https://www.githubstatus.com/api/v2/summary.json'; - return $this->checkResponse($content); - } - /** - * Returns true if response status of API Requests is good, otherwise false - * - * @param string|false $content - * @return bool - */ protected function checkResponse($content): bool { if (!$content) { @@ -67,4 +40,4 @@ protected function checkResponse($content): bool return $numberOfWorkingComponents === 2; } -} \ No newline at end of file +} diff --git a/src/Services/GitLab/GitLabStatusService.php b/src/Services/GitLab/GitLabStatusService.php index 22dec97f..f5d26c47 100644 --- a/src/Services/GitLab/GitLabStatusService.php +++ b/src/Services/GitLab/GitLabStatusService.php @@ -2,48 +2,25 @@ namespace App\Services\GitLab; +use App\Services\GitHostingProviderStatusService; use JsonException; -class GitLabStatusService +class GitLabStatusService extends GitHostingProviderStatusService { - public const STATUS_OPERATIONAL = 100; - private ?bool $status = null; - - /** - * Check if GitLab is functional - * - * @return bool true if status is good, otherwise false - */ - public function isFunctional(): bool - { - if ($this->status === null) { - $this->status = $this->checkFunctional(); - } - return $this->status; - } - /** - * Retrieve status and check if GitLab is functional + * GitLab status page. * - * @return bool true if status is good, otherwise false + * The URL to retrieve the status information in JSON is documented in the + * status.io knowledge base article for the Web Status Widget. + * @see https://status.gitlab.com/ + * @see https://status.io/pages/5b36dc6502d06804c08349f7 + * @see https://kb.status.io/developers/public-status-api/ + * @see https://kb.status.io/miscellaneous/status-widget/ + * @see https://kb.status.io/developers/status-codes/ */ - private function checkFunctional(): bool - { - // GitLab status page, see: https://status.gitlab.com/ (or https://status.io/pages/5b36dc6502d06804c08349f7) - - // more about the status.io status api, see: https://kb.status.io/developers/public-status-api/ - // https://4888742015139690.hostedstatus.com/1.0/status/5b36dc6502d06804c08349f7 - $content = file_get_contents('http://hostedstatus.com/1.0/status/5b36dc6502d06804c08349f7'); - - return $this->checkResponse($content); - } + protected const STATUS_URL = 'http://hostedstatus.com/1.0/status/5b36dc6502d06804c08349f7'; + public const STATUS_OPERATIONAL = 100; - /** - * Returns true if response status of API Requests is good, otherwise false - * - * @param string|false $content - * @return bool - */ protected function checkResponse($content): bool { if (!$content) { @@ -69,4 +46,4 @@ protected function checkResponse($content): bool return $numberOfWorkingComponents === 2; } -} \ No newline at end of file +} From 08ca3a48b669cf6974a3d3e469fee099fd1b2192 Mon Sep 17 00:00:00 2001 From: Damien Regad Date: Sun, 21 Sep 2025 23:09:03 +0200 Subject: [PATCH 05/11] Squelch PHPStorm warning Inspection complains about multiple definitions for JsonException. Just ignore it. --- src/Services/GitHub/GitHubStatusService.php | 2 ++ src/Services/GitLab/GitLabStatusService.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Services/GitHub/GitHubStatusService.php b/src/Services/GitHub/GitHubStatusService.php index 9082ff51..4dc788a4 100644 --- a/src/Services/GitHub/GitHubStatusService.php +++ b/src/Services/GitHub/GitHubStatusService.php @@ -1,5 +1,7 @@ Date: Sun, 21 Sep 2025 23:31:52 +0200 Subject: [PATCH 06/11] New abstract class GitHostingProviderBehavior Parent Class for Git Hosting Provider Behavior, factors out duplicated code from GitHubBehavior and GitLabBehavior classes (which have been reduced to a single property override). --- .../Behavior/GitHostingProviderBehavior.php | 159 ++++++++++++++++++ .../Repository/Behavior/GitHubBehavior.php | 147 +--------------- .../Repository/Behavior/GitLabBehavior.php | 148 +--------------- 3 files changed, 163 insertions(+), 291 deletions(-) create mode 100644 src/Services/Repository/Behavior/GitHostingProviderBehavior.php diff --git a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php new file mode 100644 index 00000000..0ca9f380 --- /dev/null +++ b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php @@ -0,0 +1,159 @@ +api = $api; + $this->status = $statusService; + } + + /** + * Create branch and push it to remote, then submit a pull request + * + * @param GitRepository $tempGit temporary local git repository with the patch of the language update + * @param TranslationUpdateEntity $update + * @param GitRepository $forkedGit git repository cloned of the forked repository + * + * @throws GitAddException + * @throws GitBranchException + * @throws GitCheckoutException + * @throws GitNoRemoteException + * @throws GitPushException + */ + public function sendChange(GitRepository $tempGit, TranslationUpdateEntity $update, GitRepository $forkedGit): void + { + $remoteUrl = $forkedGit->getRemoteUrl(); + $tempGit->remoteAdd($this->remote, $remoteUrl); + $branchName = 'lang_update_' . $update->getId() . '_' . $update->getUpdated(); + $tempGit->branch($branchName); + $tempGit->checkout($branchName); + + $tempGit->push($this->remote, $branchName); + + $this->api->createPullRequest( + $branchName, + $update->getRepository()->getBranch(), + $update->getLanguage(), + $update->getRepository()->getUrl(), + $remoteUrl + ); + } + + /** + * Fork original repo and return the fork's url. + * + * @param RepositoryEntity $repository + * @return string Git clone URL of the fork + */ + public function createOriginURL(RepositoryEntity $repository): string + { + return $this->api->createFork($repository->getUrl()); + } + + /** + * Remove the fork. + * + * @param GitRepository $forkedGit git repository cloned of the forked repository + * + * @throws GitNoRemoteException + */ + public function removeRemoteFork(GitRepository $forkedGit): void + { + $remoteUrl = $forkedGit->getRemoteUrl(); + $this->api->deleteFork($remoteUrl); + } + + /** + * Update from original and push to fork of translate tool + * + * @param GitRepository $forkedGit git repository cloned of the forked repository + * @param RepositoryEntity $repository + * @return bool true if the repository is changed + * + * @throws GitPullException + * @throws GitPushException + */ + public function pull(GitRepository $forkedGit, RepositoryEntity $repository): bool + { + $changed = $forkedGit->pull($repository->getUrl(), $repository->getBranch()) === GitRepository::PULL_CHANGED; + $forkedGit->push('origin', $repository->getBranch()); + return $changed; + } + + /** + * Update from original and push to fork of translate tool (assumes there are no local changes) + * + * @param GitRepository $forkedGit git repository cloned of the forked repository + * @param RepositoryEntity $repository + * @return bool true if the repository is changed + * + * @throws GitPullException + * @throws GitPushException + */ + public function reset(GitRepository $forkedGit, RepositoryEntity $repository): bool + { + $changed = $forkedGit->reset($repository->getUrl(), $repository->getBranch()) === GitRepository::PULL_CHANGED; + $forkedGit->push('origin', $repository->getBranch()); + return $changed; + } + + /** + * Check if Git Hosting provider is functional. + * + * @return bool + */ + public function isFunctional(): bool + { + return $this->status->isFunctional(); + } + + /** + * Get information about the open pull requests i.e. url and count + * + * @param RepositoryEntity $repository + * @param LanguageNameEntity $language + * @return array{count: int, listURL: string, title: string} + */ + public function getOpenPRListInfo(RepositoryEntity $repository, LanguageNameEntity $language): array + { + return $this->api->getOpenPRListInfo($repository->getUrl(), $language->getCode()); + } + +} diff --git a/src/Services/Repository/Behavior/GitHubBehavior.php b/src/Services/Repository/Behavior/GitHubBehavior.php index 8eed9617..5d35dcef 100644 --- a/src/Services/Repository/Behavior/GitHubBehavior.php +++ b/src/Services/Repository/Behavior/GitHubBehavior.php @@ -2,150 +2,7 @@ namespace App\Services\Repository\Behavior; -use Github\Exception\MissingArgumentException; -use App\Entity\LanguageNameEntity; -use App\Entity\RepositoryEntity; -use App\Entity\TranslationUpdateEntity; -use App\Services\Git\GitAddException; -use App\Services\Git\GitBranchException; -use App\Services\Git\GitCheckoutException; -use App\Services\Git\GitNoRemoteException; -use App\Services\Git\GitPullException; -use App\Services\Git\GitPushException; -use App\Services\Git\GitRepository; -use App\Services\GitHub\GitHubCreatePullRequestException; -use App\Services\GitHub\GitHubForkException; -use App\Services\GitHub\GitHubService; -use App\Services\GitHub\GitHubServiceException; -use App\Services\GitHub\GitHubStatusService; - -class GitHubBehavior implements RepositoryBehavior +class GitHubBehavior extends GitHostingProviderBehavior { - - private GitHubService $api; - private GitHubStatusService $gitHubStatus; - - public function __construct(GitHubService $api, GitHubStatusService $gitHubStatus) - { - $this->api = $api; - $this->gitHubStatus = $gitHubStatus; - } - - /** - * Create branch and push it to remote, create subsequently pull request at Github - * - * @param GitRepository $tempGit temporary local git repository with the patch of the language update - * @param TranslationUpdateEntity $update - * @param GitRepository $forkedGit git repository cloned of the forked repository - * - * @throws GitHubCreatePullRequestException - * @throws GitHubServiceException - * @throws GitAddException - * @throws GitBranchException - * @throws GitCheckoutException - * @throws GitNoRemoteException - * @throws GitPushException - * @throws MissingArgumentException - */ - public function sendChange(GitRepository $tempGit, TranslationUpdateEntity $update, GitRepository $forkedGit): void - { - - $remoteUrl = $forkedGit->getRemoteUrl(); - $tempGit->remoteAdd('github', $remoteUrl); - $branchName = 'lang_update_' . $update->getId() . '_' . $update->getUpdated(); - $tempGit->branch($branchName); - $tempGit->checkout($branchName); - - $tempGit->push('github', $branchName); - - $this->api->createPullRequest($branchName, $update->getRepository()->getBranch(), - $update->getLanguage(), $update->getRepository()->getUrl(), $remoteUrl); - } - - /** - * Fork original repo at Github and return url of the fork - * - * @param RepositoryEntity $repository - * @return string Git clone URL of the fork - * - * @throws GitHubForkException - * @throws GitHubServiceException - */ - public function createOriginURL(RepositoryEntity $repository): string - { - return $this->api->createFork($repository->getUrl()); - } - - /** - * Remove the fork on GitHub - * - * @param GitRepository $forkedGit git repository cloned of the forked repository - * - * @throws GitHubServiceException - * @throws GitNoRemoteException - */ - public function removeRemoteFork(GitRepository $forkedGit): void - { - $remoteUrl = $forkedGit->getRemoteUrl(); - $this->api->deleteFork($remoteUrl); - } - - /** - * Update from original and push to fork of translate tool - * - * @param GitRepository $forkedGit git repository cloned of the forked repository - * @param RepositoryEntity $repository - * @return bool true if the repository is changed - * - * @throws GitPullException - * @throws GitPushException - */ - public function pull(GitRepository $forkedGit, RepositoryEntity $repository): bool - { - $changed = $forkedGit->pull($repository->getUrl(), $repository->getBranch()) === GitRepository::PULL_CHANGED; - $forkedGit->push('origin', $repository->getBranch()); - return $changed; - } - - /** - * Update from original and push to fork of translate tool (assumes there are no local changes) - * - * @param GitRepository $forkedGit git repository cloned of the forked repository - * @param RepositoryEntity $repository - * @return bool true if the repository is changed - * - * @throws GitPullException - * @throws GitPushException - */ - public function reset(GitRepository $forkedGit, RepositoryEntity $repository): bool - { - $changed = $forkedGit->reset($repository->getUrl(), $repository->getBranch()) === GitRepository::PULL_CHANGED; - $forkedGit->push('origin', $repository->getBranch()); - return $changed; - } - - /** - * Check if GitHub is functional - * - * @return bool - */ - public function isFunctional(): bool - { - return $this->gitHubStatus->isFunctional(); - } - - /** - * Get information about the open pull requests i.e. url and count - * - * @param RepositoryEntity $repository - * @param LanguageNameEntity $language - * @return array{count: int, listURL: string, title: string} - * - * @throws GitHubServiceException - */ - public function getOpenPRListInfo(RepositoryEntity $repository, LanguageNameEntity $language): array - { - return $this->api->getOpenPRListInfo($repository->getUrl(), $language->getCode()); - } - + protected string $remote = 'github'; } diff --git a/src/Services/Repository/Behavior/GitLabBehavior.php b/src/Services/Repository/Behavior/GitLabBehavior.php index ec9632b9..2c2edd63 100644 --- a/src/Services/Repository/Behavior/GitLabBehavior.php +++ b/src/Services/Repository/Behavior/GitLabBehavior.php @@ -2,151 +2,7 @@ namespace App\Services\Repository\Behavior; -use App\Entity\LanguageNameEntity; -use App\Entity\RepositoryEntity; -use App\Entity\TranslationUpdateEntity; -use App\Services\Git\GitAddException; -use App\Services\Git\GitBranchException; -use App\Services\Git\GitCheckoutException; -use App\Services\Git\GitNoRemoteException; -use App\Services\Git\GitPullException; -use App\Services\Git\GitPushException; -use App\Services\Git\GitRepository; -use App\Services\GitLab\GitLabCreateMergeRequestException; -use App\Services\GitLab\GitLabForkException; -use App\Services\GitLab\GitLabService; -use App\Services\GitLab\GitLabServiceException; -use App\Services\GitLab\GitLabStatusService; - -class GitLabBehavior implements RepositoryBehavior +class GitLabBehavior extends GitHostingProviderBehavior { - - private GitLabService $api; - private GitLabStatusService $gitLabStatus; - - public function __construct(GitLabService $api, GitLabStatusService $gitLabStatus) - { - $this->api = $api; - $this->gitLabStatus = $gitLabStatus; - } - - /** - * Create branch and push it to remote, create subsequently pull request at GitLab - * - * @param GitRepository $tempGit temporary local git repository with the patch of the language update - * @param TranslationUpdateEntity $update - * @param GitRepository $forkedGit git repository cloned of the fork - * - * @throws GitLabCreateMergeRequestException - * @throws GitLabServiceException - * @throws GitAddException - * @throws GitBranchException - * @throws GitCheckoutException - * @throws GitNoRemoteException - * @throws GitPushException - */ - public function sendChange(GitRepository $tempGit, TranslationUpdateEntity $update, GitRepository $forkedGit): void - { - $remoteUrl = $forkedGit->getRemoteUrl(); - $tempGit->remoteAdd('gitlab', $remoteUrl); - $branchName = 'lang_update_' . $update->getId() . '_' . $update->getUpdated(); - $tempGit->branch($branchName); - $tempGit->checkout($branchName); - - $tempGit->push('gitlab', $branchName); - - $this->api->createPullRequest( - $branchName, $update->getRepository()->getBranch(), - $update->getLanguage(), $update->getRepository()->getUrl(), $remoteUrl - ); - } - - /** - * Fork original repo at GitLab and return url of the fork - * - * @param RepositoryEntity $repository - * @return string git clone URL of the fork - * - * @throws GitLabForkException - * @throws GitLabServiceException - */ - public function createOriginURL(RepositoryEntity $repository): string - { - return $this->api->createFork($repository->getUrl()); - } - - - /** - * Remove the fork on GitLab - * - * @param GitRepository $forkedGit git repository cloned of the fork repository - * - * @throws GitLabServiceException - * @throws GitNoRemoteException - */ - public function removeRemoteFork(GitRepository $forkedGit): void - { - $remoteUrl = $forkedGit->getRemoteUrl(); - - $this->api->deleteFork($remoteUrl); - } - - /** - * Update from original and push to fork of translate tool - * - * @param GitRepository $forkedGit git repository cloned of the forked repository - * @param RepositoryEntity $repository - * @return bool true if the repository is changed - * - * @throws GitPullException - * @throws GitPushException - */ - public function pull(GitRepository $forkedGit, RepositoryEntity $repository): bool - { - $changed = $forkedGit->pull($repository->getUrl(), $repository->getBranch()) === GitRepository::PULL_CHANGED; - $forkedGit->push('origin', $repository->getBranch()); - return $changed; - } - - /** - * Update from original and push to fork of translate tool (assumes there are no local changes) - * - * @param GitRepository $forkedGit git repository cloned of the forked repository - * @param RepositoryEntity $repository - * @return bool true if the repository is changed - * - * @throws GitPullException - * @throws GitPushException - */ - public function reset(GitRepository $forkedGit, RepositoryEntity $repository): bool - { - $changed = $forkedGit->reset($repository->getUrl(), $repository->getBranch()) === GitRepository::PULL_CHANGED; - $forkedGit->push('origin', $repository->getBranch()); - return $changed; - } - - /** - * Check if GitLab is functional - * - * @return bool - */ - public function isFunctional(): bool - { - return $this->gitLabStatus->isFunctional(); - } - - /** - * Get information about the open pull requests i.e. url and count - * - * @param RepositoryEntity $repository - * @param LanguageNameEntity $language - * @return array{count: int, listURL: string, title: string} - * - * @throws GitLabServiceException - */ - public function getOpenPRListInfo(RepositoryEntity $repository, LanguageNameEntity $language): array - { - return $this->api->getOpenPRListInfo($repository->getUrl(), $language->getCode()); - } - + protected string $remote = 'gitlab'; } \ No newline at end of file From 0cda159892e33be48fea076bccf1021c84d26fd9 Mon Sep 17 00:00:00 2001 From: Damien Regad Date: Sun, 21 Sep 2025 23:36:54 +0200 Subject: [PATCH 07/11] Change createPullRequest() method signature - Remove $languageCode parameter - Add $title and $body parameters The caller is now responsible to set the pull request title and body, to avoid duplicating the logic in the Git Service classes. --- src/Services/GitHostingProviderService.php | 5 +++-- src/Services/GitHub/GitHubService.php | 6 +++--- src/Services/GitLab/GitLabService.php | 6 +++--- .../Repository/Behavior/GitHostingProviderBehavior.php | 5 +++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Services/GitHostingProviderService.php b/src/Services/GitHostingProviderService.php index 3105eb28..4a04a0bb 100644 --- a/src/Services/GitHostingProviderService.php +++ b/src/Services/GitHostingProviderService.php @@ -83,11 +83,12 @@ abstract public function deleteFork(string $remoteUrl): void; /** * @param string $patchBranch name of branch with language update * @param string $destinationBranch name of branch at remote - * @param string $languageCode * @param string $url git url original upstream repository * @param string $patchUrl remote url + * @param string $title Title for the pull request + * @param string $body Text to be inserted as description for the pull request */ - abstract public function createPullRequest(string $patchBranch, string $destinationBranch, string $languageCode, string $url, string $patchUrl): void; + abstract public function createPullRequest(string $patchBranch, string $destinationBranch, string $url, string $patchUrl, string $title, string $body): void; /** * Get information about the open pull requests i.e. url and count diff --git a/src/Services/GitHub/GitHubService.php b/src/Services/GitHub/GitHubService.php index d82242b4..022ae9cb 100644 --- a/src/Services/GitHub/GitHubService.php +++ b/src/Services/GitHub/GitHubService.php @@ -78,7 +78,7 @@ public function deleteFork(string $remoteUrl): void * @throws GitHubServiceException * @throws MissingArgumentException */ - public function createPullRequest(string $patchBranch, string $destinationBranch, string $languageCode, string $url, string $patchUrl): void + public function createPullRequest(string $patchBranch, string $destinationBranch, string $url, string $patchUrl, string $title, string $body): void { [$user, $repository] = $this->getUsernameAndRepositoryFromURL($url); [$repoName, ] = $this->getUsernameAndRepositoryFromURL($patchUrl); @@ -87,8 +87,8 @@ public function createPullRequest(string $patchBranch, string $destinationBranch $this->client->api('pull_request')->create($user, $repository, [ 'base' => $destinationBranch, 'head' => $repoName . ':' . $patchBranch, - 'title' => 'Translation update (' . $languageCode . ')', - 'body' => 'This pull request contains some translation updates.' + 'title' => $title, + 'body' => $body, ]); } catch (RuntimeException $e) { throw new GitHubCreatePullRequestException($e->getMessage() . " $user/$repository", 0, $e); diff --git a/src/Services/GitLab/GitLabService.php b/src/Services/GitLab/GitLabService.php index 200c0a87..7673a8cc 100644 --- a/src/Services/GitLab/GitLabService.php +++ b/src/Services/GitLab/GitLabService.php @@ -111,7 +111,7 @@ public function deleteFork(string $remoteUrl): void * @throws GitLabCreateMergeRequestException * @throws GitLabServiceException */ - public function createPullRequest(string $patchBranch, string $destinationBranch, string $languageCode, string $url, string $patchUrl): void + public function createPullRequest(string $patchBranch, string $destinationBranch, string $url, string $patchUrl, string $title, string $body): void { [$userUpstream, $repositoryUpstream] = $this->getUsernameAndRepositoryFromURL($url); $idUpstream = $this->getProjectIdOfUpstream(); @@ -122,9 +122,9 @@ public function createPullRequest(string $patchBranch, string $destinationBranch "$userFork/$repositoryFork", $patchBranch, $destinationBranch, - "Translation update ($languageCode)", + $title, [ - 'description' => 'This pull request contains some translation updates.', + 'description' => $body, 'target_project_id' => $idUpstream, 'remove_source_branch' => true ] diff --git a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php index 0ca9f380..d7eaedb8 100644 --- a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php +++ b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php @@ -70,9 +70,10 @@ public function sendChange(GitRepository $tempGit, TranslationUpdateEntity $upda $this->api->createPullRequest( $branchName, $update->getRepository()->getBranch(), - $update->getLanguage(), $update->getRepository()->getUrl(), - $remoteUrl + $remoteUrl, + 'Translation update (' . $update->getLanguage() . ')', + 'This pull request contains some translation updates.' ); } From 9e83c87a608b1a6507303a8c55f654dd8e31aa22 Mon Sep 17 00:00:00 2001 From: Damien Regad Date: Sun, 21 Sep 2025 23:40:47 +0200 Subject: [PATCH 08/11] Define pull request body text in class property --- .../Repository/Behavior/GitHostingProviderBehavior.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php index d7eaedb8..54b91701 100644 --- a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php +++ b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php @@ -37,6 +37,14 @@ abstract class GitHostingProviderBehavior implements RepositoryBehavior */ protected string $remote; + /** + * Text to be inserted as description for the pull request. + * @see sendChange() + */ + protected string $prBody = <<< EOT + This pull request contains some translation updates. + EOT; + public function __construct(GitHostingProviderService $api, GitHostingProviderStatusService $statusService) { @@ -73,7 +81,7 @@ public function sendChange(GitRepository $tempGit, TranslationUpdateEntity $upda $update->getRepository()->getUrl(), $remoteUrl, 'Translation update (' . $update->getLanguage() . ')', - 'This pull request contains some translation updates.' + $this->prBody ); } From a584eb67ac8a3582b3329936d2325788740bcd81 Mon Sep 17 00:00:00 2001 From: Damien Regad Date: Mon, 22 Sep 2025 18:29:44 +0200 Subject: [PATCH 09/11] New method TranslationUpdateEntity::getSubject() Allows a consistent definition of the Mail Subject / Commit message / Pull Request title in a single location. --- src/Entity/TranslationUpdateEntity.php | 23 +++++++++++++++++++ .../Behavior/GitHostingProviderBehavior.php | 2 +- .../Repository/Behavior/PlainBehavior.php | 2 +- src/Services/Repository/Repository.php | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Entity/TranslationUpdateEntity.php b/src/Entity/TranslationUpdateEntity.php index 8ae5126c..f608ff21 100644 --- a/src/Entity/TranslationUpdateEntity.php +++ b/src/Entity/TranslationUpdateEntity.php @@ -55,6 +55,13 @@ class TranslationUpdateEntity { */ protected ?string $language = null; + /** + * Subject message. + * @see getSubject() + */ + private string $subject = 'Translation update'; + + public function setAuthor(string $author): void { $this->author = $author; } @@ -112,6 +119,22 @@ public function getLanguage(): ?string { return $this->language; } + /** + * Returns a standard subject for translation updates. + * + * This can be used e.g. as commit message, pull request title or email subject. + * If defined, the language code is added as a suffix. + * + * @return string + */ + public function getSubject(): string { + $subject = $this->subject; + if ($this->language) { + $subject .= ' (' . $this->language . ')'; + } + return $subject; + } + public function setErrorMsg(string $errorMsg): void { $this->errorMsg = $errorMsg; diff --git a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php index 54b91701..63dbf219 100644 --- a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php +++ b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php @@ -80,7 +80,7 @@ public function sendChange(GitRepository $tempGit, TranslationUpdateEntity $upda $update->getRepository()->getBranch(), $update->getRepository()->getUrl(), $remoteUrl, - 'Translation update (' . $update->getLanguage() . ')', + $update->getSubject(), $this->prBody ); } diff --git a/src/Services/Repository/Behavior/PlainBehavior.php b/src/Services/Repository/Behavior/PlainBehavior.php index 6be7bde2..bffeec32 100644 --- a/src/Services/Repository/Behavior/PlainBehavior.php +++ b/src/Services/Repository/Behavior/PlainBehavior.php @@ -38,7 +38,7 @@ public function sendChange(GitRepository $tempGit, TranslationUpdateEntity $upda $this->mailService->sendPatchEmail( $update->getRepository()->getEmail(), - 'Language Update', + $update->getSubject(), $patch, 'mail/languageUpdate.txt.twig', ['update' => $update] diff --git a/src/Services/Repository/Repository.php b/src/Services/Repository/Repository.php index da5a7535..8d856a56 100644 --- a/src/Services/Repository/Repository.php +++ b/src/Services/Repository/Repository.php @@ -500,7 +500,7 @@ private function createAndSendPatchWithException(TranslationUpdateEntity $update // add files to local temporary git repository $this->applyChanges($tmpGit, $tmpDir, $update); // commit files to local temporary git repository - $message = 'Translation update (' . $update->getLanguage() . ')'; + $message = $update->getSubject(); $author = $this->prepareAuthor($update); $tmpGit->commit($message, $author); From 47ebb53f597edcf38eb6817f36be002f05f671ec Mon Sep 17 00:00:00 2001 From: Gerrit Uitslag Date: Wed, 12 Nov 2025 23:05:49 +0100 Subject: [PATCH 10/11] Update argument names in the service configuration as well --- config/services.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/services.yaml b/config/services.yaml index 654690b3..4b78a517 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -79,15 +79,15 @@ services: App\Services\GitHub\GitHubService: arguments: - $gitHubApiToken: '%app.githubApiToken%' + $apiToken: '%app.githubApiToken%' $dataFolder: '%app.dataDir%' - $gitHubUrl: '%app.githubUrl%' + $url: '%app.githubUrl%' App\Services\GitLab\GitLabService: arguments: - $gitLabApiToken: '%app.gitlabApiToken%' + $apiToken: '%app.gitlabApiToken%' $dataFolder: '%app.dataDir%' - $gitLabUrl: '%app.gitlabUrl%' + $url: '%app.gitlabUrl%' App\Repository\TranslationUpdateEntityRepository: arguments: From 9ce944b7fef9df170a14343eac50b87789291835 Mon Sep 17 00:00:00 2001 From: Gerrit Uitslag Date: Sat, 22 Nov 2025 23:09:56 +0100 Subject: [PATCH 11/11] remote name can be generalized, so no need anymore for GitlabBehavior and GitHubBehavior. All hosting specific tasks are already handled by their respectively api. --- .../Behavior/GitHostingProviderBehavior.php | 22 +++++-------------- .../Repository/Behavior/GitHubBehavior.php | 8 ------- .../Repository/Behavior/GitLabBehavior.php | 8 ------- src/Services/Repository/RepositoryManager.php | 7 +++--- 4 files changed, 9 insertions(+), 36 deletions(-) delete mode 100644 src/Services/Repository/Behavior/GitHubBehavior.php delete mode 100644 src/Services/Repository/Behavior/GitLabBehavior.php diff --git a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php index 63dbf219..d4b3c806 100644 --- a/src/Services/Repository/Behavior/GitHostingProviderBehavior.php +++ b/src/Services/Repository/Behavior/GitHostingProviderBehavior.php @@ -15,28 +15,18 @@ use App\Services\GitHostingProviderService; use App\Services\GitHostingProviderStatusService; -/** - * Generic Git Hosting Provider Behavior. - * - * @see GitHubBehavior, GitLabBehavior - */ -abstract class GitHostingProviderBehavior implements RepositoryBehavior +class GitHostingProviderBehavior implements RepositoryBehavior { /** - * Git Service api. + * Git Service api, provider specific */ protected GitHostingProviderService $api; /** - * Git Service status. + * Service for status of the api */ protected GitHostingProviderStatusService $status; - /** - * Git repository remote name - */ - protected string $remote; - /** * Text to be inserted as description for the pull request. * @see sendChange() @@ -53,7 +43,7 @@ public function __construct(GitHostingProviderService $api, GitHostingProviderSt } /** - * Create branch and push it to remote, then submit a pull request + * Create branch and push it to the remote fork, then submit a pull request * * @param GitRepository $tempGit temporary local git repository with the patch of the language update * @param TranslationUpdateEntity $update @@ -68,12 +58,12 @@ public function __construct(GitHostingProviderService $api, GitHostingProviderSt public function sendChange(GitRepository $tempGit, TranslationUpdateEntity $update, GitRepository $forkedGit): void { $remoteUrl = $forkedGit->getRemoteUrl(); - $tempGit->remoteAdd($this->remote, $remoteUrl); + $tempGit->remoteAdd('remote_fork', $remoteUrl); $branchName = 'lang_update_' . $update->getId() . '_' . $update->getUpdated(); $tempGit->branch($branchName); $tempGit->checkout($branchName); - $tempGit->push($this->remote, $branchName); + $tempGit->push('remote_fork', $branchName); $this->api->createPullRequest( $branchName, diff --git a/src/Services/Repository/Behavior/GitHubBehavior.php b/src/Services/Repository/Behavior/GitHubBehavior.php deleted file mode 100644 index 5d35dcef..00000000 --- a/src/Services/Repository/Behavior/GitHubBehavior.php +++ /dev/null @@ -1,8 +0,0 @@ -getUrl(); if (preg_match('/^(git:\/\/|https:\/\/|git@)github\.com/i', $url)) { - return new GitHubBehavior($this->gitHubService, $this->gitHubStatus); + return new GitHostingProviderBehavior($this->gitHubService, $this->gitHubStatus); } if (preg_match('/^(git:\/\/|https:\/\/|git@)gitlab\.com/i', $url)) { //build manually as Repository is not yet available.. $repoFolder = $this->dataFolder . '/gitlab_projectids/' . $repository->getType() . '/' . $repository->getName() . '/'; $this->gitLabService->setProjectIdFolder($repoFolder); - return new GitLabBehavior($this->gitLabService, $this->gitLabStatus); + return new GitHostingProviderBehavior($this->gitLabService, $this->gitLabStatus); } return new PlainBehavior($this->mailService); }