Skip to content

Commit

Permalink
feature #101 Use git last commit date for a file as default last modi…
Browse files Browse the repository at this point in the history
…fied date (ogizanagi)

This PR was squashed before being merged into the 0.x-dev branch.

Discussion
----------

Use git last commit date for a file as default last modified date

Fixes #98

⚠️ small BC inside => use `\DateTimeInterface` or `\DateTimeImmutable` on your model objects, since this processor now returns a `DateTimeImmutable` now (before was `\DateTime`).

Commits
-------

4d87d57 Use a consistent file order for the fs provider, independent from fs
324c8d2 Extract git last modified fetcher
e3d1acb Use DateTimeImmutable
41fcd86 Use git last commit date for a file as default last modified date
  • Loading branch information
ogizanagi committed Jun 17, 2021
2 parents 320a86a + 4d87d57 commit f340be4
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 20 deletions.
9 changes: 8 additions & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use Stenope\Bundle\Routing\UrlGenerator;
use Stenope\Bundle\Serializer\Normalizer\SkippingInstantiatedObjectDenormalizer;
use Stenope\Bundle\Service\AssetUtils;
use Stenope\Bundle\Service\Git\LastModifiedFetcher;
use Stenope\Bundle\Service\NaiveHtmlCrawlerManager;
use Stenope\Bundle\Service\Parsedown;
use Stenope\Bundle\Service\SharedHtmlCrawlerManager;
Expand Down Expand Up @@ -188,7 +189,13 @@

