Skip to content

Commit 3277ea7

Browse files
author
Your Name
committed
feat: Add Gitea pull request and comment endpoints
- Implement getPullRequest to fetch PR details - Implement getPullRequestFromBranch to find PR by branch name - Implement createPullRequest to create new PRs - Implement createComment, getComment, updateComment for PR comments - Add comprehensive tests with full workflow coverage - Add edge case tests for invalid inputs - Follow PR #64 null safety pattern Tests: 7 new tests, all passing
1 parent 907a3c6 commit 3277ea7

2 files changed

Lines changed: 289 additions & 8 deletions

File tree

src/VCS/Adapter/Git/Gitea.php

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -347,19 +347,82 @@ public function deleteRepository(string $owner, string $repositoryName): bool
347347
return true;
348348
}
349349

350+
/**
351+
* Create a pull request
352+
*
353+
* @param string $owner Owner of the repository
354+
* @param string $repositoryName Name of the repository
355+
* @param string $title PR title
356+
* @param string $head Source branch
357+
* @param string $base Target branch
358+
* @param string $body PR description (optional)
359+
* @return array<mixed> Created PR details
360+
*/
361+
public function createPullRequest(string $owner, string $repositoryName, string $title, string $head, string $base, string $body = ''): array
362+
{
363+
$url = "/repos/{$owner}/{$repositoryName}/pulls";
364+
365+
$payload = [
366+
'title' => $title,
367+
'head' => $head,
368+
'base' => $base,
369+
];
370+
371+
if (!empty($body)) {
372+
$payload['body'] = $body;
373+
}
374+
375+
$response = $this->call(
376+
self::METHOD_POST,
377+
$url,
378+
['Authorization' => "token $this->accessToken"],
379+
$payload
380+
);
381+
382+
$responseBody = $response['body'] ?? [];
383+
384+
return $responseBody;
385+
}
386+
350387
public function createComment(string $owner, string $repositoryName, int $pullRequestNumber, string $comment): string
351388
{
352-
throw new Exception("Not implemented yet");
389+
$url = "/repos/{$owner}/{$repositoryName}/issues/{$pullRequestNumber}/comments";
390+
391+
$response = $this->call(self::METHOD_POST, $url, ['Authorization' => "token $this->accessToken"], ['body' => $comment]);
392+
393+
$responseBody = $response['body'] ?? [];
394+
395+
if (!array_key_exists('id', $responseBody)) {
396+
throw new Exception("Comment creation response is missing comment ID.");
397+
}
398+
399+
return (string) ($responseBody['id'] ?? '');
353400
}
354401

355402
public function getComment(string $owner, string $repositoryName, string $commentId): string
356403
{
357-
throw new Exception("Not implemented yet");
404+
$url = "/repos/{$owner}/{$repositoryName}/issues/comments/{$commentId}";
405+
406+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
407+
408+
$responseBody = $response['body'] ?? [];
409+
410+
return $responseBody['body'] ?? '';
358411
}
359412

360413
public function updateComment(string $owner, string $repositoryName, int $commentId, string $comment): string
361414
{
362-
throw new Exception("Not implemented yet");
415+
$url = "/repos/{$owner}/{$repositoryName}/issues/comments/{$commentId}";
416+
417+
$response = $this->call(self::METHOD_PATCH, $url, ['Authorization' => "token $this->accessToken"], ['body' => $comment]);
418+
419+
$responseBody = $response['body'] ?? [];
420+
421+
if (!array_key_exists('id', $responseBody)) {
422+
throw new Exception("Comment update response is missing comment ID.");
423+
}
424+
425+
return (string) ($responseBody['id'] ?? '');
363426
}
364427

365428
public function getUser(string $username): array
@@ -374,12 +437,31 @@ public function getOwnerName(string $installationId): string
374437

375438
public function getPullRequest(string $owner, string $repositoryName, int $pullRequestNumber): array
376439
{
377-
throw new Exception("Not implemented yet");
440+
$url = "/repos/{$owner}/{$repositoryName}/pulls/{$pullRequestNumber}";
441+
442+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
443+
444+
return $response['body'] ?? [];
378445
}
379446

