Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/Domains/Package/Actions/BuildPackageDownloadStatsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class BuildPackageDownloadStatsAction
public function handle(Package $package): PackageDownloadStatsData
{
$startDate = Carbon::now()->subDays(29)->startOfDay();
$previousPeriodStart = Carbon::now()->subDays(59)->startOfDay();
$previousPeriodEnd = Carbon::now()->subDays(30)->endOfDay();

$dailyCounts = $package->downloads()
->where('downloaded_at', '>=', $startDate)
Expand All @@ -23,18 +25,27 @@ public function handle(Package $package): PackageDownloadStatsData
->pluck('downloads', 'date');

$dailyDownloads = [];
$currentPeriodDownloads = 0;
for ($i = 29; $i >= 0; $i--) {
$date = Carbon::now()->subDays($i)->format('Y-m-d');
$count = (int) ($dailyCounts[$date] ?? 0);
$currentPeriodDownloads += $count;
$dailyDownloads[] = new DailyDownloadData(
date: $date,
downloads: (int) ($dailyCounts[$date] ?? 0),
downloads: $count,
);
}

$previousPeriodDownloads = (int) $package->downloads()
->whereBetween('downloaded_at', [$previousPeriodStart, $previousPeriodEnd])
->count();

$versionDailyDownloads = $this->buildVersionDailyDownloads($package, $startDate);

return new PackageDownloadStatsData(
totalDownloads: $package->downloads()->count(),
currentPeriodDownloads: $currentPeriodDownloads,
previousPeriodDownloads: $previousPeriodDownloads,
dailyDownloads: $dailyDownloads,
versionDailyDownloads: $versionDailyDownloads,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class PackageDownloadStatsData extends Data
*/
public function __construct(
public int $totalDownloads,
public int $currentPeriodDownloads,
public int $previousPeriodDownloads,
#[TypeScriptType('array<'.DailyDownloadData::class.'>')]
public array $dailyDownloads,
#[TypeScriptType('array<'.VersionDailyDownloadData::class.'>')]
Expand Down
38 changes: 37 additions & 1 deletion app/Domains/Package/Contracts/Data/PackageVersionDetailData.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
use App\Domains\Repository\Contracts\Enums\GitProvider;
use App\Domains\Security\Contracts\Data\SecurityAdvisoryMatchData;
use App\Models\PackageVersion;
use App\Services\Markdown\MarkdownRenderer;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\App;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;

Expand Down Expand Up @@ -39,14 +41,16 @@ public function __construct(
public ?array $keywords,
public bool $isStable,
public bool $isDev,
public ?string $readmeHtml = null,
/** @var SecurityAdvisoryMatchData[]|null */
public ?array $advisoryMatches = null,
) {}

public static function fromModel(
PackageVersion $version,
?GitProvider $provider = null,
?string $repoIdentifier = null
?string $repoIdentifier = null,
?string $customBaseUrl = null,
): self {
$base = PackageVersionData::fromModel($version, $provider, $repoIdentifier);
$composerJson = $version->composer_json ?? [];
Expand All @@ -65,6 +69,8 @@ public static function fromModel(
->all();
}

$readmeHtml = static::renderReadme($version, $provider, $repoIdentifier, $customBaseUrl);

return new self(
uuid: $base->uuid,
version: $base->version,
Expand All @@ -85,10 +91,40 @@ public static function fromModel(
keywords: ! empty($composerJson['keywords']) ? $composerJson['keywords'] : null,
isStable: $base->isStable(),
isDev: $base->isDev(),
readmeHtml: $readmeHtml,
advisoryMatches: $advisoryMatchesData,
);
}

protected static function renderReadme(
PackageVersion $version,
?GitProvider $provider,
?string $repoIdentifier,
?string $customBaseUrl,
): ?string {
if (empty($version->readme)) {
return null;
}

$blobBaseUrl = null;
$rawFileBaseUrl = null;

// Prefer the immutable commit SHA over the moving tag/branch name so README
// links keep pointing at the same content even if the tag is later moved.
$ref = $version->source_reference ?: $version->source_tag;

if ($provider !== null && $repoIdentifier !== null && $ref !== null && $ref !== '') {
$blobBaseUrl = $provider->blobBaseUrl($repoIdentifier, $ref, $customBaseUrl);
$rawFileBaseUrl = $provider->rawFileBaseUrl($repoIdentifier, $ref, $customBaseUrl);
}

return App::make(MarkdownRenderer::class)->render(
$version->readme,
$blobBaseUrl,
$rawFileBaseUrl,
);
}

/**
* Merge psr-4 and psr-0 autoload entries into a flat namespace → path map.
*
Expand Down
14 changes: 14 additions & 0 deletions app/Domains/Package/Http/Controllers/PackageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,23 @@ public function show(Request $request, Organization $organization, Package $pack
$versionModel,
$package->repository?->provider,
$package->repository?->repo_identifier,
$package->repository?->custom_base_url,
);
}
}

$primaryVersionModel = $package->versions()->stable()->orderBySemanticVersion('desc')->first()
?? $package->versions()->orderBySemanticVersion('desc')->first();

$primaryVersion = $primaryVersionModel
? PackageVersionDetailData::fromModel(
$primaryVersionModel,
$package->repository?->provider,
$package->repository?->repo_identifier,
$package->repository?->custom_base_url,
)
: null;

return Inertia::render('organizations/packages/show', [
'organization' => OrganizationData::fromModel($organization),
'package' => PackageData::fromModel($package),
Expand All @@ -134,6 +147,7 @@ public function show(Request $request, Organization $organization, Package $pack
'canManageVersions' => request()->user()?->can('deleteRepository', $organization) ?? false,
'canDeletePackage' => request()->user()?->can('deleteRepository', $organization) ?? false,
'activeVersion' => $activeVersion,
'primaryVersion' => $primaryVersion,
]);
}
}
47 changes: 47 additions & 0 deletions app/Domains/Repository/Actions/FetchReadmeAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace App\Domains\Repository\Actions;

use App\Domains\Repository\Contracts\Interfaces\GitProviderInterface;
use Illuminate\Support\Facades\Log;

class FetchReadmeAction
{
protected const CANDIDATE_FILENAMES = [
'README.md',
'readme.md',
'Readme.md',
'README.markdown',
'readme.markdown',
'README',
'readme',
];

protected const MAX_BYTES = 512 * 1024;

public function handle(GitProviderInterface $provider, string $ref): ?string
{
foreach (self::CANDIDATE_FILENAMES as $filename) {
$contents = $provider->getFileContent($ref, $filename);

if ($contents === null) {
continue;
}

if (strlen($contents) > self::MAX_BYTES) {
Log::info('Skipped README that exceeds the size cap', [
'repository' => $provider->getRepositoryIdentifier(),
'ref' => $ref,
'filename' => $filename,
'size_bytes' => strlen($contents),
]);

return null;
}

return $contents;
}

return null;
}
}
7 changes: 6 additions & 1 deletion app/Domains/Repository/Actions/SyncRefAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class SyncRefAction
public function __construct(
protected FindOrCreatePackageAction $findOrCreatePackage,
protected CreateDistArchiveAction $createDistArchive,
protected FetchReadmeAction $fetchReadmeAction,
) {}

/**
Expand Down Expand Up @@ -57,7 +58,9 @@ public function handle(
}
}

$result = DB::transaction(function () use ($metadata, $ref, $package, $repository, $provider): array {
$readme = $this->fetchReadmeAction->handle($provider, $ref->name);

$result = DB::transaction(function () use ($metadata, $ref, $package, $repository, $provider, $readme): array {
if (! $package) {
$package = $this->findOrCreatePackage->handle($repository, $metadata->name);
}
Expand All @@ -74,6 +77,7 @@ public function handle(
$version->update([
'normalized_version' => $metadata->normalizedVersion,
'composer_json' => $metadata->composerJson,
'readme' => $readme,
'source_url' => $sourceUrl,
'source_reference' => $ref->commit,
'source_tag' => $ref->name,
Expand All @@ -87,6 +91,7 @@ public function handle(
'version' => $metadata->version,
'normalized_version' => $metadata->normalizedVersion,
'composer_json' => $metadata->composerJson,
'readme' => $readme,
'source_url' => $sourceUrl,
'source_reference' => $ref->commit,
'source_tag' => $ref->name,
Expand Down
83 changes: 83 additions & 0 deletions app/Domains/Repository/Commands/BackfillReadmesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace App\Domains\Repository\Commands;

use App\Domains\Repository\Actions\FetchReadmeAction;
use App\Domains\Repository\Services\GitProviders\GitProviderFactory;
use App\Models\PackageVersion;
use App\Models\Repository;
use Illuminate\Console\Command;

class BackfillReadmesCommand extends Command
{
protected $signature = 'pricore:backfill-readmes
{--repository= : Limit to a single repository UUID}
{--chunk=50 : Number of versions to load per chunk}';

protected $description = 'Fetch and store README files for already-synced package versions that do not have one yet.';

public function handle(FetchReadmeAction $fetchReadmeAction): int
{
$repositoryFilter = $this->option('repository');

$query = PackageVersion::query()
->whereNull('readme')
->whereHas('package.repository', function ($query) use ($repositoryFilter) {
if ($repositoryFilter) {
$query->where('uuid', $repositoryFilter);
}
})
->with(['package.repository']);

$total = (clone $query)->count();

if ($total === 0) {
$this->info('No package versions need a README backfill.');

return self::SUCCESS;
}

$this->info("Backfilling README for {$total} version(s)...");

$providers = [];
$fetched = 0;
$missing = 0;

$query->chunkById((int) $this->option('chunk'), function ($versions) use (
$fetchReadmeAction,
&$providers,
&$fetched,
&$missing,
) {
foreach ($versions as $version) {
/** @var Repository $repository */
$repository = $version->package->repository;

$providers[$repository->uuid] ??= GitProviderFactory::make($repository);

$ref = $version->source_tag ?: $version->source_reference;

if (! $ref) {
$missing++;

continue;
}

$readme = $fetchReadmeAction->handle($providers[$repository->uuid], $ref);

if ($readme === null) {
$missing++;

continue;
}

$version->update(['readme' => $readme]);
$fetched++;
}
});

$this->info("Done. Fetched: {$fetched}. Skipped (no README found): {$missing}.");

return self::SUCCESS;
}
}
30 changes: 30 additions & 0 deletions app/Domains/Repository/Contracts/Enums/GitProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,36 @@ public function webhookRouteName(): string
};
}

