624 lines
26 KiB
PHP
624 lines
26 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Symfony\Component\Process\Process;
|
|
|
|
function branchTopologyGit(string $repository, string ...$arguments): string
|
|
{
|
|
$process = new Process(['git', ...$arguments], $repository);
|
|
$process->setTimeout(15);
|
|
$process->run();
|
|
|
|
expect($process->isSuccessful())
|
|
->toBeTrue(trim($process->getErrorOutput()) ?: trim($process->getOutput()));
|
|
|
|
return trim($process->getOutput());
|
|
}
|
|
|
|
function branchTopologyCommit(string $repository, string $path, string $contents, string $message): string
|
|
{
|
|
$absolutePath = $repository.DIRECTORY_SEPARATOR.$path;
|
|
$directory = dirname($absolutePath);
|
|
|
|
if (! is_dir($directory)) {
|
|
mkdir($directory, 0777, true);
|
|
}
|
|
|
|
file_put_contents($absolutePath, $contents);
|
|
branchTopologyGit($repository, 'add', $path);
|
|
branchTopologyGit($repository, 'commit', '-m', $message);
|
|
|
|
return branchTopologyGit($repository, 'rev-parse', 'HEAD');
|
|
}
|
|
|
|
function branchTopologyRepository(): string
|
|
{
|
|
$repository = sys_get_temp_dir().DIRECTORY_SEPARATOR.'tenantpilot-branch-topology-'.bin2hex(random_bytes(8));
|
|
mkdir($repository, 0777, true);
|
|
|
|
branchTopologyGit($repository, 'init', '-b', 'main');
|
|
branchTopologyGit($repository, 'config', 'user.email', 'branch-topology@example.test');
|
|
branchTopologyGit($repository, 'config', 'user.name', 'Branch Topology Fixture');
|
|
branchTopologyCommit($repository, 'baseline.txt', "baseline\n", 'baseline');
|
|
|
|
return $repository;
|
|
}
|
|
|
|
function branchTopologyRemoveDirectory(string $directory): void
|
|
{
|
|
if (! is_dir($directory)) {
|
|
return;
|
|
}
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::CHILD_FIRST,
|
|
);
|
|
|
|
foreach ($iterator as $item) {
|
|
$item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
|
|
}
|
|
|
|
rmdir($directory);
|
|
}
|
|
|
|
/**
|
|
* @return array{expectedBranch: string, targetCommit: string, featureCommit: string}
|
|
*/
|
|
function branchTopologyPrepareStream(
|
|
string $repository,
|
|
string $family,
|
|
bool $remoteRef,
|
|
bool $localRef = true,
|
|
bool $diverged = false,
|
|
): array {
|
|
$expectedBranch = $family === 'platform' ? 'platform-dev' : 'website-dev';
|
|
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
|
|
|
branchTopologyGit($repository, 'branch', $expectedBranch, $baseline);
|
|
|
|
if ($diverged) {
|
|
branchTopologyGit($repository, 'checkout', $expectedBranch);
|
|
$targetCommit = branchTopologyCommit(
|
|
$repository,
|
|
$family.'-integration.txt',
|
|
$family." integration\n",
|
|
$family.' integration',
|
|
);
|
|
} else {
|
|
$targetCommit = $baseline;
|
|
}
|
|
|
|
branchTopologyGit($repository, 'checkout', '-b', 'feat/'.$family.'-fixture', $baseline);
|
|
$featureCommit = branchTopologyCommit(
|
|
$repository,
|
|
$family.'-feature.txt',
|
|
$family." feature\n",
|
|
$family.' feature',
|
|
);
|
|
|
|
if ($remoteRef) {
|
|
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/'.$expectedBranch, $targetCommit);
|
|
}
|
|
|
|
if (! $localRef) {
|
|
branchTopologyGit($repository, 'branch', '-D', $expectedBranch);
|
|
}
|
|
|
|
return compact('expectedBranch', 'targetCommit', 'featureCommit');
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $arguments
|
|
* @param array<string, string|false> $environment
|
|
*/
|
|
function branchTopologyResolve(
|
|
string $repository,
|
|
array $arguments,
|
|
array $environment = [],
|
|
bool $machineReadable = true,
|
|
): Process {
|
|
$isolatedEnvironment = [
|
|
'TENANTATLAS_BRANCH_FAMILY' => false,
|
|
'TENANTATLAS_WORK_CLASSIFICATION' => false,
|
|
'TENANTATLAS_PR_TARGET' => false,
|
|
'TENANTATLAS_ACTIVE_SPEC' => false,
|
|
'TENANTATLAS_BASE_REF' => false,
|
|
'TENANTATLAS_REPO_SCOPE' => false,
|
|
'GITHUB_BASE_REF' => false,
|
|
'GITEA_BASE_REF' => false,
|
|
];
|
|
|
|
$command = [repo_path('scripts/resolve-feature-base')];
|
|
|
|
if ($machineReadable) {
|
|
$command[] = '--format=kv';
|
|
}
|
|
|
|
$process = new Process(
|
|
[...$command, ...$arguments],
|
|
$repository,
|
|
array_replace($isolatedEnvironment, $environment),
|
|
);
|
|
$process->setTimeout(15);
|
|
$process->run();
|
|
|
|
return $process;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
function branchTopologyKeyValues(string $output): array
|
|
{
|
|
$values = [];
|
|
|
|
foreach (preg_split('/\R/', trim($output)) ?: [] as $line) {
|
|
if (! str_contains($line, '=')) {
|
|
continue;
|
|
}
|
|
|
|
[$key, $value] = explode('=', $line, 2);
|
|
$values[$key] = $value;
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
function branchTopologyFixture(Closure $assertions): void
|
|
{
|
|
$repository = branchTopologyRepository();
|
|
|
|
try {
|
|
$assertions($repository);
|
|
} finally {
|
|
branchTopologyRemoveDirectory($repository);
|
|
}
|
|
}
|
|
|
|
it('[BS-001] selects the platform remote ref with its merge-base result', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$stream = branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('platform')
|
|
->and($values['expected_branch'] ?? null)->toBe('platform-dev')
|
|
->and($values['selected_ref'] ?? null)->toBe('origin/platform-dev')
|
|
->and($values['target_commit'] ?? null)->toBe($stream['targetCommit'])
|
|
->and($values['merge_base'] ?? null)->toBe($stream['targetCommit'])
|
|
->and($values['base_range'] ?? null)->toBe($stream['targetCommit'].'...HEAD');
|
|
});
|
|
});
|
|
|
|
it('[BS-002] falls back only to the local platform ref with its merge-base result', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$stream = branchTopologyPrepareStream($repository, 'platform', remoteRef: false);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('platform')
|
|
->and($values['expected_branch'] ?? null)->toBe('platform-dev')
|
|
->and($values['selected_ref'] ?? null)->toBe('platform-dev')
|
|
->and($values['target_commit'] ?? null)->toBe($stream['targetCommit'])
|
|
->and($values['merge_base'] ?? null)->toBe($stream['targetCommit'])
|
|
->and($values['base_range'] ?? null)->toBe($stream['targetCommit'].'...HEAD');
|
|
});
|
|
});
|
|
|
|
it('[BS-003] selects the website remote ref with its merge-base result', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$stream = branchTopologyPrepareStream($repository, 'website', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=website']);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('website')
|
|
->and($values['expected_branch'] ?? null)->toBe('website-dev')
|
|
->and($values['selected_ref'] ?? null)->toBe('origin/website-dev')
|
|
->and($values['target_commit'] ?? null)->toBe($stream['targetCommit'])
|
|
->and($values['merge_base'] ?? null)->toBe($stream['targetCommit'])
|
|
->and($values['base_range'] ?? null)->toBe($stream['targetCommit'].'...HEAD');
|
|
});
|
|
});
|
|
|
|
it('[BS-004] falls back only to the local website ref with its merge-base result', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$stream = branchTopologyPrepareStream($repository, 'website', remoteRef: false);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=website']);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('website')
|
|
->and($values['expected_branch'] ?? null)->toBe('website-dev')
|
|
->and($values['selected_ref'] ?? null)->toBe('website-dev')
|
|
->and($values['target_commit'] ?? null)->toBe($stream['targetCommit'])
|
|
->and($values['merge_base'] ?? null)->toBe($stream['targetCommit'])
|
|
->and($values['base_range'] ?? null)->toBe($stream['targetCommit'].'...HEAD');
|
|
});
|
|
});
|
|
|
|
it('[BS-005] accepts dev only for an explicitly classified promotion', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
|
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
|
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/dev', $baseline);
|
|
branchTopologyGit($repository, 'checkout', '-b', 'feat/promotion-fixture', $baseline);
|
|
branchTopologyCommit($repository, 'promotion.txt', "promotion\n", 'promotion feature');
|
|
$process = branchTopologyResolve($repository, [
|
|
'--branch-family=repository-wide',
|
|
'--work-classification=promotion',
|
|
'--pull-request-target=dev',
|
|
]);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('repository-wide')
|
|
->and($values['work_classification'] ?? null)->toBe('promotion')
|
|
->and($values['expected_branch'] ?? null)->toBe('dev')
|
|
->and($values['selected_ref'] ?? null)->toBe('origin/dev')
|
|
->and($values['target_commit'] ?? null)->toBe($baseline)
|
|
->and($values['merge_base'] ?? null)->toBe($baseline)
|
|
->and($values['base_range'] ?? null)->toBe($baseline.'...HEAD');
|
|
});
|
|
});
|
|
|
|
it('[BS-006] fails closed when platform family refs are missing', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
|
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
|
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/dev', $baseline);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
|
$error = $process->getErrorOutput();
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($error)->toContain('status=failed', 'reason=missing-family-ref')
|
|
->and($error)->toContain(
|
|
'branch_family=platform',
|
|
'expected_branch=platform-dev',
|
|
'attempted_refs=origin/platform-dev,platform-dev',
|
|
'guidance=Declare a consistent branch family',
|
|
'diff_ran=false',
|
|
)
|
|
->and($error)->not->toContain(
|
|
'selected_ref=origin/dev',
|
|
'selected_ref=dev',
|
|
'HEAD~1',
|
|
'merge_base=',
|
|
'base_range=',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('[BS-007] fails closed when website family refs are missing', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
|
branchTopologyGit($repository, 'branch', 'platform-dev', $baseline);
|
|
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
|
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/dev', $baseline);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=website']);
|
|
$error = $process->getErrorOutput();
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($error)->toContain(
|
|
'reason=missing-family-ref',
|
|
'branch_family=website',
|
|
'expected_branch=website-dev',
|
|
'attempted_refs=origin/website-dev,website-dev',
|
|
'guidance=Declare a consistent branch family',
|
|
'diff_ran=false',
|
|
)
|
|
->and($error)->not->toContain(
|
|
'selected_ref=origin/dev',
|
|
'selected_ref=dev',
|
|
'HEAD~1',
|
|
'merge_base=',
|
|
'base_range=',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('[BS-008] returns only the platform feature path for diverged history', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true, diverged: true);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
$changed = branchTopologyGit($repository, 'diff', '--name-only', $values['base_range']);
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($changed)->toBe('platform-feature.txt')
|
|
->and($values['merge_base'] ?? null)->not->toBe($values['target_commit'] ?? null);
|
|
});
|
|
});
|
|
|
|
it('[BS-009] returns only the website feature path for diverged history', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'website', remoteRef: true, diverged: true);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=website']);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
$changed = branchTopologyGit($repository, 'diff', '--name-only', $values['base_range']);
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($changed)->toBe('website-feature.txt')
|
|
->and($values['merge_base'] ?? null)->not->toBe($values['target_commit'] ?? null);
|
|
});
|
|
});
|
|
|
|
it('[BS-010] rejects an explicit platform family plus website PR target', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, [
|
|
'--branch-family=platform',
|
|
'--pull-request-target=website-dev',
|
|
]);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=branch-family-conflict',
|
|
'explicit=platform',
|
|
'pull_request_target=website',
|
|
'guidance=Declare a consistent branch family',
|
|
'diff_ran=false',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('[BS-011] rejects conflicting branch-name and repository-scope signals', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$specPath = $repository.'/spec.md';
|
|
file_put_contents($specPath, "# Ambiguous fixture\n\nNo branch metadata.\n");
|
|
$process = branchTopologyResolve($repository, [
|
|
'--branch-name=feat/platform-fixture',
|
|
'--repo-scope=website',
|
|
'--spec='.$specPath,
|
|
]);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=ambiguous-branch-family',
|
|
'branch_name=feat/platform-fixture',
|
|
'repo_scope=website',
|
|
'spec_path='.$specPath,
|
|
'guidance=Declare a consistent branch family',
|
|
'diff_ran=false',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('[BS-012] rejects an explicit ref from the wrong family before merge-base work', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
|
branchTopologyGit($repository, 'branch', 'platform-dev', $baseline);
|
|
branchTopologyGit($repository, 'branch', 'website-dev', $baseline);
|
|
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/website-dev', $baseline);
|
|
$process = branchTopologyResolve($repository, [
|
|
'--branch-family=platform',
|
|
'--ref=origin/website-dev',
|
|
]);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=wrong-family-ref',
|
|
'expected_branch=platform-dev',
|
|
'explicit_ref=origin/website-dev',
|
|
'guidance=Declare a consistent branch family',
|
|
'diff_ran=false',
|
|
)
|
|
->and($process->getErrorOutput())->not->toContain(
|
|
'target_commit=',
|
|
'merge_base=',
|
|
'base_range=',
|
|
);
|
|
});
|
|
});
|
|
|
|
it('proves Stage 1 priority, diagnostics, ancestry failure, and executable portability', function (): void {
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'website', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, ['--pull-request-target=website-dev']);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('website')
|
|
->and($values['family_source'] ?? null)->toBe('pull-request-target');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$specPath = $repository.'/spec.md';
|
|
file_put_contents($specPath, "**Feature Base**: platform-dev\n**Normal Merge Target**: platform-dev\n");
|
|
$process = branchTopologyResolve($repository, ['--spec='.$specPath]);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('platform')
|
|
->and($values['family_source'] ?? null)->toBe('spec-metadata');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, [
|
|
'--branch-name=feat/platform-policy-fixture',
|
|
'--repo-scope=platform',
|
|
]);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('platform')
|
|
->and($values['family_source'] ?? null)->toBe('branch-and-scope');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values)->toHaveKeys([
|
|
'status',
|
|
'branch_family',
|
|
'work_classification',
|
|
'family_source',
|
|
'expected_branch',
|
|
'selected_ref',
|
|
'target_commit',
|
|
'merge_base',
|
|
'base_range',
|
|
])
|
|
->and($values['status'])->toBe('resolved')
|
|
->and($values['target_commit'])->toMatch('/^[0-9a-f]{40}$/')
|
|
->and($values['merge_base'])->toMatch('/^[0-9a-f]{40}$/')
|
|
->and($values['base_range'])->toBe($values['merge_base'].'...HEAD');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
|
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
|
$process = branchTopologyResolve($repository, ['--pull-request-target=dev']);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=explicit-promotion-classification-required',
|
|
'diff_ran=false',
|
|
);
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$process = branchTopologyResolve($repository, ['--branch-family=unknown']);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=invalid-branch-family',
|
|
'explicit=unknown',
|
|
'diff_ran=false',
|
|
);
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$specPath = $repository.'/spec.md';
|
|
file_put_contents($specPath, "**Feature Base**: platform-dev\n**Normal Merge Target**: platform-dev\n");
|
|
$process = branchTopologyResolve($repository, [
|
|
'--pull-request-target=website-dev',
|
|
'--spec='.$specPath,
|
|
]);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=branch-family-conflict',
|
|
'selected=website',
|
|
'spec_metadata=platform',
|
|
'diff_ran=false',
|
|
);
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, ['--branch-name=feat/platform-name-only']);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=ambiguous-branch-family',
|
|
'diff_ran=false',
|
|
);
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve(
|
|
$repository,
|
|
[],
|
|
['TENANTATLAS_BRANCH_FAMILY' => 'platform'],
|
|
);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('platform')
|
|
->and($values['family_source'] ?? null)->toBe('explicit');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'website', remoteRef: true);
|
|
$process = branchTopologyResolve(
|
|
$repository,
|
|
[],
|
|
['GITHUB_BASE_REF' => 'website-dev'],
|
|
);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('website')
|
|
->and($values['family_source'] ?? null)->toBe('pull-request-target');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, [
|
|
'--branch-family=platform',
|
|
'--ref=platform-dev',
|
|
]);
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(branchTopologyKeyValues($process->getOutput())['selected_ref'] ?? null)->toBe('platform-dev');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve(
|
|
$repository,
|
|
['--pull-request-target=platform-dev'],
|
|
['GITHUB_BASE_REF' => 'refs/heads/platform-dev'],
|
|
);
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and(branchTopologyKeyValues($process->getOutput())['branch_family'] ?? null)->toBe('platform');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve($repository, [
|
|
'--branch-family=platform',
|
|
'--branch-name=feat/website-stale-hint',
|
|
'--repo-scope=website',
|
|
]);
|
|
$values = branchTopologyKeyValues($process->getOutput());
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($values['branch_family'] ?? null)->toBe('platform')
|
|
->and($values['family_source'] ?? null)->toBe('explicit');
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
$baseline = branchTopologyGit($repository, 'rev-parse', 'main');
|
|
branchTopologyGit($repository, 'branch', 'dev', $baseline);
|
|
branchTopologyGit($repository, 'update-ref', 'refs/remotes/origin/dev', $baseline);
|
|
$process = branchTopologyResolve($repository, [
|
|
'--branch-family=repository-wide',
|
|
'--work-classification=promotion',
|
|
]);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=explicit-target-required',
|
|
'work_classification=promotion',
|
|
'diff_ran=false',
|
|
);
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyPrepareStream($repository, 'platform', remoteRef: true);
|
|
$process = branchTopologyResolve(
|
|
$repository,
|
|
['--branch-family=platform'],
|
|
[],
|
|
machineReadable: false,
|
|
);
|
|
expect($process->getExitCode())->toBe(0)
|
|
->and($process->getOutput())->toContain(
|
|
'Feature base resolved.',
|
|
'Branch family: platform',
|
|
'Expected integration branch: platform-dev',
|
|
'Selected ref: origin/platform-dev',
|
|
'Target commit:',
|
|
'Merge base:',
|
|
'Base range:',
|
|
);
|
|
});
|
|
|
|
branchTopologyFixture(function (string $repository): void {
|
|
branchTopologyGit($repository, 'checkout', '--orphan', 'platform-dev');
|
|
branchTopologyGit($repository, 'rm', '-rf', '.');
|
|
branchTopologyCommit($repository, 'unrelated.txt', "unrelated\n", 'unrelated root');
|
|
branchTopologyGit($repository, 'checkout', 'main');
|
|
$process = branchTopologyResolve($repository, ['--branch-family=platform']);
|
|
expect($process->getExitCode())->toBe(2)
|
|
->and($process->getErrorOutput())->toContain(
|
|
'reason=no-common-merge-base',
|
|
'selected_ref=platform-dev',
|
|
'diff_ran=false',
|
|
);
|
|
});
|
|
|
|
$resolver = repo_path('scripts/resolve-feature-base');
|
|
$syntax = new Process(['bash', '-n', $resolver], repo_root());
|
|
$syntax->run();
|
|
expect(is_executable($resolver))->toBeTrue()
|
|
->and($syntax->getExitCode())->toBe(0, $syntax->getErrorOutput());
|
|
});
|