380447
public function getPullRequestFromBranch(string $owner, string $repositoryName, string $branch): array
381448
{
382-
throw new Exception("Not implemented yet");
449+
$url = "/repos/{$owner}/{$repositoryName}/pulls?state=open&sort=recentupdate&limit=1";
450+
451+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
452+
453+
$responseBody = $response['body'] ?? [];
454+
455+
// Filter by head branch (source branch of the PR)
456+
foreach ($responseBody as $pr) {
457+
$prHead = $pr['head'] ?? [];
458+
$prHeadRef = $prHead['ref'] ?? '';
459+
if ($prHeadRef === $branch) {
460+
return $pr;
461+
}
462+
}
463+
464+
return [];
383465
}
384466

385467
public function listBranches(string $owner, string $repositoryName): array

tests/VCS/Adapter/GiteaTest.php

Lines changed: 202 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,111 @@ public function testCreatePrivateRepository(): void
9898
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
9999
}
100100

101+
public function testCommentWorkflow(): void
102+
{
103+
$repositoryName = 'test-comment-workflow-' . \uniqid();
104+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
105+
106+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
107+
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'comment-test', 'main');
108+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'test.txt', 'test');
109+
110+
$pr = $this->vcsAdapter->createPullRequest(
111+
self::$owner,
112+
$repositoryName,
113+
'Comment Test PR',
114+
'comment-test',
115+
'main'
116+
);
117+
118+
$prNumber = $pr['number'] ?? 0;
119+
$this->assertGreaterThan(0, $prNumber);
120+
121+
$originalComment = 'This is a test comment';
122+
$commentId = $this->vcsAdapter->createComment(self::$owner, $repositoryName, $prNumber, $originalComment);
123+
124+
$this->assertNotEmpty($commentId);
125+
$this->assertIsString($commentId);
126+
127+
// Test getComment
128+
$retrievedComment = $this->vcsAdapter->getComment(self::$owner, $repositoryName, $commentId);
129+
130+
$this->assertSame($originalComment, $retrievedComment);
131+
$this->assertIsString($commentId);
132+
$this->assertNotEmpty($commentId);
133+
134+
// Test updateComment
135+
$updatedCommentText = 'This comment has been updated';
136+
$updatedCommentId = $this->vcsAdapter->updateComment(self::$owner, $repositoryName, (int)$commentId, $updatedCommentText);
137+
138+
$this->assertSame($commentId, $updatedCommentId);
139+
140+
// Verify the update
141+
$finalComment = $this->vcsAdapter->getComment(self::$owner, $repositoryName, $commentId);
142+
$this->assertSame($updatedCommentText, $finalComment);
143+
144+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
145+
}
146+
101147
public function testGetComment(): void
102148
{
103-
$this->markTestSkipped('Will be implemented in follow-up PR');
149+
$repositoryName = 'test-get-comment-' . \uniqid();
150+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
151+
152+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
153+
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'test-branch', 'main');
154+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'test.txt', 'test');
155+
156+
// Create PR
157+
$pr = $this->vcsAdapter->createPullRequest(
158+
self::$owner,
159+
$repositoryName,
160+
'Test PR',
161+
'test-branch',
162+
'main'
163+
);
164+
165+
$prNumber = $pr['number'] ?? 0;
166+
167+
// Create a comment
168+
$commentId = $this->vcsAdapter->createComment(self::$owner, $repositoryName, $prNumber, 'Test comment');
169+
170+
// Test getComment
171+
$result = $this->vcsAdapter->getComment(self::$owner, $repositoryName, $commentId);
172+
173+
$this->assertIsString($result);
174+
$this->assertSame('Test comment', $result);
175+
176+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
177+
}
178+
179+
public function testCreateCommentInvalidPR(): void
180+
{
181+
$repositoryName = 'test-comment-invalid-' . \uniqid();
182+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
183+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
184+
185+
try {
186+
$this->expectException(\Exception::class);
187+
$this->vcsAdapter->createComment(self::$owner, $repositoryName, 99999, 'Test comment');
188+
} finally {
189+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
190+
}
191+
}
192+
193+
public function testGetCommentInvalidId(): void
194+
{
195+
$repositoryName = 'test-get-comment-invalid-' . \uniqid();
196+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
197+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
198+
199+
// Getting invalid comment should return empty string
200+
$result = $this->vcsAdapter->getComment(self::$owner, $repositoryName, '99999999');
201+
202+
$this->assertIsString($result);
203+
// May be empty or throw exception depending on API
204+
205+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
104206
}
105207

