mirror of
https://github.com/pestphp/pest.git
synced 2026-09-05 14:23:34 +02:00
wip
This commit is contained in:
+60
-11
@@ -12,12 +12,14 @@ use Pest\Contracts\Plugins\Terminable;
|
||||
use Pest\Exceptions\InvalidOption;
|
||||
use Pest\Exceptions\MissingDependency;
|
||||
use Pest\Exceptions\NoAffectedTestsFound;
|
||||
use Pest\Exceptions\TiaRequiresDefaultBranch;
|
||||
use Pest\Exceptions\TiaRequiresRemote;
|
||||
use Pest\Exceptions\TiaRequiresRepositoryRoot;
|
||||
use Pest\Panic;
|
||||
use Pest\Plugins\Concerns\HandleArguments;
|
||||
use Pest\Plugins\Tia\BaselineSync;
|
||||
use Pest\Plugins\Tia\ChangedFiles;
|
||||
use Pest\Plugins\Tia\CiDefaultBranch;
|
||||
use Pest\Plugins\Tia\Contracts\State;
|
||||
use Pest\Plugins\Tia\CoverageCollector;
|
||||
use Pest\Plugins\Tia\Fingerprint;
|
||||
@@ -244,6 +246,16 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
*/
|
||||
private string $fallbackBranch = self::DEFAULT_BRANCH;
|
||||
|
||||
/**
|
||||
* Whether anything actually named the branch above.
|
||||
*
|
||||
* When nothing did, the value is a guess, and a guess is what the TIA path
|
||||
* refuses to run on: an unresolved fallback reads no baseline at all, which
|
||||
* looks exactly like a hit in the output. Runs that never asked for TIA
|
||||
* still have to write somewhere, so the guess stands for them.
|
||||
*/
|
||||
private bool $fallbackBranchResolved = false;
|
||||
|
||||
private bool $branchResolved = false;
|
||||
|
||||
/** @var array<string, true> */
|
||||
@@ -889,11 +901,15 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
$this->resolveBranch($projectRoot);
|
||||
|
||||
// After resolveBranch(), so a directory that is no repository at all
|
||||
// still reports the missing git dependency rather than a missing remote.
|
||||
// Skipped once the default branch is configured by hand: there is then
|
||||
// nothing left for a remote to answer.
|
||||
if ($this->watchPatterns->defaultBranch() === null && ! new ChangedFiles($projectRoot)->hasRemote()) {
|
||||
Panic::with(new TiaRequiresRemote);
|
||||
// still reports the missing git dependency rather than an unresolved
|
||||
// default branch. Nothing named the branch every other baseline reads
|
||||
// through, so every new branch would re-run the whole suite while the
|
||||
// output called it a hit. A repository with no remote is the likeliest
|
||||
// reason and gets said out loud.
|
||||
if (! $this->fallbackBranchResolved) {
|
||||
Panic::with(new ChangedFiles($projectRoot)->hasRemote()
|
||||
? new TiaRequiresDefaultBranch
|
||||
: new TiaRequiresRemote);
|
||||
}
|
||||
|
||||
$fingerprint = Fingerprint::compute($projectRoot);
|
||||
@@ -2001,7 +2017,10 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
// Resolved before the current branch, which throws where git is
|
||||
// missing: the fallback is advisory, so a run that cannot name its
|
||||
// branch at all should still carry the best answer available.
|
||||
$this->fallbackBranch = $this->resolveFallbackBranch($changedFiles);
|
||||
$resolved = $this->resolveFallbackBranch($changedFiles);
|
||||
|
||||
$this->fallbackBranchResolved = $resolved !== null;
|
||||
$this->fallbackBranch = $resolved ?? self::DEFAULT_BRANCH;
|
||||
|
||||
Parallel::setGlobal(self::FALLBACK_BRANCH_GLOBAL, $this->fallbackBranch);
|
||||
|
||||
@@ -2012,7 +2031,18 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
$this->branch = $changedFiles->currentBranch() ?? $this->fallbackBranch;
|
||||
}
|
||||
|
||||
private function resolveFallbackBranch(ChangedFiles $changedFiles): string
|
||||
/**
|
||||
* The branch every other baseline falls back to reading, or null when
|
||||
* nothing in the checkout can name it.
|
||||
*
|
||||
* Ordered by how much the source actually knows. Configuration first: it is
|
||||
* the escape hatch for a repository whose git-side answers disagree with its
|
||||
* branches. Then the CI provider, which states the answer outright where git
|
||||
* is at its least informed. Then git itself. Then the recorded graph, whose
|
||||
* single baseline can only have come from the branch this repository
|
||||
* integrates on.
|
||||
*/
|
||||
private function resolveFallbackBranch(ChangedFiles $changedFiles): ?string
|
||||
{
|
||||
$inherited = Parallel::getGlobal(self::FALLBACK_BRANCH_GLOBAL);
|
||||
|
||||
@@ -2020,12 +2050,31 @@ final class Tia implements AddsOutput, HandlesArguments, HandlesOriginalArgument
|
||||
return $inherited;
|
||||
}
|
||||
|
||||
// Configuration wins over autodetection: it is the escape hatch for a
|
||||
// repository whose `origin/HEAD` is unset and whose `init.defaultBranch`
|
||||
// says something else than its branches do.
|
||||
return $this->watchPatterns->defaultBranch()
|
||||
?? CiDefaultBranch::detect()
|
||||
?? $changedFiles->defaultBranch()
|
||||
?? self::DEFAULT_BRANCH;
|
||||
?? $this->soleRecordedBranch();
|
||||
}
|
||||
|
||||
/**
|
||||
* The one branch a recorded graph holds a baseline for.
|
||||
*
|
||||
* Last in the chain and deliberately narrow: with a single baseline on disk
|
||||
* there is only one branch whose results can be read at all, so naming it is
|
||||
* strictly better than resolving to a branch that holds nothing. Two or more
|
||||
* baselines carry no such implication and are left alone.
|
||||
*/
|
||||
private function soleRecordedBranch(): ?string
|
||||
{
|
||||
$json = $this->state->read(self::KEY_GRAPH);
|
||||
|
||||
if ($json === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$branches = Graph::branchesIn($json);
|
||||
|
||||
return count($branches) === 1 ? $branches[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -241,7 +241,20 @@ final readonly class ChangedFiles
|
||||
}
|
||||
}
|
||||
|
||||
return $this->gitOutput(['git', 'config', '--get', 'init.defaultBranch']);
|
||||
// `init.defaultBranch` is a setting of the machine, not of the
|
||||
// repository — it names what `git init` would have called the first
|
||||
// branch here, which is worth nothing once the repository disagrees.
|
||||
// Taken only when a branch by that name actually exists.
|
||||
$configured = $this->gitOutput(['git', 'config', '--get', 'init.defaultBranch']);
|
||||
|
||||
if ($configured === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$exists = $this->gitOutput(['git', 'rev-parse', '--verify', '--quiet', 'refs/heads/'.$configured]) !== null
|
||||
|| $this->gitOutput(['git', 'rev-parse', '--verify', '--quiet', 'refs/remotes/origin/'.$configured]) !== null;
|
||||
|
||||
return $exists ? $configured : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
/**
|
||||
* The default branch as the CI provider itself reports it.
|
||||
*
|
||||
* Worth asking before git: a CI checkout is the one place where git knows the
|
||||
* least. `actions/checkout` builds the working copy with `git init` plus a
|
||||
* single-ref `fetch` rather than a `clone`, so `origin/HEAD` is never set and
|
||||
* `init.defaultBranch` — a setting of the runner image, not of the repository —
|
||||
* is all git has left to offer. The provider, meanwhile, states the answer
|
||||
* outright in the environment it handed us.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CiDefaultBranch
|
||||
{
|
||||
/**
|
||||
* Advisory, like every other source in the chain: anything unreadable,
|
||||
* unparsable, or simply absent means "no answer", never a failure.
|
||||
*/
|
||||
public static function detect(): ?string
|
||||
{
|
||||
return self::fromGitLab() ?? self::fromGitHubEvent();
|
||||
}
|
||||
|
||||
private static function fromGitLab(): ?string
|
||||
{
|
||||
return self::environment('CI_DEFAULT_BRANCH');
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub publishes no default-branch variable, but every repository-scoped
|
||||
* event payload carries `repository.default_branch`, and the path to that
|
||||
* payload is in the environment.
|
||||
*/
|
||||
private static function fromGitHubEvent(): ?string
|
||||
{
|
||||
$path = self::environment('GITHUB_EVENT_PATH');
|
||||
|
||||
if ($path === null || ! is_file($path) || ! is_readable($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$contents = @file_get_contents($path);
|
||||
|
||||
if ($contents === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = json_decode($contents, true);
|
||||
|
||||
if (! is_array($payload) || ! is_array($payload['repository'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$branch = $payload['repository']['default_branch'] ?? null;
|
||||
|
||||
return is_string($branch) && $branch !== '' ? $branch : null;
|
||||
}
|
||||
|
||||
private static function environment(string $name): ?string
|
||||
{
|
||||
$value = getenv($name);
|
||||
|
||||
if (! is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
}
|
||||
@@ -1482,6 +1482,35 @@ final class Graph
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The branches a recorded graph holds baselines for, read straight from the
|
||||
* encoded form.
|
||||
*
|
||||
* Answerable before the graph is hydrated because the default branch has to
|
||||
* be resolved first: the fallback is what every hydrated graph reads its
|
||||
* baselines through.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function branchesIn(string $json): array
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
|
||||
if (! is_array($data) || ! is_array($data['baselines'] ?? null)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$branches = [];
|
||||
|
||||
foreach (array_keys($data['baselines']) as $branch) {
|
||||
if (is_string($branch) && $branch !== '') {
|
||||
$branches[] = $branch;
|
||||
}
|
||||
}
|
||||
|
||||
return $branches;
|
||||
}
|
||||
|
||||
public static function decode(string $json, string $projectRoot): ?self
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
|
||||
Reference in New Issue
Block a user