Skip to content

Commit bbced27

Browse files
committed
Command to inspect a recipe
1 parent 137cc29 commit bbced27

File tree

2 files changed

+172
-26
lines changed

2 files changed

+172
-26
lines changed

src/Command/RecipesCommand.php

Lines changed: 170 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,61 @@
1212
namespace Symfony\Flex\Command;
1313

1414
use Composer\Command\BaseCommand;
15-
use Composer\Factory;
15+
use Symfony\Component\Console\Input\InputArgument;
1616
use Symfony\Component\Console\Input\InputInterface;
1717
use Symfony\Component\Console\Output\OutputInterface;
1818
use Symfony\Flex\InformationOperation;
1919
use Symfony\Flex\Lock;
2020
use Symfony\Flex\Recipe;
2121

22+
/**
23+
* @author Maxime Hélias <[email protected]>
24+
*/
2225
class RecipesCommand extends BaseCommand
2326
{
2427
/** @var \Symfony\Flex\Flex */
2528
private $flex;
2629

27-
public function __construct(/* cannot be type-hinted */ $flex)
30+
/** @var Lock */
31+
private $symfonyLock;
32+
33+
public function __construct(/* cannot be type-hinted */ $flex, Lock $symfonyLock)
2834
{
2935
$this->flex = $flex;
36+
$this->symfonyLock = $symfonyLock;
3037

3138
parent::__construct();
3239
}
3340

3441
protected function configure()
3542
{
36-
$this->setName('symfony:recipes:show')
37-
->setAliases(['symfony:recipes'])
43+
$this->setName('symfony:recipes')
44+
->setAliases(['recipes'])
3845
->setDescription('Shows information about all available recipes.')
46+
->setDefinition([
47+
new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect, if not provided all packages are.'),
48+
])
3949
;
4050
}
4151

4252
protected function execute(InputInterface $input, OutputInterface $output)
4353
{
44-
$locker = $this->getComposer()->getLocker();
45-
$lockData = $locker->getLockData();
54+
$installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
4655

47-
// Merge all packages installed
48-
$packages = array_merge($lockData['packages'], $lockData['packages-dev']);
56+
// Inspect one or all packages
57+
$package = $input->getArgument('package');
58+
if (null !== $package) {
59+
$packages = [0 => ['name' => strtolower($package)]];
60+
} else {
61+
$locker = $this->getComposer()->getLocker();
62+
$lockData = $locker->getLockData();
4963

50-
$installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository();
64+
// Merge all packages installed
65+
$packages = array_merge($lockData['packages'], $lockData['packages-dev']);
66+
}
5167

5268
$operations = [];
53-
foreach ($packages as $key => $value) {
69+
foreach ($packages as $value) {
5470
if (null === $pkg = $installedRepo->findPackage($value['name'], '*')) {
5571
$this->getIO()->writeError(sprintf('<error>Package %s is not installed</error>', $value['name']));
5672

@@ -63,12 +79,21 @@ protected function execute(InputInterface $input, OutputInterface $output)
6379
$recipes = $this->flex->fetchRecipes($operations);
6480
ksort($recipes);
6581

66-
if (\count($recipes) <= 0) {
82+
$nbRecipe = \count($recipes);
83+
if ($nbRecipe <= 0) {
6784
$this->getIO()->writeError('<error>No recipe found</error>');
6885

6986
return 1;
7087
}
7188

89+
// Display the information about a specific recipe
90+
if (1 === $nbRecipe) {
91+
$this->displayPackageInformation(current($recipes));
92+
93+
return 0;
94+
}
95+
96+
// display a resume of all packages
7297
$write = [
7398
'',
7499
'<bg=blue;fg=white> </>',
@@ -77,33 +102,154 @@ protected function execute(InputInterface $input, OutputInterface $output)
77102
'',
78103
];
79104

80-
$symfonyLock = new Lock(getenv('SYMFONY_LOCKFILE') ?: str_replace('composer.json', 'symfony.lock', Factory::getComposerFile()));
81-
82105
/** @var Recipe $recipe */
83106
foreach ($recipes as $name => $recipe) {
84-
$lockRef = $symfonyLock->get($name)['recipe']['ref'] ?? null;
107+
$lockRef = $this->symfonyLock->get($name)['recipe']['ref'] ?? null;
85108

86109
$additional = '';
87-
if ($recipe->isAuto()) {
88-
$additional = '<comment>(auto recipe)</comment>';
89-
} elseif (null === $lockRef && null !== $recipe->getRef()) {
90-
$additional = '<comment>(new recipe available)</comment>';
110+
if (null === $lockRef && null !== $recipe->getRef()) {
111+
$additional = '<comment>(recipe not installed)</comment>';
91112
} elseif ($recipe->getRef() !== $lockRef) {
92113
$additional = '<comment>(update available)</comment>';
93114
}
94115
$write[] = sprintf(' * %s %s', $name, $additional);
95116
}
96117

97-
// TODO : Uncomment the lines following the implemented features
98-
//$write[] = '';
99-
//$write[] = '<fg=blue>Run</>:';
100-
//$write[] = ' * composer symfony:recipes vendor/package to see details about a recipe.';
101-
//$write[] = ' * composer symfony:recipes:update vendor/package to update that recipe.';
102-
//$write[] = ' * composer symfony:recipes:blame to see a tree of all of the installed/updated files.';
118+
$write[] = '';
119+
$write[] = 'Run:';
120+
$write[] = ' * <info>composer recipes vendor/package</info> to see details about a recipe.';
121+
$write[] = ' * <info>composer recipes:install vendor/package --force -v</info> to update that recipe.';
103122
$write[] = '';
104123

105124
$this->getIO()->write($write);
106125

107126
return 0;
108127
}
128+
129+
private function displayPackageInformation(Recipe $recipe)
130+
{
131+
$recipeLock = $this->symfonyLock->get($recipe->getName());
132+
133+
$lockRef = $recipeLock['recipe']['ref'] ?? null;
134+
$lockFiles = $recipeLock['files'] ?? null;
135+
136+
$status = '<comment>up to date</comment>';
137+
if ($recipe->isAuto()) {
138+
$status = '<comment>auto-generated recipe</comment>';
139+
} elseif (null === $lockRef && null !== $recipe->getRef()) {
140+
$status = '<comment>recipe not installed</comment>';
141+
} elseif ($recipe->getRef() !== $lockRef) {
142+
$status = '<comment>update available</comment>';
143+
}
144+
145+
$io = $this->getIO();
146+
$io->write('<info>name</info> : '.$recipe->getName());
147+
$io->write('<info>version</info> : '.$recipeLock['version']);
148+
if (!$recipe->isAuto()) {
149+
$io->write('<info>repo</info> : '.sprintf('https://%s/tree/master/%s/%s', $recipeLock['recipe']['repo'], $recipe->getName(), $recipeLock['version']));
150+
}
151+
$io->write('<info>status</info> : '.$status);
152+
153+
if (null !== $lockFiles) {
154+
$io->write('<info>files</info> : ');
155+
$io->write('');
156+
157+
$tree = $this->generateFilesTree($lockFiles);
158+
159+
$this->displayFilesTree($tree);
160+
}
161+
162+
$io->write([
163+
'',
164+
'Update this recipe by running:',
165+
sprintf('<info>composer recipes:install %s --force -v</info>', $recipe->getName()),
166+
]);
167+
}
168+
169+
private function generateFilesTree(array $files): array
170+
{
171+
$tree = [];
172+
foreach ($files as $file) {
173+
$path = explode('/', $file);
174+
175+
$tree = array_merge_recursive($tree, $this->addNode($path));
176+
}
177+
178+
return $tree;
179+
}
180+
181+
private function addNode(array $node): array
182+
{
183+
$current = array_shift($node);
184+
185+
$subTree = [];
186+
if (null !== $current) {
187+
$subTree[$current] = $this->addNode($node);
188+
}
189+
190+
return $subTree;
191+
}
192+
193+
/**
194+
* Note : We do not display file modification information with Configurator like ComposerScripts, Container, DockerComposer, Dockerfile, Env, Gitignore and Makefile.
195+
*/
196+
private function displayFilesTree(array $tree)
197+
{
198+
$endKey = array_key_last($tree);
199+
foreach ($tree as $dir => $files) {
200+
$treeBar = '';
201+
$total = \count($files);
202+
if (0 === $total || $endKey === $dir) {
203+
$treeBar = '';
204+
}
205+
206+
$info = sprintf(
207+
'%s──%s',
208+
$treeBar,
209+
$dir
210+
);
211+
$this->writeTreeLine($info);
212+
213+
$treeBar = str_replace('', ' ', $treeBar);
214+
215+
$this->displayTree($files, $treeBar);
216+
}
217+
}
218+
219+
private function displayTree(array $tree, $previousTreeBar = '', $level = 1)
220+
{
221+
$previousTreeBar = str_replace('', '', $previousTreeBar);
222+
$treeBar = $previousTreeBar.'';
223+
224+
$i = 0;
225+
$total = \count($tree);
226+
227+
foreach ($tree as $dir => $files) {
228+
++$i;
229+
if ($i === $total) {
230+
$treeBar = $previousTreeBar.'';
231+
}
232+
233+
$info = sprintf(
234+
'%s──%s',
235+
$treeBar,
236+
$dir
237+
);
238+
$this->writeTreeLine($info);
239+
240+
$treeBar = str_replace('', ' ', $treeBar);
241+
242+
$this->displayTree($files, $treeBar, $level + 1);
243+
}
244+
}
245+
246+
private function writeTreeLine($line)
247+
{
248+
$io = $this->getIO();
249+
if (!$io->isDecorated()) {
250+
$line = str_replace(['', '', '──', ''], ['`-', '|-', '-', '|'], $line);
251+
}
252+
253+
$io->write($line);
254+
}
109255
}

src/Flex.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
243243
$app->add(new Command\UpdateCommand($resolver));
244244
$app->add(new Command\RemoveCommand($resolver));
245245
$app->add(new Command\UnpackCommand($resolver));
246-
$app->add(new Command\RecipesCommand($this));
246+
$app->add(new Command\RecipesCommand($this, $this->lock));
247247
$app->add(new Command\SyncRecipesCommand($this, $this->options->get('root-dir')));
248248
$app->add(new Command\GenerateIdCommand($this));
249249
$app->add(new Command\DumpEnvCommand($this->config, $this->options));
@@ -404,7 +404,7 @@ public function install(Event $event = null)
404404
}
405405

406406
$recipes = $this->fetchRecipes($this->operations);
407-
$this->operations = []; // Reset the operation after getting recipes
407+
$this->operations = []; // This was done so that we could re-use $this->fetchRecipes() elsewhere - but keep "clearing" $this->operations, which the existing code does currently.
408408

409409
if (2 === $this->displayThanksReminder) {
410410
$love = '\\' === \DIRECTORY_SEPARATOR ? 'love' : '💖 ';

0 commit comments

Comments
 (0)