Skip to content

Commit 7907b8a

Browse files
committed
Replaces dependency on Unix tree command
1 parent 84acf77 commit 7907b8a

File tree

4 files changed

+75
-140
lines changed

4 files changed

+75
-140
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,7 @@ Available presets are `PHP`, `Python`, and `Go`. With `PHP` being the default.
197197
#### Tree command
198198

199199
The `tree` command of the lean package validator allows you to inspect the __flat__ `source` and `dist package` structure
200-
of the given project/micro-package. It requires the [tree](https://en.wikipedia.org/wiki/Tree_(command)) Unix command to
201-
be installed in version `>=2.0`.
200+
of the given project/micro-package.
202201

203202
``` bash
204203
lean-package-validator tree [<directory>] --src

bin/lean-package-validator

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ $validateCommand = new ValidateCommand(
4646
$analyser,
4747
new Validator($archive)
4848
);
49-
$treeCommand = new TreeCommand(new Tree($archive));
49+
$treeCommand = new TreeCommand(new Tree(new Archive(WORKING_DIRECTORY,'tree-temp')));
5050

5151
$application = new Application('Lean package validator', VERSION);
5252
$application->addCommands(

src/Tree.php

Lines changed: 66 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
namespace Stolt\LeanPackage;
44

5+
use FilesystemIterator;
56
use Stolt\LeanPackage\Exceptions\GitHeadNotAvailable;
67
use Stolt\LeanPackage\Exceptions\GitNotAvailable;
7-
use Stolt\LeanPackage\Exceptions\TreeNotAvailable;
8-
use Stolt\LeanPackage\Helpers\Str as OsHelper;
8+
use Symfony\Component\Finder\Finder;
99

1010
final class Tree
1111
{
@@ -23,75 +23,97 @@ public function __construct(Archive $archive)
2323
}
2424
}
2525

26-
/**
27-
* @throws TreeNotAvailable
28-
*/
2926
public function getTreeForSrc(string $directory): string
3027
{
31-
if (!$this->detectTreeCommand()) {
32-
throw new TreeNotAvailable('Unix tree command is not available.');
33-
}
34-
35-
if (!$this->detectTreeCommandVersion()) {
36-
throw new TreeNotAvailable('Required tree command version >=2.0 is not available.');
37-
}
38-
39-
$command = 'tree -aL 1 --dirsfirst ' . \escapeshellarg($directory) . ' --gitignore -I .git 2>&1';
40-
41-
\exec($command, $output);
42-
43-
$output[0] = '.';
44-
45-
return \implode(PHP_EOL, $output) . PHP_EOL;
28+
return $this->getTree($directory);
4629
}
4730

4831
/**
49-
* @throws TreeNotAvailable
5032
* @throws GitHeadNotAvailable
5133
* @throws GitNotAvailable
5234
*/
5335
public function getTreeForDistPackage(): string
5436
{
55-
if (!$this->detectTreeCommand()) {
56-
throw new TreeNotAvailable('Unix tree command is not available.');
37+
$this->archive->createArchive();
38+
$temporaryDirectory = \sys_get_temp_dir() . '/dist-release';
39+
40+
if (!\file_exists($temporaryDirectory)) {
41+
\mkdir($temporaryDirectory);
5742
}
5843

59-
$this->archive->createArchive();
44+
$command = 'tar -xf ' . \escapeshellarg($this->archive->getFilename()) . ' --directory ' . $temporaryDirectory . ' 2>&1';
6045

61-
$command = 'tar --list --exclude="*/*" --file ' . \escapeshellarg($this->archive->getFilename()) . ' | tree -aL 1 --dirsfirst --fromfile . 2>&1';
46+
\exec($command);
6247

63-
if ((new OsHelper())->isMacOs()) {
64-
$command = 'tar --list --file ' . \escapeshellarg($this->archive->getFilename()) . ' | tree -aL 1 --dirsfirst --fromfile . 2>&1';
65-
}
48+
$distReleaseTree = $this->getTree($temporaryDirectory);
6649

67-
\exec($command, $output);
6850

6951
$this->archive->removeArchive();
52+
$this->removeDirectory($temporaryDirectory);
7053

71-
return \implode(PHP_EOL, $output) . PHP_EOL;
54+
return $distReleaseTree;
7255
}
7356

74-
protected function detectTreeCommand(): bool
57+
private function getTree(string $directory): string
7558
{
76-
$command = 'where tree 2>&1';
77-
78-
if ((new OsHelper())->isWindows() === false) {
79-
$command = 'which tree 2>&1';
59+
$finder = new Finder();
60+
$finder->in($directory)->ignoreVCSIgnored(true)
61+
->ignoreDotFiles(false)->depth(0)->sortByName()->sortByType();
62+
63+
$tree[] = '.';
64+
65+
$index = 0;
66+
$directoryCount = 0;
67+
$fileCount = 0;
68+
69+
foreach ($finder as $file) {
70+
$index++;
71+
$filename = $file->getFilename();
72+
if ($file->isDir()) {
73+
$filename = $file->getFilename() . '/';
74+
$directoryCount++;
75+
}
76+
77+
if ($file->isFile()) {
78+
$fileCount++;
79+
}
80+
81+
if ($index < $finder->count()) {
82+
$tree[] = '├── ' . $filename;
83+
} else {
84+
$tree[] = '└── ' . $filename;
85+
}
8086
}
8187

82-
\exec($command, $output, $returnValue);
83-
84-
return $returnValue === 0;
88+
$tree[] = PHP_EOL;
89+
$tree[] = \sprintf(
90+
'%d %s, %d %s',
91+
$directoryCount,
92+
$directoryCount > 1 ? 'directories' : 'directory',
93+
$fileCount,
94+
$fileCount > 1 ? 'files': 'file'
95+
);
96+
$tree[] = PHP_EOL;
97+
98+
return \implode(PHP_EOL, $tree);
8599
}
86100

87-
protected function detectTreeCommandVersion(): bool
101+
protected function removeDirectory(string $directory): void
88102
{
89-
\exec('tree --version 2>&1', $output);
90-
91-
if (\strpos($output[0], 'v2')) {
92-
return true;
103+
$files = new \RecursiveIteratorIterator(
104+
new \RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
105+
\RecursiveIteratorIterator::CHILD_FIRST
106+
);
107+
108+
/** @var \SplFileInfo $fileinfo */
109+
foreach ($files as $fileinfo) {
110+
if ($fileinfo->isDir()) {
111+
@\rmdir($fileinfo->getRealPath());
112+
continue;
113+
}
114+
@\unlink($fileinfo->getRealPath());
93115
}
94116

95-
return false;
117+
@\rmdir($directory);
96118
}
97119
}

tests/Commands/TreeCommandTest.php

Lines changed: 7 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPUnit\Framework\Attributes\Test;
88
use Stolt\LeanPackage\Archive;
99
use Stolt\LeanPackage\Commands\TreeCommand;
10+
use Stolt\LeanPackage\Exceptions\GitNotAvailable;
1011
use Stolt\LeanPackage\Tests\CommandTester;
1112
use Stolt\LeanPackage\Tests\TestCase;
1213
use Stolt\LeanPackage\Tree;
@@ -17,7 +18,7 @@ class TreeCommandTest extends TestCase
1718
/**
1819
* @var Application
1920
*/
20-
private $application;
21+
private Application $application;
2122

2223
/**
2324
* Set up test environment.
@@ -44,7 +45,7 @@ protected function tearDown(): void
4445
}
4546

4647
#[Test]
47-
public function displayExpectedSrcTree(): void
48+
public function displaysExpectedSrcTree(): void
4849
{
4950
$command = $this->application->find('tree');
5051
$commandTester = new CommandTester($command);
@@ -59,107 +60,20 @@ public function displayExpectedSrcTree(): void
5960
['src', 'tests', '.github', 'docs', 'bin']
6061
);
6162

62-
file_put_contents(WORKING_DIRECTORY . '/composer.json', \json_encode(['name' => 'test-src/package']));
63-
64-
$expectedDisplay = <<<CONTENT
65-
Package: test-src/package
66-
.
67-
├── bin
68-
├── docs
69-
├── .github
70-
├── src
71-
├── tests
72-
├── composer.json
73-
└── .gitattributes
74-
75-
5 directories, 2 files
76-
77-
CONTENT;
63+
file_put_contents($this->temporaryDirectory . '/composer.json', \json_encode(['name' => 'test-src/package']));
7864

7965
$commandTester->execute([
8066
'command' => $command->getName(),
81-
'directory' => WORKING_DIRECTORY,
67+
'directory' => $this->temporaryDirectory,
8268
'--src' => true
8369
]);
8470

85-
$this->assertSame($expectedDisplay, $commandTester->getDisplay());
86-
$commandTester->assertCommandIsSuccessful();
87-
}
88-
89-
#[Test]
90-
public function displayExpectedDistPackageTree(): void
91-
{
92-
$command = $this->application->find('tree');
93-
$commandTester = new CommandTester($command);
94-
95-
$artifactFilenames = [
96-
'.gitattributes',
97-
'.gitignore',
98-
'captainhook.json',
99-
'CODE_OF_CONDUCT.md',
100-
'CONTRIBUTING.md',
101-
'infection.json5',
102-
'LICENSE.txt',
103-
'phpstan.neon',
104-
'phpunit.xml',
105-
'README.md',
106-
'sonar-project.properties',
107-
'package.json',
108-
'package-lock.json',
109-
'composer.json',
110-
];
111-
112-
$this->createTemporaryFiles(
113-
$artifactFilenames,
114-
['src', 'tests', '.github', 'docs', 'bin']
115-
);
116-
117-
file_put_contents(WORKING_DIRECTORY . '/composer.json', \json_encode(['name' => 'test-dist/package']));
118-
119-
$gitattributesContent = <<<CONTENT
120-
.gitattributes export-ignore
121-
.gitignore export-ignore
122-
captainhook.json export-ignore
123-
CODE_OF_CONDUCT.md export-ignore
124-
CONTRIBUTING.md export-ignore
125-
infection.json5 export-ignore
126-
LICENSE.txt export-ignore
127-
phpstan.neon export-ignore
128-
phpunit.xml export-ignore
129-
README.md export-ignore
130-
sonar-project.properties export-ignore
131-
package.json export-ignore
132-
package-lock.json export-ignore
133-
composer.json export-ignore
134-
tests/ export-ignore
135-
.github/ export-ignore
136-
docs/ export-ignore
137-
CONTENT;
138-
139-
$this->createTemporaryGitattributesFile($gitattributesContent);
140-
141-
142-
$expectedDisplay = <<<CONTENT
143-
Package: test-dist/package
144-
.
145-
├── bin
146-
├── composer.json
147-
└── src
148-
149-
2 directories, 1 file
150-
151-
CONTENT;
152-
153-
$commandTester->execute([
154-
'command' => $command->getName(),
155-
'directory' => WORKING_DIRECTORY,
156-
]);
157-
158-
$this->assertSame($expectedDisplay, $commandTester->getDisplay());
71+
$this->assertStringContainsString('5 directories, 2 files', $commandTester->getDisplay());
15972
$commandTester->assertCommandIsSuccessful();
16073
}
16174

16275
/**
76+
* @throws GitNotAvailable
16377
* @return Application
16478
*/
16579
protected function getApplication(): Application

0 commit comments

Comments
 (0)