Skip to content

Commit e354b4b

Browse files
committed
Add copy command
1 parent d41823a commit e354b4b

8 files changed

Lines changed: 374 additions & 116 deletions

File tree

src/Phug/Split.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace Phug;
44

55
use Phug\Split\Command\Analyze;
6-
use Phug\Split\Command\Compare;
6+
use Phug\Split\Command\Copy;
77
use Phug\Split\Command\Dist;
88
use Phug\Split\Command\Update;
99
use SimpleCli\SimpleCli;
@@ -16,6 +16,7 @@ public function getCommands(): array
1616
Analyze::class,
1717
Dist::class,
1818
Update::class,
19+
Copy::class,
1920
];
2021
}
2122

src/Phug/Split/Command/Analyze.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,14 @@
33
namespace Phug\Split\Command;
44

55
use Phug\Split;
6-
use SimpleCli\Command;
7-
use SimpleCli\Options\Help;
86
use SimpleCli\SimpleCli;
97
use Traversable;
108

119
/**
12-
* Compare master repository to sub-repositories.
10+
* Display the tree of nested packages in the mono-repository.
1311
*/
14-
class Analyze implements Command
12+
class Analyze extends CommandBase
1513
{
16-
use Help;
17-
1814
/**
1915
* @argument
2016
*
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
namespace Phug\Split\Command;
4+
5+
use Exception;
6+
use Phug\Split\Git\Commit;
7+
use Phug\Split\Git\Log;
8+
use RecursiveDirectoryIterator;
9+
use RecursiveIteratorIterator;
10+
use SimpleCli\Command;
11+
use SimpleCli\Options\Help;
12+
13+
abstract class CommandBase implements Command
14+
{
15+
use Help;
16+
17+
/**
18+
* Escape a value for git command argument or option.
19+
*
20+
* @param string $value
21+
*
22+
* @return string
23+
*/
24+
protected function gitEscape(string $value): string
25+
{
26+
return "$'".addcslashes($value, "'\\")."'";
27+
}
28+
29+
/**
30+
* Get a command using git program.
31+
*
32+
* @param string $command Git command and mandatory arguments
33+
* @param array $options CLI git command options
34+
* @param string|null $redirect redirection suffix (like '2>&1')
35+
*
36+
* @return string
37+
*/
38+
protected function getGitCommand(string $command, array $options = [], string $redirect = null): string
39+
{
40+
foreach ($options as $name => $value) {
41+
$command .= ' --'.$name.'='.$this->gitEscape($value);
42+
}
43+
44+
return $this->gitProgram.' '.$command.($redirect ? ' '.$redirect : '');
45+
}
46+
47+
/**
48+
* Execute a command using git program.
49+
*
50+
* @param string $command Git command and mandatory arguments
51+
* @param array $options CLI git command options
52+
* @param string|null $redirect redirection suffix (like '2>&1')
53+
*
54+
* @return string|null
55+
*/
56+
protected function git(string $command, array $options = [], string $redirect = null): ?string
57+
{
58+
$command = $this->getGitCommand($command, $options, $redirect);
59+
60+
if (strpos($command, '$\'') === false) {
61+
return shell_exec($command);
62+
}
63+
64+
$script = sys_get_temp_dir().'/script.sh';
65+
file_put_contents($script, "#!/bin/sh\n".$command);
66+
chmod($script, 0777);
67+
$output = shell_exec(escapeshellcmd($script).($redirect ? ' '.$redirect : ''));
68+
unlink($script);
69+
70+
return $output;
71+
}
72+
73+
/**
74+
* Return given count of latest commits as Log instance (collection of Commit instances).
75+
*
76+
* @param int $count
77+
* @param string $directory
78+
*
79+
* @throws Exception
80+
*
81+
* @return Log|Commit[]
82+
*/
83+
protected function latest($count = 1, string $directory = ''): Log
84+
{
85+
return Log::fromGitLogString($this->git("log --pretty=fuller --max-count=$count $directory"));
86+
}
87+
88+
/**
89+
* Return the last commit.
90+
*
91+
* @param string $directory
92+
*
93+
* @throws Exception
94+
*
95+
* @return Commit
96+
*/
97+
protected function last(string $directory = ''): Commit
98+
{
99+
return $this->latest(1, $directory)[0];
100+
}
101+
102+
/**
103+
* Get the hash of the linked commit in the mono-repository for the last sub-package commit.
104+
*
105+
* @return string|null
106+
*
107+
* @throws Exception
108+
*/
109+
protected function getCurrentLinkedCommitHash(): ?string
110+
{
111+
return $this->last()->findInMessage('/^'.preg_quote($this->hashPrefix).'(.+)$/m');
112+
}
113+
114+
/**
115+
* Remove a file or a directory even if not empty.
116+
*
117+
* @param string $fileOrDirectory
118+
*
119+
* @return bool
120+
*/
121+
protected function remove(string $fileOrDirectory): bool
122+
{
123+
if (is_dir($fileOrDirectory)) {
124+
$dir = new RecursiveDirectoryIterator($fileOrDirectory, RecursiveDirectoryIterator::SKIP_DOTS);
125+
$dir = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST);
126+
127+
foreach ($dir as $filename => $file) {
128+
is_file($filename)
129+
? unlink($filename)
130+
: rmdir($filename);
131+
}
132+
133+
return rmdir($fileOrDirectory);
134+
}
135+
136+
if (file_exists($fileOrDirectory)) {
137+
return unlink($fileOrDirectory);
138+
}
139+
140+
return false;
141+
}
142+
}

