Skip to content

Commit 691c878

Browse files
committed
Add a dedicated file deletion for unconfigure recipes
1 parent 5f92875 commit 691c878

10 files changed

+197
-123
lines changed

src/Configurator/CopyFromRecipeConfigurator.php

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function configure(Recipe $recipe, $config, Lock $lock, array $options =
3131
public function unconfigure(Recipe $recipe, $config, Lock $lock)
3232
{
3333
$this->write('Removing files from recipe');
34-
$this->removeFiles($config, $this->getRemovableFilesFromRecipeAndLock($recipe, $lock), $this->options->get('root-dir'));
34+
$this->removeFiles($config, $this->options->getRemovableFilesFromRecipeAndLock($recipe), $this->options->get('root-dir'));
3535
}
3636

3737
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
@@ -66,32 +66,6 @@ private function resolveTargetFolder(string $path, array $config): string
6666
return $path;
6767
}
6868

69-
private function getRemovableFilesFromRecipeAndLock(Recipe $recipe, Lock $lock): array
70-
{
71-
$lockedFiles = array_unique(
72-
array_reduce(
73-
array_column($lock->all(), 'files'),
74-
function (array $carry, array $package) {
75-
return array_merge($carry, $package);
76-
},
77-
[]
78-
)
79-
);
80-
81-
$removableFiles = $recipe->getFiles();
82-
83-
$lockedFiles = array_map('realpath', $lockedFiles);
84-
85-
// Compare file paths by their real path to abstract OS differences
86-
foreach (array_keys($removableFiles) as $file) {
87-
if (\in_array(realpath($file), $lockedFiles)) {
88-
unset($removableFiles[$file]);
89-
}
90-
}
91-
92-
return $removableFiles;
93-
}
94-
9569
private function copyFiles(array $manifest, array $files, array $options): array
9670
{
9771
$copiedFiles = [];

src/FilesManager.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Flex;
13+
14+
use Composer\IO\IOInterface;
15+
use Composer\Util\ProcessExecutor;
16+
17+
/**
18+
* @author Maxime Hélias <[email protected]>
19+
*/
20+
class FilesManager
21+
{
22+
private $io;
23+
protected $path;
24+
25+
private $writtenFiles = [];
26+
private $files;
27+
28+
public function __construct(IOInterface $io, Lock $lock, string $rootDir)
29+
{
30+
$this->io = $io;
31+
32+
$this->path = new Path($rootDir);
33+
$this->files = array_count_values(
34+
array_map(
35+
function (string $file) {
36+
return realpath($file) ?: '';
37+
}, array_reduce(
38+
array_column($lock->all(), 'files'),
39+
function (array $carry, array $package) {
40+
return array_merge($carry, $package);
41+
},
42+
[]
43+
)
44+
)
45+
);
46+
}
47+
48+
public function shouldWriteFile(string $file, bool $overwrite, bool $skipQuestion): bool
49+
{
50+
if (isset($this->writtenFiles[$file])) {
51+
return false;
52+
}
53+
$this->writtenFiles[$file] = true;
54+
55+
if (!file_exists($file)) {
56+
return true;
57+
}
58+
59+
if (!$overwrite) {
60+
return false;
61+
}
62+
63+
if (!filesize($file)) {
64+
return true;
65+
}
66+
67+
if ($skipQuestion) {
68+
return true;
69+
}
70+
71+
exec('git status --short --ignored --untracked-files=all -- '.ProcessExecutor::escape($file).' 2>&1', $output, $status);
72+
73+
if (0 !== $status) {
74+
return $this->io->askConfirmation(\sprintf('Cannot determine the state of the "%s" file, overwrite anyway? [y/N] ', $file), false);
75+
}
76+
77+
if (empty($output[0]) || preg_match('/^[ AMDRCU][ D][ \t]/', $output[0])) {
78+
return true;
79+
}
80+
81+
$name = basename($file);
82+
$name = \strlen($output[0]) - \strlen($name) === strrpos($output[0], $name) ? substr($output[0], 3) : $name;
83+
84+
return $this->io->askConfirmation(\sprintf('File "%s" has uncommitted changes, overwrite? [y/N] ', $name), false);
85+
}
86+
87+
public function getRemovableFilesFromRecipeAndLock(Recipe $recipe): array
88+
{
89+
$removableFiles = $recipe->getFiles();
90+
// Compare file paths by their real path to abstract OS differences
91+
foreach (array_keys($removableFiles) as $file) {
92+
$file = realpath($file);
93+
if (!isset($this->files[$file])) {
94+
continue;
95+
}
96+
97+
--$this->files[$file];
98+
99+
if ($this->files[$file] <= 0) {
100+
unset($removableFiles[$file]);
101+
}
102+
}
103+
104+
return $removableFiles;
105+
}
106+
}

src/Flex.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class Flex implements PluginInterface, EventSubscriberInterface
6666

6767
private $config;
6868
private $options;
69+
private $filesManager;
6970
private $configurator;
7071
private $downloader;
7172

@@ -111,7 +112,13 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
111112
$this->composer = $composer;
112113
$this->io = $io;
113114
$this->config = $composer->getConfig();
114-
$this->options = $this->initOptions();
115+
116+
$composerFile = Factory::getComposerFile();
117+
$composerLock = 'json' === pathinfo($composerFile, \PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile.'.lock';
118+
$symfonyLock = str_replace('composer', 'symfony', basename($composerLock));
119+
$this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: \dirname($composerLock).'/'.(basename($composerLock) !== $symfonyLock ? $symfonyLock : 'symfony.lock'));
120+
121+
$this->options = $this->initOptions($this->io, $this->lock);
115122

116123
// if Flex is being upgraded, the original operations from the original Flex
117124
// instance are stored in the static property, so we can reuse them now.
@@ -130,12 +137,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
130137
$this->filter = new PackageFilter($io, $symfonyRequire, $this->downloader);
131138
}
132139