// Tagged processors:
$container->services()->defaults()->tag(tags\content_processor)
->set(LastModifiedProcessor::class)
->set(LastModifiedProcessor::class)->args([
'$property' => 'lastModified',
'$gitLastModified' => inline_service(LastModifiedFetcher::class)->args([
'$gitPath' => 'git',
'$logger' => service(LoggerInterface::class)->nullOnInvalid(),
]),
])
->set(SlugProcessor::class)
->set(HtmlIdProcessor::class)
->args([
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ parameters:

excludes_analyse:
- tests/fixtures/app/var/
- tests/fixtures/Unit/Service/Git/bin

level: 1

Expand Down
12 changes: 6 additions & 6 deletions src/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ final class Content
private string $type;
private string $rawContent;
private string $format;
private ?\DateTime $lastModified;
private ?\DateTime $createdAt;
private ?\DateTimeImmutable $lastModified;
private ?\DateTimeImmutable $createdAt;
private array $metadata;

public function __construct(
string $slug,
string $type,
string $rawContent,
string $format,
?\DateTime $lastModified = null,
?\DateTime $createdAt = null,
?\DateTimeImmutable $lastModified = null,
?\DateTimeImmutable $createdAt = null,
array $metadata = []
) {
$this->slug = $slug;
Expand All @@ -51,12 +51,12 @@ public function getRawContent(): string
return $this->rawContent;
}

public function getLastModified(): ?\DateTime
public function getLastModified(): ?\DateTimeImmutable
{
return $this->lastModified;
}

public function getCreatedAt(): ?\DateTime
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
Expand Down
30 changes: 26 additions & 4 deletions src/Processor/LastModifiedProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,48 @@

use Stenope\Bundle\Behaviour\ProcessorInterface;
use Stenope\Bundle\Content;
use Stenope\Bundle\Provider\Factory\LocalFilesystemProviderFactory;
use Stenope\Bundle\Service\Git\LastModifiedFetcher;

/**
* Set a "LastModified" property based on file date
* Set a "LastModified" property based on the last modified date set by the provider.
* E.g, for the {@see LocalFilesystemProvider}, the file mtime on the filesystem.
*
* If available, for local files, it'll use Git to get the last commit date for this file.
*/
class LastModifiedProcessor implements ProcessorInterface
{
private string $property;
private ?LastModifiedFetcher $gitLastModified;

public function __construct(string $property = 'lastModified')
public function __construct(string $property = 'lastModified', ?LastModifiedFetcher $gitLastModified = null)
{
$this->property = $property;
$this->gitLastModified = $gitLastModified;
}

public function __invoke(array &$data, Content $content): void
{
if (\array_key_exists($this->property, $data)) {
// Last modified already set.
// Last modified already set (even if explicitly set as null).
return;
}

$data[$this->property] = $content->getLastModified();

if (LocalFilesystemProviderFactory::TYPE !== ($content->getMetadata()['provider'] ?? null)) {
// Won't attempt with a non local filesystem content.
return;
}

$data[$this->property] = $content->getLastModified() ? $content->getLastModified()->format(\DateTimeInterface::RFC3339) : null;
if (null === $this->gitLastModified) {
return;
}

$filePath = $content->getMetadata()['path'];

if ($lastModified = ($this->gitLastModified)($filePath)) {
$data[$this->property] = $lastModified;
}
}
}
3 changes: 2 additions & 1 deletion src/Provider/LocalFilesystemProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private function fromFile(\SplFileInfo $file): Content
$this->supportedClass,
file_get_contents($file->getPathname()),
self::getFormat($file),
new \DateTime("@{$file->getMTime()}"),
new \DateTimeImmutable("@{$file->getMTime()}"),
null,
[
'path' => $file->getRealPath(),
Expand Down Expand Up @@ -131,6 +131,7 @@ private function files(): Finder
->exclude($excludedDirs)
->notPath(array_map(fn ($exclude) => $this->convertPattern($exclude), $excludedPatterns))
->path(array_map(fn ($pattern) => $this->convertPattern($pattern), $this->patterns))
->sortByName()
;

if ($this->depth) {
Expand Down
83 changes: 83 additions & 0 deletions src/Service/Git/LastModifiedFetcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

/*
* This file is part of the "StenopePHP/Stenope" bundle.
*
* @author Thomas Jarrand <[email protected]>
*/

namespace Stenope\Bundle\Service\Git;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
use Symfony\Contracts\Service\ResetInterface;

class LastModifiedFetcher implements ResetInterface
{
/** Git executable path on the system / PATH used to get the last commit date for the file, or null to disable. */
private ?string $gitPath;
private LoggerInterface $logger;

private static ?bool $gitAvailable = null;

public function __construct(
?string $gitPath = 'git',
?LoggerInterface $logger = null
) {
$this->gitPath = $gitPath;
$this->logger = $logger ?? new NullLogger();
}

/**
* @throws ProcessFailedException
*/
public function __invoke(string $filePath): ?\DateTimeImmutable
{
if (null === $this->gitPath || false === self::$gitAvailable) {
// Don't go further if the git command is not available or the git feature is disabled
return null;
}

$executable = explode(' ', $this->gitPath);

if (null === self::$gitAvailable) {
// Check once if the git command is available
$process = new Process([...$executable, '--version']);
$process->run();

if (!$process->isSuccessful()) {
self::$gitAvailable = false;

$this->logger->warning('Git was not found at path "{gitPath}". Check the binary path is correct or part of your PATH.', [
'gitPath' => $this->gitPath,
'output' => $process->getOutput(),
'err_output' => $process->getErrorOutput(),
]);

return null;
}

self::$gitAvailable = true;
}

$process = new Process([...$executable, 'log', '-1', '--format=%cd', '--date=iso', $filePath]);
$process->run();

if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}

if ($output = $process->getOutput()) {
return new \DateTimeImmutable(trim($output));
}

return null;
}

public function reset(): void
{
self::$gitAvailable = null;
}
}
16 changes: 8 additions & 8 deletions tests/Integration/Command/DebugCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ public function provide testList data(): iterable
{
yield 'list' => [Author::class,
<<<TXT
* tom32i
* john.doe
* ogi
* tom32i
TXT
];

Expand Down Expand Up @@ -89,22 +89,22 @@ public function provide testList data(): iterable

yield 'filter property (data prefix)' => [Author::class,
<<<TXT
* tom32i
* ogi
* tom32i
TXT
, ['_.core'], ];

yield 'filter property (d prefix)' => [Author::class,
<<<TXT
* tom32i
* ogi
* tom32i
TXT
, ['d.core'], ];

yield 'filter property (_ prefix)' => [Author::class,
<<<TXT
* tom32i
* ogi
* tom32i
TXT
, ['_.core'], ];

Expand All @@ -122,24 +122,24 @@ public function provide testList data(): iterable

yield 'filter contains' => [Author::class,
<<<TXT
* tom32i
* ogi
* tom32i
TXT
, ['contains(_.slug, "i")'], ];

yield 'filter dates' => [Recipe::class,
<<<TXT
* tomiritsu
* ogito
* tomiritsu
TXT
, ['_.date > date("2019-01-01") and _.date < date("2020-01-01")'], ];

yield 'filter and order' => [Author::class,
<<<TXT
* ogi
* tom32i
* ogi
TXT
, ['_.core'], ['slug'], ];
, ['_.core'], ['desc:slug'], ];

yield 'multiple filters' => [Author::class,
<<<TXT
Expand Down
73 changes: 73 additions & 0 deletions tests/Unit/Processor/LastModifiedProcessorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/*
* This file is part of the "StenopePHP/Stenope" bundle.
*
* @author Thomas Jarrand <[email protected]>
*/

namespace Stenope\Bundle\Tests\Unit\Processor;

use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Stenope\Bundle\Content;
use Stenope\Bundle\Processor\LastModifiedProcessor;
use Stenope\Bundle\Provider\Factory\LocalFilesystemProviderFactory;
use Stenope\Bundle\Service\Git\LastModifiedFetcher;

class LastModifiedProcessorTest extends TestCase
{
use ProphecyTrait;

public function testFromContent(): void
{
$processor = new LastModifiedProcessor('lastModified');

$data = [];
$content = new Content('slug', 'type', 'content', 'format', new \DateTimeImmutable('2021-06-14 10:25:47 +0200'));

$processor->__invoke($data, $content);

self::assertInstanceOf(\DateTimeImmutable::class, $data['lastModified']);
self::assertEquals($content->getLastModified(), $data['lastModified']);
}

public function testFromGit(): void
{
$gitFetcher = $this->prophesize(LastModifiedFetcher::class);
$gitFetcher->__invoke(Argument::type('string'))
->willReturn($expectedDate = new \DateTimeImmutable('2021-05-10 10:00:00 +0000'))
->shouldBeCalledOnce()
;

$processor = new LastModifiedProcessor('lastModified', $gitFetcher->reveal());

$data = [];
$content = new Content('slug', 'type', 'content', 'format', new \DateTimeImmutable('2021-06-14 10:25:47 +0200'), new \DateTimeImmutable(), [
'provider' => LocalFilesystemProviderFactory::TYPE,
'path' => 'some-path.md',
]);

$processor->__invoke($data, $content);

self::assertInstanceOf(\DateTimeImmutable::class, $data['lastModified']);
self::assertEquals($expectedDate, $data['lastModified']);
}

public function testWontUseGitOnNonFilesProvider(): void
{
$gitFetcher = $this->prophesize(LastModifiedFetcher::class);
$gitFetcher->__invoke(Argument::type('string'))->shouldNotBeCalled();

$processor = new LastModifiedProcessor('lastModified', $gitFetcher->reveal());

$data = [];
$content = new Content('slug', 'type', 'content', 'format', new \DateTimeImmutable('2021-06-14 10:25:47 +0200'));

$processor->__invoke($data, $content);

self::assertInstanceOf(\DateTimeImmutable::class, $data['lastModified']);
self::assertEquals($content->getLastModified(), $data['lastModified']);
}
}
Loading

0 comments on commit f340be4

Please sign in to comment.