-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Download Git submodules #160
Draft
alexandre-daubois
wants to merge
1
commit into
php:main
Choose a base branch
from
alexandre-daubois:git-submodules
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+287
−0
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Php\Pie\Platform\Git\Exception; | ||
|
||
use RuntimeException; | ||
|
||
use function sprintf; | ||
|
||
class InvalidGitBinaryPath extends RuntimeException | ||
{ | ||
public static function fromNonExistentGitBinary(string $phpBinaryPath): self | ||
{ | ||
return new self(sprintf( | ||
'The git binary at "%s" does not exist', | ||
$phpBinaryPath, | ||
)); | ||
} | ||
|
||
public static function fromNonExecutableGitBinary(string $phpBinaryPath): self | ||
{ | ||
return new self(sprintf( | ||
'The git binary at "%s" is not executable', | ||
$phpBinaryPath, | ||
)); | ||
} | ||
|
||
public static function fromInvalidGitBinary(string $phpBinaryPath): self | ||
{ | ||
return new self(sprintf( | ||
'The git binary at "%s" does not appear to be a git binary', | ||
$phpBinaryPath, | ||
)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Php\Pie\Platform\Git; | ||
|
||
use Composer\Util\Platform; | ||
use Php\Pie\Util\Process; | ||
use RuntimeException; | ||
|
||
use function array_filter; | ||
use function array_map; | ||
use function array_merge; | ||
use function explode; | ||
use function file_exists; | ||
use function file_get_contents; | ||
use function is_executable; | ||
use function preg_match; | ||
use function rtrim; | ||
use function trim; | ||
|
||
/** | ||
* @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks | ||
* | ||
* @immutable | ||
*/ | ||
class GitBinaryPath | ||
{ | ||
private function __construct( | ||
public readonly string $gitBinaryPath, | ||
) { | ||
} | ||
|
||
/** @param non-empty-string $gitBinary */ | ||
public static function fromGitBinaryPath(string $gitBinary): self | ||
{ | ||
self::assertValidLookingGitBinary($gitBinary); | ||
|
||
return new self($gitBinary); | ||
} | ||
|
||
/** @return array<string> The list of cloned submodules */ | ||
public function fetchSubmodules(string $path): array | ||
{ | ||
$modulesPath = rtrim($path, '/') . '/.gitmodules'; | ||
|
||
if (! file_exists($modulesPath)) { | ||
throw new RuntimeException('No .gitmodules file found in the specified path.'); | ||
} | ||
|
||
$content = file_get_contents($modulesPath); | ||
if ($content === false) { | ||
throw new RuntimeException('Unable to read .gitmodules file.'); | ||
} | ||
|
||
$modules = $this->parseGitModules($content); | ||
|
||
if (! $modules) { | ||
return []; | ||
} | ||
|
||
return $this->processModules($modules, $path); | ||
} | ||
|
||
/** | ||
* @param string $content Raw content of .gitmodules file | ||
* | ||
* @return array<Submodule> List of parsed modules | ||
*/ | ||
private function parseGitModules(string $content): array | ||
{ | ||
$lines = array_filter(array_map('trim', explode("\n", $content))); | ||
$modules = []; | ||
$currentName = null; | ||
$currentPath = ''; | ||
$currentUrl = ''; | ||
|
||
$modulePattern = '/^\[submodule "(?P<name>[^"]+)"]$/'; | ||
$configPattern = '/^(?P<key>path|url)\s*=\s*(?P<value>.+)$/'; | ||
|
||
foreach ($lines as $line) { | ||
// do we enter a new module? | ||
if (preg_match($modulePattern, $line, $matches)) { | ||
if ($currentName !== null) { | ||
$modules[] = new Submodule($currentPath, $currentUrl); | ||
} | ||
|
||
$currentName = $matches['name']; | ||
$currentPath = ''; | ||
$currentUrl = ''; | ||
|
||
continue; | ||
} | ||
|
||
if ($currentName === null || ! preg_match($configPattern, $line, $matches)) { | ||
continue; | ||
} | ||
|
||
if ($matches['key'] === 'path') { | ||
$currentPath = trim($matches['value']); | ||
} elseif ($matches['key'] === 'url') { | ||
$currentUrl = trim($matches['value']); | ||
} | ||
} | ||
|
||
if ($currentName !== null) { | ||
$modules[] = new Submodule($currentPath, $currentUrl); | ||
} | ||
|
||
return $modules; | ||
} | ||
|
||
/** | ||
* Process the parsed modules by cloning them and handling recursive submodules. | ||
* | ||
* @param non-empty-array<Submodule> $modules List of parsed modules | ||
* | ||
* @return array<array-key, string> The list of cloned submodules | ||
*/ | ||
private function processModules(array $modules, string $basePath): array | ||
{ | ||
$clonedModules = []; | ||
|
||
foreach ($modules as $module) { | ||
if (! $module->path || ! $module->url) { | ||
// incomplete module, skip | ||
continue; | ||
} | ||
|
||
$targetPath = rtrim($basePath, '/') . '/' . $module->path; | ||
|
||
Process::run([$this->gitBinaryPath, 'clone', $module->url, $targetPath], $basePath, timeout: null); | ||
$clonedModules[] = $module->url; | ||
|
||
if (! file_exists($targetPath . '/.gitmodules')) { | ||
continue; | ||
} | ||
|
||
$clonedModules = array_merge($clonedModules, $this->fetchSubmodules($targetPath)); | ||
} | ||
|
||
return $clonedModules; | ||
} | ||
|
||
private static function assertValidLookingGitBinary(string $gitBinary): void | ||
{ | ||
if (! file_exists($gitBinary)) { | ||
throw Exception\InvalidGitBinaryPath::fromNonExistentgitBinary($gitBinary); | ||
} | ||
|
||
if (! Platform::isWindows() && ! is_executable($gitBinary)) { | ||
throw Exception\InvalidGitBinaryPath::fromNonExecutableGitBinary($gitBinary); | ||
} | ||
|
||
$output = Process::run([$gitBinary, '--version']); | ||
|
||
if (! preg_match('/git version \d+\.\d+\.\d+/', $output)) { | ||
throw Exception\InvalidGitBinaryPath::fromInvalidGitBinary($gitBinary); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Php\Pie\Platform\Git; | ||
|
||
/** | ||
* @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks | ||
* | ||
* @immutable | ||
*/ | ||
final class Submodule | ||
{ | ||
public function __construct( | ||
public readonly string $path, | ||
public readonly string $url, | ||
) { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#!/usr/bin/env bash | ||
|
||
echo "Hah! I am not really Git."; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Php\PieUnitTest\Platform\Git; | ||
|
||
use Composer\Util\Platform; | ||
use Php\Pie\Platform\Git\Exception\InvalidGitBinaryPath; | ||
use Php\Pie\Platform\Git\GitBinaryPath; | ||
use PHPUnit\Framework\Attributes\CoversClass; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
#[CoversClass(GitBinaryPath::class)] | ||
class GitBinaryPathTest extends TestCase | ||
{ | ||
private const FAKE_GIT_EXECUTABLE = __DIR__ . '/../../../assets/fake-git.sh'; | ||
|
||
public function testNonExistentPhpBinaryIsRejected(): void | ||
{ | ||
$this->expectException(InvalidGitBinaryPath::class); | ||
$this->expectExceptionMessage('does not exist'); | ||
GitBinaryPath::fromGitBinaryPath(__DIR__ . '/path/to/a/non/existent/git/binary'); | ||
} | ||
|
||
public function testNonExecutablePhpBinaryIsRejected(): void | ||
{ | ||
if (Platform::isWindows()) { | ||
self::markTestSkipped('is_executable always returns false on Windows'); | ||
} | ||
|
||
$this->expectException(InvalidGitBinaryPath::class); | ||
$this->expectExceptionMessage('is not executable'); | ||
GitBinaryPath::fromGitBinaryPath(__FILE__); | ||
} | ||
|
||
public function testInvalidGitBinaryIsRejected(): void | ||
{ | ||
$this->expectException(InvalidGitBinaryPath::class); | ||
$this->expectExceptionMessage('does not appear to be a git binary'); | ||
GitBinaryPath::fromGitBinaryPath(self::FAKE_GIT_EXECUTABLE); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This path is specific to macOS package manager, it won't work on other systems.
I'm not even sure it is needed to specify git path as it should be in
$PATH
of the userThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes indeed, it was for test purposes to validate the implementation. Thanks for spotting it 👍