133-
$composerFile = Factory::getComposerFile();
134-
$composerLock = 'json' === pathinfo($composerFile, \PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile.'.lock';
135-
$symfonyLock = str_replace('composer', 'symfony', basename($composerLock));
136-
137140
$this->configurator = new Configurator($composer, $io, $this->options);
138-
$this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: \dirname($composerLock).'/'.(basename($composerLock) !== $symfonyLock ? $symfonyLock : 'symfony.lock'));
139141

140142
$disable = true;
141143
foreach (array_merge($composer->getPackage()->getRequires() ?? [], $composer->getPackage()->getDevRequires() ?? []) as $link) {
@@ -701,7 +703,7 @@ public function getLock(): Lock
701703
return $this->lock;
702704
}
703705

704-
private function initOptions(): Options
706+
private function initOptions(IOInterface $io, Lock $lock): Options
705707
{
706708
$extra = $this->composer->getPackage()->getExtra();
707709

@@ -716,7 +718,7 @@ private function initOptions(): Options
716718
'runtime' => $extra['runtime'] ?? [],
717719
], $extra);
718720

719-
return new Options($options, $this->io);
721+
return new Options($options, new FilesManager($io, $lock, $options['root-dir']));
720722
}
721723

722724
private function formatOrigin(Recipe $recipe): string

src/Options.php

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,18 @@
1111

1212
namespace Symfony\Flex;
1313

