Skip to content
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

Add git packages as VCS repositories #79

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ report
*.sublime-project
*.sublime-workspace
testbench/
skeleton-cache/
1 change: 1 addition & 0 deletions config/packager.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Default: http://github.com/Jeroen-G/packager-skeleton/archive/master.zip
*/
'skeleton' => 'http://github.com/Jeroen-G/packager-skeleton/archive/master.zip',
'cache_skeleton' => false,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be a proper place to document this? I'm thinking a line in just the config file might be enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I wonder if it's even worth keeping. The only benefit is that you save about a second of download time (if even that). On the other hands, it adds a layer of complexity, and people might end up using an outdated skeleton without knowing it.


/*
* You can set defaults for the following placeholders.
Expand Down
11 changes: 10 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,14 @@ $ php artisan packager:git https://github.com/author/repository

**Result:**
This will register the package in the app's `composer.json` file.
If the `packager:git` command is used, the entire Git repository is cloned. If `packager:get` is used, the package will be downloaded, without a repository. This also works with Bitbucket repositories, but you have to provide the flag `--host=bitbucket` for the `packager:get` command.
If the `packager:git` command is used, the entire Git repository is cloned (you can optionally specify the branch/version to clone using the `--branch` option). If `packager:get` is used, the package will be downloaded, without a repository. This also works with Bitbucket repositories, but you have to provide the flag `--host=bitbucket` for the `packager:get` command.

**Options:**
```bash
$ php artisan packager:get https://github.com/author/repository --branch=develop
$ php artisan packager:get https://github.com/author/repository MyVendor MyPackage
$ php artisan packager:git https://github.com/author/repository MyVendor MyPackage
$ php artisan packager:git github-user/github-repo --branch=dev-mybranch
```
It is possible to specify a branch with the `--branch` option. If you specify a vendor and name directly after the url, those will be used instead of the pieces of the url.

Expand Down Expand Up @@ -142,6 +143,14 @@ You first need to run
$ composer require sensiolabs/security-checker
```

## Managing dependencies
When you install a new package using `packager:new`, `packager:get` or `packager:git`, the package dependencies will automatically be installed into the parent project's `vendor/` folder.

Installing or updating package dependencies should *not* be done directly from the `packages/` folder.

When you've edited the `composer.json` file in your package folder, you should run `composer update` from the root folder of the parent project.

If your package was installed using the `packager:git` command, any changes you make to the package's `composer.json` file will not be detected by the parent project until the changes have been committed.

## Issues with cURL SSL certificate
It turns out that, especially on Windows, there might arise some problems with the downloading of the skeleton, due to a file regarding SSL certificates missing on the OS. This can be solved by opening up your .env file and putting this in it:
Expand Down
12 changes: 11 additions & 1 deletion src/Commands/CheckPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
namespace JeroenG\Packager\Commands;

use Illuminate\Console\Command;
use JeroenG\Packager\ComposerHandler;
use SensioLabs\Security\SecurityChecker;
use SensioLabs\Security\Formatters\SimpleFormatter;
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;

/**
* List all locally installed packages.
Expand All @@ -13,6 +15,7 @@
**/
class CheckPackage extends Command
{
use ComposerHandler;
/**
* The name and signature of the console command.
* @var string
Expand All @@ -34,7 +37,14 @@ public function handle()
{
$this->info('Using the SensioLabs Security Checker the composer.lock of the package is scanned for known security vulnerabilities in the dependencies.');
$this->info('Make sure you have a composer.lock file first (for example by running "composer install" in the folder');

try {
$this->findInstalledPath('sensiolabs/security-checker');
} catch (DirectoryNotFoundException $e) {
$this->warn('SensioLabs Security Checker is not installed.');
$this->info('Run the following command and try again:');
$this->getOutput()->writeln('composer require sensiolabs/security-checker');
return 1;
}
$checker = new SecurityChecker();
$formatter = new SimpleFormatter($this->getHelperSet()->get('formatter'));
$vendor = $this->argument('vendor');
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/EnablePackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function handle()

// Install the package
$this->info('Installing package...');
$this->conveyor->installPackage();
$this->conveyor->installPackageFromPath();
$this->makeProgress();

// Finished removing the package, end of the progress bar
Expand Down
4 changes: 2 additions & 2 deletions src/Commands/GetPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function __construct(Conveyor $conveyor, Wrapping $wrapping)
public function handle()
{
// Start the progress bar
$this->startProgressBar(4);
$this->startProgressBar(5);

// Common variables
if ($this->option('host') == 'bitbucket') {
Expand Down Expand Up @@ -109,7 +109,7 @@ public function handle()

// Install the package
$this->info('Installing package...');
$this->conveyor->installPackage();
$this->conveyor->installPackageFromPath();
$this->makeProgress();

// Finished creating the package, end of the progress bar
Expand Down
47 changes: 16 additions & 31 deletions src/Commands/GitPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace JeroenG\Packager\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;
use JeroenG\Packager\Conveyor;
use JeroenG\Packager\Wrapping;
use Illuminate\Console\Command;
use JeroenG\Packager\ProgressBar;
use JeroenG\Packager\Wrapping;

/**
* Get an existing package from a remote git repository with its VCS.
Expand All @@ -24,7 +24,8 @@ class GitPackage extends Command
protected $signature = 'packager:git
{url : The url of the git repository}
{vendor? : The vendor part of the namespace}
{name? : The name of package for the namespace}';
{name? : The name of package for the namespace}
{--constraint=dev-master : The version to install}';

/**
* The console command description.
Expand Down Expand Up @@ -65,65 +66,49 @@ public function handle()
{
// Start the progress bar
$this->startProgressBar(4);

// Common variables
$source = $this->argument('url');
$origin = rtrim(strtolower($source), '/');

if (is_null($this->argument('vendor')) || is_null($this->argument('name'))) {
$origin = strtolower(rtrim($source, '/'));
// If only "user/repository" is provided as origin, assume a https Github repository
if (preg_match('/^[\w-]+\/[\w-]+$/', $origin)) {
$origin = 'https://github.com/'.$origin;
}
if ($this->argument('vendor') === null || $this->argument('name') === null) {
$this->setGitVendorAndPackage($origin);
} else {
$this->conveyor->vendor($this->argument('vendor'));
$this->conveyor->package($this->argument('name'));
}

// Start creating the package
$this->info('Creating package '.$this->conveyor->vendor().'\\'.$this->conveyor->package().'...');
$this->conveyor->checkIfPackageExists();
$this->makeProgress();

// Install package from VCS
$this->info('Installing package from VCS...');
$this->conveyor->installPackageFromVcs($origin, $this->option('constraint'));
$this->makeProgress();
// Create the package directory
$this->info('Creating packages directory...');
$this->conveyor->makeDir($this->conveyor->packagesPath());

// Clone the repository
$this->info('Cloning repository...');
exec("git clone $source ".$this->conveyor->packagePath(), $output, $exit_code);

if ($exit_code != 0) {
$this->error('Unable to clone repository');
$this->warn('Please check credentials and try again');

return;
}

$this->makeProgress();

// Create the vendor directory
$this->info('Creating vendor...');
$this->conveyor->makeDir($this->conveyor->vendorPath());
$this->makeProgress();

$this->info('Installing package...');
$this->conveyor->installPackage();
$this->info('Symlinking package to '.$this->conveyor->packagePath());
$this->conveyor->symlinkInstalledPackage();
$this->makeProgress();

// Finished creating the package, end of the progress bar
$this->finishProgress('Package cloned successfully!');
}

protected function setGitVendorAndPackage($origin)
{
$pieces = explode('/', $origin);

if (Str::contains($origin, 'https')) {
$vendor = $pieces[3];
$package = $pieces[4];
} else {
$vendor = explode(':', $pieces[0])[1];
$package = rtrim($pieces[1], '.git');
}

$this->conveyor->vendor($vendor);
$this->conveyor->package($package);
}
Expand Down
45 changes: 39 additions & 6 deletions src/Commands/ListPackages.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace JeroenG\Packager\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Symfony\Component\Process\Process;

/**
* List all locally installed packages.
Expand Down Expand Up @@ -35,14 +37,45 @@ public function handle()
$repositories = $composer['repositories'] ?? [];
$packages = [];
foreach ($repositories as $name => $info) {
$path = $info['url'];
$pattern = '{'.addslashes($packages_path).'(.*)$}';
if (preg_match($pattern, $path, $match)) {
$packages[] = explode(DIRECTORY_SEPARATOR, $match[1]);
if ($info['type'] === 'path') {
$path = $info['url'];
$pattern = '{'.addslashes($packages_path).'(.*)$}';
if (preg_match($pattern, $path, $match)) {
$status = $this->getGitStatus($path);
$packages[] = [$name, 'packages/'.$match[1], $status];
}
} elseif ($info['type'] === 'vcs') {
$path = $packages_path.$name;
if (file_exists($path)) {
$pattern = '{'.addslashes($packages_path).'(.*)$}';
if (preg_match($pattern, $path, $match)) {
$status = $this->getGitStatus($path);
$packages[] = [$name, 'packages/'.$match[1], $status];
}
}
}
}

$headers = ['Package', 'Path'];
$headers = ['Package', 'Path', 'Git status'];
$this->table($headers, $packages);
}

protected function getGitStatus($path)
{
if (!File::exists($path.'/.git')) {
return 'Not initialized';
}
$status = '<info>Up to date</info>';
(new Process(['git', 'fetch', '--all'], $path))->run();
(new Process(['git', '--git-dir='.$path.'/.git', '--work-tree='.$path, 'status', '-sb'], $path))->run(function (
$type,
$buffer
) use (&$status) {
if (preg_match('/^##/', $buffer)) {
if (preg_match('/\[(.*)\]$/', $buffer, $match)) {
$status = '<comment>'.ucfirst($match[1]).'</comment>';
}
}
});
return $status;
}
}
2 changes: 1 addition & 1 deletion src/Commands/NewPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public function handle()

// Add path repository to composer.json and install package
$this->info('Installing package...');
$this->conveyor->installPackage();
$this->conveyor->installPackageFromPath();

$this->makeProgress();

Expand Down
121 changes: 121 additions & 0 deletions src/ComposerHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

namespace JeroenG\Packager;

use Closure;
use RuntimeException;
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;

trait ComposerHandler
{
use ProcessRunner;

protected function removeComposerRepository($name)
{
return self::modifyComposerJson(function (array $composer) use ($name){
unset($composer['repositories'][$name]);
return $composer;
}, base_path());
}

/**
* Determines the path to Composer executable
* @todo Might not work on Windows
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a Windows machine, so I will test this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I've removed the @todo

* @return string
*/
protected static function getComposerExecutable(): string
Copy link
Owner

@Jeroen-G Jeroen-G Jun 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you choose to make this (and the ones below) static?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Mostly just for convenience and old habits.
The whole initial setup of the Testbench environment runs in a static context (setupBeforeClass), before Laravel has even been initialized. This is necessary because I can't switch the app's base_path once it's already up and running.
But honestly I should change it back to non-static, since I can just call the methods from the $instance variable. I just didn't notice earlier :)

{
return trim(shell_exec('which composer')) ?: 'composer';
}

protected function removePackage(string $packageName): array
{
return self::runComposerCommand([
'remove',
strtolower($packageName),
'--no-progress',
]);
}

protected function requirePackage(string $packageName, string $version = null, bool $prefer_source = true): bool
{
$package = strtolower($packageName);
if ($version) {
$package .= ':'.$version;
}
$result = self::runComposerCommand([
'require',
$package,
'--prefer-'.($prefer_source ? 'source' : 'dist'),
'--prefer-stable',
'--no-suggest',
'--no-progress',
]);
if (!$result['success']) {
return false;
}
return true;
}

protected function addComposerRepository(string $name, string $type = 'path', string $url = null)
{
$params = [
'type' => $type,
'url' => $url
];
return self::modifyComposerJson(function (array $composer) use ($params, $name){
$composer['repositories'][$name] = $params;
return $composer;
}, base_path());
}

/**
* Find the package's path in Composer's vendor folder
* @param string $packageName
* @return string
* @throws RuntimeException
*/
protected function findInstalledPath(string $packageName): string
{
$packageName = strtolower($packageName);
$result = self::runComposerCommand([
'info',
$packageName,
'--path']);
if ($result['success'] &&preg_match('{'.$packageName.' (.*)$}m', $result['output'], $match)) {
return trim($match[1]);
}
throw new DirectoryNotFoundException('Package ' . $packageName.' not found in vendor folder');
}

/**
* @param string $path
* @return array
*/
protected static function getComposerJsonArray(string $path): array
{
return json_decode(file_get_contents($path), true);
}

protected static function modifyComposerJson(Closure $callback, string $composer_path)
{
$composer_path = rtrim($composer_path, '/');
if (!preg_match('/composer\.json$/', $composer_path)){
$composer_path .= '/composer.json';
}
$original = self::getComposerJsonArray($composer_path);
$modified = $callback($original);
return file_put_contents($composer_path, json_encode($modified, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
}

/**
* @param array $command
* @param string|null $cwd
* @return array
*/
protected static function runComposerCommand(array $command, string $cwd = null): array
{
array_unshift($command, 'php', '-n', self::getComposerExecutable());
return self::runProcess($command, $cwd);
}
}