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
22 changes: 22 additions & 0 deletions src/Audit/AbandonmentReason.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);

/*
* This file is part of Packagist.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
* Nils Adermann <naderman@naderman.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Audit;

enum AbandonmentReason: string
{
case Manual = 'manual';
case RepositoryArchived = 'repository_archived';
case ComposerJson = 'composer_json';
case RepositoryArchivedAndComposerJson = 'repository_archived_and_composer_json';
case Unknown = 'unknown';
}
4 changes: 2 additions & 2 deletions src/Audit/AuditRecordType.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ enum AuditRecordType: string
case VersionDeleted = 'version_deleted';

case VersionReferenceChanged = 'version_reference_changed';
case PackageAbandoned = 'package_abandoned'; // TODO
case PackageUnabandoned = 'package_unabandoned'; // TODO
case PackageAbandoned = 'package_abandoned';
case PackageUnabandoned = 'package_unabandoned';

// user management
case UserCreated = 'user_created'; // TODO
Expand Down
15 changes: 15 additions & 0 deletions src/Audit/Display/AuditLogDisplayFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ public function buildSingle(AuditRecord $record): AuditLogDisplayInterface
$record->attributes['repository_to'],
$this->buildActor($record->attributes['actor']),
),
AuditRecordType::PackageAbandoned => new PackageAbandonedDisplay(
$record->datetime,
$record->attributes['name'],
$record->attributes['repository'],
$record->attributes['replacement_package'] ?? null,
$record->attributes['reason'] ?? null,
$this->buildActor($record->attributes['actor']),
),
AuditRecordType::PackageUnabandoned => new PackageUnabandonedDisplay(
$record->datetime,
$record->attributes['name'],
$record->attributes['repository'],
$record->attributes['previous_replacement_package'] ?? null,
$this->buildActor($record->attributes['actor']),
),
AuditRecordType::VersionDeleted => new VersionDeletedDisplay(
$record->datetime,
$record->attributes['name'],
Expand Down
39 changes: 39 additions & 0 deletions src/Audit/Display/PackageAbandonedDisplay.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types=1);

/*
* This file is part of Packagist.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
* Nils Adermann <naderman@naderman.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Audit\Display;

use App\Audit\AuditRecordType;

readonly class PackageAbandonedDisplay extends AbstractAuditLogDisplay
{
public function __construct(
\DateTimeImmutable $datetime,
public string $packageName,
public string $repository,
public ?string $replacementPackage,
public ?string $reason,
ActorDisplay $actor,
) {
parent::__construct($datetime, $actor);
}

public function getType(): AuditRecordType
{
return AuditRecordType::PackageAbandoned;
}

public function getTemplateName(): string
{
return 'audit_log/display/package_abandoned.html.twig';
}
}
38 changes: 38 additions & 0 deletions src/Audit/Display/PackageUnabandonedDisplay.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types=1);

/*
* This file is part of Packagist.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
* Nils Adermann <naderman@naderman.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Audit\Display;

use App\Audit\AuditRecordType;

readonly class PackageUnabandonedDisplay extends AbstractAuditLogDisplay
{
public function __construct(
\DateTimeImmutable $datetime,
public string $packageName,
public string $repository,
public ?string $previousReplacementPackage,
ActorDisplay $actor,
) {
parent::__construct($datetime, $actor);
}

public function getType(): AuditRecordType
{
return AuditRecordType::PackageUnabandoned;
}

public function getTemplateName(): string
{
return 'audit_log/display/package_unabandoned.html.twig';
}
}
11 changes: 11 additions & 0 deletions src/Entity/AuditRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace App\Entity;

use App\Audit\AbandonmentReason;
use App\Audit\AuditRecordType;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
Expand Down Expand Up @@ -109,6 +110,16 @@ public static function maintainerRemoved(Package $package, User $maintainer, ?Us
return new self(AuditRecordType::MaintainerRemoved, ['name' => $package->getName(), 'maintainer' => self::getUserData($maintainer), 'actor' => self::getUserData($actor)], $actor?->getId(), $package->getVendor(), $package->getId(), $maintainer->getId());
}

public static function packageAbandoned(Package $package, ?User $actor, ?string $replacementPackage, ?AbandonmentReason $reason = null): self
{
return new self(AuditRecordType::PackageAbandoned, ['name' => $package->getName(), 'repository' => $package->getRepository(), 'replacement_package' => $replacementPackage, 'reason' => $reason?->value, 'actor' => self::getUserData($actor, 'automation')], $actor?->getId(), $package->getVendor(), $package->getId());
}

public static function packageUnabandoned(Package $package, ?User $actor, ?string $previousReplacementPackage): self
{
return new self(AuditRecordType::PackageUnabandoned, ['name' => $package->getName(), 'repository' => $package->getRepository(), 'previous_replacement_package' => $previousReplacementPackage, 'actor' => self::getUserData($actor, 'automation')], $actor?->getId(), $package->getVendor(), $package->getId());
}

/**
* @return array{id: int, username: string}|string
*/
Expand Down
5 changes: 5 additions & 0 deletions src/Entity/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace App\Entity;

