From c479a26d71f0c4388c7aff8cb74f10434009c2e2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 8 Jun 2013 17:49:51 +0200 Subject: [PATCH] Add workaround for msysgit failing to handle symlinks on windows, fixes #1048, fixes #1418 --- src/Composer/Downloader/GitDownloader.php | 29 ++++++++++++++ src/Composer/Util/ProcessExecutor.php | 14 +++++-- .../Test/Downloader/GitDownloaderTest.php | 39 ++++++++++--------- 3 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index d1c072ecf..492468871 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -28,6 +28,7 @@ class GitDownloader extends VcsDownloader public function doDownload(PackageInterface $package, $path) { $this->cleanEnv(); + $path = $this->normalizePath($path); $ref = $package->getSourceReference(); $flag = defined('PHP_WINDOWS_VERSION_MAJOR') ? '/D ' : ''; @@ -50,6 +51,7 @@ class GitDownloader extends VcsDownloader public function doUpdate(PackageInterface $initial, PackageInterface $target, $path) { $this->cleanEnv(); + $path = $this->normalizePath($path); $ref = $target->getSourceReference(); $this->io->write(" Checking out ".$ref); @@ -74,6 +76,7 @@ class GitDownloader extends VcsDownloader */ public function getLocalChanges($path) { + $path = $this->normalizePath($path); if (!is_dir($path.'/.git')) { return; } @@ -91,6 +94,7 @@ class GitDownloader extends VcsDownloader */ protected function cleanChanges($path, $update) { + $path = $this->normalizePath($path); if (!$changes = $this->getLocalChanges($path)) { return; } @@ -163,6 +167,7 @@ class GitDownloader extends VcsDownloader */ protected function reapplyChanges($path) { + $path = $this->normalizePath($path); if ($this->hasStashedChanges) { $this->hasStashedChanges = false; $this->io->write(' Re-applying stashed changes'); @@ -385,6 +390,7 @@ class GitDownloader extends VcsDownloader */ protected function getCommitLogs($fromReference, $toReference, $path) { + $path = $this->normalizePath($path); $command = sprintf('git log %s..%s --pretty=format:"%%h - %%an: %%s"', $fromReference, $toReference); if (0 !== $this->process->execute($command, $output, $path)) { @@ -400,6 +406,7 @@ class GitDownloader extends VcsDownloader */ protected function discardChanges($path) { + $path = $this->normalizePath($path); if (0 !== $this->process->execute('git reset --hard', $output, $path)) { throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); } @@ -411,6 +418,7 @@ class GitDownloader extends VcsDownloader */ protected function stashChanges($path) { + $path = $this->normalizePath($path); if (0 !== $this->process->execute('git stash', $output, $path)) { throw new \RuntimeException("Could not stash changes\n\n:".$this->process->getErrorOutput()); } @@ -427,4 +435,25 @@ class GitDownloader extends VcsDownloader // added in git 1.7.1, prevents prompting the user for username/password putenv('GIT_ASKPASS=echo'); } + + protected function normalizePath($path) + { + if (defined('PHP_WINDOWS_VERSION_MAJOR') && strlen($path) > 0) { + $basePath = $path; + $removed = array(); + + while (!is_dir($basePath) && $basePath !== '\\') { + array_unshift($removed, basename($basePath)); + $basePath = dirname($basePath); + } + + if ($basePath === '\\') { + return $path; + } + + $path = rtrim(realpath($basePath) . '/' . implode('/', $removed), '/'); + } + + return $path; + } } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 16657ba96..c0925186e 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -42,15 +42,21 @@ class ProcessExecutor */ public function execute($command, &$output = null, $cwd = null) { - $this->captureOutput = count(func_get_args()) > 1; - $this->errorOutput = null; - $process = new Process($command, $cwd, null, null, static::getTimeout()); - if ($this->io && $this->io->isDebug()) { $safeCommand = preg_replace('{(://[^:/\s]+:)[^@\s/]+}i', '$1****', $command); $this->io->write('Executing command ('.($cwd ?: 'CWD').'): '.$safeCommand); } + // make sure that null translate to the proper directory in case the dir is a symlink + // and we call a git command, because msysgit does not handle symlinks properly + if (null === $cwd && defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($command, 'git') && getcwd()) { + $cwd = realpath(getcwd()); + } + + $this->captureOutput = count(func_get_args()) > 1; + $this->errorOutput = null; + $process = new Process($command, $cwd, null, null, static::getTimeout()); + $callback = is_callable($output) ? $output : array($this, 'outputHandler'); $process->run($callback); diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 2da584c03..842a8d3f0 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -60,7 +60,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('dev-master')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->getCmd("git clone 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(0)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -68,17 +68,17 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $processExecutor->expects($this->at(1)) ->method('execute') - ->with($this->equalTo($this->getCmd("git branch -r")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git branch -r")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(2)) ->method('execute') - ->with($this->equalTo($this->getCmd("git checkout 'master'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git checkout 'master'")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') - ->with($this->equalTo($this->getCmd("git reset --hard '1234567890123456789012345678901234567890'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git reset --hard '1234567890123456789012345678901234567890'")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, null, $processExecutor); @@ -99,28 +99,28 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('1.0.0')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->getCmd("git clone 'git://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git://github.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone 'git://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'git://github.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(0)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(1)); - $expectedGitCommand = $this->getCmd("git clone 'https://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone 'https://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://github.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(2)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(1)); - $expectedGitCommand = $this->getCmd("git clone 'http://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'http://github.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone 'http://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'http://github.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(4)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(0)); - $expectedGitCommand = $this->getCmd("git remote set-url --push origin 'git@github.com:composer/composer.git'"); + $expectedGitCommand = $this->winCompat("git remote set-url --push origin 'git@github.com:composer/composer.git'"); $processExecutor->expects($this->at(5)) ->method('execute') - ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(6)) @@ -130,7 +130,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $processExecutor->expects($this->at(7)) ->method('execute') - ->with($this->equalTo($this->getCmd("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); @@ -151,7 +151,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('1.0.0')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->getCmd("git clone 'http://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'http://github.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone 'http://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'http://github.com/composer/composer' && git fetch composer"); $processExecutor->expects($this->at(0)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -173,7 +173,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase */ public function testDownloadThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitCommand = $this->getCmd("git clone 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); + $expectedGitCommand = $this->winCompat("git clone 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer 'https://example.com/composer/composer' && git fetch composer"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) ->method('getSourceReference') @@ -208,7 +208,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase public function testUpdate() { - $expectedGitUpdateCommand = $this->getCmd("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) @@ -223,7 +223,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $processExecutor->expects($this->at(0)) ->method('execute') - ->with($this->equalTo($this->getCmd("git remote -v"))) + ->with($this->equalTo($this->winCompat("git remote -v"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) ->method('execute') @@ -235,7 +235,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue(0)); $processExecutor->expects($this->at(3)) ->method('execute') - ->with($this->equalTo($this->getCmd("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->with($this->equalTo($this->winCompat("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo($this->winCompat('composerPath'))) ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, new Config(), $processExecutor); @@ -247,7 +247,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase */ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitUpdateCommand = $this->getCmd("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); + $expectedGitUpdateCommand = $this->winCompat("git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) @@ -259,7 +259,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); $processExecutor->expects($this->at(0)) ->method('execute') - ->with($this->equalTo($this->getCmd("git remote -v"))) + ->with($this->equalTo($this->winCompat("git remote -v"))) ->will($this->returnValue(0)); $processExecutor->expects($this->at(1)) ->method('execute') @@ -272,7 +272,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase public function testRemove() { - $expectedGitResetCommand = $this->getCmd("cd 'composerPath' && git status --porcelain --untracked-files=no"); + $expectedGitResetCommand = $this->winCompat("cd 'composerPath' && git status --porcelain --untracked-files=no"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); @@ -297,10 +297,11 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase $this->assertEquals('source', $downloader->getInstallationSource()); } - private function getCmd($cmd) + private function winCompat($cmd) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $cmd = str_replace('cd ', 'cd /D ', $cmd); + $cmd = str_replace('composerPath', getcwd().'/composerPath', $cmd); return strtr($cmd, "'", '"'); }