first commit.
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Toolbox;
|
||||
|
||||
/**
|
||||
* Provee los archivos del core. La versión se resuelve por tags semver
|
||||
* (`vX.Y.Z`); no hay archivo VERSION. Si no hay tags, la versión es desconocida
|
||||
* y se trabaja sobre la rama por defecto, fijando el commit en el lock.
|
||||
*/
|
||||
final class CoreProvider
|
||||
{
|
||||
private function __construct(private readonly string $dir)
|
||||
{
|
||||
}
|
||||
|
||||
public static function fromDirectory(string $dir): self
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
throw new ToolboxException("El directorio del core no existe: {$dir}");
|
||||
}
|
||||
|
||||
return new self(rtrim($dir, '/'));
|
||||
}
|
||||
|
||||
public static function fromUrl(string $url, string $cacheDir, Console $console): self
|
||||
{
|
||||
$dir = rtrim($cacheDir, '/') . '/core';
|
||||
|
||||
if (!is_dir($dir . '/.git')) {
|
||||
if (!is_dir(dirname($dir))) {
|
||||
mkdir(dirname($dir), 0777, true);
|
||||
}
|
||||
$console->out(' clonando core desde ' . $url);
|
||||
exec('git clone -q --tags ' . escapeshellarg($url) . ' ' . escapeshellarg($dir) . ' 2>&1', $out, $code);
|
||||
if ($code !== 0) {
|
||||
throw new ToolboxException('No se pudo clonar el core: ' . implode("\n", $out));
|
||||
}
|
||||
} else {
|
||||
exec('git -C ' . escapeshellarg($dir) . ' fetch -q --tags 2>&1');
|
||||
}
|
||||
|
||||
$provider = new self($dir);
|
||||
$version = $provider->version();
|
||||
|
||||
// Tags opcionales: si hay, se usa la release más alta; si no, la rama por defecto.
|
||||
if ($version !== null) {
|
||||
exec('git -C ' . escapeshellarg($dir) . ' checkout -q ' . escapeshellarg('v' . $version) . ' 2>&1');
|
||||
} else {
|
||||
exec('git -C ' . escapeshellarg($dir) . ' checkout -q 2>&1');
|
||||
exec('git -C ' . escapeshellarg($dir) . ' pull -q --ff-only 2>/dev/null');
|
||||
}
|
||||
|
||||
return $provider;
|
||||
}
|
||||
|
||||
public function dir(): string
|
||||
{
|
||||
return $this->dir;
|
||||
}
|
||||
|
||||
public function isGit(): bool
|
||||
{
|
||||
return is_dir($this->dir . '/.git');
|
||||
}
|
||||
|
||||
/**
|
||||
* Versión más alta entre los tags `vX.Y.Z`, o null si no hay tags.
|
||||
*/
|
||||
public function version(): ?string
|
||||
{
|
||||
if (!$this->isGit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
exec('git -C ' . escapeshellarg($this->dir) . ' tag -l ' . escapeshellarg('v*') . ' 2>/dev/null', $tags, $code);
|
||||
if ($code !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$best = null;
|
||||
foreach ($tags as $tag) {
|
||||
$candidate = ltrim(trim($tag), 'vV');
|
||||
try {
|
||||
Semver::parse($candidate);
|
||||
} catch (ToolboxException) {
|
||||
continue;
|
||||
}
|
||||
if ($best === null || Semver::compare($candidate, $best) > 0) {
|
||||
$best = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $best;
|
||||
}
|
||||
|
||||
public function commit(): ?string
|
||||
{
|
||||
if (!$this->isGit()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
exec('git -C ' . escapeshellarg($this->dir) . ' rev-parse HEAD 2>/dev/null', $out, $code);
|
||||
$commit = trim($out[0] ?? '');
|
||||
|
||||
return $code === 0 && $commit !== '' ? $commit : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefijos/exactos que no forman parte del core distribuible: herramientas
|
||||
* del repositorio, control de versiones y documentación. `.htaccess` NO se
|
||||
* excluye (sí es del core).
|
||||
*/
|
||||
private const EXCLUDED = [
|
||||
'.git/', '.github/', '.opencode/', '.openspec/', '.idea/', '.vscode/',
|
||||
'node_modules/', 'vendor/', 'dist/',
|
||||
'.gitignore', '.gitattributes', '.editorconfig', '.DS_Store', 'Thumbs.db',
|
||||
'readme.org', 'README.org', 'readme.md', 'README.md', 'readme.txt',
|
||||
];
|
||||
|
||||
/**
|
||||
* Archivos del core distribuible. Si la fuente es un repositorio git se
|
||||
* parte de sus archivos RASTREADOS (`git ls-files`, los mismos del remoto);
|
||||
* si no, se recorre el directorio. En ambos casos se filtra la lista negra
|
||||
* (herramientas y documentación).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function files(): array
|
||||
{
|
||||
$files = $this->isGit() ? $this->trackedFiles() : $this->walkFiles();
|
||||
$files = array_values(array_filter($files, fn (string $file) => !$this->isExcluded($file)));
|
||||
sort($files);
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function trackedFiles(): array
|
||||
{
|
||||
exec('git -C ' . escapeshellarg($this->dir) . ' ls-files 2>/dev/null', $out, $code);
|
||||
if ($code !== 0) {
|
||||
return $this->walkFiles();
|
||||
}
|
||||
|
||||
return array_values(array_filter($out, fn (string $file) => $file !== ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function walkFiles(): array
|
||||
{
|
||||
$files = [];
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($this->dir, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $item) {
|
||||
if ($item->isDir()) {
|
||||
continue;
|
||||
}
|
||||
$files[] = substr($item->getPathname(), strlen($this->dir) + 1);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function isExcluded(string $relative): bool
|
||||
{
|
||||
foreach (self::EXCLUDED as $excluded) {
|
||||
if ($relative === $excluded || str_starts_with($relative, $excluded)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function read(string $relative): string
|
||||
{
|
||||
$path = $this->dir . '/' . ltrim($relative, '/');
|
||||
|
||||
return (string) file_get_contents($path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user