106208
public function testGetRepositoryTreeWithSlashInBranchName(): void
@@ -340,7 +442,59 @@ public function testListRepositoryContentsNonExistingPath(): void
340442

341443
public function testGetPullRequest(): void
342444
{
343-
$this->markTestSkipped('Will be implemented in follow-up PR');
445+
$repositoryName = 'test-get-pull-request-' . \uniqid();
446+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
447+
448+
// Create initial file on main branch
449+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
450+
451+
// Create feature branch and add file
452+
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-branch', 'main');
453+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'feature.txt', 'feature content');
454+
455+
// Create pull request
456+
$pr = $this->vcsAdapter->createPullRequest(
457+
self::$owner,
458+
$repositoryName,
459+
'Test PR',
460+
'feature-branch',
461+
'main',
462+
'Test PR description'
463+
);
464+
465+
$prNumber = $pr['number'] ?? 0;
466+
$this->assertGreaterThan(0, $prNumber);
467+
468+
// Now test getPullRequest
469+
$result = $this->vcsAdapter->getPullRequest(self::$owner, $repositoryName, $prNumber);
470+
471+
$this->assertIsArray($result);
472+
$this->assertArrayHasKey('number', $result);
473+
$this->assertArrayHasKey('title', $result);
474+
$this->assertArrayHasKey('state', $result);
475+
$this->assertArrayHasKey('head', $result);
476+
$this->assertArrayHasKey('base', $result);
477+
478+
$this->assertSame($prNumber, $result['number']);
479+
$this->assertSame('Test PR', $result['title']);
480+
$this->assertSame('open', $result['state']);
481+
482+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
483+
}
484+
485+
public function testGetPullRequestWithInvalidNumber(): void
486+
{
487+
$repositoryName = 'test-get-pull-request-invalid-' . \uniqid();
488+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
489+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
490+
491+
// Try to get non-existent PR
492+
$result = $this->vcsAdapter->getPullRequest(self::$owner, $repositoryName, 99999);
493+
494+
// Should return empty or have error handling
495+
$this->assertIsArray($result);
496+
497+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
344498
}
345499

346500
public function testGenerateCloneCommand(): void
@@ -505,7 +659,52 @@ public function testGetOwnerName(): void
505659

506660
public function testGetPullRequestFromBranch(): void
507661
{
508-
$this->markTestSkipped('Will be implemented in follow-up PR');
662+
$repositoryName = 'test-get-pr-from-branch-' . \uniqid();
663+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
664+
665+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
666+
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'my-feature', 'main');
667+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'feature.txt', 'content');
668+
669+
// Create PR
670+
$pr = $this->vcsAdapter->createPullRequest(
671+
self::$owner,
672+
$repositoryName,
673+
'Feature PR',
674+
'my-feature',
675+
'main'
676+
);
677+
678+
$this->assertArrayHasKey('number', $pr);
679+
680+
// Test getPullRequestFromBranch
681+
$result = $this->vcsAdapter->getPullRequestFromBranch(self::$owner, $repositoryName, 'my-feature');
682+
683+
$this->assertIsArray($result);
684+
$this->assertNotEmpty($result);
685+
$this->assertArrayHasKey('head', $result);
686+
687+
$resultHead = $result['head'] ?? [];
688+
$this->assertSame('my-feature', $resultHead['ref'] ?? '');
689+
690+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
691+
}
692+
693+
public function testGetPullRequestFromBranchNoPR(): void
694+
{
695+
$repositoryName = 'test-get-pr-no-pr-' . \uniqid();
696+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
697+
698+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
699+
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'lonely-branch', 'main');
700+
701+
// Don't create a PR - just test the method
702+
$result = $this->vcsAdapter->getPullRequestFromBranch(self::$owner, $repositoryName, 'lonely-branch');
703+
704+
$this->assertIsArray($result);
705+
$this->assertEmpty($result);
706+
707+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
509708
}
510709

511710
public function testCreateComment(): void

0 commit comments

Comments
 (0)