diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9b2611d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,43 @@ +language: php + +dist: trusty +root: false + +env: + global: + - setup=stable + +cache: + directories: + - $HOME/.composer/cache + +matrix: + fast_finish: true + allow_failures: + - php: hhvm + include: + - php: 5.6 + - php: 5.6 + env: setup=lowest + - php: 7.0 + - php: 7.0 + env: setup=lowest + - php: 7.1 + - php: 7.1 + env: setup=lowest + - php: 7.2 + - php: 7.2 + env: setup=lowest + - php: hhvm + - php: hhvm + env: setup=lowest + +before_install: + - travis_retry composer self-update + - travis_retry composer config -g github-oauth.github.com "$GITHUB_TOKEN" + +install: + - if [[ $setup = 'stable' ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest; fi + - if [[ $setup = 'lowest' ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-lowest --prefer-stable --no-suggest; fi + +script: composer run test diff --git a/README.md b/README.md index 41982bc..b1ac001 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +[![Build Status](https://travis-ci.org/slowprog/CopyFile.svg?branch=master)](https://travis-ci.org/slowprog/CopyFile) +[![Latest Stable Version](https://poser.pugx.org/slowprog/composer-copy-file/version)](https://packagist.org/packages/slowprog/composer-copy-file) +[![Total Downloads](https://poser.pugx.org/slowprog/composer-copy-file/downloads)](https://packagist.org/packages/slowprog/composer-copy-file) + # Composer copy file Composer script copying your files after install. Supports copying of entire directories, individual files and complex nested directories. @@ -26,10 +30,39 @@ For example copy fonts: } ``` +In a development you may use `-dev` suffix. For example copy non-minified in development and minified in production: + +``` +{ + "require":{ + "twbs/bootstrap": "~3.3", + "slowprog/composer-copy-file": "~0.2" + }, + "scripts": { + "post-install-cmd": [ + "SlowProg\\CopyFile\\ScriptHandler::copy" + ], + "post-update-cmd": [ + "SlowProg\\CopyFile\\ScriptHandler::copy" + ] + }, + "extra": { + "copy-file": { + "vendor/twbs/bootstrap/dist/js/bootstrap.min.js": "web/js/bootstrap.js" + }, + "copy-file-dev": { + "vendor/twbs/bootstrap/dist/js/bootstrap.js": "web/js/bootstrap.js" + } + } +} +``` + ## Use cases You need to be careful when using a last slash. The file-destination is different from the directory-destination with the slash. +If in destination directory already exists copy of file, then it will be override. To overwrite only older files append `?` in end of destination path. + Source directory hierarchy: ``` @@ -104,3 +137,33 @@ dir/ file1.txt file_rename.txt ``` + +4. Override only older files: + + ``` + { + "extra": { + "copy-file": { + "dir/subdir/": "web/other/?" + } + } + } + ``` + + Preset: + + ``` + web/ + other/ + file1.txt - Recently modified + file2.txt - Old rotten file + ``` + + Result: + + ``` + web/ + other/ + file1.txt - Not changed + file2.txt - Replaced + ``` \ No newline at end of file diff --git a/ScriptHandler.php b/ScriptHandler.php index 3091245..5f9d8aa 100644 --- a/ScriptHandler.php +++ b/ScriptHandler.php @@ -15,12 +15,13 @@ class ScriptHandler public static function copy(Event $event) { $extras = $event->getComposer()->getPackage()->getExtra(); + $extraField = $event->isDevMode() && isset($extras['copy-file-dev']) ? 'copy-file-dev' : 'copy-file'; - if (!isset($extras['copy-file'])) { + if (!isset($extras[$extraField])) { throw new \InvalidArgumentException('The dirs or files needs to be configured through the extra.copy-file setting.'); } - $files = $extras['copy-file']; + $files = $extras[$extraField]; if ($files === array_values($files)) { throw new \InvalidArgumentException('The extra.copy-file must be hash like "{: }".'); @@ -30,6 +31,12 @@ public static function copy(Event $event) $io = $event->getIO(); foreach ($files as $from => $to) { + // check the overwrite newer files disable flag (? in end of path) + $overwriteNewerFiles = substr($to, -1) != '?'; + if (!$overwriteNewerFiles) { + $to = substr($to, 0, -1); + } + // Check the renaming of file for direct moving (file-to-file) $isRenameFile = substr($to, -1) != '/' && !is_dir($from); @@ -59,7 +66,7 @@ public static function copy(Event $event) $dest = sprintf('%s/%s', $to, $file->getRelativePathname()); try { - $fs->copy($file, $dest); + $fs->copy($file, $dest, $overwriteNewerFiles); } catch (IOException $e) { throw new \InvalidArgumentException(sprintf('Could not copy %s', $file->getBaseName()), $e->getCode(), $e); } @@ -67,9 +74,9 @@ public static function copy(Event $event) } else { try { if ($isRenameFile) { - $fs->copy($from, $to); + $fs->copy($from, $to, $overwriteNewerFiles); } else { - $fs->copy($from, $to.'/'.basename($from)); + $fs->copy($from, $to.'/'.basename($from), $overwriteNewerFiles); } } catch (IOException $e) { throw new \InvalidArgumentException(sprintf('Could not copy %s', $from), $e->getCode(), $e); diff --git a/composer.json b/composer.json index 3121f28..52b0bb7 100644 --- a/composer.json +++ b/composer.json @@ -11,16 +11,23 @@ } ], "require": { - "php": ">=5.3.3" + "php": ">=5.6" }, "require-dev": { "composer/composer": "1.0.*@dev", "symfony/filesystem": "~2.7", "symfony/finder": "~2.7", - "phpunit/phpunit": "~5.0", - "mikey179/vfsStream": "~1" + "phpunit/phpunit": "5.7.27", + "mikey179/vfsStream": "~1", + "php-mock/php-mock-phpunit": "~1" }, "autoload": { "psr-4": { "SlowProg\\CopyFile\\": "" } + }, + "autoload-dev": { + "classmap": [ "tests/" ] + }, + "scripts": { + "test": "phpunit --verbose" } } diff --git a/phpunit.xml b/phpunit.xml index 9f720ad..4bb4964 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,7 +12,7 @@ syntaxCheck="false"> - ./tests/ + ./tests/ diff --git a/tests/CopyFileTest.php b/tests/CopyFileTest.php index 4b212f7..7102a87 100644 --- a/tests/CopyFileTest.php +++ b/tests/CopyFileTest.php @@ -1,64 +1,51 @@ getFilesystem(); - - $this->assertTrue($root->hasChild('from/file1')); - $this->assertTrue($root->hasChild('from/file2')); + $this->assertTrue($this->root->hasChild('from/file1')); + $this->assertTrue($this->root->hasChild('from/file2')); - $this->assertTrue($root->hasChild('file3')); + $this->assertTrue($this->root->hasChild('file3')); - $this->assertTrue($root->hasChild('from_complex/file4')); - $this->assertTrue($root->hasChild('from_complex/sub_dir/file5')); + $this->assertTrue($this->root->hasChild('from_complex/file4')); + $this->assertTrue($this->root->hasChild('from_complex/sub_dir/file5')); } public function testCopyDirToDir() { - $root = $this->getFilesystem(); - - $this->assertFalse($root->hasChild('to/file1')); - $this->assertFalse($root->hasChild('to/file2')); + $this->assertFalse($this->root->hasChild('to/file1')); + $this->assertFalse($this->root->hasChild('to/file2')); ScriptHandler::copy($this->getEventMock([ vfsStream::url('root/from')=> vfsStream::url('root/to') ])); - $this->assertTrue($root->hasChild('to/file1')); - $this->assertTrue($root->hasChild('to/file2')); + $this->assertTrue($this->root->hasChild('to/file1')); + $this->assertTrue($this->root->hasChild('to/file2')); } public function testCopyDirToNotExistsDir() { - $root = $this->getFilesystem(); - - $this->assertFalse($root->hasChild('not_exists')); + $this->assertFalse($this->root->hasChild('not_exists')); ScriptHandler::copy($this->getEventMock([ vfsStream::url('root/from')=> vfsStream::url('root/not_exists') ])); - $this->assertTrue($root->hasChild('not_exists')); - $this->assertTrue($root->hasChild('not_exists/file1')); - $this->assertTrue($root->hasChild('not_exists/file2')); + $this->assertTrue($this->root->hasChild('not_exists')); + $this->assertTrue($this->root->hasChild('not_exists/file1')); + $this->assertTrue($this->root->hasChild('not_exists/file2')); } public function testCopyFromNotExistsDir() { - $root = $this->getFilesystem(); - $this->expectException(InvalidArgumentException::class); ScriptHandler::copy($this->getEventMock([ @@ -68,8 +55,6 @@ public function testCopyFromNotExistsDir() public function testCopyDirToFile() { - $root = $this->getFilesystem(); - $this->expectException(InvalidArgumentException::class); ScriptHandler::copy($this->getEventMock([ @@ -79,144 +64,74 @@ public function testCopyDirToFile() public function testCopyFileToDir() { - $root = $this->getFilesystem(); - - $this->assertFalse($root->hasChild('to/file3')); + $this->assertFalse($this->root->hasChild('to/file3')); ScriptHandler::copy($this->getEventMock([ vfsStream::url('root/file3')=> vfsStream::url('root/to/') ])); - $this->assertTrue($root->hasChild('to/file3')); + $this->assertTrue($this->root->hasChild('to/file3')); } public function testCopyFileToFile() { - $root = $this->getFilesystem(); - - $this->assertFalse($root->hasChild('to/file_new')); + $this->assertFalse($this->root->hasChild('to/file_new')); ScriptHandler::copy($this->getEventMock([ vfsStream::url('root/file3')=> vfsStream::url('root/to/file_new') ])); - $this->assertTrue($root->hasChild('to/file_new')); + $this->assertTrue($this->root->hasChild('to/file_new')); } public function testCopyFromComplexDir() { - $root = $this->getFilesystem(); - - $this->assertFalse($root->hasChild('to/file4')); - $this->assertFalse($root->hasChild('to/sub_dir/file5')); - $this->assertFalse($root->hasChild('to/git_keep_dir')); + $this->assertFalse($this->root->hasChild('to/file4')); + $this->assertFalse($this->root->hasChild('to/sub_dir/file5')); + $this->assertFalse($this->root->hasChild('to/git_keep_dir')); ScriptHandler::copy($this->getEventMock([ vfsStream::url('root/from_complex')=> vfsStream::url('root/to') ])); - $this->assertTrue($root->hasChild('to/file4')); - $this->assertTrue($root->hasChild('to/sub_dir/file5')); - $this->assertTrue($root->hasChild('to/git_keep_dir')); + $this->assertTrue($this->root->hasChild('to/file4')); + $this->assertTrue($this->root->hasChild('to/sub_dir/file5')); + $this->assertTrue($this->root->hasChild('to/git_keep_dir')); } - public function testConfigError() + public function testRewriteExists() { - $root = $this->getFilesystem(); + $this->root->lastModified(0); + $this->root->getChild('dynamic_dir/file1')->lastModified(1); - $this->expectException(InvalidArgumentException::class); + $exchanged = vfsStream::url('root/dynamic_dir/file1'); + $unaltered = vfsStream::url('root/dynamic_dir/file2'); - ScriptHandler::copy($this->getEventMock([])); - ScriptHandler::copy($this->getEventMock(['to', 'from', 'file3'])); - ScriptHandler::copy($this->getEventMock(null)); - ScriptHandler::copy($this->getEventMock('some string')); - } + // allow filemtime check apply for vfs protocol + $this->getFunctionMock('Symfony\Component\Filesystem', 'parse_url') + ->expects($this->any())->willReturn(null); - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getEventMock($copyFileConfig) - { - $event = $this->getMockBuilder('Composer\Script\Event') - ->disableOriginalConstructor() - ->getMock(); + // preset filemtime check + $this->getFunctionMock('Symfony\Component\Filesystem', 'filemtime') + ->expects($this->any())->willReturnCallback(function ($filename) use ($unaltered) { + return $filename === $unaltered ? 1 : 2; + }); - $event - ->expects($this->once()) - ->method('getComposer') - ->will($this->returnValue($this->getComposerMock($copyFileConfig))); + ScriptHandler::copy($this->getEventMock(array( + vfsStream::url('root/from') => vfsStream::url('root/dynamic_dir') . '?' + ))); - $event - ->method('getIO') - ->will($this->returnValue($this->createMock('\Composer\IO\IOInterface'))); - - return $event; - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getComposerMock($copyFileConfig) - { - $package = $this->getPackageMock($copyFileConfig); - - $composer = $this->getMockBuilder('Composer\Composer') - ->disableOriginalConstructor() - ->getMock(); - - $composer - ->expects($this->once()) - ->method('getPackage') - ->will($this->returnValue($package)); - - return $composer; + $this->assertFileNotEquals(vfsStream::url('root/from/file1'), $exchanged); + $this->assertFileEquals(vfsStream::url('root/from/file2'), $unaltered); } - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getPackageMock($copyFileConfig) + public function testConfigError() { - $extra = null; - - if (!is_null($copyFileConfig)) { - $extra = [ - 'copy-file' => $copyFileConfig, - ]; - } - - $package = $this->getMockBuilder('Composer\Package\RootPackageInterface') - ->disableOriginalConstructor() - ->getMock(); - - $package - ->expects($this->once()) - ->method('getExtra') - ->will($this->returnValue($extra)); - - return $package; - } + $this->expectException(InvalidArgumentException::class); - private function getFilesystem() - { - $structure = [ - 'from' => [ - 'file1' => 'Some content', - 'file2' => 'Some content', - ], - 'to' => [], - 'file3' => 'Some content', - 'from_complex' => [ - 'file4' => 'Some content', - 'sub_dir' => [ - 'file5' => 'Some content', - ], - 'git_keep_dir' => [ - '.gitkeep' => '', - ], - ], - ]; - - return vfsStream::setup('root', null, $structure); + ScriptHandler::copy($this->getEventMock([])); + ScriptHandler::copy($this->getEventMock(['to', 'from', 'file3'])); + ScriptHandler::copy($this->getEventMock(null)); + ScriptHandler::copy($this->getEventMock('some string')); } } diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..d92f7f6 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,114 @@ +root = $this->getFilesystem(); + } + + /** + * @param array|string|null $copyFileConfig + * @return \PHPUnit_Framework_MockObject_MockObject|\Composer\Script\Event + */ + protected function getEventMock($copyFileConfig) + { + $event = $this->getMockBuilder('\Composer\Script\Event') + ->disableOriginalConstructor() + ->getMock(); + + $event + ->expects($this->once()) + ->method('getComposer') + ->will($this->returnValue($this->getComposerMock($copyFileConfig))); + + $event + ->method('getIO') + ->will($this->returnValue($this->createMock('\Composer\IO\IOInterface'))); + + return $event; + } + + /** + * @param array|string|null $copyFileConfig + * @return \PHPUnit_Framework_MockObject_MockObject|\Composer\Composer + */ + protected function getComposerMock($copyFileConfig) + { + $package = $this->getPackageMock($copyFileConfig); + + $composer = $this->getMockBuilder('\Composer\Composer') + ->disableOriginalConstructor() + ->getMock(); + + $composer + ->expects($this->once()) + ->method('getPackage') + ->will($this->returnValue($package)); + + return $composer; + } + + /** + * @param array|string|null $copyFileConfig + * @return \PHPUnit_Framework_MockObject_MockObject|\Composer\Package\RootPackageInterface + */ + protected function getPackageMock($copyFileConfig) + { + $extra = null; + + if (!is_null($copyFileConfig)) { + $extra = array( + 'copy-file' => $copyFileConfig, + ); + } + + $package = $this->getMockBuilder('\Composer\Package\RootPackageInterface') + ->disableOriginalConstructor() + ->getMock(); + + $package + ->expects($this->once()) + ->method('getExtra') + ->will($this->returnValue($extra)); + + return $package; + } + + /** + * @return \org\bovigo\vfs\vfsStreamDirectory + */ + protected function getFilesystem() + { + $structure = array( + 'from' => array( + 'file1' => 'Some content', + 'file2' => 'Some content', + ), + 'to' => array(), + 'file3' => 'Some content', + 'from_complex' => array( + 'file4' => 'Some content', + 'sub_dir' => array( + 'file5' => 'Some content', + ), + 'git_keep_dir' => array( + '.gitkeep' => '', + ), + ), + 'dynamic_dir' => array( + 'file1' => 'Exchanged content', + 'file2' => 'Unaltered content', + ), + ); + + return vfsStream::setup('root', null, $structure); + } +} \ No newline at end of file