Skip to content

Commit

Permalink
Extract git and exec logic
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianfeldmann committed Dec 13, 2023
1 parent 6140181 commit f2c0432
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 78 deletions.
93 changes: 15 additions & 78 deletions src/ComposerPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,9 @@ class ComposerPlugin implements PluginInterface, EventSubscriberInterface
private string $configuration;

/**
* Path to the .git directory
*
* @var string
*/
private string $gitDirectory;

/**
* @var bool
* @var \CaptainHook\HookInstaller\DotGit
*/
private bool $isWorktree = false;
private DotGit $dotGit;

/**
* Activate the plugin
Expand Down Expand Up @@ -133,8 +126,8 @@ public function installHooks(Event $event): void
{
$this->io->write('<info>CaptainHook Hook Installer</info>');

$this->detectConfiguration();
$this->detectGitDir();
$this->detectConfiguration();
$this->detectCaptainExecutable();

if ($this->shouldExecutionBeSkipped()) {
Expand All @@ -149,7 +142,8 @@ public function installHooks(Event $event): void
return;
}

$this->io->write(' - Detect configuration: <comment>found ' . $this->configuration . '</comment>');
$this->io->write(' - Detect executable: <comment>' . $this->executable . '</comment>');
$this->io->write(' - Detect configuration: <comment>' . $this->configuration . '</comment>');
$this->io->write(' - Install hooks: ', false);
$this->install();
$this->io->write('<comment>done</comment>');
Expand All @@ -174,35 +168,11 @@ private function detectConfiguration(): void
*/
private function detectGitDir(): void
{
$path = getcwd();

while (file_exists($path)) {
$possibleGitDir = $path . '/.git';
if (is_dir($possibleGitDir)) {
$this->gitDirectory = $possibleGitDir;
return;
} elseif (is_file($possibleGitDir)) {
$gitfile = file($possibleGitDir);
$match = [];
preg_match('#^gitdir: (?<gitdir>[a-zA-Z/\.]*\.git)#', $gitfile[0] ?? '', $match);
$dir = $match['gitdir'] ?? '';
if (is_dir($dir)) {
$this->isWorktree = true;
}

}

// if we checked the root directory already, break to prevent endless loop
if ($path === dirname($path)) {
break;
}

$path = \dirname($path);
try {
$this->dotGit = DotGit::searchInPath(getcwd());
} catch (RuntimeException $e) {
throw new RuntimeException($this->pluginErrorMessage($e->getMessage()));
}
if ($this->isWorktree) {
return;
}
throw new RuntimeException($this->pluginErrorMessage('git directory not found'));
}

/**
Expand All @@ -217,7 +187,6 @@ private function detectCaptainExecutable(): void
$this->executable = $extra['captainhook']['exec'];
return;
}

$this->executable = (string) $this->composer->getConfig()->get('bin-dir') . '/captainhook';
}

Expand All @@ -236,7 +205,7 @@ private function shouldExecutionBeSkipped(): bool
$this->io->write(' <comment>disabling plugin due to CI-environment</comment>');
return true;
}
if ($this->isWorktree) {
if ($this->dotGit->isAdditionalWorktree()) {
$this->io->write(' <comment>ARRRRR! We ARRR in a worktree, install is skipped!</comment>');
return true;
}
Expand Down Expand Up @@ -270,43 +239,11 @@ private function isForceInstall(): bool
*/
private function install(): void
{
// Respect composer CLI settings
$ansi = $this->io->isDecorated() ? ' --ansi' : ' --no-ansi';
$executable = escapeshellarg($this->executable);

// captainhook config and repository settings
$configuration = ' -c ' . escapeshellarg($this->configuration);
$repository = ' -g ' . escapeshellarg($this->gitDirectory);
$forceOrSkip = $this->isForceInstall() ? ' -f' : ' -s';

// sub process settings
$cmd = PHP_BINARY . ' ' . $executable . ' install'
. $ansi . ' --no-interaction' . $forceOrSkip
. $configuration . $repository;
$pipes = [];
$spec = [
0 => ['file', 'php://stdin', 'r'],
1 => ['file', 'php://stdout', 'w'],
2 => ['file', 'php://stderr', 'w'],
];

$process = @proc_open($cmd, $spec, $pipes);

if ($this->io->isVerbose()) {
$this->io->write('Running process : ' . $cmd);
}
if (!is_resource($process)) {
throw new RuntimeException($this->pluginErrorMessage('no-process'));
}

// Loop on process until it exits normally.
do {
$status = proc_get_status($process);
} while ($status && $status['running']);
$exitCode = $status['exitcode'] ?? -1;
proc_close($process);
if ($exitCode !== 0) {
$this->io->writeError($this->pluginErrorMessage('installation process failed'));
try {
$installer = new Installer($this->executable, $this->configuration, $this->dotGit->gitDirectory());
$installer->install($this->io, $this->isForceInstall());
} catch (\Exception $e) {
throw new RuntimeException($this->pluginErrorMessage($e->getMessage()));
}
}

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

/**
* This file is part of CaptainHook
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CaptainHook\HookInstaller;

use RuntimeException;

/**
* DotGit
*
* @package CaptainHook
* @author Sebastian Feldmann <sf@sebastian-feldmann.info>
* @link https://github.com/captainhookphp/captainhook
* @since Class available since Release 0.9.4
*/
class DotGit
{
/**
* Path to the .git file or directory
*
* @var string
*/
private string $dotGit = '';

/**
* Path to the real .git directory
*
* @var string
*/
private string $gitDir = '';

/**
* Is just an additional worktree
*
* @var bool
*/
private bool $isAdditionalWorktree = false;

/**
* Constructor
*
* @param string $pathToDotGit
* @throws \RuntimeException
*/
private function __construct(string $pathToDotGit)
{
// default repository with a .git directory
if (is_dir($pathToDotGit)) {
$this->gitDir = $pathToDotGit;
return;
}
// additional worktree with a .git file referencing the original .git directory
if (is_file($pathToDotGit)) {
$dotGitContent = file_get_contents($pathToDotGit);
$match = [];
preg_match('#^gitdir: (?<gitdir>.*\.git)#', $dotGitContent, $match);
$dir = $match['gitdir'] ?? '';
if (is_dir($dir)) {
$this->gitDir = $dir;
$this->isAdditionalWorktree = true;
return;
}
}
throw new RuntimeException('invalid .git path');
}

/**
* Returns the path to the .git file or directory
*
* @return string
*/
public function path(): string
{
return $this->dotGit;
}

/**
* Always returns the path .git repository directory
*
* @return string
*/
public function gitDirectory(): string
{
return $this->gitDir;
}

/**
* Returns true if the .git file indicated that we are in an additional worktree
*
* @return bool
*/
public function isAdditionalWorktree(): bool
{
return $this->isAdditionalWorktree;
}

/**
* Looks for a .git file or directory from a given path in all parent directories
*
* @param string $path
* @return \CaptainHook\HookInstaller\DotGit
* @throws \RuntimeException
*/
public static function searchInPath(string $path): DotGit
{
while (file_exists($path)) {
$dotGitPath = $path . '/.git';
if (file_exists($dotGitPath)) {
return new self($dotGitPath);
}
// if we checked the root directory already, break to prevent endless loop
if ($path === dirname($path)) {
break;
}
$path = dirname($path);
}
throw new RuntimeException('git directory not found');
}
}
99 changes: 99 additions & 0 deletions src/Installer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

/**
* This file is part of CaptainHook
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CaptainHook\HookInstaller;

use Composer\IO\IOInterface;
use RuntimeException;

class Installer
{
/**
* Path to CaptainHook phar file
*
* @var string
*/
private string $executable;

/**
* Path to the CaptainHook configuration file
*
* @var string
*/
private string $configuration;

/**
* Path to the .git directory
*
* @var string
*/
private string $gitDirectory;

/**
* Captain constructor
*
* @param string $executable
* @param string $configuration
* @param string $gitDirectory
*/
public function __construct(string $executable, string $configuration, string $gitDirectory)
{
$this->executable = $executable;
$this->configuration = $configuration;
$this->gitDirectory = $gitDirectory;
}

/**
* Install the hooks by executing the Cap'n
*
* @param \Composer\IO\IOInterface $io
* @param bool $force
* @return void
*/
public function install(IOInterface $io, bool $force): void
{
// Respect composer CLI settings
$ansi = $io->isDecorated() ? ' --ansi' : ' --no-ansi';
$executable = escapeshellarg($this->executable);

// captainhook config and repository settings
$configuration = ' -c ' . escapeshellarg($this->configuration);
$repository = ' -g ' . escapeshellarg($this->gitDirectory);
$forceOrSkip = $force ? ' -f' : ' -s';

// sub process settings
$cmd = PHP_BINARY . ' ' . $executable . ' install'
. $ansi . ' --no-interaction' . $forceOrSkip
. $configuration . $repository;
$pipes = [];
$spec = [
0 => ['file', 'php://stdin', 'r'],
1 => ['file', 'php://stdout', 'w'],
2 => ['file', 'php://stderr', 'w'],
];

$process = @proc_open($cmd, $spec, $pipes);

if (!is_resource($process)) {
throw new RuntimeException('no-process');
}

// Loop on process until it exits
do {
$status = proc_get_status($process);
} while ($status && $status['running']);
$exitCode = $status['exitcode'] ?? -1;
proc_close($process);
if ($exitCode !== 0) {
throw new RuntimeException('installation process failed');
}
}
}

0 comments on commit f2c0432

Please sign in to comment.