Skip to content

Commit 92f67f8

Browse files
committed
Adds a flavour option
1 parent 1bbafa7 commit 92f67f8

6 files changed

Lines changed: 277 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1111

1212
### Added
1313
- Expanded the `validation` to also validate __negated__ export-ignore directives. Closes [#70](https://github.com/raphaelstolt/lean-package-validator/issues/70).
14+
- New `--flavour` option for the `create` command which influences the `.gitattributes` generation. Closes [#71](https://github.com/raphaelstolt/lean-package-validator/issues/71).
1415

1516
## [v5.9.1] - 2026-05-17
1617

peck.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"agentic",
3939
"mago",
4040
"alignable",
41-
"reorganise"
41+
"reorganise",
42+
"flavour"
4243
],
4344
"paths": []
4445
}

src/Analyser.php

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
class Analyser
1212
{
13+
const EXPORT_IGNORE_CLASSIC = 'classic';
14+
const EXPORT_IGNORE_NEGATED = 'negated';
15+
1316
const EXPORT_IGNORES_PLACEMENT_PLACEHOLDER = '{{ export_ignores_placement }}';
1417
/**
1518
* The directory to analyse
@@ -575,18 +578,83 @@ public function getGitignoredPatterns(): array
575578
return $this->getGitignorePatterns($gitignoreFile);
576579
}
577580

581+
private function getNegatedGitattributesContent(): string
582+
{
583+
$postfixLessExportIgnores = $this->collectExpectedNegatedExportIgnores();
584+
585+
\sort($postfixLessExportIgnores, SORT_STRING | SORT_FLAG_CASE);
586+
587+
$postfixLessExportIgnores = \array_map(function (string $fileToNegate): string {
588+
if (\is_dir($this->directory . DIRECTORY_SEPARATOR . $fileToNegate)) {
589+
$fileToNegate .= DIRECTORY_SEPARATOR;
590+
}
591+
592+
return $fileToNegate;
593+
}, $postfixLessExportIgnores);
594+
595+
if (\count($postfixLessExportIgnores) > 0) {
596+
if ($this->sortFromDirectoriesToFiles === false && ($this->isAlignExportIgnoresEnabled() || $this->isStrictAlignmentComparisonEnabled())) {
597+
$postfixLessExportIgnores = $this->getAlignedExportIgnoreArtifacts(
598+
$postfixLessExportIgnores
599+
);
600+
}
601+
602+
if ($this->sortFromDirectoriesToFiles) {
603+
$postfixLessExportIgnores = $this->getByDirectoriesToFilesExportIgnoreArtifacts(
604+
$postfixLessExportIgnores
605+
);
606+
}
607+
608+
$content = "* export-ignore" . PHP_EOL . PHP_EOL
609+
. \implode(" -export-ignore" . $this->preferredEol, $postfixLessExportIgnores)
610+
. " -export-ignore" . $this->preferredEol;
611+
612+
if ($this->hasGitattributesFile()) {
613+
$exportIgnoreContent = \rtrim($content);
614+
$content = $this->getPresentNonExportIgnoresContent();
615+
616+
if (\strstr($content, self::EXPORT_IGNORES_PLACEMENT_PLACEHOLDER)) {
617+
$content = \str_replace(
618+
self::EXPORT_IGNORES_PLACEMENT_PLACEHOLDER,
619+
$exportIgnoreContent,
620+
$content
621+
);
622+
} else {
623+
$content = $content
624+
. \str_repeat($this->preferredEol, 2)
625+
. $exportIgnoreContent;
626+
}
627+
} else {
628+
$content = "* text=auto eol=lf" . \str_repeat($this->preferredEol, 2) . $content;
629+
}
630+
631+
return $content;
632+
}
633+
634+
return '';
635+
}
636+
578637
/**
579638
* Return the expected .gitattributes content.
580639
*
581640
* @param array $postfixLessExportIgnores Expected patterns without an export-ignore postfix.
641+
* @param string $flavour The flavour of the .gitattributes file content. Possible values are classic and negated.
582642
* @return string
583643
*/
584-
public function getExpectedGitattributesContent(array $postfixLessExportIgnores = []): string
644+
public function getExpectedGitattributesContent(array $postfixLessExportIgnores = [], string $flavour = self::EXPORT_IGNORE_CLASSIC): string
585645
{
586-
if ($postfixLessExportIgnores === []) {
646+
if ($flavour !== self::EXPORT_IGNORE_CLASSIC && $flavour !== self::EXPORT_IGNORE_NEGATED) {
647+
throw new \InvalidArgumentException("Invalid flavour provided. Expected 'classic' or 'negated'.");
648+
}
649+
650+
if ($postfixLessExportIgnores === [] && $flavour === self::EXPORT_IGNORE_CLASSIC) {
587651
$postfixLessExportIgnores = $this->collectExpectedExportIgnores();
588652
}
589653

654+
if ($flavour === self::EXPORT_IGNORE_NEGATED) {
655+
return $this->getNegatedGitattributesContent();
656+
}
657+
590658
if (!$this->hasGitattributesFile() && \count($postfixLessExportIgnores) > 0) {
591659
$postfixLessExportIgnores[] = '.gitattributes';
592660
}
@@ -691,6 +759,41 @@ public function getPresentExportIgnoresToPreserve(array $globPatternMatchingExpo
691759
return $exportIgnoresToPreserve;
692760
}
693761

762+
public function collectExpectedNegatedExportIgnores(): array
763+
{
764+
$expectedNegatedExportIgnores = [];
765+
766+
\chdir($this->directory);
767+
768+
$globMatches = Glob::glob($this->globPattern, Glob::GLOB_BRACE);
769+
770+
if (!\is_array($globMatches)) {
771+
return $expectedNegatedExportIgnores;
772+
}
773+
774+
$globMatches = \array_values(
775+
\array_filter($globMatches, function (string $fileToIgnore): bool {
776+
if ($this->isKeepLicenseEnabled() && \preg_match('/(License.*)/i', $fileToIgnore)) {
777+
return false;
778+
}
779+
780+
if ($this->isKeepReadmeEnabled() && \preg_match('/(Readme.*)/i', $fileToIgnore)) {
781+
return false;
782+
}
783+
784+
return true;
785+
})
786+
);
787+
788+
$allFiles = Glob::glob('{*}', Glob::GLOB_BRACE);
789+
790+
if (!\is_array($allFiles) || count ($allFiles) === 0) {
791+
return $expectedNegatedExportIgnores;
792+
}
793+
794+
return array_diff($allFiles, $globMatches);
795+
}
796+
694797
/**
695798
* Collect the expected export-ignored files.
696799
*
@@ -773,7 +876,7 @@ public function collectExpectedExportIgnores(): array
773876
}
774877

775878
/**
776-
* Detect most frequently used end of line sequence.
879+
* Detect the most frequently used end-of-line sequence.
777880
*
778881
* @param string $content The content to detect the eol in.
779882
*
@@ -930,7 +1033,7 @@ public function getReformattedGitattributesContent(): string
9301033
}
9311034

9321035
/**
933-
* Get the present non export-ignore entries of
1036+
* Get the present non-export-ignore entries of
9341037
* the .gitattributes file.
9351038
*
9361039
* @return string

src/Commands/CreateCommand.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,25 @@ protected function configure(): void
5555
$this->addAgenticOutputOption(function (...$args) {
5656
$this->getDefinition()->addOption(new InputOption(...$args));
5757
});
58+
$flavourDescription = 'Generate the .gitattributes file with the given flavour';
59+
60+
$this->addOption('flavour', 'f', InputOption::VALUE_OPTIONAL, $flavourDescription, 'classic');
5861
}
5962

6063
protected function execute(InputInterface $input, OutputInterface $output): int
6164
{
6265
$directory = (string) $input->getArgument('directory') ?: \getcwd();
6366
$this->analyser->setDirectory($directory);
67+
6468
$isAgenticRun = $this->isAgenticRun($input);
6569

70+
$generationFlavour = $input->getOption('flavour') ?: 'classic';
71+
72+
if (!\in_array($generationFlavour, ['classic', 'negated'], true)) {
73+
$output->writeln('<error>Invalid flavour specified. Use <info>classic</info> or <info>negated</info>.</error>');
74+
return self::FAILURE;
75+
}
76+
6677
// Apply options that influence generation
6778
if (!$this->applyGenerationOptions($input, $output, $this->analyser)) {
6879
return self::FAILURE;
@@ -80,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8091
return self::FAILURE;
8192
}
8293

83-
$expected = $this->analyser->getExpectedGitattributesContent();
94+
$expected = $this->analyser->getExpectedGitattributesContent([], $generationFlavour);
8495

8596
if ($expected === '') {
8697
$message = 'Unable to determine expected .gitattributes content for the given directory.';

tests/AnalyserTest.php

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@
1818
class AnalyserTest extends TestCase
1919
{
2020
/**
21-
* Set up test environment.
21+
* Set up the test environment.
2222
*/
2323
protected function setUp(): void
2424
{
2525
$this->setUpTemporaryDirectory();
2626
}
2727

2828
/**
29-
* Tear down test environment.
29+
* Tear down the test environment.
3030
*
3131
* @return void
3232
*/
@@ -37,6 +37,103 @@ protected function tearDown(): void
3737
}
3838
}
3939

40+
#[Test]
41+
public function throwsExceptionOnNonExpectedFlavour(): void
42+
{
43+
$analyser = (new Analyser(new Finder(new PhpPreset())))->setDirectory($this->temporaryDirectory);
44+
45+
$this->expectException(\InvalidArgumentException::class);
46+
$this->expectExceptionMessage(
47+
"Invalid flavour provided. Expected 'classic' or 'negated'."
48+
);
49+
50+
$analyser->getExpectedGitattributesContent([], 'non-existing-flavour');
51+
}
52+
53+
#[Test]
54+
public function returnsExpectedNegatedGitattributesContent(): void
55+
{
56+
$analyser = (new Analyser(new Finder(new PhpPreset())))->setDirectory($this->temporaryDirectory);
57+
58+
$artifactFilenames = [
59+
'composer.json',
60+
'.travis.yml',
61+
'phpspec.yml.dist',
62+
'README.md',
63+
'peck.json'
64+
];
65+
66+
$this->createTemporaryFiles(
67+
$artifactFilenames,
68+
['src', 'tests', 'bin', 'resources']
69+
);
70+
71+
$negatedGitattributesContent = $analyser->getExpectedGitattributesContent(
72+
[],
73+
Analyser::EXPORT_IGNORE_NEGATED
74+
);
75+
76+
$expectedGitattributesContent = <<<CONTENT
77+
* text=auto eol=lf
78+
79+
* export-ignore
80+
81+
bin/ -export-ignore
82+
composer.json -export-ignore
83+
resources/ -export-ignore
84+
src/ -export-ignore
85+
CONTENT;
86+
87+
$this->assertStringContainsStringIgnoringLineEndings(
88+
$expectedGitattributesContent,
89+
$negatedGitattributesContent
90+
);
91+
}
92+
93+
#[Test]
94+
public function returnsExpectedNegatedGitattributesContentWithAlignmentAndKeptLicense(): void
95+
{
96+
$analyser = (new Analyser(new Finder(new PhpPreset())))->setDirectory($this->temporaryDirectory);
97+
$analyser->alignExportIgnores()->keepLicense();
98+
99+
$artifactFilenames = [
100+
'composer.json',
101+
'.travis.yml',
102+
'phpspec.yml.dist',
103+
'README.md',
104+
'peck.json',
105+
'LICENSE.md'
106+
];
107+
108+
$this->createTemporaryFiles(
109+
$artifactFilenames,
110+
['src', 'tests', 'bin', 'resources']
111+
);
112+
113+
$negatedGitattributesContent = $analyser->getExpectedGitattributesContent(
114+
[],
115+
Analyser::EXPORT_IGNORE_NEGATED
116+
);
117+
118+
$expectedGitattributesContent = <<<CONTENT
119+
* text=auto eol=lf
120+
121+
* export-ignore
122+
123+
bin/ -export-ignore
124+
composer.json -export-ignore
125+
LICENSE.md -export-ignore
126+
resources/ -export-ignore
127+
src/ -export-ignore
128+
CONTENT;
129+
130+
$this->assertStringContainsStringIgnoringLineEndings(
131+
$expectedGitattributesContent,
132+
$negatedGitattributesContent
133+
);
134+
}
135+
136+
40137
#[Test]
41138
public function hasCompleteExportIgnoresFailsOnEmptyExportIgnores(): void
42139
{

tests/Commands/CreateCommandTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,62 @@ protected function tearDown(): void
2626
$this->removeDirectory($this->temporaryDirectory);
2727
}
2828

29+
#[Test]
30+
public function failsForUnknownFlavour(): void
31+
{
32+
$analyser = (new Analyser(new Finder(new PhpPreset())))->setDirectory($this->temporaryDirectory);
33+
$repository = new GitattributesFileRepository($analyser);
34+
$command = new CreateCommand($analyser, $repository);
35+
36+
TestCommand::for($command)
37+
->addArgument($this->temporaryDirectory)
38+
->addOption('flavour', 'unknown')
39+
->execute()
40+
->assertFaulty()
41+
->assertOutputContains('Invalid flavour specified. Use classic or negated');
42+
}
43+
44+
#[Test]
45+
public function createsAGitattributesFileWithNegatedExportIgnoreDirectives(): void
46+
{
47+
$analyser = (new Analyser(new Finder(new PhpPreset())))->setDirectory($this->temporaryDirectory);
48+
$repository = new GitattributesFileRepository($analyser);
49+
$command = new CreateCommand($analyser, $repository);
50+
51+
$artifactFilenames = ['LICENSE.md', '.gitignore', 'composer.json'];
52+
53+
$this->createTemporaryFiles(
54+
$artifactFilenames,
55+
['tests', 'src', 'bin']
56+
);
57+
58+
TestCommand::for($command)
59+
->addArgument($this->temporaryDirectory)
60+
->addOption('flavour', Analyser::EXPORT_IGNORE_NEGATED)
61+
->addOption('keep-license')
62+
->execute()
63+
->assertSuccessful();
64+
65+
$expectedGitattributesContent = <<<CONTENT
66+
# This file was generated by the lean package validator (http://git.io/lean-package-validator).
67+
68+
* text=auto eol=lf
69+
70+
* export-ignore
71+
72+
bin/ -export-ignore
73+
composer.json -export-ignore
74+
LICENSE.md -export-ignore
75+
src/ -export-ignore
76+
CONTENT;
77+
78+
$this->assertFileExists($this->temporaryDirectory . DIRECTORY_SEPARATOR . '.gitattributes');
79+
$this->assertStringContainsStringIgnoringLineEndings(
80+
$expectedGitattributesContent,
81+
\file_get_contents($this->temporaryDirectory . DIRECTORY_SEPARATOR . '.gitattributes')
82+
);
83+
}
84+
2985
#[Test]
3086
public function createsNewGitattributesFileWithHeaderAndExpectedContent(): void
3187
{

0 commit comments

Comments
 (0)