use App\Audit\AbandonmentReason;
use App\Service\UpdaterWorker;
use App\Util\HttpDownloaderOptionsFactory;
use App\Validator\Copyright;
Expand Down Expand Up @@ -206,6 +207,10 @@ class Package
* @internal
*/
public ?string $vcsDriverError = null;
/**
* @internal
*/
public ?AbandonmentReason $abandonmentReason = null;

/**
* @var array<string, Version>|null lookup table for versions
Expand Down
19 changes: 19 additions & 0 deletions src/EventListener/PackageListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace App\EventListener;

use App\Audit\AbandonmentReason;
use App\Entity\AuditRecord;
use App\Entity\Package;
use App\Entity\User;
Expand Down Expand Up @@ -64,6 +65,24 @@ public function preUpdate(Package $package, PreUpdateEventArgs $event): void
// buffering things to be inserted in postUpdate once we can confirm it is done
$this->buffered[] = AuditRecord::canonicalUrlChange($package, $this->getUser(), $event->getOldValue('repository'));
}

if ($event->hasChangedField('abandoned')) {
$newValue = $event->getNewValue('abandoned');
if ($newValue === true) {
$reason = $package->abandonmentReason;

if ($this->getUser()) {
$reason = AbandonmentReason::Manual;
} else {
$reason = $reason ?? AbandonmentReason::Unknown;
}

$this->buffered[] = AuditRecord::packageAbandoned($package, $this->getUser(), $package->getReplacementPackage(), $reason);
} else {
$oldReplacementPackage = $event->hasChangedField('replacementPackage') ? $event->getOldValue('replacementPackage') : $package->getReplacementPackage();
$this->buffered[] = AuditRecord::packageUnabandoned($package, $this->getUser(), $oldReplacementPackage);
}
}
}

/**
Expand Down
43 changes: 41 additions & 2 deletions src/Package/Updater.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace App\Package;

use App\Audit\AbandonmentReason;
use App\Entity\ConflictLink;
use App\Entity\Dependent;
use App\Entity\DevRequireLink;
Expand Down Expand Up @@ -251,7 +252,7 @@ public function update(IOInterface $io, Config $config, Package $package, VcsRep
}
$processedVersions[strtolower($version->getVersion())] = $version;

$result = $this->updateInformation($io, $versionRepository, $package, $existingVersions, $version, $flags, $rootIdentifier);
$result = $this->updateInformation($io, $versionRepository, $package, $existingVersions, $version, $flags, $rootIdentifier, $driver);
$versionId = false;
if ($result['updated']) {
\assert($result['object'] instanceof Version);
Expand Down Expand Up @@ -364,7 +365,7 @@ public function update(IOInterface $io, Config $config, Package $package, VcsRep
*
* @return array{updated: true, id: int|null, version: string, object: Version}|array{updated: false, id: int|null, version: string, object: null}
*/
private function updateInformation(IOInterface $io, VersionRepository $versionRepo, Package $package, array $existingVersions, CompletePackageInterface $data, int $flags, string $rootIdentifier): array
private function updateInformation(IOInterface $io, VersionRepository $versionRepo, Package $package, array $existingVersions, CompletePackageInterface $data, int $flags, string $rootIdentifier, VcsDriverInterface $driver): array
{
$em = $this->getEM();
$version = new Version();
Expand Down Expand Up @@ -427,6 +428,7 @@ private function updateInformation(IOInterface $io, VersionRepository $versionRe
$package->setType($this->sanitize($data->getType()));
if ($data->isAbandoned() && !$package->isAbandoned()) {
$io->write('Marking package abandoned as per composer metadata from '.$version->getVersion());
$package->abandonmentReason = $this->detectAbandonmentReason($driver, $rootIdentifier);
$package->setAbandoned(true);
if ($data->getReplacementPackage()) {
$package->setReplacementPackage($data->getReplacementPackage());
Expand Down Expand Up @@ -845,4 +847,41 @@ private function sanitize(?string $str): ?string

return Preg::replace("{[\x01-\x1A]}u", '', $str);
}

private function detectAbandonmentReason(VcsDriverInterface $driver, string $rootIdentifier): AbandonmentReason
{
$isArchived = false;
$composerHasAbandoned = false;

// is repository archived (GitHub or GitLab)
if ($driver instanceof GitHubDriver || $driver instanceof GitLabDriver) {
try {
$repoData = $driver->getRepoData();
$isArchived = !empty($repoData['archived']);
} catch (\Exception $e) {
// If we can't get repo data, assume not archived
}
}

// is abandoned field in composer.json explicitly set
try {
$composerJson = $driver->getFileContent('composer.json', $rootIdentifier);
if ($composerJson) {
$composerData = json_decode($composerJson, true);
$composerHasAbandoned = isset($composerData['abandoned']);
}
} catch (\Exception $e) {
// composer.json couldn't be read, so the abandoned state couldn't be retrieved
}

if ($isArchived && $composerHasAbandoned) {
return AbandonmentReason::RepositoryArchivedAndComposerJson;
} elseif ($isArchived) {
return AbandonmentReason::RepositoryArchived;
} elseif ($composerHasAbandoned) {
return AbandonmentReason::ComposerJson;
}

return AbandonmentReason::Unknown;
}
}
30 changes: 30 additions & 0 deletions templates/audit_log/display/package_abandoned.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<strong>
{%- if display.packageName is existing_package -%}
<a href="{{ path('view_package', { 'name': display.packageName }) }}">{{ display.packageName }}</a>
{%- else -%}
{{ display.packageName }}
{%- endif -%}
</strong><br>
Repository: {{ display.repository }}<br>
{%- if display.reason -%}
Reason:
{% if display.reason == 'repository_archived' -%}
Repository archived on GitHub/GitLab
{% elseif display.reason == 'composer_json' -%}
<em>abandoned</em> flag set in composer.json
{% elseif display.reason == 'repository_archived_and_composer_json' -%}
Repository archived and <em>abandoned</em> flag set in composer.json
{% elseif display.reason == 'manual' -%}
Manually set by maintainer
{% else -%}
{{ display.reason }}
{% endif -%}<br>
{%- endif -%}
{%- if display.replacementPackage -%}
Replacement: {% if display.replacementPackage is existing_package -%}
<a href="{{ path('view_package', { 'name': display.replacementPackage }) }}">{{ display.replacementPackage }}</a>
{%- else -%}
{{ display.replacementPackage }}
{%- endif -%}<br>
{%- endif -%}
Abandoned by: {{ display.actor.username }}
16 changes: 16 additions & 0 deletions templates/audit_log/display/package_unabandoned.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<strong>
{%- if display.packageName is existing_package -%}
<a href="{{ path('view_package', { 'name': display.packageName }) }}">{{ display.packageName }}</a>
{%- else -%}
{{ display.packageName }}
{%- endif -%}
</strong><br>
Repository: {{ display.repository }}<br>
{%- if display.previousReplacementPackage -%}
Previous replacement: {% if display.previousReplacementPackage is existing_package -%}
<a href="{{ path('view_package', { 'name': display.previousReplacementPackage }) }}">{{ display.previousReplacementPackage }}</a>
{%- else -%}
{{ display.previousReplacementPackage }}
{%- endif -%}<br>
{%- endif -%}
Unabandoned by: {{ display.actor.username }}
Loading