src/Phug/Split/Command/Copy.php

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<?php
2+
3+
namespace Phug\Split\Command;
4+
5+
use Phug\Split;
6+
use Phug\Split\Command\Options\GitProgram;
7+
use Phug\Split\Command\Options\HashPrefix;
8+
use SimpleCli\SimpleCli;
9+
use Traversable;
10+
11+
/**
12+
* Copy files from the mono-repository to a sub-package at the current linked revision.
13+
*/
14+
class Copy extends CommandBase
15+
{
16+
use HashPrefix, GitProgram;
17+
18+
/**
19+
* @argument
20+
*
21+
* Source mono-repository URL.
22+
*
23+
* @var string
24+
*/
25+
public $repository;
26+
27+
/**
28+
* @argument
29+
*
30+
* Destination directory.
31+
*
32+
* @var string
33+
*/
34+
public $destination = '.';
35+
36+
/**
37+
* @option
38+
*
39+
* Glob filters separated by commas to select the files to copy.
40+
*
41+
* @var string
42+
*/
43+
public $filters = 'composer.json';
44+
45+
/**
46+
* @param Split $cli
47+
*
48+
* @return bool
49+
*/
50+
public function run(SimpleCli $cli): bool
51+
{
52+
if (!$this->repository) {
53+
return $cli->error('Please provide an input repository URL.');
54+
}
55+
56+
$hash = $this->getCurrentLinkedCommitHash();
57+
58+
if (!$hash) {
59+
return $cli->error('Last commit must be linked to a mono-repository commit.');
60+
}
61+
62+
$destination = realpath($this->destination);
63+
64+
if (!$destination) {
65+
return $cli->error('Destination directory "'.$this->destination.'" does not seem to exist.');
66+
}
67+
68+
$workDirectory = sys_get_temp_dir().'/split-copy-'.mt_rand(0, 9999999);
69+
mkdir($workDirectory, 0777, true);
70+
$cli->chdir($workDirectory);
71+
$this->git('clone '.$this->repository.' .');
72+
$this->git("reset --hard $hash");
73+
74+
foreach (explode(',', $this->filters) as $filter) {
75+
shell_exec('cp -r '.$filter.' '.escapeshellarg($destination.DIRECTORY_SEPARATOR));
76+
}
77+
78+
$cli->writeLine('copy');
79+
$this->remove($workDirectory);
80+
81+
return true;
82+
}
83+
84+
protected function calculatePackagesTree(Split $cli): bool
85+
{
86+
$this->directory = realpath($this->directory);
87+
88+
if (!$this->directory) {
89+
return $cli->error('Input directory not found.');
90+
}
91+
92+
$cli->chdir($this->directory);
93+
94+
if (!file_exists($this->composerFile)) {
95+
return $cli->error('Root project directory should contains a '.$this->composerFile.' file.');
96+
}
97+
98+
$data = json_decode(file_get_contents($this->composerFile), true);
99+
$vendorDirectory = ($data['config'] ?? [])['vendor-dir'] ?? 'vendor';
100+
101+
$cli->writeLine($data['name']);
102+
$this->ast = $this->mapDirectories('.', function (string $path, string $element) use ($vendorDirectory) {
103+
if ($element === $vendorDirectory) {
104+
return null;
105+
}
106+
107+
return $this->scanDirectories($path);
108+
});
109+
110+
return true;
111+
}
112+
113+
protected function getPackages(): iterable
114+
{
115+
if ($this->ast instanceof Traversable) {
116+
$this->ast = iterator_to_array($this->ast);
117+
}
118+
119+
return $this->ast;
120+
}
121+
122+
protected function dumpPackagesTree(Split $cli, iterable $packages, int $level = 0): bool
123+
{
124+
$count = count($packages);
125+
126+
foreach ($packages as $index => $package) {
127+
$symbol = $index === $count - 1 ? '' : '';
128+
$cli->writeLine(str_repeat(' ', $level).' '.$symbol.' '.$package['name'], 'light_cyan');
129+
$this->dumpPackagesTree($cli, $package['children']);
130+
}
131+
132+
return true;
133+
}
134+
135+
protected function mapDirectories(string $directory, callable $callback): iterable
136+
{
137+
foreach (scandir($directory) as $element) {
138+
if (substr($element, 0, 1) === '.') {
139+
continue;
140+
}
141+
142+
$path = $directory.DIRECTORY_SEPARATOR.$element;
143+
144+
if (is_dir($path)) {
145+
$result = $callback($path, $element);
146+
147+
if ($result !== null) {
148+
foreach ($result as $item) {
149+
yield $item;
150+
}
151+
}
152+
}
153+
}
154+
}
155+
156+
protected function getPackage(string $directory, array $data): array
157+
{
158+
return [
159+
'name' => $data['name'],
160+
'children' => [],
161+
];
162+
}
163+
164+
protected function scanDirectories(string $directory): iterable
165+
{
166+
$mainPackage = null;
167+
$composerPath = $directory.DIRECTORY_SEPARATOR.$this->composerFile;
168+
169+
if (file_exists($composerPath)) {
170+
$data = json_decode(file_get_contents($composerPath), true);
171+
$mainPackage = $this->getPackage($directory, $data);
172+
}
173+
174+
foreach ($this->mapDirectories($directory, function (string $path) {
175+
return $this->scanDirectories($path);
176+
}) as $package) {
177+
if ($mainPackage) {
178+
$mainPackage['children'][] = $package;
179+
180+
continue;
181+
}
182+
183+
yield $package;
184+
}
185+
186+
if ($mainPackage) {
187+
yield $mainPackage;
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)