Add ticket branch discovery by ticket key

This commit is contained in:
2026-03-26 09:16:17 +01:00
parent fee14d93a7
commit 0b95477f4e
12 changed files with 475 additions and 0 deletions
@@ -3,6 +3,7 @@
namespace App\Actions\Git\Contracts;
use App\Actions\Git\Data\GitAuthenticatedUser;
use App\Actions\Git\Data\GitBranch;
use App\Actions\Git\Data\GitRepository;
use App\Actions\Git\GitProviderKey;
@@ -16,4 +17,14 @@ interface GitProviderClient
* @return array<int, GitRepository>
*/
public function listRepositories(string $baseUrl, string $token): array;
/**
* @return array<int, GitBranch>
*/
public function listBranches(
string $baseUrl,
string $token,
string $owner,
string $repository,
): array;
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Actions\Git\Data;
/**
* Provider-agnostic representation of a repository branch.
*/
final readonly class GitBranch
{
public function __construct(
public string $name,
) {}
}
@@ -4,6 +4,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\GitRepository;
use App\Actions\Git\GitProviderKey;
use Illuminate\Http\Client\PendingRequest;
@@ -75,6 +76,43 @@ class GiteaProviderClient implements GitProviderClient
return $repositories;
}
/**
* @return array<int, GitBranch>
*/
public function listBranches(
string $baseUrl,
string $token,
string $owner,
string $repository,
): array {
$page = 1;
$branches = [];
do {
/** @var array<int, array{name: string}> $payload */
$payload = $this->request($baseUrl, $token)
->get(
'/repos/'.rawurlencode($owner).'/'.rawurlencode($repository).'/branches',
[
'limit' => 100,
'page' => $page,
],
)
->throw()
->json();
foreach ($payload as $branch) {
$branches[] = new GitBranch(
name: $branch['name'],
);
}
$page++;
} while (count($payload) === 100);
return $branches;
}
private function request(string $baseUrl, string $token): PendingRequest
{
return Http::acceptJson()
@@ -0,0 +1,151 @@
<?php
namespace App\Actions\Git;
use App\Actions\Git\Data\GitBranch;
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 ResolveTicketBranchMatches
{
public function __construct(
private readonly GitProviderManager $gitProviderManager,
private readonly TicketKeyReferenceMatcher $ticketKeyReferenceMatcher,
) {}
/**
* @return array<int, array{
* branches: array<int, array{name: string}>,
* error: string|null,
* 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 [
'branches' => [],
'error' => null,
'repository_connection' => $this->repositoryConnectionPayload(
$repositoryConnection,
false,
),
];
}
try {
$branches = collect(
$this->gitProviderManager
->client($userGitConnection->provider)
->listBranches(
$userGitConnection->base_url,
$userGitConnection->api_token,
$repositoryConnection->owner_name,
$repositoryConnection->repository_name,
),
)
->filter(fn (GitBranch $branch): bool => $this->ticketKeyReferenceMatcher->matches(
$ticket->ticket_key,
$branch->name,
))
->map(fn (GitBranch $branch): array => [
'name' => $branch->name,
])
->values()
->all();
$error = null;
} catch (Throwable) {
$branches = [];
$error = 'KanbanGit could not load branches from this repository right now.';
}
return [
'branches' => $branches,
'error' => $error,
'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();
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Actions\Git;
class TicketKeyReferenceMatcher
{
public function matches(string $ticketKey, string $referenceName): bool
{
return preg_match(
'/^'.preg_quote($ticketKey, '/').'(?=$|[-_\/])/',
$referenceName,
) === 1;
}
}
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Actions\Git\ResolveTicketBranchMatches;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Actions\Tickets\UpdateProjectTicket;
use App\Http\Requests\UpdateProjectTicketRequest;
@@ -18,6 +19,7 @@ class ProjectTicketController extends Controller
{
public function __construct(
private readonly ProjectPermissionService $projectPermissionService,
private readonly ResolveTicketBranchMatches $resolveTicketBranchMatches,
private readonly UpdateProjectTicket $updateProjectTicket,
) {}
@@ -46,6 +48,11 @@ class ProjectTicketController extends Controller
'title' => $parentTicket->title,
]),
'project' => $project,
'repositoryBranches' => $this->resolveTicketBranchMatches->handle(
$request->user(),
$project,
$ticket,
),
'status' => session('status'),
'ticket' => $ticket->load([
'fieldValues.ticketFieldDefinition',