diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 96ba526cf..756218da5 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -25,8 +25,8 @@ class GitDownloader extends VcsDownloader public function doDownload(PackageInterface $package, $path) { $ref = $package->getSourceReference(); - $command = 'git clone %s %s && cd %2$s && git checkout %3$s && git reset --hard %3$s && git remote add composer %1$s'; - $this->io->write(" Cloning ".$package->getSourceReference()); + $command = 'git clone %s %s && cd %2$s && git remote add composer %1$s && git fetch composer'; + $this->io->write(" Cloning ".$ref); $commandCallable = function($url) use ($ref, $path, $command) { return sprintf($command, escapeshellarg($url), escapeshellarg($path), escapeshellarg($ref)); @@ -34,6 +34,8 @@ class GitDownloader extends VcsDownloader $this->runCommand($commandCallable, $package->getSourceUrl(), $path); $this->setPushUrl($package, $path); + + $this->updateToCommit($path, $ref, $package->getPrettyVersion(), $package->getReleaseDate()); } /** @@ -42,8 +44,8 @@ class GitDownloader extends VcsDownloader public function doUpdate(PackageInterface $initial, PackageInterface $target, $path) { $ref = $target->getSourceReference(); - $this->io->write(" Checking out ".$target->getSourceReference()); - $command = 'cd %s && git remote set-url composer %s && git fetch composer && git fetch --tags composer && git checkout %3$s && git reset --hard %3$s'; + $this->io->write(" Checking out ".$ref); + $command = 'cd %s && git remote set-url composer %s && git fetch composer && git fetch --tags composer'; // capture username/password from github URL if there is one $this->process->execute(sprintf('cd %s && git remote -v', escapeshellarg($path)), $output); @@ -56,6 +58,59 @@ class GitDownloader extends VcsDownloader }; $this->runCommand($commandCallable, $target->getSourceUrl()); + $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate()); + } + + protected function updateToCommit($path, $reference, $branch, $date) + { + $template = 'git checkout %s && git reset --hard %1$s'; + + $command = sprintf($template, escapeshellarg($reference)); + if (0 === $this->process->execute($command, $output, $path)) { + return; + } + + // reference was not found (prints "fatal: reference is not a tree: $ref") + if ($date && false !== strpos($this->process->getErrorOutput(), $reference)) { + $branch = preg_replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $branch); + $date = $date->format('U'); + + // guess which remote branch to look at first + $command = 'git branch -r'; + if (0 !== $this->process->execute($command, $output, $path)) { + 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))) { + // find the previous commit by date in the given branch + if (0 === $this->process->execute(sprintf($guessTemplate, $date, escapeshellarg(trim($line))), $output, $path)) { + $newReference = trim($output); + } + + break; + } + } + + if (empty($newReference)) { + // no matching branch found, find the previous commit by date in all commits + 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); + } + + // checkout the new recovered ref + $command = sprintf($template, escapeshellarg($newReference)); + 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()); } /** diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 43d805b79..0475a0688 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -144,7 +144,7 @@ class Factory $lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile . '.lock'; - $locker = new Package\Locker(new JsonFile($lockFile, new RemoteFilesystem($io)), $rm, md5_file($composerFile)); + $locker = new Package\Locker(new JsonFile($lockFile, new RemoteFilesystem($io)), $rm, $im, md5_file($composerFile)); $composer->setLocker($locker); } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 074dfc399..d10c438f8 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -401,6 +401,10 @@ class Installer $lockData = $this->locker->getLockData(); foreach ($lockData['packages'] as $lockedPackage) { if (!empty($lockedPackage['source-reference']) && strtolower($lockedPackage['package']) === $package->getName()) { + // update commit date to allow recovery in case the commit disappeared + if (!empty($lockedPackage['commit-date'])) { + $package->setReleaseDate(new \DateTime('@'.$lockedPackage['commit-date'])); + } $package->setSourceReference($lockedPackage['source-reference']); break; } diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 8fd78af21..2aeda8657 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -107,7 +107,7 @@ class InstallationManager public function isPackageInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) { if ($package instanceof AliasPackage) { - return $repo->hasPackage($package); + return $repo->hasPackage($package) && $this->isPackageInstalled($repo, $package->getAliasOf()); } return $this->getInstaller($package->getType())->isInstalled($repo, $package); diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 54a9deea3..1f64209a8 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -13,18 +13,22 @@ namespace Composer\Package; use Composer\Json\JsonFile; +use Composer\Installer\InstallationManager; use Composer\Repository\RepositoryManager; +use Composer\Util\ProcessExecutor; use Composer\Package\AliasPackage; /** * Reads/writes project lockfile (composer.lock). * * @author Konstantin Kudryashiv + * @author Jordi Boggiano */ class Locker { private $lockFile; private $repositoryManager; + private $installationManager; private $hash; private $lockDataCache; @@ -33,12 +37,14 @@ class Locker * * @param JsonFile $lockFile lockfile loader * @param RepositoryManager $repositoryManager repository manager instance + * @param InstallationManager $installationManager installation manager instance * @param string $hash unique hash of the current composer configuration */ - public function __construct(JsonFile $lockFile, RepositoryManager $repositoryManager, $hash) + public function __construct(JsonFile $lockFile, RepositoryManager $repositoryManager, InstallationManager $installationManager, $hash) { $this->lockFile = $lockFile; $this->repositoryManager = $repositoryManager; + $this->installationManager = $installationManager; $this->hash = $hash; } @@ -217,6 +223,12 @@ class Locker if ($package->isDev() && !$alias) { $spec['source-reference'] = $package->getSourceReference(); + if ('git' === $package->getSourceType() && $path = $this->installationManager->getInstallPath($package)) { + $process = new ProcessExecutor(); + if (0 === $process->execute('git log -n1 --pretty=%ct '.escapeshellarg($package->getSourceReference()), $output, $path)) { + $spec['commit-date'] = trim($output); + } + } } if ($alias) { diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 81a7172e3..639913498 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -50,12 +50,17 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('https://example.com/composer/composer')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->getCmd("git clone 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git checkout 'ref' && git reset --hard 'ref' && git remote add composer 'https://example.com/composer/composer'"); - $processExecutor->expects($this->once()) + $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"); + $processExecutor->expects($this->at(0)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) ->will($this->returnValue(0)); + $processExecutor->expects($this->at(1)) + ->method('execute') + ->with($this->equalTo($this->getCmd("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->will($this->returnValue(0)); + $downloader = $this->getDownloaderMock(null, $processExecutor); $downloader->download($packageMock, 'composerPath'); } @@ -71,19 +76,19 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->will($this->returnValue('https://github.com/composer/composer')); $processExecutor = $this->getMock('Composer\Util\ProcessExecutor'); - $expectedGitCommand = $this->getCmd("git clone 'git://github.com/composer/composer' 'composerPath' && cd 'composerPath' && git checkout 'ref' && git reset --hard 'ref' && git remote add composer 'git://github.com/composer/composer'"); + $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"); $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 checkout 'ref' && git reset --hard 'ref' && git remote add composer 'https://github.com/composer/composer'"); + $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"); $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 checkout 'ref' && git reset --hard 'ref' && git remote add composer 'http://github.com/composer/composer'"); + $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"); $processExecutor->expects($this->at(4)) ->method('execute') ->with($this->equalTo($expectedGitCommand)) @@ -95,6 +100,11 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->with($this->equalTo($expectedGitCommand), $this->equalTo(null), $this->equalTo('composerPath')) ->will($this->returnValue(0)); + $processExecutor->expects($this->at(6)) + ->method('execute') + ->with($this->equalTo($this->getCmd("git checkout 'ref' && git reset --hard 'ref'")), $this->equalTo(null), $this->equalTo('composerPath')) + ->will($this->returnValue(0)); + $downloader = $this->getDownloaderMock(null, $processExecutor); $downloader->download($packageMock, 'composerPath'); } @@ -104,7 +114,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase */ public function testDownloadThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitCommand = $this->getCmd("git clone 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git checkout 'ref' && git reset --hard 'ref' && git remote add composer 'https://example.com/composer/composer'"); + $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"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); $packageMock->expects($this->any()) ->method('getSourceReference') @@ -139,7 +149,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase public function testUpdate() { - $expectedGitUpdateCommand = $this->getCmd("cd 'composerPath' && git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer && git checkout 'ref' && git reset --hard 'ref'"); + $expectedGitUpdateCommand = $this->getCmd("cd 'composerPath' && git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); $expectedGitResetCommand = $this->getCmd("cd 'composerPath' && git status --porcelain --untracked-files=no"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); @@ -162,6 +172,10 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase ->method('execute') ->with($this->equalTo($expectedGitUpdateCommand)) ->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')) + ->will($this->returnValue(0)); $downloader = $this->getDownloaderMock(null, $processExecutor); $downloader->update($packageMock, $packageMock, 'composerPath'); @@ -172,7 +186,7 @@ class GitDownloaderTest extends \PHPUnit_Framework_TestCase */ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails() { - $expectedGitUpdateCommand = $this->getCmd("cd 'composerPath' && git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer && git checkout 'ref' && git reset --hard 'ref'"); + $expectedGitUpdateCommand = $this->getCmd("cd 'composerPath' && git remote set-url composer 'git://github.com/composer/composer' && git fetch composer && git fetch --tags composer"); $expectedGitResetCommand = $this->getCmd("cd 'composerPath' && git status --porcelain --untracked-files=no"); $packageMock = $this->getMock('Composer\Package\PackageInterface'); diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 8361af192..0dfff7b0b 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -170,7 +170,7 @@ class InstallerTest extends TestCase ->method('exists') ->will($this->returnValue(true)); - $locker = new Locker($lockJsonMock, $repositoryManager, md5(json_encode($composerConfig))); + $locker = new Locker($lockJsonMock, $repositoryManager, $composer->getInstallationManager(), md5(json_encode($composerConfig))); $composer->setLocker($locker); $autoloadGenerator = $this->getMock('Composer\Autoload\AutoloadGenerator'); diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index cc587a6f9..4bccc63f8 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -13,6 +13,7 @@ namespace Composer\Test\Mock; use Composer\Installer\InstallationManager; use Composer\Repository\RepositoryInterface; +use Composer\Package\PackageInterface; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -26,6 +27,11 @@ class InstallationManagerMock extends InstallationManager private $uninstalled = array(); private $trace = array(); + public function getInstallPath(PackageInterface $package) + { + return ''; + } + public function install(RepositoryInterface $repo, InstallOperation $operation) { $this->installed[] = $operation->getPackage(); diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index a67beb277..2f93d77e2 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -19,7 +19,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase public function testIsLocked() { $json = $this->createJsonFileMock(); - $locker = new Locker($json, $this->createRepositoryManagerMock(), 'md5'); + $locker = new Locker($json, $this->createRepositoryManagerMock(), $this->createInstallationManagerMock(), 'md5'); $json ->expects($this->any()) @@ -37,8 +37,9 @@ class LockerTest extends \PHPUnit_Framework_TestCase { $json = $this->createJsonFileMock(); $repo = $this->createRepositoryManagerMock(); + $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, 'md5'); + $locker = new Locker($json, $repo, $inst, 'md5'); $json ->expects($this->once()) @@ -54,8 +55,9 @@ class LockerTest extends \PHPUnit_Framework_TestCase { $json = $this->createJsonFileMock(); $repo = $this->createRepositoryManagerMock(); + $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, 'md5'); + $locker = new Locker($json, $repo, $inst, 'md5'); $json ->expects($this->once()) @@ -87,8 +89,9 @@ class LockerTest extends \PHPUnit_Framework_TestCase { $json = $this->createJsonFileMock(); $repo = $this->createRepositoryManagerMock(); + $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, 'md5'); + $locker = new Locker($json, $repo, $inst, 'md5'); $json ->expects($this->once()) @@ -122,8 +125,9 @@ class LockerTest extends \PHPUnit_Framework_TestCase { $json = $this->createJsonFileMock(); $repo = $this->createRepositoryManagerMock(); + $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, 'md5'); + $locker = new Locker($json, $repo, $inst, 'md5'); $package1 = $this->createPackageMock(); $package2 = $this->createPackageMock(); @@ -168,8 +172,9 @@ class LockerTest extends \PHPUnit_Framework_TestCase { $json = $this->createJsonFileMock(); $repo = $this->createRepositoryManagerMock(); + $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, 'md5'); + $locker = new Locker($json, $repo, $inst, 'md5'); $package1 = $this->createPackageMock(); $package1 @@ -186,8 +191,9 @@ class LockerTest extends \PHPUnit_Framework_TestCase { $json = $this->createJsonFileMock(); $repo = $this->createRepositoryManagerMock(); + $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, 'md5'); + $locker = new Locker($json, $repo, $inst, 'md5'); $json ->expects($this->once()) @@ -201,8 +207,9 @@ class LockerTest extends \PHPUnit_Framework_TestCase { $json = $this->createJsonFileMock(); $repo = $this->createRepositoryManagerMock(); + $inst = $this->createInstallationManagerMock(); - $locker = new Locker($json, $repo, 'md5'); + $locker = new Locker($json, $repo, $inst, 'md5'); $json ->expects($this->once()) @@ -232,6 +239,15 @@ class LockerTest extends \PHPUnit_Framework_TestCase return $mock; } + private function createInstallationManagerMock() + { + $mock = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->disableOriginalConstructor() + ->getMock(); + + return $mock; + } + private function createPackageMock() { return $this->getMockBuilder('Composer\Package\PackageInterface')