Files
landing-api/app/Models/Repository.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:55:27 +02:00

137 lines
3.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\GitProvider;
use Database\Factories\RepositoryFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* A git repository belonging to the collective.
*
* Only `provider`, `full_name`, `project_id` and the two visibility flags are
* entered by hand. Everything else is a cache of what the provider reported,
* refreshed by App\Jobs\SyncRepository — which is the whole point: the site
* shows real activity without anyone updating it.
*/
#[Fillable([
'project_id', 'provider', 'full_name', 'is_primary', 'show_publicly',
])]
class Repository extends Model
{
/** @use HasFactory<RepositoryFactory> */
use HasFactory;
/**
* Columns owned by the sync process rather than by a human. Kept out of
* the fillable list above so a stray form submission cannot fake stats.
*
* @var array<int, string>
*/
public const SYNCED_COLUMNS = [
'html_url', 'description', 'homepage', 'default_branch',
'primary_language', 'license', 'topics', 'languages', 'stars',
'forks', 'watchers', 'open_issues_count', 'contributors_count',
'commits_last_year', 'is_archived', 'is_fork', 'pushed_at',
'repo_created_at',
];
protected function casts(): array
{
return [
'provider' => GitProvider::class,
'topics' => 'array',
'languages' => 'array',
'is_primary' => 'boolean',
'show_publicly' => 'boolean',
'is_archived' => 'boolean',
'is_fork' => 'boolean',
'pushed_at' => 'datetime',
'repo_created_at' => 'datetime',
'last_synced_at' => 'datetime',
];
}
/**
* @return BelongsTo<Project, $this>
*/
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
/**
* @return HasMany<RepositoryIssue, $this>
*/
public function issues(): HasMany
{
return $this->hasMany(RepositoryIssue::class);
}
/**
* Issues suitable for someone who has never contributed before.
*
* @return HasMany<RepositoryIssue, $this>
*/
public function goodFirstIssues(): HasMany
{
return $this->issues()->where('is_good_first_issue', true);
}
public function owner(): string
{
return str_contains($this->full_name, '/')
? explode('/', $this->full_name)[0]
: $this->full_name;
}
public function name(): string
{
return str_contains($this->full_name, '/')
? explode('/', $this->full_name, 2)[1]
: $this->full_name;
}
/**
* A repository is considered dormant when nothing has been pushed for a
* while. The public site uses this to avoid advertising dead work as live.
*/
public function isDormant(int $days = 180): bool
{
return $this->pushed_at === null || $this->pushed_at->lt(now()->subDays($days));
}
public function hasSyncedSuccessfully(): bool
{
return $this->last_synced_at !== null && blank($this->sync_error);
}
/**
* @param Builder<$this> $query
* @return Builder<$this>
*/
public function scopePubliclyVisible(Builder $query): Builder
{
return $query->where('show_publicly', true);
}
/**
* Repositories whose sync keeps failing, so the panel can flag them rather
* than silently showing stale numbers forever.
*
* @param Builder<$this> $query
* @return Builder<$this>
*/
public function scopeFailing(Builder $query): Builder
{
return $query->where('sync_failures', '>=', 3);
}
}