Add project repository connection settings

This commit is contained in:
2026-03-26 09:11:37 +01:00
parent 5f52d050ee
commit fee14d93a7
21 changed files with 1597 additions and 5 deletions
@@ -0,0 +1,296 @@
<?php
namespace App\Http\Controllers;
use App\Actions\Git\Data\GitRepository;
use App\Actions\Git\GitProviderManager;
use App\Actions\ProjectPermissions\ProjectPermissionService;
use App\Http\Requests\StoreRepositoryConnectionRequest;
use App\Http\Requests\UpdateRepositoryConnectionRequest;
use App\Models\Project;
use App\Models\RepositoryConnection;
use App\Models\User;
use App\Models\UserGitConnection;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
use Throwable;
class ProjectRepositorySettingsController extends Controller
{
public function __construct(
private readonly GitProviderManager $gitProviderManager,
private readonly ProjectPermissionService $projectPermissionService,
) {}
public function show(Request $request, Project $project): Response
{
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
403,
);
return Inertia::render('projects/settings/Repositories', [
'availableRepositorySources' => $this->availableRepositorySources($request->user()),
'project' => $project,
'repositoryConnections' => $project->repositoryConnections()
->get()
->map(fn (RepositoryConnection $repositoryConnection): array => [
'base_url' => $repositoryConnection->base_url,
'created_at' => $repositoryConnection->created_at?->toISOString(),
'has_personal_access' => $this->hasPersonalAccess($request->user(), $repositoryConnection),
'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(),
])
->values(),
'status' => session('status'),
]);
}
public function store(
StoreRepositoryConnectionRequest $request,
Project $project,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
403,
);
$repositoryAttributes = $this->repositoryAttributesForSelection(
$request->user(),
$request->userGitConnectionId(),
$request->providerRepositoryId(),
);
$this->ensureRepositoryConnectionIsUnique($project, $repositoryAttributes);
$repositoryConnection = $project->repositoryConnections()->create($repositoryAttributes);
return to_route('projects.settings.repositories', $project)
->with('status', "{$repositoryConnection->repository_full_name} was linked to {$project->name}.");
}
public function update(
UpdateRepositoryConnectionRequest $request,
Project $project,
RepositoryConnection $repositoryConnection,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
403,
);
$repositoryAttributes = $this->repositoryAttributesForSelection(
$request->user(),
$request->userGitConnectionId(),
$request->providerRepositoryId(),
);
$this->ensureRepositoryConnectionIsUnique(
$project,
$repositoryAttributes,
$repositoryConnection,
);
$repositoryConnection->update($repositoryAttributes);
return to_route('projects.settings.repositories', $project)
->with('status', "{$repositoryConnection->fresh()->repository_full_name} connection was updated.");
}
public function destroy(
Request $request,
Project $project,
RepositoryConnection $repositoryConnection,
): RedirectResponse {
abort_unless(
$this->projectPermissionService->userCanManageProjectAccess($request->user(), $project),
403,
);
$repositoryLabel = $repositoryConnection->repository_full_name;
$repositoryConnection->delete();
return to_route('projects.settings.repositories', $project)
->with('status', "{$repositoryLabel} was removed from the project.");
}
/**
* @return array<int, array{
* account_login: string,
* account_name: string|null,
* base_url: string,
* error: string|null,
* provider: string,
* provider_label: string,
* repositories: array<int, array{
* full_name: string,
* owner_name: string,
* provider_repository_id: string,
* repository_name: string,
* repository_url: string,
* visibility_label: string
* }>,
* user_git_connection_id: int
* }>
*/
private function availableRepositorySources(User $user): array
{
/** @var Collection<int, UserGitConnection> $gitConnections */
$gitConnections = $user->gitConnections()
->get();
return $gitConnections
->map(function (UserGitConnection $gitConnection): array {
try {
$repositories = collect(
$this->gitProviderManager
->client($gitConnection->provider)
->listRepositories($gitConnection->base_url, $gitConnection->api_token),
)
->map(fn (GitRepository $repository): array => [
'full_name' => $repository->fullName,
'owner_name' => $repository->owner,
'provider_repository_id' => $repository->providerRepositoryId,
'repository_name' => $repository->name,
'repository_url' => $repository->htmlUrl,
'visibility_label' => $repository->isPrivate ? 'Private' : 'Public',
])
->sortBy('full_name')
->values()
->all();
$error = null;
} catch (Throwable) {
$repositories = [];
$error = 'KanbanGit could not load repositories from this Git connection right now.';
}
return [
'account_login' => $gitConnection->account_login,
'account_name' => $gitConnection->account_name,
'base_url' => $gitConnection->base_url,
'error' => $error,
'provider' => $gitConnection->provider->value,
'provider_label' => $gitConnection->provider->label(),
'repositories' => $repositories,
'user_git_connection_id' => $gitConnection->id,
];
})
->values()
->all();
}
/**
* @param array{
* base_url: string,
* owner_name: string,
* provider: string,
* provider_repository_id: string,
* repository_full_name: string,
* repository_name: string,
* repository_url: string
* } $repositoryAttributes
*/
private function ensureRepositoryConnectionIsUnique(
Project $project,
array $repositoryAttributes,
?RepositoryConnection $repositoryConnection = null,
): void {
$query = $project->repositoryConnections()
->where('provider', $repositoryAttributes['provider'])
->where('base_url', $repositoryAttributes['base_url'])
->where('provider_repository_id', $repositoryAttributes['provider_repository_id']);
if ($repositoryConnection !== null) {
$query->whereKeyNot($repositoryConnection->id);
}
if ($query->exists()) {
throw ValidationException::withMessages([
'provider_repository_id' => 'This repository is already linked to the project.',
]);
}
}
private function hasPersonalAccess(User $user, RepositoryConnection $repositoryConnection): bool
{
return $user->gitConnections()
->where('provider', $repositoryConnection->provider)
->where('base_url', $repositoryConnection->base_url)
->exists();
}
/**
* @return array{
* base_url: string,
* owner_name: string,
* provider: string,
* provider_repository_id: string,
* repository_full_name: string,
* repository_name: string,
* repository_url: string
* }
*/
private function repositoryAttributesForSelection(
User $user,
int $userGitConnectionId,
string $providerRepositoryId,
): array {
$userGitConnection = $this->ownedUserGitConnection($user, $userGitConnectionId);
$repository = $this->repositoryFromConnection(
$userGitConnection,
$providerRepositoryId,
);
if ($repository === null) {
throw ValidationException::withMessages([
'provider_repository_id' => 'The selected repository is not available through this Git connection.',
]);
}
return [
'base_url' => $userGitConnection->base_url,
'owner_name' => $repository->owner,
'provider' => $userGitConnection->provider,
'provider_repository_id' => $repository->providerRepositoryId,
'repository_full_name' => $repository->fullName,
'repository_name' => $repository->name,
'repository_url' => $repository->htmlUrl,
];
}
private function ownedUserGitConnection(User $user, int $userGitConnectionId): UserGitConnection
{
$userGitConnection = UserGitConnection::query()->findOrFail($userGitConnectionId);
abort_unless($userGitConnection->user_id === $user->id, 403);
return $userGitConnection;
}
private function repositoryFromConnection(
UserGitConnection $userGitConnection,
string $providerRepositoryId,
): ?GitRepository {
return collect(
$this->gitProviderManager
->client($userGitConnection->provider)
->listRepositories($userGitConnection->base_url, $userGitConnection->api_token),
)
->first(fn (GitRepository $repository): bool => $repository->providerRepositoryId === $providerRepositoryId);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreRepositoryConnectionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user() !== null;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'provider_repository_id' => ['required', 'string', 'max:255'],
'user_git_connection_id' => ['required', 'integer', 'exists:user_git_connections,id'],
];
}
public function providerRepositoryId(): string
{
return (string) $this->validated('provider_repository_id');
}
public function userGitConnectionId(): int
{
return (int) $this->validated('user_git_connection_id');
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRepositoryConnectionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user() !== null;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'provider_repository_id' => ['required', 'string', 'max:255'],
'user_git_connection_id' => ['required', 'integer', 'exists:user_git_connections,id'],
];
}
public function providerRepositoryId(): string
{
return (string) $this->validated('provider_repository_id');
}
public function userGitConnectionId(): int
{
return (int) $this->validated('user_git_connection_id');
}
}
+7
View File
@@ -53,6 +53,13 @@ class Project extends Model
->orderBy('position');
}
public function repositoryConnections(): HasMany
{
return $this->hasMany(RepositoryConnection::class)
->orderBy('provider')
->orderBy('repository_full_name');
}
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class)
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Models;
use App\Actions\Git\GitProviderKey;
use Database\Factories\RepositoryConnectionFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'base_url',
'owner_name',
'project_id',
'provider',
'provider_repository_id',
'repository_full_name',
'repository_name',
'repository_url',
])]
class RepositoryConnection extends Model
{
/** @use HasFactory<RepositoryConnectionFactory> */
use HasFactory;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'provider' => GitProviderKey::class,
];
}
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
}