Skip to content

Commit 8919ece

Browse files
committed
Adds new dedicated create and update commands
1 parent eaa9854 commit 8919ece

13 files changed

+668
-19
lines changed

.github/workflows/test-windows.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on: push
55
jobs:
66
build:
77
name: "PHPUnit (PHP ${{ matrix.php }})"
8-
runs-on: "windows-latest"
8+
runs-on: "windows-2022"
99

1010
strategy:
1111
matrix:

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
- New dedicated `update` and `create` commands. Closes [#55](https://github.com/raphaelstolt/lean-package-validator/issues/55).
12+
1013
## [v4.7.1] - 2025-09-15
1114

1215
### Fixed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ with a nonexistent `.gitattributes` file implicates the `--create` option.
9090
``` bash
9191
lean-package-validator validate --overwrite [<directory>]
9292
```
93+
> [!WARNING]
94+
> As of release `v5.0` the `--create` and `--overwrite` options are deprecated and will be removed in the next major
95+
> release. Please migrate to the dedicated commands `create` and `update`.
9396
9497
The `--glob-pattern` option allows you to overwrite the default pattern used
9598
to match common repository artifacts. The amount of pattern in the grouping
@@ -193,6 +196,17 @@ cat .gitattributes | lean-package-validator validate --stdin-input
193196

194197
### Additional commands
195198

199+
#### Create command
200+
201+
The `create` command will create a `.gitattributes` file in the given directory. This command replaces the `--create`
202+
option of the `validate` command. Please migrate to the dedicated commands.
203+
204+
205+
#### Update command
206+
207+
The `update` command will update a present `.gitattributes` file in the given directory. This command replaces the `--overwrite`
208+
option of the `validate` command. Please migrate to the dedicated commands.
209+
196210
#### Init command
197211

198212
The `init` command will create an initial `.lpv` file with the default patterns used to match common repository artifacts.

bin/lean-package-validator

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ if (false === $autoloaded) {
2424
\define('WORKING_DIRECTORY', \getcwd());
2525
\define('VERSION', '4.7.1');
2626

27+
use Stolt\LeanPackage\Commands\CreateCommand;
2728
use Stolt\LeanPackage\Commands\InitCommand;
2829
use Stolt\LeanPackage\Commands\TreeCommand;
30+
use Stolt\LeanPackage\Commands\UpdateCommand;
2931
use Stolt\LeanPackage\Commands\ValidateCommand;
3032
use Stolt\LeanPackage\Analyser;
3133
use Stolt\LeanPackage\Archive;
3234
use Stolt\LeanPackage\Archive\Validator;
35+
use Stolt\LeanPackage\GitattributesFileRepository;
3336
use Stolt\LeanPackage\Helpers\PhpInputReader;
3437
use Stolt\LeanPackage\Presets\Finder;
3538
use Stolt\LeanPackage\Presets\PhpPreset;
@@ -48,10 +51,12 @@ $validateCommand = new ValidateCommand(
4851
new Validator($archive),
4952
new PhpInputReader()
5053
);
54+
$createCommand = new CreateCommand($analyser, new GItattributesFileRepository($analyser));
55+
$updateCommand = new UpdateCommand($analyser, new GItattributesFileRepository($analyser));
5156
$treeCommand = new TreeCommand(new Tree(new Archive(WORKING_DIRECTORY,'tree-temp')));
5257

5358
$application = new Application('Lean package validator', VERSION);
5459
$application->addCommands(
55-
[$initCommand, $validateCommand, $treeCommand]
60+
[$initCommand, $validateCommand, $createCommand, $updateCommand, $treeCommand]
5661
);
5762
$application->run();
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Stolt\LeanPackage\Commands\Concerns;
6+
7+
use SplFileInfo;
8+
use Stolt\LeanPackage\Analyser;
9+
use Stolt\LeanPackage\Exceptions\InvalidGlobPattern;
10+
use Stolt\LeanPackage\Exceptions\InvalidGlobPatternFile;
11+
use Stolt\LeanPackage\Exceptions\NonExistentGlobPatternFile;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Input\InputOption;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
trait GeneratesGitattributesOptions
17+
{
18+
protected string $defaultPreset = 'Php';
19+
20+
protected function getDefaultLpvFile(): string
21+
{
22+
$base = \defined('WORKING_DIRECTORY') ? WORKING_DIRECTORY : (\getcwd() ?: '.');
23+
24+
return $base . DIRECTORY_SEPARATOR . '.lpv';
25+
}
26+
27+
protected function addGenerationOptions(callable $addOption): void
28+
{
29+
// Glob/preset related
30+
$addOption('glob-pattern', null, InputOption::VALUE_REQUIRED, 'Use this glob pattern to match artifacts that should be export-ignored');
31+
$addOption('glob-pattern-file', null, InputOption::VALUE_OPTIONAL, 'Use this file with glob patterns to match export-ignored artifacts', $this->getDefaultLpvFile());
32+
$addOption('preset', null, InputOption::VALUE_OPTIONAL, 'Use the glob pattern of the given language preset', $this->defaultPreset);
33+
34+
// Keep rules
35+
$addOption('keep-license', null, InputOption::VALUE_NONE, 'Do not export-ignore the license file');
36+
$addOption('keep-readme', null, InputOption::VALUE_NONE, 'Do not export-ignore the README file');
37+
$addOption('keep-glob-pattern', null, InputOption::VALUE_REQUIRED, 'Do not export-ignore matching glob pattern e.g. {LICENSE.*,README.*,docs*}');
38+
39+
// Layout/ordering
40+
$addOption('align-export-ignores', 'a', InputOption::VALUE_NONE, 'Align export-ignores on create or overwrite');
41+
$addOption('sort-from-directories-to-files', 's', InputOption::VALUE_NONE, 'Sort export-ignores from directories to files');
42+
$addOption('enforce-strict-order', null, InputOption::VALUE_NONE, 'Enforce strict order comparison (useful for consistency)');
43+
}
44+
45+
/**
46+
* Apply generation-related options to the analyser.
47+
*/
48+
protected function applyGenerationOptions(InputInterface $input, OutputInterface $output, Analyser $analyser): bool
49+
{
50+
$globPattern = $input->getOption('glob-pattern');
51+
$globPatternFile = (string) $input->getOption('glob-pattern-file');
52+
$chosenPreset = (string) $input->getOption('preset');
53+
54+
$keepLicense = (bool) $input->getOption('keep-license');
55+
$keepReadme = (bool) $input->getOption('keep-readme');
56+
$keepGlobPattern = (string) ($input->getOption('keep-glob-pattern') ?? '');
57+
58+
$alignExportIgnores = (bool) $input->getOption('align-export-ignores');
59+
$sortFromDirectoriesToFiles = (bool) $input->getOption('sort-from-directories-to-files');
60+
$enforceStrictOrderComparison = (bool) $input->getOption('enforce-strict-order');
61+
62+
// Order comparison (for consistency in generation/validation flow)
63+
if ($enforceStrictOrderComparison && $sortFromDirectoriesToFiles === false) {
64+
$output->writeln('+ Enforcing strict order comparison.', OutputInterface::VERBOSITY_VERBOSE);
65+
$analyser->enableStrictOrderComparison();
66+
}
67+
68+
if ($sortFromDirectoriesToFiles) {
69+
$output->writeln('+ Sorting from files to directories.', OutputInterface::VERBOSITY_VERBOSE);
70+
$analyser->sortFromDirectoriesToFiles();
71+
}
72+
73+
if ($keepLicense) {
74+
$output->writeln('+ Keeping the license file.', OutputInterface::VERBOSITY_VERBOSE);
75+
$analyser->keepLicense();
76+
}
77+
78+
if ($keepReadme) {
79+
$output->writeln('+ Keeping the README file.', OutputInterface::VERBOSITY_VERBOSE);
80+
$analyser->keepReadme();
81+
}
82+
83+
if ($keepGlobPattern !== '') {
84+
$output->writeln(\sprintf('+ Keeping files matching the glob pattern <info>%s</info>.', $keepGlobPattern), OutputInterface::VERBOSITY_VERBOSE);
85+
try {
86+
$analyser->setKeepGlobPattern($keepGlobPattern);
87+
} catch (InvalidGlobPattern $e) {
88+
$warning = "Warning: The provided glob pattern '{$keepGlobPattern}' is considered invalid.";
89+
$output->writeln('<error>' . $warning . '</error>');
90+
$output->writeln($e->getMessage(), OutputInterface::VERBOSITY_DEBUG);
91+
92+
return false;
93+
}
94+
}
95+
96+
if ($alignExportIgnores) {
97+
$output->writeln('+ Aligning the export-ignores.', OutputInterface::VERBOSITY_VERBOSE);
98+
$analyser->alignExportIgnores();
99+
}
100+
101+
// Glob selection/override order: explicit pattern -> glob file -> preset
102+
if ($globPattern || $globPattern === '') {
103+
try {
104+
$output->writeln("+ Using glob pattern <info>{$globPattern}</info>.", OutputInterface::VERBOSITY_VERBOSE);
105+
$analyser->setGlobPattern((string) $globPattern);
106+
} catch (InvalidGlobPattern $e) {
107+
$warning = "Warning: The provided glob pattern '{$globPattern}' is considered invalid.";
108+
$output->writeln('<error>' . $warning . '</error>');
109+
$output->writeln($e->getMessage(), OutputInterface::VERBOSITY_DEBUG);
110+
111+
return false;
112+
}
113+
} elseif ($this->isGlobPatternFileSettable($globPatternFile)) {
114+
try {
115+
if ($this->isDefaultGlobPatternFilePresent()) {
116+
$analyser->setGlobPatternFromFile($globPatternFile);
117+
}
118+
if ($globPatternFile) {
119+
$globPatternFileInfo = new SplFileInfo($globPatternFile);
120+
$output->writeln('+ Using ' . $globPatternFileInfo->getBasename() . ' file as glob pattern input.', OutputInterface::VERBOSITY_VERBOSE);
121+
122+
$analyser->setGlobPatternFromFile($globPatternFile);
123+
}
124+
} catch (NonExistentGlobPatternFile $e) {
125+
$warning = "Warning: The provided glob pattern file '{$globPatternFile}' doesn't exist.";
126+
$output->writeln('<error>' . $warning . '</error>');
127+
$output->writeln($e->getMessage(), OutputInterface::VERBOSITY_DEBUG);
128+
129+
return false;
130+
} catch (InvalidGlobPatternFile $e) {
131+
$warning = "Warning: The provided glob pattern file '{$globPatternFile}' is considered invalid.";
132+
$output->writeln('<error>' . $warning . '</error>');
133+
$output->writeln($e->getMessage(), OutputInterface::VERBOSITY_DEBUG);
134+
135+
return false;
136+
}
137+
} elseif ($chosenPreset) {
138+
try {
139+
$output->writeln('+ Using the ' . $chosenPreset . ' language preset.', OutputInterface::VERBOSITY_VERBOSE);
140+
$analyser->setGlobPatternFromPreset($chosenPreset);
141+
} catch (\Stolt\LeanPackage\Exceptions\PresetNotAvailable $e) {
142+
$warning = 'Warning: The chosen language preset ' . $chosenPreset . ' is not available. Maybe contribute it?.';
143+
$output->writeln('<error>' . $warning . '</error>');
144+
145+
return false;
146+
}
147+
}
148+
149+
return true;
150+
}
151+
152+
// Minimal copies of helper checks used in ValidateCommand
153+
protected function isGlobPatternFileSettable(string $file): bool
154+
{
155+
if ($this->isGlobPatternFileProvided($file)) {
156+
return true;
157+
}
158+
159+
return $this->isDefaultGlobPatternFilePresent();
160+
}
161+
162+
protected function isGlobPatternFileProvided(string $file): bool
163+
{
164+
return $file !== $this->getDefaultLpvFile();
165+
}
166+
167+
protected function isDefaultGlobPatternFilePresent(): bool
168+
{
169+
return \file_exists($this->getDefaultLpvFile());
170+
}
171+
}

src/Commands/CreateCommand.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Stolt\LeanPackage\Commands;
6+
7+
use Stolt\LeanPackage\Analyser;
8+
use Stolt\LeanPackage\Commands\Concerns\GeneratesGitattributesOptions;
9+
use Stolt\LeanPackage\GitattributesFileRepository;
10+
use Symfony\Component\Console\Command\Command;
11+
use Symfony\Component\Console\Input\InputArgument;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Input\InputOption;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
final class CreateCommand extends Command
17+
{
18+
use GeneratesGitattributesOptions;
19+
20+
/**
21+
* @var string $defaultName
22+
*/
23+
protected static $defaultName = 'create';
24+
/**
25+
* @var string $defaultDescription
26+
*/
27+
protected static $defaultDescription = 'Create a new .gitattributes file for a project/micro-package repository';
28+
29+
public function __construct(
30+
private readonly Analyser $analyser,
31+
private readonly GitattributesFileRepository $repository
32+
) {
33+
parent::__construct();
34+
}
35+
36+
protected function configure(): void
37+
{
38+
$this
39+
->addArgument(
40+
'directory',
41+
InputArgument::OPTIONAL,
42+
'The package directory to create the .gitattributes file in',
43+
\defined('WORKING_DIRECTORY') ? WORKING_DIRECTORY : \getcwd()
44+
)->setName(self::$defaultName)->setDescription(self::$defaultDescription);
45+
46+
// Add common generation options
47+
$this->addGenerationOptions(function (...$args) {
48+
$this->getDefinition()->addOption(new InputOption(...$args));
49+
});
50+
}
51+
52+
protected function execute(InputInterface $input, OutputInterface $output): int
53+
{
54+
$directory = (string) $input->getArgument('directory') ?: \getcwd();
55+
$this->analyser->setDirectory($directory);
56+
57+
// Apply options that influence generation
58+
if (!$this->applyGenerationOptions($input, $output, $this->analyser)) {
59+
return self::FAILURE;
60+
}
61+
62+
$gitattributesPath = $this->analyser->getGitattributesFilePath();
63+
64+
if (\file_exists($gitattributesPath)) {
65+
$output->writeln('A .gitattributes file already exists. Use the update command to modify it.');
66+
67+
return self::FAILURE;
68+
}
69+
70+
$expected = $this->analyser->getExpectedGitattributesContent();
71+
72+
if ($expected === '') {
73+
$output->writeln('Unable to determine expected .gitattributes content for the given directory.');
74+
75+
return self::FAILURE;
76+
}
77+
78+
try {
79+
$this->repository->createGitattributesFile($expected);
80+
} catch (\Throwable $e) {
81+
$output->writeln('Creation of .gitattributes file failed.');
82+
83+
return self::FAILURE;
84+
}
85+
86+
$output->writeln("A .gitattributes file has been created at {$directory}.");
87+
88+
return self::SUCCESS;
89+
}
90+
}

0 commit comments

Comments
 (0)