From 8a03d19e58f105fb968a17af9afdf61a02329745 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:46:14 +0000 Subject: [PATCH 01/17] Add Gogs VCS adapter, tests, Docker service and CI config Co-authored-by: Meldiron <19310830+Meldiron@users.noreply.github.com> Agent-Logs-Url: https://github.com/utopia-php/vcs/sessions/275932b7-e04b-49d9-8ba8-8df4655d9373 --- docker-compose.yml | 48 +++++++++++++++++- src/VCS/Adapter/Git/Gogs.php | 48 ++++++++++++++++++ tests/VCS/Adapter/GogsTest.php | 62 ++++++++++++++++++++++++ tests/resources/gogs-custom/conf/app.ini | 36 ++++++++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/VCS/Adapter/Git/Gogs.php create mode 100644 tests/VCS/Adapter/GogsTest.php create mode 100644 tests/resources/gogs-custom/conf/app.ini diff --git a/docker-compose.yml b/docker-compose.yml index b7bc513f..cd22deb9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: - ./phpunit.xml:/usr/local/src/phpunit.xml - gitea-data:/data:ro - forgejo-data:/forgejo-data:ro + - gogs-data:/gogs-data:ro environment: - TESTS_GITHUB_PRIVATE_KEY - TESTS_GITHUB_APP_IDENTIFIER @@ -15,6 +16,7 @@ services: - TESTS_GITEA_URL=http://gitea:3000 - TESTS_GITEA_REQUEST_CATCHER_URL=http://request-catcher:5000 - TESTS_FORGEJO_URL=http://forgejo:3000 + - TESTS_GOGS_URL=http://gogs:3000 depends_on: gitea: condition: service_healthy @@ -24,6 +26,10 @@ services: condition: service_healthy forgejo-bootstrap: condition: service_completed_successfully + gogs: + condition: service_healthy + gogs-bootstrap: + condition: service_completed_successfully request-catcher: condition: service_started @@ -115,6 +121,46 @@ services: fi " + gogs: + image: gogs/gogs:0.13 + environment: + - GOGS_CUSTOM=/data/gogs + volumes: + - gogs-data:/data + - ./tests/resources/gogs-custom/conf/app.ini:/data/gogs/conf/app.ini + ports: + - "3002:3000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 15s + + gogs-bootstrap: + image: gogs/gogs:0.13 + volumes: + - gogs-data:/data + depends_on: + gogs: + condition: service_healthy + entrypoint: /bin/sh + environment: + - GOGS_ADMIN_USERNAME=${GOGS_ADMIN_USERNAME:-utopia} + - GOGS_ADMIN_PASSWORD=${GOGS_ADMIN_PASSWORD:-password} + - GOGS_ADMIN_EMAIL=${GOGS_ADMIN_EMAIL:-utopia@example.com} + command: > + -c " + su git -c \"/opt/gogs/gogs admin create-user --name=$$GOGS_ADMIN_USERNAME --password=$$GOGS_ADMIN_PASSWORD --email=$$GOGS_ADMIN_EMAIL --admin\" || true && + if [ ! -f /data/gogs/token.txt ]; then + apk add --no-cache curl jq && + TOKEN=$$(curl -s -X POST http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens -H 'Content-Type: application/json' -d '{\"name\":\"bootstrap-token\"}' -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD | jq -r '.sha1') && + mkdir -p /data/gogs && + echo $$TOKEN > /data/gogs/token.txt; + fi + " + volumes: gitea-data: - forgejo-data: \ No newline at end of file + forgejo-data: + gogs-data: \ No newline at end of file diff --git a/src/VCS/Adapter/Git/Gogs.php b/src/VCS/Adapter/Git/Gogs.php new file mode 100644 index 00000000..c652e70b --- /dev/null +++ b/src/VCS/Adapter/Git/Gogs.php @@ -0,0 +1,48 @@ + List of commit statuses + */ + public function getCommitStatuses(string $owner, string $repositoryName, string $commitHash): array + { + $statuses = parent::getCommitStatuses($owner, $repositoryName, $commitHash); + + return array_map(function ($status) { + if (isset($status['state']) && !isset($status['status'])) { + $status['status'] = $status['state']; + } + return $status; + }, $statuses); + } +} diff --git a/tests/VCS/Adapter/GogsTest.php b/tests/VCS/Adapter/GogsTest.php new file mode 100644 index 00000000..d689faa8 --- /dev/null +++ b/tests/VCS/Adapter/GogsTest.php @@ -0,0 +1,62 @@ +setupGogs(); + } + + $adapter = new Gogs(new Cache(new None())); + $gogsUrl = System::getEnv('TESTS_GOGS_URL', 'http://gogs:3000') ?? ''; + + $adapter->initializeVariables( + installationId: '', + privateKey: '', + appId: '', + accessToken: static::$accessToken, + refreshToken: '' + ); + $adapter->setEndpoint($gogsUrl); + if (empty(static::$owner)) { + $orgName = 'test-org-' . \uniqid(); + static::$owner = $adapter->createOrganization($orgName); + } + + $this->vcsAdapter = $adapter; + } + + protected function setupGogs(): void + { + $tokenFile = '/gogs-data/gogs/token.txt'; + + if (file_exists($tokenFile)) { + $contents = file_get_contents($tokenFile); + if ($contents !== false) { + static::$accessToken = trim($contents); + } + } + } +} diff --git a/tests/resources/gogs-custom/conf/app.ini b/tests/resources/gogs-custom/conf/app.ini new file mode 100644 index 00000000..ea90f610 --- /dev/null +++ b/tests/resources/gogs-custom/conf/app.ini @@ -0,0 +1,36 @@ +APP_NAME = Gogs +RUN_MODE = prod +RUN_USER = git + +[server] +DOMAIN = gogs +HTTP_PORT = 3000 +ROOT_URL = http://gogs:3000/ +LOCAL_ROOT_URL = http://gogs:3000/ +DISABLE_SSH = true + +[database] +DB_TYPE = sqlite3 +PATH = /data/gogs.db + +[repository] +ROOT = /data/repositories + +[security] +INSTALL_LOCK = true +# SECRET_KEY is intentionally hardcoded — this config is for local/CI testing only. +SECRET_KEY = verySecretGogsKey1234567890 + +[service] +REGISTER_EMAIL_CONFIRM = false +ENABLE_NOTIFY_MAIL = false +DISABLE_REGISTRATION = false +ENABLE_CAPTCHA = false + +[webhook] +DELIVER_TIMEOUT = 10 +SKIP_TLS_VERIFY = true + +[log] +MODE = console +LEVEL = Info From 91907fd03a14a7f8af2ade42ba8f696b5503ac96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:36:48 +0000 Subject: [PATCH 02/17] Fix Gogs container health: use /install endpoint bootstrap instead of pre-configured app.ini Co-authored-by: Meldiron <19310830+Meldiron@users.noreply.github.com> Agent-Logs-Url: https://github.com/utopia-php/vcs/sessions/a31418bd-fb1d-47ab-8ff3-8c8e56db0365 --- docker-compose.yml | 10 +++---- tests/resources/gogs-custom/conf/app.ini | 36 ------------------------ 2 files changed, 4 insertions(+), 42 deletions(-) delete mode 100644 tests/resources/gogs-custom/conf/app.ini diff --git a/docker-compose.yml b/docker-compose.yml index cd22deb9..86460a08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -123,11 +123,8 @@ services: gogs: image: gogs/gogs:0.13 - environment: - - GOGS_CUSTOM=/data/gogs volumes: - gogs-data:/data - - ./tests/resources/gogs-custom/conf/app.ini:/data/gogs/conf/app.ini ports: - "3002:3000" healthcheck: @@ -151,10 +148,11 @@ services: - GOGS_ADMIN_EMAIL=${GOGS_ADMIN_EMAIL:-utopia@example.com} command: > -c " - su git -c \"/opt/gogs/gogs admin create-user --name=$$GOGS_ADMIN_USERNAME --password=$$GOGS_ADMIN_PASSWORD --email=$$GOGS_ADMIN_EMAIL --admin\" || true && if [ ! -f /data/gogs/token.txt ]; then - apk add --no-cache curl jq && - TOKEN=$$(curl -s -X POST http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens -H 'Content-Type: application/json' -d '{\"name\":\"bootstrap-token\"}' -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD | jq -r '.sha1') && + curl -s -o /dev/null -X POST http://gogs:3000/install -d db_type=SQLite3 -d db_path=/data/gogs.db -d app_name=Gogs -d repo_root_path=/data/repositories -d run_user=git -d domain=gogs -d http_port=3000 -d app_url=http://gogs:3000/ -d log_root_path=/data/gogs/log -d default_branch=master -d admin_name=$$GOGS_ADMIN_USERNAME -d admin_passwd=$$GOGS_ADMIN_PASSWORD -d admin_confirm_passwd=$$GOGS_ADMIN_PASSWORD -d admin_email=$$GOGS_ADMIN_EMAIL && + sleep 3 && + printf '\n[webhook]\nDELIVER_TIMEOUT = 10\nSKIP_TLS_VERIFY = true\n' >> /data/gogs/conf/app.ini && + TOKEN=$$(curl -s -X POST http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens -H 'Content-Type: application/json' -d '{\"name\":\"bootstrap-token\"}' -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD | tr -d ' ' | sed -n 's/.*\"sha1\":\"\([^\"]*\)\".*/\1/p') && mkdir -p /data/gogs && echo $$TOKEN > /data/gogs/token.txt; fi diff --git a/tests/resources/gogs-custom/conf/app.ini b/tests/resources/gogs-custom/conf/app.ini deleted file mode 100644 index ea90f610..00000000 --- a/tests/resources/gogs-custom/conf/app.ini +++ /dev/null @@ -1,36 +0,0 @@ -APP_NAME = Gogs -RUN_MODE = prod -RUN_USER = git - -[server] -DOMAIN = gogs -HTTP_PORT = 3000 -ROOT_URL = http://gogs:3000/ -LOCAL_ROOT_URL = http://gogs:3000/ -DISABLE_SSH = true - -[database] -DB_TYPE = sqlite3 -PATH = /data/gogs.db - -[repository] -ROOT = /data/repositories - -[security] -INSTALL_LOCK = true -# SECRET_KEY is intentionally hardcoded — this config is for local/CI testing only. -SECRET_KEY = verySecretGogsKey1234567890 - -[service] -REGISTER_EMAIL_CONFIRM = false -ENABLE_NOTIFY_MAIL = false -DISABLE_REGISTRATION = false -ENABLE_CAPTCHA = false - -[webhook] -DELIVER_TIMEOUT = 10 -SKIP_TLS_VERIFY = true - -[log] -MODE = console -LEVEL = Info From aaa2cc06f9efe14e1350385e21f7142a98b6635e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Mar 2026 20:40:00 +0100 Subject: [PATCH 03/17] Fix gogs setup --- docker-compose.yml | 54 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 86460a08..e6c02817 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -135,28 +135,58 @@ services: start_period: 15s gogs-bootstrap: - image: gogs/gogs:0.13 + image: alpine/curl:8.12.1 volumes: - gogs-data:/data depends_on: gogs: condition: service_healthy - entrypoint: /bin/sh environment: - GOGS_ADMIN_USERNAME=${GOGS_ADMIN_USERNAME:-utopia} - GOGS_ADMIN_PASSWORD=${GOGS_ADMIN_PASSWORD:-password} - GOGS_ADMIN_EMAIL=${GOGS_ADMIN_EMAIL:-utopia@example.com} - command: > - -c " - if [ ! -f /data/gogs/token.txt ]; then - curl -s -o /dev/null -X POST http://gogs:3000/install -d db_type=SQLite3 -d db_path=/data/gogs.db -d app_name=Gogs -d repo_root_path=/data/repositories -d run_user=git -d domain=gogs -d http_port=3000 -d app_url=http://gogs:3000/ -d log_root_path=/data/gogs/log -d default_branch=master -d admin_name=$$GOGS_ADMIN_USERNAME -d admin_passwd=$$GOGS_ADMIN_PASSWORD -d admin_confirm_passwd=$$GOGS_ADMIN_PASSWORD -d admin_email=$$GOGS_ADMIN_EMAIL && - sleep 3 && - printf '\n[webhook]\nDELIVER_TIMEOUT = 10\nSKIP_TLS_VERIFY = true\n' >> /data/gogs/conf/app.ini && - TOKEN=$$(curl -s -X POST http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens -H 'Content-Type: application/json' -d '{\"name\":\"bootstrap-token\"}' -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD | tr -d ' ' | sed -n 's/.*\"sha1\":\"\([^\"]*\)\".*/\1/p') && - mkdir -p /data/gogs && - echo $$TOKEN > /data/gogs/token.txt; + entrypoint: /bin/sh + command: + - -ce + - | + if [ -f /data/gogs/token.txt ]; then exit 0; fi + + apk add --no-cache jq + + curl -s -o /dev/null -X POST http://gogs:3000/install \ + -d db_type=SQLite3 \ + -d db_path=/data/gogs.db \ + -d app_name=Gogs \ + -d repo_root_path=/data/repositories \ + -d run_user=git \ + -d domain=gogs \ + -d http_port=3000 \ + -d app_url=http://gogs:3000/ \ + -d log_root_path=/data/gogs/log \ + -d default_branch=master \ + -d admin_name=$$GOGS_ADMIN_USERNAME \ + -d admin_passwd=$$GOGS_ADMIN_PASSWORD \ + -d admin_confirm_passwd=$$GOGS_ADMIN_PASSWORD \ + -d admin_email=$$GOGS_ADMIN_EMAIL \ + || true + + sleep 3 + + if ! grep -q '\[webhook\]' /data/gogs/conf/app.ini; then + printf '\n[webhook]\nDELIVER_TIMEOUT = 10\nSKIP_TLS_VERIFY = true\n' >> /data/gogs/conf/app.ini fi - " + + RESPONSE=$$(curl -s -X POST http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens \ + -H 'Content-Type: application/json' \ + -d "{\"name\":\"bootstrap-$$(date +%s)\"}" \ + -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD) + echo "Token API response: $$RESPONSE" + + TOKEN=$$(echo "$$RESPONSE" | jq -r '.sha1') + if [ -z "$$TOKEN" ] || [ "$$TOKEN" = "null" ]; then echo 'Failed to extract token'; exit 1; fi + + mkdir -p /data/gogs + echo $$TOKEN > /data/gogs/token.txt volumes: gitea-data: From 0bc93392335152629bb1f40fc500025901c39647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Mar 2026 20:40:08 +0100 Subject: [PATCH 04/17] Default brank override support --- tests/VCS/Adapter/GiteaTest.php | 37 +++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index fabc431b..f95d269d 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -13,6 +13,7 @@ class GiteaTest extends Base { protected static string $accessToken = ''; protected static string $owner = ''; + protected static string $defaultBranch = 'main'; protected string $webhookEventHeader = 'X-Gitea-Event'; protected string $webhookSignatureHeader = 'X-Gitea-Signature'; @@ -110,7 +111,7 @@ public function testCommentWorkflow(): void try { $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'comment-test', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'comment-test', static::$defaultBranch); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'test.txt', 'test', 'Add test file', 'comment-test'); $pr = $this->vcsAdapter->createPullRequest( @@ -118,7 +119,7 @@ public function testCommentWorkflow(): void $repositoryName, 'Comment Test PR', 'comment-test', - 'main' + static::$defaultBranch ); $prNumber = $pr['number'] ?? 0; @@ -151,7 +152,7 @@ public function testGetComment(): void $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'test-branch', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'test-branch', static::$defaultBranch); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'test.txt', 'test', 'Add test', 'test-branch'); // Create PR @@ -160,7 +161,7 @@ public function testGetComment(): void $repositoryName, 'Test PR', 'test-branch', - 'main' + static::$defaultBranch ); $prNumber = $pr['number'] ?? 0; @@ -216,7 +217,7 @@ public function testGetRepositoryTreeWithSlashInBranchName(): void $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature/test-branch', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature/test-branch', static::$defaultBranch); $tree = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, 'feature/test-branch'); @@ -451,7 +452,7 @@ public function testGetPullRequest(): void $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-branch', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-branch', static::$defaultBranch); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'feature.txt', 'feature content', 'Add feature', 'feature-branch'); $pr = $this->vcsAdapter->createPullRequest( @@ -459,7 +460,7 @@ public function testGetPullRequest(): void $repositoryName, 'Test PR', 'feature-branch', - 'main', + static::$defaultBranch, 'Test PR description' ); @@ -626,7 +627,7 @@ public function testUpdateComment(): void try { $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'test-branch', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'test-branch', static::$defaultBranch); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'test.txt', 'test', 'Add test', 'test-branch'); // Create PR @@ -635,7 +636,7 @@ public function testUpdateComment(): void $repositoryName, 'Test PR', 'test-branch', - 'main' + static::$defaultBranch ); $prNumber = $pr['number'] ?? 0; @@ -1239,7 +1240,7 @@ public function testGetPullRequestFromBranch(): void $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'my-feature', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'my-feature', static::$defaultBranch); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'feature.txt', 'content', 'Add feature', 'my-feature'); // Create PR @@ -1248,7 +1249,7 @@ public function testGetPullRequestFromBranch(): void $repositoryName, 'Feature PR', 'my-feature', - 'main' + static::$defaultBranch ); $this->assertArrayHasKey('number', $pr); @@ -1272,7 +1273,7 @@ public function testGetPullRequestFromBranchNoPR(): void $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'lonely-branch', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'lonely-branch', static::$defaultBranch); // Don't create a PR - just test the method $result = $this->vcsAdapter->getPullRequestFromBranch(static::$owner, $repositoryName, 'lonely-branch'); @@ -1290,7 +1291,7 @@ public function testCreateComment(): void try { $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'test-branch', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'test-branch', static::$defaultBranch); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'test.txt', 'test', 'Add test', 'test-branch'); // Create PR @@ -1299,7 +1300,7 @@ public function testCreateComment(): void $repositoryName, 'Test PR', 'test-branch', - 'main' + static::$defaultBranch ); $prNumber = $pr['number'] ?? 0; @@ -1348,7 +1349,7 @@ public function testCreateFileOnBranch(): void try { $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Main'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature', static::$defaultBranch); // Create file on specific branch $result = $this->vcsAdapter->createFile( @@ -1385,8 +1386,8 @@ public function testListBranches(): void $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); // Create additional branches - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-1', 'main'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-2', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-1', static::$defaultBranch); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-2', static::$defaultBranch); $branches = []; $maxAttempts = 10; @@ -1402,7 +1403,7 @@ public function testListBranches(): void $this->assertIsArray($branches); $this->assertNotEmpty($branches); - $this->assertContains('main', $branches); + $this->assertContains(static::$defaultBranch, $branches); $this->assertContains('feature-1', $branches); $this->assertContains('feature-2', $branches); $this->assertGreaterThanOrEqual(3, count($branches)); From 55699782a755952046ea234f94a777f1fd94bb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Mar 2026 20:42:20 +0100 Subject: [PATCH 05/17] Fix gogs implementation --- src/VCS/Adapter/Git/Gogs.php | 507 +++++++++++++++++++++++++++++++-- tests/VCS/Adapter/GogsTest.php | 5 +- 2 files changed, 494 insertions(+), 18 deletions(-) diff --git a/src/VCS/Adapter/Git/Gogs.php b/src/VCS/Adapter/Git/Gogs.php index c652e70b..1a704a4a 100644 --- a/src/VCS/Adapter/Git/Gogs.php +++ b/src/VCS/Adapter/Git/Gogs.php @@ -2,14 +2,14 @@ namespace Utopia\VCS\Adapter\Git; +use Exception; + class Gogs extends Gitea { protected string $endpoint = 'http://gogs:3000/api/v1'; /** * Get Adapter Name - * - * @return string */ public function getName(): string { @@ -21,28 +21,503 @@ protected function getHookType(): string return 'gogs'; } + /** + * Create new repository + * + * Gogs uses /org/{org}/repos (singular) instead of Gitea's /orgs/{org}/repos (plural). + * + * @return array Details of new repository + */ + public function createRepository(string $owner, string $repositoryName, bool $private): array + { + $url = "/org/{$owner}/repos"; + + $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "token $this->accessToken"], [ + 'name' => $repositoryName, + 'private' => $private, + 'auto_init' => true, + 'readme' => 'Default', + ]); + + return $response['body'] ?? []; + } + + /** + * Create organization for the authenticated user. + * + * Gogs uses POST /user/orgs instead of Gitea's POST /orgs. + */ + public function createOrganization(string $orgName): string + { + $url = "/user/orgs"; + + $response = $this->call(self::METHOD_POST, $url, ['Authorization' => "token $this->accessToken"], [ + 'username' => $orgName, + ]); + + $responseBody = $response['body'] ?? []; + + return $responseBody['username'] ?? ''; + } + + /** + * Search repositories in organization + * + * Gogs requires the `q` parameter for search to return results. + * When no search query is given, we pass '*' as a wildcard. + * + * @return array + */ + public function searchRepositories(string $owner, int $page, int $per_page, string $search = ''): array + { + if (empty($search)) { + $search = '_'; // Gogs requires q param; underscore matches most repo names + } + + return parent::searchRepositories($owner, $page, $per_page, $search); + } + + /** + * Get repository tree + * + * Gogs does not support recursive tree listing. For recursive mode, + * we manually traverse subdirectories. + * + * @return array + */ + public function getRepositoryTree(string $owner, string $repositoryName, string $branch, bool $recursive = false): array + { + $url = "/repos/{$owner}/{$repositoryName}/git/trees/" . urlencode($branch); + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode === 404) { + return []; + } + + $responseBody = $response['body'] ?? []; + $entries = $responseBody['tree'] ?? []; + $paths = []; + + foreach ($entries as $entry) { + $paths[] = $entry['path']; + + if ($recursive && ($entry['type'] ?? '') === 'tree') { + $subPaths = $this->getRepositoryTree($owner, $repositoryName, $entry['sha'], true); + foreach ($subPaths as $subPath) { + $paths[] = $entry['path'] . '/' . $subPath; + } + } + } + + return $paths; + } + + /** + * Get repository name by ID + * + * Gogs does not support /repositories/{id}. Uses search as fallback. + */ + public function getRepositoryName(string $repositoryId): string + { + throw new Exception("getRepositoryName by ID is not supported by Gogs"); + } + + /** + * Get owner name + * + * Gogs does not support /repositories/{id}. + */ + public function getOwnerName(string $installationId, ?int $repositoryId = null): string + { + throw new Exception("getOwnerName by repository ID is not supported by Gogs"); + } + + /** + * Get details of a commit + * + * Gogs uses /repos/{owner}/{repo}/commits/{sha} (not /git/commits/{sha} like Gitea). + * + * @return array Details of the commit + */ + public function getCommit(string $owner, string $repositoryName, string $commitHash): array + { + $url = "/repos/{$owner}/{$repositoryName}/commits/{$commitHash}"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Commit not found or inaccessible"); + } + + $responseBody = $response['body'] ?? []; + $commitData = $responseBody['commit'] ?? []; + $commitAuthor = $commitData['author'] ?? []; + $author = $responseBody['author'] ?? []; + + return [ + 'commitAuthor' => $commitAuthor['name'] ?? 'Unknown', + 'commitMessage' => $commitData['message'] ?? 'No message', + 'commitAuthorAvatar' => $author['avatar_url'] ?? '', + 'commitAuthorUrl' => $author['html_url'] ?? '', + 'commitHash' => $responseBody['sha'] ?? '', + 'commitUrl' => $responseBody['html_url'] ?? '', + ]; + } + + /** + * Get latest commit of a branch + * + * Gogs ignores the sha query param, so we validate the branch exists first. + * + * @return array Details of the commit + */ + public function getLatestCommit(string $owner, string $repositoryName, string $branch): array + { + // Gogs ignores sha param — verify branch exists first + $branches = $this->listBranches($owner, $repositoryName); + if (!in_array($branch, $branches, true)) { + throw new Exception("Branch '{$branch}' not found"); + } + + return parent::getLatestCommit($owner, $repositoryName, $branch); + } + + /** + * Create a file in a repository + * + * Gogs PUT /contents/{path} only works on the default branch and cannot + * target a specific branch. When a branch is specified we fall back to + * git CLI so the file lands on the correct branch. + * + * @return array + */ + public function createFile(string $owner, string $repositoryName, string $filepath, string $content, string $message = 'Add file', string $branch = ''): array + { + if (empty($branch)) { + // Default branch — use Gogs API (PUT) + $url = "/repos/{$owner}/{$repositoryName}/contents/{$filepath}"; + + $response = $this->call( + self::METHOD_PUT, + $url, + ['Authorization' => "token $this->accessToken"], + [ + 'content' => base64_encode($content), + 'message' => $message, + ] + ); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to create file {$filepath}: HTTP {$responseHeadersStatusCode}"); + } + + return $response['body'] ?? []; + } + + // Specific branch — use git CLI + $this->gitCreateFile($owner, $repositoryName, $branch, $filepath, $content, $message); + + return []; + } + + /** + * Create a branch + * + * Gogs does not support branch creation via API, so we use git CLI. + * + * @return array + */ + public function createBranch(string $owner, string $repositoryName, string $newBranchName, string $oldBranchName): array + { + $dir = $this->gitClone($owner, $repositoryName, $oldBranchName); + + try { + $this->exec("git -C {$dir} checkout -b " . escapeshellarg($newBranchName)); + $this->exec("git -C {$dir} push origin " . escapeshellarg($newBranchName)); + } finally { + $this->exec("rm -rf {$dir}"); + } + + return ['name' => $newBranchName]; + } + + /** + * Clone a repository into a temporary directory and checkout a branch. + */ + private function gitClone(string $owner, string $repositoryName, string $branch = ''): string + { + $cloneUrl = str_replace('://', "://{$owner}:{$this->accessToken}@", $this->giteaUrl) . "/{$owner}/{$repositoryName}.git"; + + $dir = escapeshellarg(sys_get_temp_dir() . '/gogs-' . uniqid()); + + $branchArg = ''; + if (!empty($branch)) { + $branchArg = ' -b ' . escapeshellarg($branch); + } + + $this->exec("git clone --depth=1{$branchArg} " . escapeshellarg($cloneUrl) . " {$dir}"); + $this->exec("git -C {$dir} config user.email 'gogs@test.local'"); + $this->exec("git -C {$dir} config user.name 'Gogs Test'"); + + return trim($dir, "'\""); + } + + /** + * Create a file via git CLI: clone, write, commit, push. + */ + private function gitCreateFile(string $owner, string $repositoryName, string $branch, string $filepath, string $content, string $message): void + { + $dir = $this->gitClone($owner, $repositoryName, $branch); + + try { + $fullPath = $dir . '/' . $filepath; + $parentDir = dirname($fullPath); + + if (!is_dir($parentDir)) { + mkdir($parentDir, 0777, true); + } + + file_put_contents($fullPath, $content); + + $this->exec("git -C " . escapeshellarg($dir) . " add " . escapeshellarg($filepath)); + $this->exec("git -C " . escapeshellarg($dir) . " commit -m " . escapeshellarg($message)); + $this->exec("git -C " . escapeshellarg($dir) . " push origin " . escapeshellarg($branch)); + } finally { + $this->exec("rm -rf " . escapeshellarg($dir)); + } + } + + /** + * Execute a shell command and throw on failure. + */ + private function exec(string $command): string + { + $output = []; + $exitCode = 0; + + \exec($command . ' 2>&1', $output, $exitCode); + + $outputStr = implode("\n", $output); + + if ($exitCode !== 0) { + throw new Exception("Command failed (exit {$exitCode}): {$command}\n{$outputStr}"); + } + + return $outputStr; + } + + /** + * List repository languages + * + * Gogs does not support the languages endpoint. + * + * @return array + */ + public function listRepositoryLanguages(string $owner, string $repositoryName): array + { + throw new Exception("Listing repository languages is not supported by Gogs"); + } + + /** + * Create a tag + * + * Gogs does not support tag creation via API. + * + * @return array + */ + public function createTag(string $owner, string $repositoryName, string $tagName, string $target, string $message = ''): array + { + throw new Exception("Tag creation via API is not supported by Gogs"); + } + + /** + * Create a pull request + * + * Gogs does not have a pull request API. + * + * @return array + */ + public function createPullRequest(string $owner, string $repositoryName, string $title, string $head, string $base, string $body = ''): array + { + throw new Exception("Pull request API is not supported by Gogs"); + } + + /** + * Get a pull request + * + * @return array + */ + public function getPullRequest(string $owner, string $repositoryName, int $pullRequestNumber): array + { + throw new Exception("Pull request API is not supported by Gogs"); + } + + /** + * Get pull request from branch + * + * @return array + */ + public function getPullRequestFromBranch(string $owner, string $repositoryName, string $branch): array + { + throw new Exception("Pull request API is not supported by Gogs"); + } + + /** + * Get a comment by ID + * + * Gogs has no single-comment-by-ID endpoint. Lists all repo comments and filters. + */ + public function getComment(string $owner, string $repositoryName, string $commentId): string + { + $comment = $this->findComment($owner, $repositoryName, (int) $commentId); + + return $comment['body'] ?? ''; + } + + /** + * Update a comment + * + * Gogs requires the issue index in the path. We extract it from the comment's html_url. + */ + public function updateComment(string $owner, string $repositoryName, int $commentId, string $comment): string + { + $existing = $this->findComment($owner, $repositoryName, $commentId); + + if (empty($existing)) { + throw new Exception("Comment {$commentId} not found"); + } + + $issueIndex = $this->extractIssueIndexFromComment($existing); + if ($issueIndex === null) { + throw new Exception("Could not determine issue index for comment {$commentId}"); + } + + $url = "/repos/{$owner}/{$repositoryName}/issues/{$issueIndex}/comments/{$commentId}"; + + $response = $this->call(self::METHOD_PATCH, $url, ['Authorization' => "token $this->accessToken"], ['body' => $comment]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to update comment: HTTP {$responseHeadersStatusCode}"); + } + + $responseBody = $response['body'] ?? []; + + if (!array_key_exists('id', $responseBody)) { + throw new Exception("Comment update response is missing comment ID."); + } + + return (string) ($responseBody['id'] ?? ''); + } + + /** + * Find a comment by ID by listing all repo comments. + * + * @return array The comment, or empty array if not found. + */ + private function findComment(string $owner, string $repositoryName, int $commentId): array + { + $url = "/repos/{$owner}/{$repositoryName}/issues/comments"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + $responseBody = $response['body'] ?? []; + + if (!is_array($responseBody)) { + return []; + } + + foreach ($responseBody as $comment) { + if (($comment['id'] ?? 0) === $commentId) { + return $comment; + } + } + + return []; + } + + /** + * Extract the issue index from a comment's html_url. + * e.g. "http://gogs:3000/org/repo/issues/1#issuecomment-2" → 1 + */ + private function extractIssueIndexFromComment(array $comment): ?int + { + $htmlUrl = $comment['html_url'] ?? ''; + + if (preg_match('/\/issues\/(\d+)#/', $htmlUrl, $matches)) { + return (int) $matches[1]; + } + + return null; + } + + /** + * Update commit status + * + * Gogs does not support commit statuses API. + */ + public function updateCommitStatus(string $repositoryName, string $commitHash, string $owner, string $state, string $description = '', string $target_url = '', string $context = ''): void + { + throw new Exception("Commit status API is not supported by Gogs"); + } + /** * Get commit statuses * - * Overrides the Gitea implementation to normalise the 'state' field - * returned by Gogs into the 'status' field used by the rest of the - * adapter interface (Gitea changed the JSON key from 'state' to - * 'status', but Gogs still uses 'state'). + * Gogs does not support commit statuses API. * - * @param string $owner Owner of the repository - * @param string $repositoryName Name of the repository - * @param string $commitHash SHA of the commit - * @return array List of commit statuses + * @return array */ public function getCommitStatuses(string $owner, string $repositoryName, string $commitHash): array { - $statuses = parent::getCommitStatuses($owner, $repositoryName, $commitHash); + throw new Exception("Commit status API is not supported by Gogs"); + } - return array_map(function ($status) { - if (isset($status['state']) && !isset($status['status'])) { - $status['status'] = $status['state']; + /** + * List branches + * + * Gogs supports listing branches but without pagination parameters. + * + * @return array + */ + public function listBranches(string $owner, string $repositoryName): array + { + $url = "/repos/{$owner}/{$repositoryName}/branches"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + + if ($responseHeadersStatusCode === 404) { + return []; + } + + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to list branches: HTTP {$responseHeadersStatusCode}"); + } + + $responseBody = $response['body'] ?? []; + + if (!is_array($responseBody)) { + return []; + } + + $branches = []; + foreach ($responseBody as $branch) { + if (is_array($branch) && array_key_exists('name', $branch)) { + $branches[] = $branch['name']; } - return $status; - }, $statuses); + } + + return $branches; } } diff --git a/tests/VCS/Adapter/GogsTest.php b/tests/VCS/Adapter/GogsTest.php index d689faa8..0797b113 100644 --- a/tests/VCS/Adapter/GogsTest.php +++ b/tests/VCS/Adapter/GogsTest.php @@ -11,12 +11,12 @@ class GogsTest extends GiteaTest { protected static string $accessToken = ''; - protected static string $owner = ''; - + protected string $webhookEventHeader = 'X-Gogs-Event'; protected string $webhookSignatureHeader = 'X-Gogs-Signature'; protected string $avatarDomain = 'gravatar.com'; + protected static string $defaultBranch = 'master'; protected function createVCSAdapter(): Git { @@ -40,6 +40,7 @@ public function setUp(): void refreshToken: '' ); $adapter->setEndpoint($gogsUrl); + if (empty(static::$owner)) { $orgName = 'test-org-' . \uniqid(); static::$owner = $adapter->createOrganization($orgName); From 41adce0f08f9c242c64e88a46ee2550df8f37ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Mar 2026 20:48:12 +0100 Subject: [PATCH 06/17] Fix more default branch tests --- composer.lock | 28 ++++++++++++------------- tests/VCS/Adapter/GiteaTest.php | 36 ++++++++++++++++----------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/composer.lock b/composer.lock index 8220b30c..20e2f044 100644 --- a/composer.lock +++ b/composer.lock @@ -206,23 +206,23 @@ }, { "name": "google/protobuf", - "version": "v4.33.5", + "version": "v4.33.6", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d" + "reference": "84b008c23915ed94536737eae46f41ba3bccfe67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d", - "reference": "ebe8010a61b2ae0cff0d246fe1c4d44e9f7dfa6d", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/84b008c23915ed94536737eae46f41ba3bccfe67", + "reference": "84b008c23915ed94536737eae46f41ba3bccfe67", "shasum": "" }, "require": { "php": ">=8.1.0" }, "require-dev": { - "phpunit/phpunit": ">=5.0.0 <8.5.27" + "phpunit/phpunit": ">=10.5.62 <11.0.0" }, "suggest": { "ext-bcmath": "Need to support JSON deserialization" @@ -244,9 +244,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.5" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.6" }, - "time": "2026-01-29T20:49:00+00:00" + "time": "2026-03-18T17:32:05+00:00" }, { "name": "nyholm/psr7", @@ -4048,16 +4048,16 @@ }, { "name": "utopia-php/system", - "version": "0.10.0", + "version": "0.10.1", "source": { "type": "git", "url": "https://github.com/utopia-php/system.git", - "reference": "6441a9c180958a373e5ddb330264dd638539dfdb" + "reference": "7c1669533bb9c285de19191270c8c1439161a78a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/system/zipball/6441a9c180958a373e5ddb330264dd638539dfdb", - "reference": "6441a9c180958a373e5ddb330264dd638539dfdb", + "url": "https://api.github.com/repos/utopia-php/system/zipball/7c1669533bb9c285de19191270c8c1439161a78a", + "reference": "7c1669533bb9c285de19191270c8c1439161a78a", "shasum": "" }, "require": { @@ -4098,9 +4098,9 @@ ], "support": { "issues": "https://github.com/utopia-php/system/issues", - "source": "https://github.com/utopia-php/system/tree/0.10.0" + "source": "https://github.com/utopia-php/system/tree/0.10.1" }, - "time": "2025-10-15T19:12:00+00:00" + "time": "2026-03-15T21:07:41+00:00" } ], "aliases": [], @@ -4112,5 +4112,5 @@ "php": ">=8.0" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index f95d269d..231abf32 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -301,7 +301,7 @@ public function testGetRepositoryTree(): void $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'src/lib.php', 'vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, 'main', false); + $tree = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, static::$defaultBranch, false); $this->assertIsArray($tree); $this->assertContains('README.md', $tree); @@ -309,7 +309,7 @@ public function testGetRepositoryTree(): void $this->assertCount(2, $tree); // Only README.md and src folder at root // Test recursive (should show all files including nested) - $treeRecursive = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, 'main', true); + $treeRecursive = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, static::$defaultBranch, true); $this->assertIsArray($treeRecursive); $this->assertContains('README.md', $treeRecursive); @@ -363,7 +363,7 @@ public function testGetRepositoryContentWithRef(): void $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'test.txt', 'main branch content'); - $result = $this->vcsAdapter->getRepositoryContent(static::$owner, $repositoryName, 'test.txt', 'main'); + $result = $this->vcsAdapter->getRepositoryContent(static::$owner, $repositoryName, 'test.txt', static::$defaultBranch); $this->assertIsArray($result); $this->assertSame('main branch content', $result['content']); @@ -510,7 +510,7 @@ public function testGenerateCloneCommand(): void $command = $this->vcsAdapter->generateCloneCommand( static::$owner, $repositoryName, - 'main', + static::$defaultBranch, \Utopia\VCS\Adapter\Git::CLONE_TYPE_BRANCH, '/tmp/test-clone-' . \uniqid(), '/' @@ -534,7 +534,7 @@ public function testGenerateCloneCommandWithCommitHash(): void try { $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, 'main'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); $commitHash = $commit['commitHash']; $command = $this->vcsAdapter->generateCloneCommand( @@ -562,7 +562,7 @@ public function testGenerateCloneCommandWithTag(): void // Create initial file and get commit hash $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test Tag'); - $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, 'main'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); $commitHash = $commit['commitHash']; // Create a tag @@ -598,7 +598,7 @@ public function testGenerateCloneCommandWithInvalidRepository(): void $command = $this->vcsAdapter->generateCloneCommand( 'nonexistent-owner-' . \uniqid(), 'nonexistent-repo-' . \uniqid(), - 'main', + static::$defaultBranch, \Utopia\VCS\Adapter\Git::CLONE_TYPE_BRANCH, $directory, '/' @@ -666,7 +666,7 @@ public function testUpdateCommitStatus(): void try { $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, 'main'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); $commitHash = $commit['commitHash']; $this->vcsAdapter->updateCommitStatus( @@ -735,7 +735,7 @@ public function testGetCommit(): void $customMessage = 'Test commit message'; $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test Commit', $customMessage); - $latestCommit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, 'main'); + $latestCommit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); $commitHash = $latestCommit['commitHash']; $result = $this->vcsAdapter->getCommit(static::$owner, $repositoryName, $commitHash); @@ -766,7 +766,7 @@ public function testGetLatestCommit(): void $secondMessage = 'Second commit'; $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test', $firstMessage); - $commit1 = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, 'main'); + $commit1 = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); $this->assertIsArray($commit1); $this->assertArrayHasKey('commitHash', $commit1); @@ -786,7 +786,7 @@ public function testGetLatestCommit(): void $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'test.txt', 'test content', $secondMessage); - $commit2 = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, 'main'); + $commit2 = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); $this->assertIsArray($commit2); $this->assertNotEmpty($commit2['commitHash']); @@ -880,7 +880,7 @@ public function testGetEventPush(): void $this->assertArrayHasKey('owner', $result); $this->assertArrayHasKey('affectedFiles', $result); - $this->assertSame('main', $result['branch']); + $this->assertSame(static::$defaultBranch, $result['branch']); $this->assertSame('def456', $result['commitHash']); $this->assertSame('test-repo', $result['repositoryName']); $this->assertSame('test-owner', $result['owner']); @@ -915,7 +915,7 @@ public function testGetEventPullRequest(): void ], ], 'base' => [ - 'ref' => 'main', + 'ref' => static::$defaultBranch, 'sha' => 'def456', 'user' => [ 'login' => 'base-owner', @@ -977,7 +977,7 @@ public function testGetEventPullRequestExternal(): void ], ], 'base' => [ - 'ref' => 'main', + 'ref' => static::$defaultBranch, ], 'user' => [ 'avatar_url' => 'http://gitea:3000/avatars/external', @@ -1420,7 +1420,7 @@ public function testCreateTag(): void try { $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, 'main'); + $commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch); $commitHash = $commit['commitHash']; $result = $this->vcsAdapter->createTag( @@ -1518,7 +1518,7 @@ public function testWebhookPushEvent(): void $event = $this->vcsAdapter->getEvent('push', $payload); $this->assertIsArray($event); - $this->assertSame('main', $event['branch']); + $this->assertSame(static::$defaultBranch, $event['branch']); $this->assertSame($repositoryName, $event['repositoryName']); $this->assertSame(static::$owner, $event['owner']); $this->assertNotEmpty($event['commitHash']); @@ -1538,7 +1538,7 @@ public function testWebhookPullRequestEvent(): void // Create all files BEFORE configuring webhook // so those push events don't pollute the catcher $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-branch', 'main'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-branch', static::$defaultBranch); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'feature.txt', 'content', 'Add feature', 'feature-branch'); $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; @@ -1553,7 +1553,7 @@ public function testWebhookPullRequestEvent(): void $repositoryName, 'Test Webhook PR', 'feature-branch', - 'main' + static::$defaultBranch ); // Wait for pull_request webhook to arrive automatically From af8b3d575b0b3b0e720801f0030fba7957f44d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Mar 2026 20:48:41 +0100 Subject: [PATCH 07/17] Skip failing gogs tests --- tests/VCS/Adapter/GogsTest.php | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/VCS/Adapter/GogsTest.php b/tests/VCS/Adapter/GogsTest.php index 0797b113..685dfa45 100644 --- a/tests/VCS/Adapter/GogsTest.php +++ b/tests/VCS/Adapter/GogsTest.php @@ -60,4 +60,42 @@ protected function setupGogs(): void } } } + + // --- Skip tests for unsupported Gogs features --- + + // Pull request API + public function testCommentWorkflow(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testGetComment(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testGetPullRequest(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testGetPullRequestWithInvalidNumber(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testGetPullRequestFromBranch(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testGetPullRequestFromBranchNoPR(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testUpdateComment(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testCreateComment(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testWebhookPullRequestEvent(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + + // Repository by ID + public function testGetRepositoryName(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } + public function testGetRepositoryNameWithInvalidId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } + public function testGetOwnerName(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } + public function testGetOwnerNameWithZeroRepositoryId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } + public function testGetOwnerNameWithoutRepositoryId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } + public function testGetOwnerNameWithInvalidRepositoryId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } + public function testGetOwnerNameWithNullRepositoryId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } + + // Tag creation + public function testCreateTag(): void { $this->markTestSkipped('Gogs does not support tag creation via API'); } + public function testGenerateCloneCommandWithTag(): void { $this->markTestSkipped('Gogs does not support tag creation via API'); } + + // Commit status + public function testUpdateCommitStatus(): void { $this->markTestSkipped('Gogs does not support commit status API'); } + public function testUpdateCommitStatusWithInvalidCommit(): void { $this->markTestSkipped('Gogs does not support commit status API'); } + public function testUpdateCommitStatusWithNonExistingRepository(): void { $this->markTestSkipped('Gogs does not support commit status API'); } + + // Repository languages + public function testListRepositoryLanguages(): void { $this->markTestSkipped('Gogs does not support repository languages endpoint'); } + public function testListRepositoryLanguagesEmptyRepo(): void { $this->markTestSkipped('Gogs does not support repository languages endpoint'); } + + // Webhook (missing Fetch dependency) + public function testWebhookPushEvent(): void { $this->markTestSkipped('Gogs webhook test requires request-catcher URL config'); } } From 3cbc1a142d02501dd891ae922178d0c07851d159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Mar 2026 21:19:16 +0100 Subject: [PATCH 08/17] Fixing more gogs tests --- src/VCS/Adapter/Git/Gitea.php | 4 +- src/VCS/Adapter/Git/Gogs.php | 158 ++++++++++++++++++++------------ tests/VCS/Adapter/GiteaTest.php | 8 +- tests/VCS/Adapter/GogsTest.php | 15 +-- 4 files changed, 106 insertions(+), 79 deletions(-) diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index 33d5e495..f9efec8e 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -71,7 +71,7 @@ public function initializeVariables(string $installationId, string $privateKey, return; } - throw new Exception("accessToken is required for Gitea adapter."); + throw new Exception("accessToken is required for this adapter."); } /** @@ -613,7 +613,7 @@ public function getUser(string $username): array public function getOwnerName(string $installationId, ?int $repositoryId = null): string { if ($repositoryId === null || $repositoryId <= 0) { - throw new Exception("repositoryId is required for Gitea"); + throw new Exception("repositoryId is required for this adapter"); } $url = "/repositories/{$repositoryId}"; diff --git a/src/VCS/Adapter/Git/Gogs.php b/src/VCS/Adapter/Git/Gogs.php index 1a704a4a..a3b8188f 100644 --- a/src/VCS/Adapter/Git/Gogs.php +++ b/src/VCS/Adapter/Git/Gogs.php @@ -3,6 +3,7 @@ namespace Utopia\VCS\Adapter\Git; use Exception; +use Utopia\VCS\Exception\RepositoryNotFound; class Gogs extends Gitea { @@ -63,18 +64,34 @@ public function createOrganization(string $orgName): string /** * Search repositories in organization * - * Gogs requires the `q` parameter for search to return results. - * When no search query is given, we pass '*' as a wildcard. + * When no search query is given, Gogs search API returns empty results, + * so we fall back to listing org repos directly via /orgs/{org}/repos. * * @return array */ public function searchRepositories(string $owner, int $page, int $per_page, string $search = ''): array { - if (empty($search)) { - $search = '_'; // Gogs requires q param; underscore matches most repo names + if (!empty($search)) { + return parent::searchRepositories($owner, $page, $per_page, $search); } - return parent::searchRepositories($owner, $page, $per_page, $search); + // List all repos for the org directly + $url = "/orgs/{$owner}/repos"; + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + $responseBody = $response['body'] ?? []; + if (!is_array($responseBody)) { + $responseBody = []; + } + + $total = count($responseBody); + $offset = ($page - 1) * $per_page; + $pagedRepos = array_slice($responseBody, $offset, $per_page); + + return [ + 'items' => $pagedRepos, + 'total' => $total, + ]; } /** @@ -118,21 +135,71 @@ public function getRepositoryTree(string $owner, string $repositoryName, string /** * Get repository name by ID * - * Gogs does not support /repositories/{id}. Uses search as fallback. + * Gogs does not have /repositories/{id}. Searches all repos to find by ID. */ public function getRepositoryName(string $repositoryId): string { - throw new Exception("getRepositoryName by ID is not supported by Gogs"); + $repo = $this->findRepositoryById((int) $repositoryId); + + return $repo['name']; } /** - * Get owner name + * Get owner name by repository ID * - * Gogs does not support /repositories/{id}. + * Gogs does not have /repositories/{id}. Searches all repos to find by ID. */ public function getOwnerName(string $installationId, ?int $repositoryId = null): string { - throw new Exception("getOwnerName by repository ID is not supported by Gogs"); + if ($repositoryId === null || $repositoryId <= 0) { + throw new Exception("repositoryId is required for this adapter"); + } + + $repo = $this->findRepositoryById($repositoryId); + $owner = $repo['owner'] ?? []; + + if (empty($owner['login'])) { + throw new Exception("Owner login missing or empty in response"); + } + + return $owner['login']; + } + + /** + * Find a repository by its numeric ID using the search API. + * + * @return array Repository data + */ + private function findRepositoryById(int $repositoryId): array + { + $page = 1; + $limit = 50; + + while ($page <= 100) { + $url = "/repos/search?q=_&limit={$limit}&page={$page}"; + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + $responseBody = $response['body'] ?? []; + $repos = $responseBody['data'] ?? []; + + if (empty($repos)) { + break; + } + + foreach ($repos as $repo) { + if (($repo['id'] ?? 0) === $repositoryId) { + return $repo; + } + } + + if (count($repos) < $limit) { + break; + } + + $page++; + } + + throw new RepositoryNotFound("Repository not found"); } /** @@ -190,41 +257,34 @@ public function getLatestCommit(string $owner, string $repositoryName, string $b /** * Create a file in a repository * - * Gogs PUT /contents/{path} only works on the default branch and cannot - * target a specific branch. When a branch is specified we fall back to - * git CLI so the file lands on the correct branch. + * Gogs uses PUT /repos/{owner}/{repo}/contents/{path}. + * For non-default branches we use git CLI, because the Gogs API `branch` + * param creates a new branch rather than targeting an existing one. * * @return array */ public function createFile(string $owner, string $repositoryName, string $filepath, string $content, string $message = 'Add file', string $branch = ''): array { - if (empty($branch)) { - // Default branch — use Gogs API (PUT) - $url = "/repos/{$owner}/{$repositoryName}/contents/{$filepath}"; - - $response = $this->call( - self::METHOD_PUT, - $url, - ['Authorization' => "token $this->accessToken"], - [ - 'content' => base64_encode($content), - 'message' => $message, - ] - ); - - $responseHeaders = $response['headers'] ?? []; - $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; - if ($responseHeadersStatusCode >= 400) { - throw new Exception("Failed to create file {$filepath}: HTTP {$responseHeadersStatusCode}"); - } + $url = "/repos/{$owner}/{$repositoryName}/contents/{$filepath}"; + + $response = $this->call( + self::METHOD_PUT, + $url, + ['Authorization' => "token $this->accessToken"], + [ + 'content' => base64_encode($content), + 'message' => $message, + 'branch' => $branch + ] + ); - return $response['body'] ?? []; + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to create file {$filepath}: HTTP {$responseHeadersStatusCode}"); } - // Specific branch — use git CLI - $this->gitCreateFile($owner, $repositoryName, $branch, $filepath, $content, $message); - - return []; + return $response['body'] ?? []; } /** @@ -269,30 +329,6 @@ private function gitClone(string $owner, string $repositoryName, string $branch return trim($dir, "'\""); } - /** - * Create a file via git CLI: clone, write, commit, push. - */ - private function gitCreateFile(string $owner, string $repositoryName, string $branch, string $filepath, string $content, string $message): void - { - $dir = $this->gitClone($owner, $repositoryName, $branch); - - try { - $fullPath = $dir . '/' . $filepath; - $parentDir = dirname($fullPath); - - if (!is_dir($parentDir)) { - mkdir($parentDir, 0777, true); - } - - file_put_contents($fullPath, $content); - - $this->exec("git -C " . escapeshellarg($dir) . " add " . escapeshellarg($filepath)); - $this->exec("git -C " . escapeshellarg($dir) . " commit -m " . escapeshellarg($message)); - $this->exec("git -C " . escapeshellarg($dir) . " push origin " . escapeshellarg($branch)); - } finally { - $this->exec("rm -rf " . escapeshellarg($dir)); - } - } /** * Execute a shell command and throw on failure. diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 231abf32..38e22a96 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -830,7 +830,7 @@ public function testGetLatestCommitWithInvalidBranch(): void public function testGetEventPush(): void { $payload = json_encode([ - 'ref' => 'refs/heads/main', + 'ref' => 'refs/heads/' . static::$defaultBranch, 'before' => 'abc123', 'after' => 'def456', 'created' => false, @@ -1180,7 +1180,7 @@ public function testGetOwnerName(): void public function testGetOwnerNameWithZeroRepositoryId(): void { $this->expectException(\Exception::class); - $this->expectExceptionMessage('repositoryId is required for Gitea'); + $this->expectExceptionMessage('repositoryId is required for this adapter'); $this->vcsAdapter->getOwnerName('', 0); } @@ -1188,7 +1188,7 @@ public function testGetOwnerNameWithZeroRepositoryId(): void public function testGetOwnerNameWithoutRepositoryId(): void { $this->expectException(\Exception::class); - $this->expectExceptionMessage('repositoryId is required for Gitea'); + $this->expectExceptionMessage('repositoryId is required for this adapter'); $this->vcsAdapter->getOwnerName(''); } @@ -1203,7 +1203,7 @@ public function testGetOwnerNameWithInvalidRepositoryId(): void public function testGetOwnerNameWithNullRepositoryId(): void { $this->expectException(\Exception::class); - $this->expectExceptionMessage('repositoryId is required for Gitea'); + $this->expectExceptionMessage('repositoryId is required for this adapter'); $this->vcsAdapter->getOwnerName('', null); } diff --git a/tests/VCS/Adapter/GogsTest.php b/tests/VCS/Adapter/GogsTest.php index 685dfa45..a9e87c16 100644 --- a/tests/VCS/Adapter/GogsTest.php +++ b/tests/VCS/Adapter/GogsTest.php @@ -61,6 +61,9 @@ protected function setupGogs(): void } } + // Webhook delivery (Gogs queues but does not deliver webhooks in test environment) + public function testWebhookPushEvent(): void { $this->markTestSkipped('Gogs webhook delivery not working in test environment'); } + // --- Skip tests for unsupported Gogs features --- // Pull request API @@ -74,15 +77,6 @@ public function testUpdateComment(): void { $this->markTestSkipped('Gogs does no public function testCreateComment(): void { $this->markTestSkipped('Gogs does not support pull request API'); } public function testWebhookPullRequestEvent(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - // Repository by ID - public function testGetRepositoryName(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } - public function testGetRepositoryNameWithInvalidId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } - public function testGetOwnerName(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } - public function testGetOwnerNameWithZeroRepositoryId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } - public function testGetOwnerNameWithoutRepositoryId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } - public function testGetOwnerNameWithInvalidRepositoryId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } - public function testGetOwnerNameWithNullRepositoryId(): void { $this->markTestSkipped('Gogs does not support /repositories/{id} endpoint'); } - // Tag creation public function testCreateTag(): void { $this->markTestSkipped('Gogs does not support tag creation via API'); } public function testGenerateCloneCommandWithTag(): void { $this->markTestSkipped('Gogs does not support tag creation via API'); } @@ -95,7 +89,4 @@ public function testUpdateCommitStatusWithNonExistingRepository(): void { $this- // Repository languages public function testListRepositoryLanguages(): void { $this->markTestSkipped('Gogs does not support repository languages endpoint'); } public function testListRepositoryLanguagesEmptyRepo(): void { $this->markTestSkipped('Gogs does not support repository languages endpoint'); } - - // Webhook (missing Fetch dependency) - public function testWebhookPushEvent(): void { $this->markTestSkipped('Gogs webhook test requires request-catcher URL config'); } } From 1550e08e368c4ad00031ed1a0cbd9e6b5b17029f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 23 Mar 2026 21:33:39 +0100 Subject: [PATCH 09/17] Fix more tests --- src/VCS/Adapter/Git/Gogs.php | 90 ----------------------------- tests/VCS/Adapter/ForgejoTest.php | 2 +- tests/VCS/Adapter/GiteaTest.php | 8 +-- tests/VCS/Adapter/GogsTest.php | 94 ++++++++++++++++++++++++------- tests/VCS/Base.php | 4 +- 5 files changed, 82 insertions(+), 116 deletions(-) diff --git a/src/VCS/Adapter/Git/Gogs.php b/src/VCS/Adapter/Git/Gogs.php index a3b8188f..e45aaf69 100644 --- a/src/VCS/Adapter/Git/Gogs.php +++ b/src/VCS/Adapter/Git/Gogs.php @@ -405,96 +405,6 @@ public function getPullRequestFromBranch(string $owner, string $repositoryName, throw new Exception("Pull request API is not supported by Gogs"); } - /** - * Get a comment by ID - * - * Gogs has no single-comment-by-ID endpoint. Lists all repo comments and filters. - */ - public function getComment(string $owner, string $repositoryName, string $commentId): string - { - $comment = $this->findComment($owner, $repositoryName, (int) $commentId); - - return $comment['body'] ?? ''; - } - - /** - * Update a comment - * - * Gogs requires the issue index in the path. We extract it from the comment's html_url. - */ - public function updateComment(string $owner, string $repositoryName, int $commentId, string $comment): string - { - $existing = $this->findComment($owner, $repositoryName, $commentId); - - if (empty($existing)) { - throw new Exception("Comment {$commentId} not found"); - } - - $issueIndex = $this->extractIssueIndexFromComment($existing); - if ($issueIndex === null) { - throw new Exception("Could not determine issue index for comment {$commentId}"); - } - - $url = "/repos/{$owner}/{$repositoryName}/issues/{$issueIndex}/comments/{$commentId}"; - - $response = $this->call(self::METHOD_PATCH, $url, ['Authorization' => "token $this->accessToken"], ['body' => $comment]); - - $responseHeaders = $response['headers'] ?? []; - $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; - if ($responseHeadersStatusCode >= 400) { - throw new Exception("Failed to update comment: HTTP {$responseHeadersStatusCode}"); - } - - $responseBody = $response['body'] ?? []; - - if (!array_key_exists('id', $responseBody)) { - throw new Exception("Comment update response is missing comment ID."); - } - - return (string) ($responseBody['id'] ?? ''); - } - - /** - * Find a comment by ID by listing all repo comments. - * - * @return array The comment, or empty array if not found. - */ - private function findComment(string $owner, string $repositoryName, int $commentId): array - { - $url = "/repos/{$owner}/{$repositoryName}/issues/comments"; - - $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); - - $responseBody = $response['body'] ?? []; - - if (!is_array($responseBody)) { - return []; - } - - foreach ($responseBody as $comment) { - if (($comment['id'] ?? 0) === $commentId) { - return $comment; - } - } - - return []; - } - - /** - * Extract the issue index from a comment's html_url. - * e.g. "http://gogs:3000/org/repo/issues/1#issuecomment-2" → 1 - */ - private function extractIssueIndexFromComment(array $comment): ?int - { - $htmlUrl = $comment['html_url'] ?? ''; - - if (preg_match('/\/issues\/(\d+)#/', $htmlUrl, $matches)) { - return (int) $matches[1]; - } - - return null; - } - /** * Update commit status * diff --git a/tests/VCS/Adapter/ForgejoTest.php b/tests/VCS/Adapter/ForgejoTest.php index 4917a849..ad1ec502 100644 --- a/tests/VCS/Adapter/ForgejoTest.php +++ b/tests/VCS/Adapter/ForgejoTest.php @@ -30,7 +30,7 @@ public function setUp(): void } $adapter = new Forgejo(new Cache(new None())); - $forgejoUrl = System::getEnv('TESTS_FORGEJO_URL', 'http://forgejo:3000') ?? ''; + $forgejoUrl = System::getEnv('TESTS_FORGEJO_URL', 'http://forgejo:3000'); $adapter->initializeVariables( installationId: '', diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 38e22a96..7adeda8c 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -31,7 +31,7 @@ public function setUp(): void } $adapter = new Gitea(new Cache(new None())); - $giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? ''; + $giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000'); $adapter->initializeVariables( installationId: '', @@ -1345,7 +1345,7 @@ public function testCreateFile(): void public function testCreateFileOnBranch(): void { $repositoryName = 'test-create-file-branch-'.\uniqid(); - $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); + $res = $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); try { $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Main'); @@ -1484,7 +1484,7 @@ public function testWebhookPushEvent(): void $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); try { - $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000'); $this->deleteLastWebhookRequest(); $this->vcsAdapter->createWebhook(static::$owner, $repositoryName, $catcherUrl . '/webhook', $secret); @@ -1541,7 +1541,7 @@ public function testWebhookPullRequestEvent(): void $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-branch', static::$defaultBranch); $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'feature.txt', 'content', 'Add feature', 'feature-branch'); - $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000'); $this->vcsAdapter->createWebhook(static::$owner, $repositoryName, $catcherUrl . '/webhook', $secret); // Clear after setup so only PR event will arrive diff --git a/tests/VCS/Adapter/GogsTest.php b/tests/VCS/Adapter/GogsTest.php index a9e87c16..da4d21ca 100644 --- a/tests/VCS/Adapter/GogsTest.php +++ b/tests/VCS/Adapter/GogsTest.php @@ -12,7 +12,7 @@ class GogsTest extends GiteaTest { protected static string $accessToken = ''; protected static string $owner = ''; - + protected string $webhookEventHeader = 'X-Gogs-Event'; protected string $webhookSignatureHeader = 'X-Gogs-Signature'; protected string $avatarDomain = 'gravatar.com'; @@ -30,7 +30,7 @@ public function setUp(): void } $adapter = new Gogs(new Cache(new None())); - $gogsUrl = System::getEnv('TESTS_GOGS_URL', 'http://gogs:3000') ?? ''; + $gogsUrl = System::getEnv('TESTS_GOGS_URL', 'http://gogs:3000'); $adapter->initializeVariables( installationId: '', @@ -62,31 +62,87 @@ protected function setupGogs(): void } // Webhook delivery (Gogs queues but does not deliver webhooks in test environment) - public function testWebhookPushEvent(): void { $this->markTestSkipped('Gogs webhook delivery not working in test environment'); } + public function testWebhookPushEvent(): void + { + $this->markTestSkipped('Gogs webhook delivery not working in test environment'); + } + + public function testCreateFileOnBranch(): void + { + $this->markTestSkipped('Gogs createFile doesnt seem to work on existing branches.'); + } // --- Skip tests for unsupported Gogs features --- // Pull request API - public function testCommentWorkflow(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - public function testGetComment(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - public function testGetPullRequest(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - public function testGetPullRequestWithInvalidNumber(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - public function testGetPullRequestFromBranch(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - public function testGetPullRequestFromBranchNoPR(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - public function testUpdateComment(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - public function testCreateComment(): void { $this->markTestSkipped('Gogs does not support pull request API'); } - public function testWebhookPullRequestEvent(): void { $this->markTestSkipped('Gogs does not support pull request API'); } + public function testCommentWorkflow(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } + public function testGetComment(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } + public function testGetPullRequest(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } + public function testGetPullRequestWithInvalidNumber(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } + public function testGetPullRequestFromBranch(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } + public function testGetPullRequestFromBranchNoPR(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } + public function testUpdateComment(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } + public function testCreateComment(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } + public function testWebhookPullRequestEvent(): void + { + $this->markTestSkipped('Gogs does not support pull request API'); + } // Tag creation - public function testCreateTag(): void { $this->markTestSkipped('Gogs does not support tag creation via API'); } - public function testGenerateCloneCommandWithTag(): void { $this->markTestSkipped('Gogs does not support tag creation via API'); } + public function testCreateTag(): void + { + $this->markTestSkipped('Gogs does not support tag creation via API'); + } + public function testGenerateCloneCommandWithTag(): void + { + $this->markTestSkipped('Gogs does not support tag creation via API'); + } // Commit status - public function testUpdateCommitStatus(): void { $this->markTestSkipped('Gogs does not support commit status API'); } - public function testUpdateCommitStatusWithInvalidCommit(): void { $this->markTestSkipped('Gogs does not support commit status API'); } - public function testUpdateCommitStatusWithNonExistingRepository(): void { $this->markTestSkipped('Gogs does not support commit status API'); } + public function testUpdateCommitStatus(): void + { + $this->markTestSkipped('Gogs does not support commit status API'); + } + public function testUpdateCommitStatusWithInvalidCommit(): void + { + $this->markTestSkipped('Gogs does not support commit status API'); + } + public function testUpdateCommitStatusWithNonExistingRepository(): void + { + $this->markTestSkipped('Gogs does not support commit status API'); + } // Repository languages - public function testListRepositoryLanguages(): void { $this->markTestSkipped('Gogs does not support repository languages endpoint'); } - public function testListRepositoryLanguagesEmptyRepo(): void { $this->markTestSkipped('Gogs does not support repository languages endpoint'); } + public function testListRepositoryLanguages(): void + { + $this->markTestSkipped('Gogs does not support repository languages endpoint'); + } + public function testListRepositoryLanguagesEmptyRepo(): void + { + $this->markTestSkipped('Gogs does not support repository languages endpoint'); + } } diff --git a/tests/VCS/Base.php b/tests/VCS/Base.php index 3ad42889..026da39c 100644 --- a/tests/VCS/Base.php +++ b/tests/VCS/Base.php @@ -37,7 +37,7 @@ abstract public function testGetRepositoryTree(): void; /** @return array */ protected function getLastWebhookRequest(): array { - $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000'); $client = new Client(); $response = $client->fetch( @@ -78,7 +78,7 @@ protected function assertEventually(callable $probe, int $timeoutMs = 15000, int protected function deleteLastWebhookRequest(): void { - $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000'); $client = new Client(); $client->fetch( From 79550c219bb94aa31766fc7e4e0b80eba3f908c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 24 Mar 2026 10:36:57 +0100 Subject: [PATCH 10/17] Enable tags tests --- docker-compose.yml | 2 +- src/VCS/Adapter/Git/Gogs.php | 68 +++++++++++++++++++++++++++++++--- tests/VCS/Adapter/GogsTest.php | 15 -------- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e6c02817..c9de843a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -122,7 +122,7 @@ services: " gogs: - image: gogs/gogs:0.13 + image: gogs/gogs:0.14 volumes: - gogs-data:/data ports: diff --git a/src/VCS/Adapter/Git/Gogs.php b/src/VCS/Adapter/Git/Gogs.php index e45aaf69..4a060ece 100644 --- a/src/VCS/Adapter/Git/Gogs.php +++ b/src/VCS/Adapter/Git/Gogs.php @@ -257,14 +257,25 @@ public function getLatestCommit(string $owner, string $repositoryName, string $b /** * Create a file in a repository * - * Gogs uses PUT /repos/{owner}/{repo}/contents/{path}. - * For non-default branches we use git CLI, because the Gogs API `branch` - * param creates a new branch rather than targeting an existing one. + * For the default branch (or when no branch is specified), uses the Gogs + * contents API. For non-default branches, uses git CLI because the Gogs + * API returns 500 when targeting an existing non-default branch. * * @return array */ public function createFile(string $owner, string $repositoryName, string $filepath, string $content, string $message = 'Add file', string $branch = ''): array { + if (!empty($branch)) { + // Check if branch is the default branch + $url = "/repos/{$owner}/{$repositoryName}"; + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + $defaultBranch = $response['body']['default_branch'] ?? 'master'; + + if ($branch !== $defaultBranch) { + return $this->createFileViaCli($owner, $repositoryName, $filepath, $content, $message, $branch); + } + } + $url = "/repos/{$owner}/{$repositoryName}/contents/{$filepath}"; $response = $this->call( @@ -274,7 +285,6 @@ public function createFile(string $owner, string $repositoryName, string $filepa [ 'content' => base64_encode($content), 'message' => $message, - 'branch' => $branch ] ); @@ -287,6 +297,33 @@ public function createFile(string $owner, string $repositoryName, string $filepa return $response['body'] ?? []; } + /** + * Create a file on a non-default branch using git CLI. + * + * @return array + */ + private function createFileViaCli(string $owner, string $repositoryName, string $filepath, string $content, string $message, string $branch): array + { + $dir = $this->gitClone($owner, $repositoryName, $branch); + + try { + $fullPath = $dir . '/' . $filepath; + $dirPath = dirname($fullPath); + if (!is_dir($dirPath)) { + mkdir($dirPath, 0777, true); + } + file_put_contents($fullPath, $content); + + $this->exec("git -C {$dir} add " . escapeshellarg($filepath)); + $this->exec("git -C {$dir} commit -m " . escapeshellarg($message)); + $this->exec("git -C {$dir} push origin " . escapeshellarg($branch)); + } finally { + $this->exec("rm -rf {$dir}"); + } + + return ['content' => ['path' => $filepath]]; + } + /** * Create a branch * @@ -364,13 +401,32 @@ public function listRepositoryLanguages(string $owner, string $repositoryName): /** * Create a tag * - * Gogs does not support tag creation via API. + * Gogs does not support tag creation via API, so we use git CLI. * * @return array */ public function createTag(string $owner, string $repositoryName, string $tagName, string $target, string $message = ''): array { - throw new Exception("Tag creation via API is not supported by Gogs"); + $dir = $this->gitClone($owner, $repositoryName); + + try { + $this->exec("git -C {$dir} fetch origin " . escapeshellarg($target)); + if (!empty($message)) { + $this->exec("git -C {$dir} tag -a " . escapeshellarg($tagName) . " " . escapeshellarg($target) . " -m " . escapeshellarg($message)); + } else { + $this->exec("git -C {$dir} tag " . escapeshellarg($tagName) . " " . escapeshellarg($target)); + } + $this->exec("git -C {$dir} push origin " . escapeshellarg($tagName)); + } finally { + $this->exec("rm -rf {$dir}"); + } + + return [ + 'name' => $tagName, + 'commit' => [ + 'sha' => $target, + ], + ]; } /** diff --git a/tests/VCS/Adapter/GogsTest.php b/tests/VCS/Adapter/GogsTest.php index da4d21ca..2f79cf2f 100644 --- a/tests/VCS/Adapter/GogsTest.php +++ b/tests/VCS/Adapter/GogsTest.php @@ -67,11 +67,6 @@ public function testWebhookPushEvent(): void $this->markTestSkipped('Gogs webhook delivery not working in test environment'); } - public function testCreateFileOnBranch(): void - { - $this->markTestSkipped('Gogs createFile doesnt seem to work on existing branches.'); - } - // --- Skip tests for unsupported Gogs features --- // Pull request API @@ -112,16 +107,6 @@ public function testWebhookPullRequestEvent(): void $this->markTestSkipped('Gogs does not support pull request API'); } - // Tag creation - public function testCreateTag(): void - { - $this->markTestSkipped('Gogs does not support tag creation via API'); - } - public function testGenerateCloneCommandWithTag(): void - { - $this->markTestSkipped('Gogs does not support tag creation via API'); - } - // Commit status public function testUpdateCommitStatus(): void { From e39b5a9860e740f623d18a5f855fd8970f23453a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 24 Mar 2026 15:24:12 +0530 Subject: [PATCH 11/17] fix: use pre-written app.ini mount for Gogs webhook support --- docker-compose.yml | 1 + resources/gogs/app.ini | 31 +++++++++++++++++++++++++++++++ tests/VCS/Adapter/GogsTest.php | 5 ----- 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 resources/gogs/app.ini diff --git a/docker-compose.yml b/docker-compose.yml index c9de843a..95b07470 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -125,6 +125,7 @@ services: image: gogs/gogs:0.14 volumes: - gogs-data:/data + - ./resources/gogs/app.ini:/data/gogs/conf/app.ini ports: - "3002:3000" healthcheck: diff --git a/resources/gogs/app.ini b/resources/gogs/app.ini new file mode 100644 index 00000000..2041e581 --- /dev/null +++ b/resources/gogs/app.ini @@ -0,0 +1,31 @@ +BRAND_NAME = Gogs +RUN_USER = git +RUN_MODE = prod + +[database] +TYPE = sqlite3 +PATH = /data/gogs.db + +[repository] +ROOT = /data/repositories +DEFAULT_BRANCH = master + +[server] +DOMAIN = gogs +HTTP_PORT = 3000 +EXTERNAL_URL = http://gogs:3000/ +DISABLE_SSH = true + +[security] +INSTALL_LOCK = true +SECRET_KEY = changeThisToARandomString +LOCAL_NETWORK_ALLOWLIST = * + +[webhook] +DELIVER_TIMEOUT = 10 +SKIP_TLS_VERIFY = true + +[log] +MODE = file +LEVEL = Info +ROOT_PATH = /data/gogs/log \ No newline at end of file diff --git a/tests/VCS/Adapter/GogsTest.php b/tests/VCS/Adapter/GogsTest.php index 2f79cf2f..fe291fa5 100644 --- a/tests/VCS/Adapter/GogsTest.php +++ b/tests/VCS/Adapter/GogsTest.php @@ -61,11 +61,6 @@ protected function setupGogs(): void } } - // Webhook delivery (Gogs queues but does not deliver webhooks in test environment) - public function testWebhookPushEvent(): void - { - $this->markTestSkipped('Gogs webhook delivery not working in test environment'); - } // --- Skip tests for unsupported Gogs features --- From b0e4d0c48aa152f73d5cc01862729d99215fb5bd Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 24 Mar 2026 15:41:40 +0530 Subject: [PATCH 12/17] updated with suggestions --- docker-compose.yml | 59 ++++++++++++++---------------------------- resources/gogs/app.ini | 2 +- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 95b07470..e1fd8a28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,59 +136,40 @@ services: start_period: 15s gogs-bootstrap: - image: alpine/curl:8.12.1 + image: gogs/gogs:0.14 volumes: - gogs-data:/data depends_on: gogs: condition: service_healthy + entrypoint: /bin/sh environment: - GOGS_ADMIN_USERNAME=${GOGS_ADMIN_USERNAME:-utopia} - GOGS_ADMIN_PASSWORD=${GOGS_ADMIN_PASSWORD:-password} - GOGS_ADMIN_EMAIL=${GOGS_ADMIN_EMAIL:-utopia@example.com} - entrypoint: /bin/sh command: - - -ce + - -c - | - if [ -f /data/gogs/token.txt ]; then exit 0; fi - - apk add --no-cache jq - - curl -s -o /dev/null -X POST http://gogs:3000/install \ - -d db_type=SQLite3 \ - -d db_path=/data/gogs.db \ - -d app_name=Gogs \ - -d repo_root_path=/data/repositories \ - -d run_user=git \ - -d domain=gogs \ - -d http_port=3000 \ - -d app_url=http://gogs:3000/ \ - -d log_root_path=/data/gogs/log \ - -d default_branch=master \ - -d admin_name=$$GOGS_ADMIN_USERNAME \ - -d admin_passwd=$$GOGS_ADMIN_PASSWORD \ - -d admin_confirm_passwd=$$GOGS_ADMIN_PASSWORD \ - -d admin_email=$$GOGS_ADMIN_EMAIL \ - || true + USER=git /app/gogs/gogs admin create-user \ + --admin \ + --name $$GOGS_ADMIN_USERNAME \ + --password $$GOGS_ADMIN_PASSWORD \ + --email $$GOGS_ADMIN_EMAIL \ + --config /data/gogs/conf/app.ini || true - sleep 3 - - if ! grep -q '\[webhook\]' /data/gogs/conf/app.ini; then - printf '\n[webhook]\nDELIVER_TIMEOUT = 10\nSKIP_TLS_VERIFY = true\n' >> /data/gogs/conf/app.ini + if [ ! -f /data/gogs/token.txt ]; then + sleep 2 + RESPONSE=$$(curl -s -X POST http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"bootstrap\"}" \ + -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD) + echo "Token response: $$RESPONSE" + TOKEN=$$(echo "$$RESPONSE" | grep -o '"sha1":"[^"]*"' | cut -d'"' -f4) + if [ -z "$$TOKEN" ]; then echo "Failed to get token"; exit 1; fi + mkdir -p /data/gogs + echo $$TOKEN > /data/gogs/token.txt fi - RESPONSE=$$(curl -s -X POST http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens \ - -H 'Content-Type: application/json' \ - -d "{\"name\":\"bootstrap-$$(date +%s)\"}" \ - -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD) - echo "Token API response: $$RESPONSE" - - TOKEN=$$(echo "$$RESPONSE" | jq -r '.sha1') - if [ -z "$$TOKEN" ] || [ "$$TOKEN" = "null" ]; then echo 'Failed to extract token'; exit 1; fi - - mkdir -p /data/gogs - echo $$TOKEN > /data/gogs/token.txt - volumes: gitea-data: forgejo-data: diff --git a/resources/gogs/app.ini b/resources/gogs/app.ini index 2041e581..eb99ff4e 100644 --- a/resources/gogs/app.ini +++ b/resources/gogs/app.ini @@ -28,4 +28,4 @@ SKIP_TLS_VERIFY = true [log] MODE = file LEVEL = Info -ROOT_PATH = /data/gogs/log \ No newline at end of file +ROOT_PATH = /data/gogs/log From 7c9a22fb6833669f502d4e398d233e0a30c9287d Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 24 Mar 2026 15:58:50 +0530 Subject: [PATCH 13/17] updated with suggestions-2 --- docker-compose.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e1fd8a28..fc488878 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -159,12 +159,13 @@ services: if [ ! -f /data/gogs/token.txt ]; then sleep 2 - RESPONSE=$$(curl -s -X POST http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens \ + TOKEN=$$(curl -s \ + -X POST \ + -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD \ -H "Content-Type: application/json" \ -d "{\"name\":\"bootstrap\"}" \ - -u $$GOGS_ADMIN_USERNAME:$$GOGS_ADMIN_PASSWORD) - echo "Token response: $$RESPONSE" - TOKEN=$$(echo "$$RESPONSE" | grep -o '"sha1":"[^"]*"' | cut -d'"' -f4) + http://gogs:3000/api/v1/users/$$GOGS_ADMIN_USERNAME/tokens \ + | grep -o '"sha1":"[^"]*"' | cut -d'"' -f4) if [ -z "$$TOKEN" ]; then echo "Failed to get token"; exit 1; fi mkdir -p /data/gogs echo $$TOKEN > /data/gogs/token.txt From c9f73c1053ecd4eb3e0638038cf88eedc3cb13ab Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 24 Mar 2026 16:05:36 +0530 Subject: [PATCH 14/17] updated with random string --- resources/gogs/app.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/gogs/app.ini b/resources/gogs/app.ini index eb99ff4e..84455215 100644 --- a/resources/gogs/app.ini +++ b/resources/gogs/app.ini @@ -18,7 +18,7 @@ DISABLE_SSH = true [security] INSTALL_LOCK = true -SECRET_KEY = changeThisToARandomString +SECRET_KEY = aRandomString LOCAL_NETWORK_ALLOWLIST = * [webhook] From 89b654e371cabfcf6ab5384aabf920d5a5b9e99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 24 Mar 2026 11:40:29 +0100 Subject: [PATCH 15/17] Fix tests --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index fc488878..75912bc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -139,6 +139,7 @@ services: image: gogs/gogs:0.14 volumes: - gogs-data:/data + - ./resources/gogs/app.ini:/data/gogs/conf/app.ini depends_on: gogs: condition: service_healthy From f09164da8824a92ed8fa37e96aa7ac457b14c9b1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Apr 2026 20:35:58 +0530 Subject: [PATCH 16/17] fix: resolve testGetPullRequestFiles failures for Forgejo and Gogs --- src/VCS/Adapter/Git/Gitea.php | 1 + tests/VCS/Adapter/GiteaTest.php | 14 +++++++------- tests/VCS/Adapter/GogsTest.php | 5 +++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index 0b4ada42..b821d5c1 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -101,6 +101,7 @@ public function createRepository(string $owner, string $repositoryName, bool $pr ]); return $response['body'] ?? []; + return is_array($body) ? $body : []; } public function createOrganization(string $orgName): string diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 3334d8ff..861b9a9b 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -487,14 +487,14 @@ public function testGetPullRequest(): void public function testGetPullRequestFiles(): void { $repositoryName = 'test-get-pull-request-files-' . \uniqid(); - $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + $this->vcsAdapter->createRepository(static::$owner, $repositoryName, false); - $this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test'); - $this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-branch', static::$defaultBranch); - $this->vcsAdapter->createFile(self::$owner, $repositoryName, 'feature.txt', 'feature content', 'Add feature', 'feature-branch'); + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test'); + $this->vcsAdapter->createBranch(static::$owner, $repositoryName, 'feature-branch', static::$defaultBranch); + $this->vcsAdapter->createFile(static::$owner, $repositoryName, 'feature.txt', 'feature content', 'Add feature', 'feature-branch'); $pr = $this->vcsAdapter->createPullRequest( - self::$owner, + static::$owner, $repositoryName, 'Test PR Files', 'feature-branch', @@ -504,7 +504,7 @@ public function testGetPullRequestFiles(): void $prNumber = $pr['number'] ?? 0; $this->assertGreaterThan(0, $prNumber); - $result = $this->vcsAdapter->getPullRequestFiles(self::$owner, $repositoryName, $prNumber); + $result = $this->vcsAdapter->getPullRequestFiles(static::$owner, $repositoryName, $prNumber); $this->assertIsArray($result); $this->assertNotEmpty($result); @@ -512,7 +512,7 @@ public function testGetPullRequestFiles(): void $filenames = array_column($result, 'filename'); $this->assertContains('feature.txt', $filenames); - $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } public function testGetPullRequestWithInvalidNumber(): void diff --git a/tests/VCS/Adapter/GogsTest.php b/tests/VCS/Adapter/GogsTest.php index fe291fa5..d7f04193 100644 --- a/tests/VCS/Adapter/GogsTest.php +++ b/tests/VCS/Adapter/GogsTest.php @@ -125,4 +125,9 @@ public function testListRepositoryLanguagesEmptyRepo(): void { $this->markTestSkipped('Gogs does not support repository languages endpoint'); } + + public function testGetPullRequestFiles(): void + { + $this->markTestSkipped('Gogs does not support pull request files API'); + } } From 8bec208abee86a3e94d1ea2093c653e7b5af1ad8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Apr 2026 09:04:22 +0530 Subject: [PATCH 17/17] removed dead code --- src/VCS/Adapter/Git/Gitea.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index b821d5c1..bc6544ef 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -101,7 +101,7 @@ public function createRepository(string $owner, string $repositoryName, bool $pr ]); return $response['body'] ?? []; - return is_array($body) ? $body : []; + // return is_array($body) ? $body : []; } public function createOrganization(string $orgName): string