2011-04-17 22:14:44 +00:00
< ? php
/*
* This file is part of Composer .
*
* ( c ) Nils Adermann < naderman @ naderman . de >
* Jordi Boggiano < j . boggiano @ seld . be >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Composer\Downloader ;
use Composer\Package\PackageInterface ;
/**
* @ author Jordi Boggiano < j . boggiano @ seld . be >
*/
2012-01-22 20:14:56 +00:00
class GitDownloader extends VcsDownloader
2011-04-17 22:14:44 +00:00
{
2012-10-07 13:19:55 +00:00
private $hasStashedChanges = false ;
2011-09-25 10:39:08 +00:00
/**
* { @ inheritDoc }
*/
2012-01-22 20:14:56 +00:00
public function doDownload ( PackageInterface $package , $path )
2011-09-25 10:39:08 +00:00
{
2012-04-04 15:11:50 +00:00
$ref = $package -> getSourceReference ();
2012-06-27 16:28:49 +00:00
$command = 'git clone %s %s && cd %2$s && git remote add composer %1$s && git fetch composer' ;
2012-06-20 09:46:59 +00:00
$this -> io -> write ( " Cloning " . $ref );
2012-03-06 23:58:37 +00:00
2012-06-27 16:28:49 +00:00
// added in git 1.7.1, prevents prompting the user
putenv ( 'GIT_ASKPASS=echo' );
2012-03-10 16:49:08 +00:00
$commandCallable = function ( $url ) use ( $ref , $path , $command ) {
2012-04-04 15:11:50 +00:00
return sprintf ( $command , escapeshellarg ( $url ), escapeshellarg ( $path ), escapeshellarg ( $ref ));
2012-03-10 16:49:08 +00:00
};
$this -> runCommand ( $commandCallable , $package -> getSourceUrl (), $path );
2012-04-03 17:49:57 +00:00
$this -> setPushUrl ( $package , $path );
2012-06-20 09:46:59 +00:00
2012-06-20 10:05:18 +00:00
$this -> updateToCommit ( $path , $ref , $package -> getPrettyVersion (), $package -> getReleaseDate ());
2011-09-28 22:48:17 +00:00
}
2011-09-25 10:39:08 +00:00
2011-09-28 22:48:17 +00:00
/**
* { @ inheritDoc }
*/
2012-01-22 20:14:56 +00:00
public function doUpdate ( PackageInterface $initial , PackageInterface $target , $path )
2011-04-17 22:14:44 +00:00
{
2012-04-04 15:11:50 +00:00
$ref = $target -> getSourceReference ();
2012-06-20 09:46:59 +00:00
$this -> io -> write ( " Checking out " . $ref );
$command = 'cd %s && git remote set-url composer %s && git fetch composer && git fetch --tags composer' ;
2012-03-10 16:49:08 +00:00
2012-05-06 15:19:30 +00:00
// capture username/password from github URL if there is one
$this -> process -> execute ( sprintf ( 'cd %s && git remote -v' , escapeshellarg ( $path )), $output );
if ( preg_match ( '{^composer\s+https://(.+):(.+)@github.com/}im' , $output , $match )) {
$this -> io -> setAuthorization ( 'github.com' , $match [ 1 ], $match [ 2 ]);
}
2012-03-10 16:49:08 +00:00
$commandCallable = function ( $url ) use ( $ref , $path , $command ) {
2012-04-04 15:11:50 +00:00
return sprintf ( $command , escapeshellarg ( $path ), escapeshellarg ( $url ), escapeshellarg ( $ref ));
2012-03-10 16:49:08 +00:00
};
$this -> runCommand ( $commandCallable , $target -> getSourceUrl ());
2012-06-20 10:05:18 +00:00
$this -> updateToCommit ( $path , $ref , $target -> getPrettyVersion (), $target -> getReleaseDate ());
2012-06-20 09:46:59 +00:00
}
2012-06-25 23:18:10 +00:00
/**
* { @ inheritDoc }
*/
2012-08-18 12:34:24 +00:00
public function getLocalChanges ( $path )
2012-06-25 23:18:10 +00:00
{
$command = sprintf ( 'cd %s && git status --porcelain --untracked-files=no' , escapeshellarg ( $path ));
if ( 0 !== $this -> process -> execute ( $command , $output )) {
throw new \RuntimeException ( 'Failed to execute ' . $command . " \n \n " . $this -> process -> getErrorOutput ());
}
2012-08-18 12:34:24 +00:00
return trim ( $output ) ? : null ;
2012-06-25 23:18:10 +00:00
}
2012-10-07 13:19:55 +00:00
/**
* { @ inhertiDoc }
*/
protected function cleanChanges ( $path , $update )
{
if ( ! $this -> io -> isInteractive ()) {
return parent :: cleanChanges ( $path , $update );
}
if ( ! $changes = $this -> getLocalChanges ( $path )) {
return ;
}
$changes = array_map ( function ( $elem ) {
return ' ' . $elem ;
}, preg_split ( '{\s*\r?\n\s*}' , $changes ));
$this -> io -> write ( ' <error>The package has modified files:</error>' );
$this -> io -> write ( array_slice ( $changes , 0 , 10 ));
if ( count ( $changes ) > 10 ) {
$this -> io -> write ( ' <info>' . count ( $changes ) - 10 . ' more files modified, choose "v" to view the full list</info>' );
}
while ( true ) {
switch ( $this -> io -> ask ( ' <info>Discard changes [y,n,v,' . ( $update ? 's,' : '' ) . '?]?</info> ' , '?' )) {
case 'y' :
if ( 0 !== $this -> process -> execute ( 'git reset --hard' , $output , $path )) {
throw new \RuntimeException ( " Could not reset changes \n \n : " . $this -> process -> getErrorOutput ());
}
break 2 ;
case 's' :
if ( ! $update ) {
goto help ;
}
if ( 0 !== $this -> process -> execute ( 'git stash' , $output , $path )) {
throw new \RuntimeException ( " Could not stash changes \n \n : " . $this -> process -> getErrorOutput ());
}
$this -> hasStashedChanges = true ;
break 2 ;
case 'n' :
throw new \RuntimeException ( 'Update aborted' );
case 'v' :
$this -> io -> write ( $changes );
break ;
case '?' :
default :
help :
$this -> io -> write ( array (
' y - discard changes and apply the ' . ( $update ? 'update' : 'uninstall' ),
' n - abort the ' . ( $update ? 'update' : 'uninstall' ) . ' and let you manually clean things up' ,
' v - view modified files' ,
));
if ( $update ) {
$this -> io -> write ( ' s - stash changes and try to reapply them after the update' );
}
$this -> io -> write ( ' ? - print help' );
break ;
}
}
}
/**
* { @ inhertiDoc }
*/
protected function reapplyChanges ( $path )
{
if ( $this -> hasStashedChanges ) {
$this -> io -> write ( ' <info>Re-applying stashed changes' );
if ( 0 !== $this -> process -> execute ( 'git stash pop' , $output , $path )) {
throw new \RuntimeException ( " Failed to apply stashed changes: \n \n " . $this -> process -> getErrorOutput ());
}
}
}
2012-06-20 09:46:59 +00:00
protected function updateToCommit ( $path , $reference , $branch , $date )
{
$template = 'git checkout %s && git reset --hard %1$s' ;
2012-07-10 17:02:06 +00:00
// check whether non-commitish are branches or tags, and fetch branches with the remote name
$gitRef = $reference ;
if ( ! preg_match ( '{^[a-f0-9]{40}$}' , $reference )
&& 0 === $this -> process -> execute ( 'git branch -r' , $output , $path )
&& preg_match ( '{^\s+composer/' . preg_quote ( $reference ) . '$}m' , $output )
) {
$gitRef = 'composer/' . $reference ;
}
$command = sprintf ( $template , escapeshellarg ( $gitRef ));
2012-06-20 09:46:59 +00:00
if ( 0 === $this -> process -> execute ( $command , $output , $path )) {
return ;
}
// reference was not found (prints "fatal: reference is not a tree: $ref")
2012-06-20 10:05:18 +00:00
if ( $date && false !== strpos ( $this -> process -> getErrorOutput (), $reference )) {
2012-06-20 09:46:59 +00:00
$branch = preg_replace ( '{(?:^dev-|(?:\.x)?-dev$)}i' , '' , $branch );
2012-06-20 10:05:18 +00:00
$date = $date -> format ( 'U' );
2012-06-20 09:46:59 +00:00
2012-06-20 10:09:09 +00:00
// guess which remote branch to look at first
2012-06-20 09:46:59 +00:00
$command = 'git branch -r' ;
2012-06-20 10:09:09 +00:00
if ( 0 !== $this -> process -> execute ( $command , $output , $path )) {
2012-06-20 09:46:59 +00:00
throw new \RuntimeException ( 'Failed to execute ' . $command . " \n \n " . $this -> process -> getErrorOutput ());
}
$guessTemplate = 'git log --until=%s --date=raw -n1 --pretty=%%H %s' ;
foreach ( $this -> process -> splitLines ( $output ) as $line ) {
if ( preg_match ( '{^composer/' . preg_quote ( $branch ) . '(?:\.x)?$}i' , trim ( $line ))) {
2012-06-20 10:09:09 +00:00
// find the previous commit by date in the given branch
2012-06-20 09:46:59 +00:00
if ( 0 === $this -> process -> execute ( sprintf ( $guessTemplate , $date , escapeshellarg ( trim ( $line ))), $output , $path )) {
$newReference = trim ( $output );
}
break ;
}
}
if ( empty ( $newReference )) {
2012-06-20 10:09:09 +00:00
// no matching branch found, find the previous commit by date in all commits
2012-06-20 09:46:59 +00:00
if ( 0 !== $this -> process -> execute ( sprintf ( $guessTemplate , $date , '--all' ), $output , $path )) {
throw new \RuntimeException ( 'Failed to execute ' . $command . " \n \n " . $this -> process -> getErrorOutput ());
}
$newReference = trim ( $output );
}
2012-06-20 10:09:09 +00:00
// checkout the new recovered ref
2012-07-10 17:02:06 +00:00
$command = sprintf ( $template , escapeshellarg ( $reference ));
2012-06-20 09:46:59 +00:00
if ( 0 === $this -> process -> execute ( $command , $output , $path )) {
$this -> io -> write ( ' ' . $reference . ' is gone (history was rewritten?), recovered by checking out ' . $newReference );
return ;
}
}
throw new \RuntimeException ( 'Failed to execute ' . $command . " \n \n " . $this -> process -> getErrorOutput ());
2011-04-17 22:14:44 +00:00
}
2012-03-10 16:49:08 +00:00
/**
* Runs a command doing attempts for each protocol supported by github .
*
2012-05-22 10:07:08 +00:00
* @ param callable $commandCallable A callable building the command for the given url
* @ param string $url
* @ param string $path The directory to remove for each attempt ( null if not needed )
2012-03-10 16:49:08 +00:00
* @ throws \RuntimeException
*/
protected function runCommand ( $commandCallable , $url , $path = null )
{
2012-04-04 07:54:27 +00:00
$handler = array ( $this , 'outputHandler' );
2012-06-27 16:00:52 +00:00
// public github, autoswitch protocols
2012-09-07 22:45:18 +00:00
if ( preg_match ( '{^(?:https?|git)(://github.com/.*)}' , $url , $match )) {
$protocols = $this -> config -> get ( 'github-protocols' );
if ( ! is_array ( $protocols )) {
throw new \RuntimeException ( 'Config value "github-protocols" must be an array, got ' . gettype ( $protocols ));
}
2012-05-18 12:41:57 +00:00
$messages = array ();
2012-03-10 16:49:08 +00:00
foreach ( $protocols as $protocol ) {
2012-04-04 15:11:50 +00:00
$url = $protocol . $match [ 1 ];
2012-04-04 07:54:27 +00:00
if ( 0 === $this -> process -> execute ( call_user_func ( $commandCallable , $url ), $handler )) {
2012-03-10 16:49:08 +00:00
return ;
}
2012-05-18 12:41:57 +00:00
$messages [] = '- ' . $url . " \n " . preg_replace ( '#^#m' , ' ' , $this -> process -> getErrorOutput ());
2012-03-10 16:49:08 +00:00
if ( null !== $path ) {
$this -> filesystem -> removeDirectory ( $path );
}
}
2012-04-05 15:30:50 +00:00
2012-04-06 11:21:04 +00:00
// failed to checkout, first check git accessibility
2012-05-18 12:41:57 +00:00
$this -> throwException ( 'Failed to clone ' . $url . ' via git, https and http protocols, aborting.' . " \n \n " . implode ( " \n " , $messages ), $url );
2012-04-05 15:26:15 +00:00
}
2012-03-10 16:49:08 +00:00
$command = call_user_func ( $commandCallable , $url );
2012-04-04 07:54:27 +00:00
if ( 0 !== $this -> process -> execute ( $command , $handler )) {
2012-05-06 15:19:30 +00:00
if ( preg_match ( '{^git@github.com:(.+?)\.git$}i' , $url , $match ) && $this -> io -> isInteractive ()) {
2012-06-27 16:00:52 +00:00
// private github repository without git access, try https with auth
2012-05-06 15:19:30 +00:00
$retries = 3 ;
$retrying = false ;
do {
if ( $retrying ) {
$this -> io -> write ( 'Invalid credentials' );
}
if ( ! $this -> io -> hasAuthorization ( 'github.com' ) || $retrying ) {
$username = $this -> io -> ask ( 'Username: ' );
$password = $this -> io -> askAndHideAnswer ( 'Password: ' );
$this -> io -> setAuthorization ( 'github.com' , $username , $password );
}
$auth = $this -> io -> getAuthorization ( 'github.com' );
$url = 'https://' . $auth [ 'username' ] . ':' . $auth [ 'password' ] . '@github.com/' . $match [ 1 ] . '.git' ;
$command = call_user_func ( $commandCallable , $url );
if ( 0 === $this -> process -> execute ( $command , $handler )) {
return ;
}
2012-05-12 16:24:07 +00:00
if ( null !== $path ) {
$this -> filesystem -> removeDirectory ( $path );
}
2012-05-06 15:19:30 +00:00
$retrying = true ;
} while ( -- $retries );
}
2012-05-12 16:24:07 +00:00
if ( null !== $path ) {
$this -> filesystem -> removeDirectory ( $path );
}
2012-04-06 11:21:04 +00:00
$this -> throwException ( 'Failed to execute ' . $command . " \n \n " . $this -> process -> getErrorOutput (), $url );
2012-03-10 16:49:08 +00:00
}
}
2012-04-03 17:49:57 +00:00
2012-04-04 07:54:27 +00:00
public function outputHandler ( $type , $buffer )
{
if ( $type !== 'out' ) {
return ;
}
if ( $this -> io -> isVerbose ()) {
$this -> io -> write ( $buffer , false );
}
}
2012-04-06 11:21:04 +00:00
protected function throwException ( $message , $url )
{
if ( 0 !== $this -> process -> execute ( 'git --version' , $ignoredOutput )) {
throw new \RuntimeException ( 'Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . " \n \n " . $this -> process -> getErrorOutput ());
}
throw new \RuntimeException ( $message );
}
2012-04-03 17:49:57 +00:00
protected function setPushUrl ( PackageInterface $package , $path )
{
// set push url for github projects
if ( preg_match ( '{^(?:https?|git)://github.com/([^/]+)/([^/]+?)(?:\.git)?$}' , $package -> getSourceUrl (), $match )) {
$pushUrl = 'git@github.com:' . $match [ 1 ] . '/' . $match [ 2 ] . '.git' ;
2012-04-04 15:11:10 +00:00
$cmd = sprintf ( 'git remote set-url --push origin %s' , escapeshellarg ( $pushUrl ));
$this -> process -> execute ( $cmd , $ignoredOutput , $path );
2012-04-03 17:49:57 +00:00
}
}
2012-06-25 13:23:54 +00:00
2012-08-18 14:01:44 +00:00
/**
* { @ inheritDoc }
*/
protected function getCommitLogs ( $fromReference , $toReference , $path )
2012-06-25 13:23:54 +00:00
{
2012-08-18 14:01:44 +00:00
$command = sprintf ( 'cd %s && git log %s..%s --pretty=format:"%%h - %%an: %%s"' , escapeshellarg ( $path ), $fromReference , $toReference );
2012-06-25 13:23:54 +00:00
if ( 0 !== $this -> process -> execute ( $command , $output )) {
throw new \RuntimeException ( 'Failed to execute ' . $command . " \n \n " . $this -> process -> getErrorOutput ());
}
return $output ;
}
2011-09-17 13:12:45 +00:00
}