Add ticket pull request visibility

This commit is contained in:
2026-03-26 09:19:56 +01:00
parent 0b95477f4e
commit 789675f83f
11 changed files with 495 additions and 3 deletions
@@ -4,6 +4,7 @@ namespace App\Actions\Git\Contracts;
use App\Actions\Git\Data\GitAuthenticatedUser;
use App\Actions\Git\Data\GitBranch;
use App\Actions\Git\Data\GitPullRequest;
use App\Actions\Git\Data\GitRepository;
use App\Actions\Git\GitProviderKey;
@@ -27,4 +28,14 @@ interface GitProviderClient
string $owner,
string $repository,
): array;
/**
* @return array<int, GitPullRequest>
*/
public function listPullRequests(
string $baseUrl,
string $token,
string $owner,
string $repository,
): array;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Actions\Git\Data;
/**
* Provider-agnostic representation of a pull request.
*/
final readonly class GitPullRequest
{
public function __construct(
public string $number,
public string $title,
public string $status,
public string $sourceBranch,
public string $htmlUrl,
) {}
}
@@ -5,6 +5,7 @@ namespace App\Actions\Git\Gitea;
use App\Actions\Git\Contracts\GitProviderClient;
use App\Actions\Git\Data\GitAuthenticatedUser;
use App\Actions\Git\Data\GitBranch;
use App\Actions\Git\Data\GitPullRequest;
use App\Actions\Git\Data\GitRepository;
use App\Actions\Git\GitProviderKey;
use Illuminate\Http\Client\PendingRequest;
@@ -113,6 +114,58 @@ class GiteaProviderClient implements GitProviderClient
return $branches;
}
/**
* @return array<int, GitPullRequest>
*/
public function listPullRequests(
string $baseUrl,
string $token,
string $owner,
string $repository,
): array {
$page = 1;
$pullRequests = [];
do {
/** @var array<int, array{
* html_url: string,
* merged?: bool,
* number: int|string,
* state: string,
* title: string,
* head: array{ref: string}
* }> $payload
*/
$payload = $this->request($baseUrl, $token)
->get(
'/repos/'.rawurlencode($owner).'/'.rawurlencode($repository).'/pulls',
[
'limit' => 100,
'page' => $page,
'state' => 'all',
],
)
->throw()
->json();
foreach ($payload as $pullRequest) {
$status = ($pullRequest['merged'] ?? false) ? 'merged' : $pullRequest['state'];
$pullRequests[] = new GitPullRequest(
number: (string) $pullRequest['number'],
title: $pullRequest['title'],
status: $status,
sourceBranch: $pullRequest['head']['ref'],
htmlUrl: $pullRequest['html_url'],
);
}
$page++;
} while (count($payload) === 100);
return $pullRequests;
}
private function request(string $baseUrl, string $token): PendingRequest
{
return Http::acceptJson()
@@ -0,0 +1,161 @@
<?php
namespace App\Actions\Git;
use App\Actions\Git\Data\GitPullRequest;
use App\Models\Project;
use App\Models\RepositoryConnection;
use App\Models\Ticket;
use App\Models\User;
use App\Models\UserGitConnection;
use Illuminate\Support\Collection;
use Throwable;
class ResolveTicketPullRequestMatches
{
public function __construct(
private readonly GitProviderManager $gitProviderManager,
private readonly TicketKeyReferenceMatcher $ticketKeyReferenceMatcher,
) {}
/**
* @return array<int, array{
* error: string|null,
* pull_requests: array<int, array{
* html_url: string,
* number: string,
* source_branch: string,
* status: string,
* title: string
* }>,
* repository_connection: array{
* base_url: string,
* created_at: string|null,
* has_personal_access: bool,
* id: int,
* owner_name: string,
* project_id: int,
* provider: string,
* provider_label: string,
* provider_repository_id: string,
* repository_full_name: string,
* repository_name: string,
* repository_url: string,
* updated_at: string|null
* }
* }>
*/
public function handle(User $user, Project $project, Ticket $ticket): array
{
/** @var Collection<int, RepositoryConnection> $repositoryConnections */
$repositoryConnections = $project->repositoryConnections()
->get();
return $repositoryConnections
->map(function (RepositoryConnection $repositoryConnection) use ($ticket, $user): array {
$userGitConnection = $this->matchingUserGitConnection($user, $repositoryConnection);
$hasPersonalAccess = $userGitConnection !== null;
if ($userGitConnection === null) {
return [
'error' => null,
'pull_requests' => [],
'repository_connection' => $this->repositoryConnectionPayload(
$repositoryConnection,
false,
),
];
}
try {
$pullRequests = collect(
$this->gitProviderManager
->client($userGitConnection->provider)
->listPullRequests(
$userGitConnection->base_url,
$userGitConnection->api_token,
$repositoryConnection->owner_name,
$repositoryConnection->repository_name,
),
)
->filter(fn (GitPullRequest $pullRequest): bool => $this->ticketKeyReferenceMatcher->matches(
$ticket->ticket_key,
$pullRequest->sourceBranch,
))
->map(fn (GitPullRequest $pullRequest): array => [
'html_url' => $pullRequest->htmlUrl,
'number' => $pullRequest->number,
'source_branch' => $pullRequest->sourceBranch,
'status' => $pullRequest->status,
'title' => $pullRequest->title,
])
->values()
->all();
$error = null;
} catch (Throwable) {
$error = 'KanbanGit could not load pull requests from this repository right now.';
$pullRequests = [];
}
return [
'error' => $error,
'pull_requests' => $pullRequests,
'repository_connection' => $this->repositoryConnectionPayload(
$repositoryConnection,
$hasPersonalAccess,
),
];
})
->values()
->all();
}
/**
* @return array{
* base_url: string,
* created_at: string|null,
* has_personal_access: bool,
* id: int,
* owner_name: string,
* project_id: int,
* provider: string,
* provider_label: string,
* provider_repository_id: string,
* repository_full_name: string,
* repository_name: string,
* repository_url: string,
* updated_at: string|null
* }
*/
private function repositoryConnectionPayload(
RepositoryConnection $repositoryConnection,
bool $hasPersonalAccess,
): array {
return [
'base_url' => $repositoryConnection->base_url,
'created_at' => $repositoryConnection->created_at?->toISOString(),
'has_personal_access' => $hasPersonalAccess,
'id' => $repositoryConnection->id,
'owner_name' => $repositoryConnection->owner_name,
'project_id' => $repositoryConnection->project_id,
'provider' => $repositoryConnection->provider->value,
'provider_label' => $repositoryConnection->provider->label(),
'provider_repository_id' => $repositoryConnection->provider_repository_id,
'repository_full_name' => $repositoryConnection->repository_full_name,
'repository_name' => $repositoryConnection->repository_name,
'repository_url' => $repositoryConnection->repository_url,
'updated_at' => $repositoryConnection->updated_at?->toISOString(),
];
}
private function matchingUserGitConnection(
User $user,
RepositoryConnection $repositoryConnection,
): ?UserGitConnection {
return $user->gitConnections()
->where('provider', $repositoryConnection->provider)
->where('base_url', $repositoryConnection->base_url)
->first();
}
}
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Actions\Git\ResolveTicketBranchMatches;
use App\Actions\Git\ResolveTicketPullRequestMatches;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Tickets\UpdateProjectTicket;
use App\Http\Requests\UpdateProjectTicketRequest;
@@ -20,6 +21,7 @@ class ProjectTicketController extends Controller
public function __construct(
private readonly ProjectPermissionService $projectPermissionService,
private readonly ResolveTicketBranchMatches $resolveTicketBranchMatches,
private readonly ResolveTicketPullRequestMatches $resolveTicketPullRequestMatches,
private readonly UpdateProjectTicket $updateProjectTicket,
) {}
@@ -53,6 +55,11 @@ class ProjectTicketController extends Controller
$project,
$ticket,
),
'repositoryPullRequests' => $this->resolveTicketPullRequestMatches->handle(
$request->user(),
$project,
$ticket,
),
'status' => session('status'),
'ticket' => $ticket->load([
'fieldValues.ticketFieldDefinition',