/**
* Base URL (with trailing slash) for resolving relative <img src> in a repository file.
*
* Returns null for providers where the layout is unknown (generic Git).
*/
public function rawFileBaseUrl(string $repoIdentifier, string $ref, ?string $baseUrl = null): ?string
{
return match ($this) {
self::GitHub => "https://raw.githubusercontent.com/{$repoIdentifier}/{$ref}/",
self::GitLab => rtrim($baseUrl ?? 'https://gitlab.com', '/')."/{$repoIdentifier}/-/raw/{$ref}/",
self::Bitbucket => "https://bitbucket.org/{$repoIdentifier}/raw/{$ref}/",
self::Git => null,
};
}

/**
* Base URL (with trailing slash) for resolving relative <a href> in a repository file.
*
* Returns null for providers where the layout is unknown (generic Git).
*/
public function blobBaseUrl(string $repoIdentifier, string $ref, ?string $baseUrl = null): ?string
{
return match ($this) {
self::GitHub => "https://github.com/{$repoIdentifier}/blob/{$ref}/",
self::GitLab => rtrim($baseUrl ?? 'https://gitlab.com', '/')."/{$repoIdentifier}/-/blob/{$ref}/",
self::Bitbucket => "https://bitbucket.org/{$repoIdentifier}/src/{$ref}/",
self::Git => null,
};
}

/**
* @return array<string, string>
*/
Expand Down
2 changes: 2 additions & 0 deletions app/Models/PackageVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* @property string $version
* @property string $normalized_version
* @property array<array-key, mixed> $composer_json
* @property string|null $readme
* @property string|null $source_url
* @property string|null $source_reference
* @property string|null $source_tag
Expand All @@ -46,6 +47,7 @@
* @method static Builder<static>|PackageVersion whereDistUrl($value)
* @method static Builder<static>|PackageVersion whereNormalizedVersion($value)
* @method static Builder<static>|PackageVersion wherePackageUuid($value)
* @method static Builder<static>|PackageVersion whereReadme($value)
* @method static Builder<static>|PackageVersion whereReleasedAt($value)
* @method static Builder<static>|PackageVersion whereSourceReference($value)
* @method static Builder<static>|PackageVersion whereSourceUrl($value)
Expand Down
Loading