Add ticket branch discovery by ticket key #17
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -66,4 +66,6 @@
|
||||
|
||||
- later:
|
||||
- if deleting ticket type select a new ticket type which all existing ticket should get or select delete all tickets of that type
|
||||
- repository_connections check base Url maybe unnessecary?
|
||||
- repository_connections check base Url maybe unnessecary?
|
||||
- Git listBranches replace baseUrl, owner, rperoName with GItReprository
|
||||
- replace Kanbangit with env App Name
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, useForm, Head, Link } from '@inertiajs/vue3';
|
||||
import { computed } from 'vue';
|
||||
import ProjectTicketLinkController from '@/actions/App/Http/Controllers/ProjectTicketLinkController';
|
||||
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { edit as editGitConnections } from '@/routes/git-connections';
|
||||
import { index as backlogIndex } from '@/routes/projects/backlog';
|
||||
import { index as boardIndex } from '@/routes/projects/board';
|
||||
import { index as projectIndex, show } from '@/routes/projects';
|
||||
@@ -25,6 +27,15 @@ import type { BreadcrumbItem, ProjectTicketPageProps } from '@/types';
|
||||
|
||||
const props = defineProps<ProjectTicketPageProps>();
|
||||
|
||||
const visibleRepositoryBranches = computed(() =>
|
||||
props.repositoryBranches.filter(
|
||||
(repositoryBranch) =>
|
||||
repositoryBranch.branches.length > 0 ||
|
||||
repositoryBranch.error !== null ||
|
||||
!repositoryBranch.repository_connection.has_personal_access,
|
||||
),
|
||||
);
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Projects',
|
||||
@@ -427,6 +438,106 @@ const formatDateTime = (value: string): string => {
|
||||
</Card>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Card class="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Branches</CardTitle>
|
||||
<CardDescription>
|
||||
Matching branches are discovered from linked
|
||||
repositories when the branch name starts with
|
||||
this ticket key.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-4">
|
||||
<div
|
||||
v-if="visibleRepositoryBranches.length === 0"
|
||||
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
|
||||
>
|
||||
No matching branches were found across the
|
||||
linked repositories you can access.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="repositoryBranch in visibleRepositoryBranches"
|
||||
:key="
|
||||
repositoryBranch.repository_connection.id
|
||||
"
|
||||
class="rounded-xl border border-border px-4 py-4"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<Badge variant="secondary">
|
||||
{{
|
||||
repositoryBranch
|
||||
.repository_connection
|
||||
.provider_label
|
||||
}}
|
||||
</Badge>
|
||||
<span class="font-medium">
|
||||
{{
|
||||
repositoryBranch
|
||||
.repository_connection
|
||||
.repository_full_name
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="mt-2 text-sm text-muted-foreground"
|
||||
>
|
||||
{{
|
||||
repositoryBranch
|
||||
.repository_connection.base_url
|
||||
}}
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
!repositoryBranch
|
||||
.repository_connection
|
||||
.has_personal_access
|
||||
"
|
||||
class="mt-3 space-y-3"
|
||||
>
|
||||
<p
|
||||
class="text-sm text-muted-foreground"
|
||||
>
|
||||
Connect a matching Git account in
|
||||
your user settings to inspect
|
||||
branches for this repository.
|
||||
</p>
|
||||
|
||||
<Button as-child variant="outline" size="sm">
|
||||
<Link :href="editGitConnections()">
|
||||
Open Git connections
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="repositoryBranch.error"
|
||||
class="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-100"
|
||||
>
|
||||
{{ repositoryBranch.error }}
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-3 flex flex-wrap gap-2">
|
||||
<Badge
|
||||
v-for="branch in repositoryBranch.branches"
|
||||
:key="branch.name"
|
||||
variant="outline"
|
||||
class="px-3 py-1"
|
||||
>
|
||||
{{ branch.name }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Linked tickets</CardTitle>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type TicketBranchModel = {
|
||||
name: string;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { RepositoryConnectionModel } from '@/types/models/repository-connection-model';
|
||||
import type { TicketBranchModel } from '@/types/models/ticket-branch-model';
|
||||
|
||||
export type TicketRepositoryBranchModel = {
|
||||
branches: TicketBranchModel[];
|
||||
error: string | null;
|
||||
repository_connection: RepositoryConnectionModel;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import type { ProjectModel } from '@/types/models/project-model';
|
||||
import type { RepositoryConnectionModel } from '@/types/models/repository-connection-model';
|
||||
import type { TeamModel } from '@/types/models/team-model';
|
||||
import type { TicketFieldDefinitionModel } from '@/types/models/ticket-field-definition-model';
|
||||
import type { TicketRepositoryBranchModel } from '@/types/models/ticket-repository-branch-model';
|
||||
import type { TicketLinkModel } from '@/types/models/ticket-link-model';
|
||||
import type { TicketLinkTypeModel } from '@/types/models/ticket-link-type-model';
|
||||
import type { TicketReferenceModel } from '@/types/models/ticket-reference-model';
|
||||
@@ -101,6 +102,7 @@ export type ProjectTicketPageProps = {
|
||||
linkedTickets: TicketLinkModel[];
|
||||
parentTicketOptions: TicketReferenceModel[];
|
||||
project: ProjectModel;
|
||||
repositoryBranches: TicketRepositoryBranchModel[];
|
||||
status?: string | null;
|
||||
ticket: TicketModel;
|
||||
ticketFieldDefinitions: TicketFieldDefinitionModel[];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Tests\Feature\Git;
|
||||
|
||||
use App\Actions\Git\Data\GitAuthenticatedUser;
|
||||
use App\Actions\Git\Data\GitBranch;
|
||||
use App\Actions\Git\Data\GitRepository;
|
||||
use App\Actions\Git\Gitea\GiteaProviderClient;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -80,4 +81,40 @@ class GiteaProviderTest extends TestCase
|
||||
&& $request->hasHeader('Authorization', 'token repo-token');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the provider lists branches for a specific repository.
|
||||
*/
|
||||
public function test_provider_can_list_repository_branches(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'https://git.example.test/api/v1/repos/core-team/kanbangit/branches*' => Http::response([
|
||||
[
|
||||
'name' => 'CORE-1-login-form',
|
||||
],
|
||||
[
|
||||
'name' => 'main',
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$branches = app(GiteaProviderClient::class)->listBranches(
|
||||
'https://git.example.test',
|
||||
'branch-token',
|
||||
'core-team',
|
||||
'kanbangit',
|
||||
);
|
||||
|
||||
$this->assertCount(2, $branches);
|
||||
$this->assertContainsOnlyInstancesOf(GitBranch::class, $branches);
|
||||
$this->assertSame('CORE-1-login-form', $branches[0]->name);
|
||||
|
||||
Http::assertSent(function ($request): bool {
|
||||
return str_starts_with(
|
||||
$request->url(),
|
||||
'https://git.example.test/api/v1/repos/core-team/kanbangit/branches',
|
||||
) && $request->hasHeader('Authorization', 'token branch-token');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Tests\Feature\Projects;
|
||||
use App\Actions\ProjectPermissions\ProjectPermissionService;
|
||||
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
|
||||
use App\Models\Project;
|
||||
use App\Models\RepositoryConnection;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketFieldDefinition;
|
||||
use App\Models\TicketFieldValue;
|
||||
@@ -13,7 +14,9 @@ use App\Models\TicketLinkType;
|
||||
use App\Models\TicketPriority;
|
||||
use App\Models\TicketWorklogEntry;
|
||||
use App\Models\User;
|
||||
use App\Models\UserGitConnection;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -97,10 +100,85 @@ class ProjectTicketControllerTest extends TestCase
|
||||
->where('ticket.worklog_entries.0.actor.name', $viewer->name)
|
||||
->where('linkedTickets.0.linked_ticket.ticket_key', 'CORE-2')
|
||||
->where('linkedTickets.0.ticket_link_type.label', 'Relates to')
|
||||
->where('repositoryBranches', [])
|
||||
->where('canEditTicket', false),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure ticket details expose matching branches and inaccessible repository links.
|
||||
*/
|
||||
public function test_ticket_details_include_matching_branches_and_inaccessible_repository_links(): void
|
||||
{
|
||||
Http::preventStrayRequests();
|
||||
Http::fake([
|
||||
'https://git.example.test/api/v1/repos/core-team/kanbangit/branches*' => Http::response([
|
||||
['name' => 'CORE-1-login-form'],
|
||||
['name' => 'CORE-1/fix-oauth'],
|
||||
['name' => 'CORE-10-should-not-match'],
|
||||
['name' => 'main'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$viewer = User::factory()->create();
|
||||
$project = Project::factory()->create([
|
||||
'key' => 'CORE',
|
||||
]);
|
||||
|
||||
app(InitializeProjectTicketMetadata::class)->handle($project);
|
||||
|
||||
$priority = TicketPriority::factory()->for($project)->create([
|
||||
'name' => 'Medium',
|
||||
'position' => 1,
|
||||
]);
|
||||
|
||||
$ticket = Ticket::factory()->create([
|
||||
'project_id' => $project->id,
|
||||
'sequence_number' => 1,
|
||||
'ticket_key' => 'CORE-1',
|
||||
'ticket_priority_id' => $priority->id,
|
||||
'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id,
|
||||
'title' => 'Branch detail test',
|
||||
]);
|
||||
|
||||
RepositoryConnection::factory()->for($project)->create([
|
||||
'base_url' => 'https://git.example.test',
|
||||
'owner_name' => 'core-team',
|
||||
'provider_repository_id' => '501',
|
||||
'repository_full_name' => 'core-team/kanbangit',
|
||||
'repository_name' => 'kanbangit',
|
||||
'repository_url' => 'https://git.example.test/core-team/kanbangit',
|
||||
]);
|
||||
RepositoryConnection::factory()->for($project)->create([
|
||||
'base_url' => 'https://git.other.test',
|
||||
'owner_name' => 'other-team',
|
||||
'provider_repository_id' => '902',
|
||||
'repository_full_name' => 'other-team/mobile',
|
||||
'repository_name' => 'mobile',
|
||||
'repository_url' => 'https://git.other.test/other-team/mobile',
|
||||
]);
|
||||
|
||||
UserGitConnection::factory()->for($viewer)->create([
|
||||
'base_url' => 'https://git.example.test',
|
||||
]);
|
||||
|
||||
app(ProjectPermissionService::class)->syncUserPermissions($project, $viewer, ['view']);
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->get(route('projects.tickets.show', [$project, $ticket]))
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('projects/Ticket')
|
||||
->where('repositoryBranches.0.repository_connection.repository_full_name', 'core-team/kanbangit')
|
||||
->has('repositoryBranches.0.branches', 2)
|
||||
->where('repositoryBranches.0.branches.0.name', 'CORE-1-login-form')
|
||||
->where('repositoryBranches.0.branches.1.name', 'CORE-1/fix-oauth')
|
||||
->where('repositoryBranches.1.repository_connection.repository_full_name', 'other-team/mobile')
|
||||
->where('repositoryBranches.1.repository_connection.has_personal_access', false)
|
||||
->where('repositoryBranches.1.branches', []),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure updating ticket details requires the project edit permission.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user