14-
use Composer\IO\IOInterface;
15-
use Composer\Util\ProcessExecutor;
16-
1714
/**
1815
* @author Fabien Potencier <[email protected]>
1916
*/
2017
class Options
2118
{
2219
private $options;
23-
private $writtenFiles = [];
24-
private $io;
20+
private $filesManager;
2521

26-
public function __construct(array $options = [], ?IOInterface $io = null)
22+
public function __construct(array $options = [], ?FilesManager $filesManager = null)
2723
{
2824
$this->options = $options;
29-
$this->io = $io;
25+
$this->filesManager = $filesManager;
3026
}
3127

3228
public function get(string $name)
@@ -64,41 +60,20 @@ public function expandTargetDir(string $target): string
6460

6561
public function shouldWriteFile(string $file, bool $overwrite, bool $skipQuestion): bool
6662
{
67-
if (isset($this->writtenFiles[$file])) {
68-
return false;
69-
}
70-
$this->writtenFiles[$file] = true;
71-
72-
if (!file_exists($file)) {
63+
if (null === $this->filesManager) {
7364
return true;
7465
}
7566

76-
if (!$overwrite) {
77-
return false;
78-
}
79-
80-
if (!filesize($file)) {
81-
return true;
82-
}
83-
84-
if ($skipQuestion) {
85-
return true;
86-
}
87-
88-
exec('git status --short --ignored --untracked-files=all -- '.ProcessExecutor::escape($file).' 2>&1', $output, $status);
89-
90-
if (0 !== $status) {
91-
return $this->io && $this->io->askConfirmation(\sprintf('Cannot determine the state of the "%s" file, overwrite anyway? [y/N] ', $file), false);
92-
}
67+
return $this->filesManager->shouldWriteFile($file, $overwrite, $skipQuestion);
68+
}
9369

94-
if (empty($output[0]) || preg_match('/^[ AMDRCU][ D][ \t]/', $output[0])) {
95-
return true;
70+
public function getRemovableFilesFromRecipeAndLock(Recipe $recipe): array
71+
{
72+
if (null === $this->filesManager) {
73+
return [];
9674
}
9775

98-
$name = basename($file);
99-
$name = \strlen($output[0]) - \strlen($name) === strrpos($output[0], $name) ? substr($output[0], 3) : $name;
100-
101-
return $this->io && $this->io->askConfirmation(\sprintf('File "%s" has uncommitted changes, overwrite? [y/N] ', $name), false);
76+
return $this->filesManager->getRemovableFilesFromRecipeAndLock($recipe);
10277
}
10378

10479
public function toArray(): array

tests/Command/UpdateRecipesCommandTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Symfony\Flex\Configurator;
2525
use Symfony\Flex\Downloader;
2626
use Symfony\Flex\Flex;
27+
use Symfony\Flex\Lock;
2728
use Symfony\Flex\Options;
2829
use Symfony\Flex\ParallelDownloader;
2930

tests/Configurator/CopyDirectoryFromPackageConfiguratorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ protected function tearDown(): void
194194

195195
private function createConfigurator(): CopyFromPackageConfigurator
196196
{
197-
return new CopyFromPackageConfigurator($this->composer, $this->io, new Options(['root-dir' => FLEX_TEST_DIR], $this->io));
197+
return new CopyFromPackageConfigurator($this->composer, $this->io, new Options(['root-dir' => FLEX_TEST_DIR]));
198198
}
199199

200200
private function cleanUpTargetFiles()

tests/Configurator/CopyFromPackageConfiguratorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ protected function tearDown(): void
168168

169169
private function createConfigurator(): CopyFromPackageConfigurator
170170
{
171-
return new CopyFromPackageConfigurator($this->composer, $this->io, new Options(['root-dir' => FLEX_TEST_DIR], $this->io));
171+
return new CopyFromPackageConfigurator($this->composer, $this->io, new Options(['root-dir' => FLEX_TEST_DIR]));
172172
}
173173

174174
private function cleanUpTargetFiles()

tests/Configurator/CopyFromRecipeConfiguratorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ protected function tearDown(): void
301301

302302
private function createConfigurator(): CopyFromRecipeConfigurator
303303
{
304-
return new CopyFromRecipeConfigurator($this->getMockBuilder(Composer::class)->getMock(), $this->io, new Options(['root-dir' => FLEX_TEST_DIR, 'config-dir' => 'config'], $this->io));
304+
return new CopyFromRecipeConfigurator($this->getMockBuilder(Composer::class)->getMock(), $this->io, new Options(['root-dir' => FLEX_TEST_DIR, 'config-dir' => 'config']));
305305
}
306306

307307
private function cleanUpTargetFiles()

tests/FilesManagerTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Flex\Tests;
13+
14+
use Composer\IO\IOInterface;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Process\Process;
17+
use Symfony\Flex\FilesManager;
18+
use Symfony\Flex\Lock;
19+
20+
class FilesManagerTest extends TestCase
21+
{
22+
public function testShouldWrite()
23+
{
24+
@mkdir(FLEX_TEST_DIR);
25+
(new Process(['git', 'init'], FLEX_TEST_DIR))->mustRun();
26+
(new Process(['git', 'config', 'user.name', 'Unit test'], FLEX_TEST_DIR))->mustRun();
27+
(new Process(['git', 'config', 'user.email', ''], FLEX_TEST_DIR))->mustRun();
28+
29+
$filePath = FLEX_TEST_DIR.'/a.txt';
30+
file_put_contents($filePath, 'a');
31+
(new Process(['git', 'add', '-A'], FLEX_TEST_DIR))->mustRun();
32+
(new Process(['git', 'commit', '-m', 'setup of original files'], FLEX_TEST_DIR))->mustRun();
33+
34+
file_put_contents($filePath, 'b');
35+
36+
$io = $this->getMockBuilder(IOInterface::class)->getMock();
37+
$io->method('askConfirmation')->willReturn(true);
38+
39+
$lock = $this->getMockBuilder(Lock::class)->disableOriginalConstructor()->getMock();
40+
$lock->method('all')->willReturn([
41+
'symfony/my-package' => [
42+
'files' => [
43+
$filePath,
44+
],
45+
],
46+
]);
47+
48+
$filesManager = new FilesManager($io, $lock, FLEX_TEST_DIR);
49+
50+
// We need to set the writtenFiles property to reset the state
51+
$reflection = new \ReflectionProperty(FilesManager::class, 'writtenFiles');
52+
$reflection->setAccessible(true);
53+
54+
$this->assertTrue($filesManager->shouldWriteFile('non-existing-file.txt', false, false));
55+
$this->assertFalse($filesManager->shouldWriteFile($filePath, false, false));
56+
57+
// It allowed to write the file
58+
$reflection->setValue($filesManager, []);
59+
$this->assertTrue($filesManager->shouldWriteFile($filePath, true, false));
60+
61+
// We skip all questions, so we're able to write
62+
$reflection->setValue($filesManager, []);
63+
$this->assertTrue($filesManager->shouldWriteFile($filePath, true, true));
64+
}
65+
}

0 commit comments

Comments
 (0)