diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..59343a7
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.yml]
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..e76bf01
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,20 @@
+# Folders
+/.github export-ignore
+/tests export-ignore
+
+# Files
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.php_cs.dist export-ignore
+.phplint.yml export-ignore
+phpunit.xml.dist export-ignore
+
+# Enforce checkout with linux lf consistent over all plattforms
+.php_cs.dist text eol=lf
+*.json text eol=lf
+*.md text eol=lf
+*.php text eol=lf
+*.xml text eol=lf
+*.xml.dist text eol=lf
+*.yml text eol=lf
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..cfd2afd
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,65 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+ build-php:
+ name: PHP ${{ matrix.php-versions }} with Composer ${{ matrix.composer-versions }} on ${{ matrix.operating-system }}
+ runs-on: ${{ matrix.operating-system }}
+ strategy:
+ max-parallel: 4
+ matrix:
+ operating-system:
+ - 'ubuntu-latest'
+ - 'windows-latest'
+ php-versions:
+ - '7.3'
+ - '7.4'
+ composer-versions:
+ - 'v1'
+ - 'v2'
+ steps:
+
+ - name: Checkout Code
+ uses: actions/checkout@v2
+
+ - name: Set up PHP Version
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ tools: composer:${{ matrix.composer-versions }}
+
+ - name: Environment Check
+ run: |
+ php --version
+ composer --version
+
+ - name: Require Composer@v1
+ if: ${{ matrix.composer-versions == 'v1' }}
+ run: |
+ composer require "composer/composer:^1.10" --dev --no-update
+
+ - name: Require Composer@v2
+ if: ${{ matrix.composer-versions == 'v2' }}
+ run: |
+ composer require "composer/composer:2.0.x-dev" --dev --no-update
+
+ - name: Install
+ run: |
+ composer install --no-progress
+
+ - name: Info
+ run: |
+ composer info
+
+ - name: Lint
+ run: |
+ composer test:php:lint
+
+ - name: CGL
+ run: |
+ composer cgl
+
+ - name: Unit Tests
+ run: |
+ composer test:php:unit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ae29318
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+bin
+build
+vendor
+*.cache
+composer.lock
diff --git a/.php_cs.dist b/.php_cs.dist
new file mode 100644
index 0000000..0f4f89d
--- /dev/null
+++ b/.php_cs.dist
@@ -0,0 +1,69 @@
+setRiskyAllowed(true)
+ ->setRules([
+ '@PSR2' => true,
+ 'declare_strict_types' => true,
+ 'header_comment' => [
+ 'header' => $header
+ ],
+ 'general_phpdoc_annotation_remove' => [
+ 'annotations' => [
+ 'author'
+ ]
+ ],
+ 'no_leading_import_slash' => true,
+ 'no_trailing_comma_in_singleline_array' => true,
+ 'no_singleline_whitespace_before_semicolons' => true,
+ 'no_unused_imports' => true,
+ 'concat_space' => ['spacing' => 'one'],
+ 'no_whitespace_in_blank_line' => true,
+ 'ordered_imports' => true,
+ 'single_quote' => true,
+ 'no_empty_statement' => true,
+ 'no_extra_blank_lines' => true,
+ 'phpdoc_no_package' => true,
+ 'phpdoc_scalar' => true,
+ 'no_blank_lines_after_phpdoc' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'whitespace_after_comma_in_array' => true,
+ 'function_typehint_space' => true,
+ 'single_line_comment_style' => true,
+ 'no_alias_functions' => true,
+ 'lowercase_cast' => true,
+ 'no_leading_namespace_whitespace' => true,
+ 'native_function_casing' => true,
+ 'self_accessor' => true,
+ 'no_short_bool_cast' => true,
+ 'no_unneeded_control_parentheses' => true
+ ])
+ ->setFinder(
+ PhpCsFixer\Finder::create()
+ ->in(['src', 'tests'])
+ );
diff --git a/.phplint.yml b/.phplint.yml
new file mode 100644
index 0000000..6a81694
--- /dev/null
+++ b/.phplint.yml
@@ -0,0 +1,9 @@
+path: ./
+jobs: 10
+cache: .phplint.cache
+extensions:
+ - php
+exclude:
+ - bin
+ - build
+ - vendor
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b3343da
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 Benjamin Kott, http://www.bk2k.info
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cc9471c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# Project Configuration Installer
+
+Installer for Configuration Packages.
+Example: https://github.com/benjaminkott/config-phpcsfixer-typo3
+
+## Build your own configuration package
+
+Adapt the `composer.json` of your **configuration package**.
+
+1. Ensure the type is set to `project-configuration`.
+1. Ensure `bk2k/configuration-installer` is required in any version.
+
+```json
+{
+ "type": "project-configuration",
+ "require": {
+ "bk2k/configuration-installer": "*"
+ }
+}
+ ```
+
+### Add a manifest to your configuration package root.
+
+The `manifest.json` file instructs the installer.
+
+1. It defines which `files` should be copied to your project
+1. It defines which `gitignore` entries will be added to your projects .gitignore file.
+
+
+```json
+{
+ "files": {
+ ".php_cs.dist": ".php_cs.dist"
+ },
+ "gitignore": [
+ "/.php_cs.dist",
+ "/.php_cs.cache"
+ ]
+}
+```
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..c32f68f
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,73 @@
+{
+ "name": "bk2k/configuration-installer",
+ "type": "composer-plugin",
+ "description": "Configuration Installer",
+ "homepage": "https://github.com/benjaminkott/configuration-installer",
+ "authors": [
+ {
+ "name": "Benjamin Kott",
+ "email": "info@bk2k.info",
+ "role": "Developer",
+ "homepage": "http://www.bk2k.info/"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/benjaminkott/configuration-installer/issues"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "license": "MIT",
+ "require": {
+ "php": "^7.3",
+ "composer-plugin-api": "^1.1 || ^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "^1.10 || ^2.0@dev",
+ "friendsofphp/php-cs-fixer": "^2.16 || ^3.0@dev",
+ "overtrue/phplint": "^2.0",
+ "phpunit/phpunit": "^9.2"
+ },
+ "autoload": {
+ "psr-4": {
+ "BK2K\\ConfigurationInstaller\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "BK2K\\ConfigurationInstallerTest\\": "tests/"
+ }
+ },
+ "config": {
+ "bin-dir": "bin",
+ "optimize-autoloader": true,
+ "sort-packages": true,
+ "vendor-dir": "vendor"
+ },
+ "scripts": {
+ "test:php:lint": [
+ "phplint"
+ ],
+ "test:php:unit": [
+ "Composer\\Config::disableProcessTimeout",
+ "phpunit"
+ ],
+ "test:php:cover": [
+ "Composer\\Config::disableProcessTimeout",
+ "phpunit --coverage-html build/coverage-report"
+ ],
+ "test": [
+ "@cgl",
+ "@test:php:lint",
+ "@test:php:unit"
+ ],
+ "cgl:fix": [
+ "php-cs-fixer fix"
+ ],
+ "cgl": [
+ "php-cs-fixer fix --dry-run"
+ ]
+ },
+ "extra": {
+ "class": "BK2K\\ConfigurationInstaller\\Installer\\Plugin"
+ }
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..40b2590
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,24 @@
+
+
+
+
+
+ tests
+
+
+
+
+ src
+
+
+
+
diff --git a/src/Configuration/File.php b/src/Configuration/File.php
new file mode 100644
index 0000000..ae56ae4
--- /dev/null
+++ b/src/Configuration/File.php
@@ -0,0 +1,32 @@
+source = $source;
+ $this->target = $target;
+ }
+
+ public function getSource(): string
+ {
+ return $this->source;
+ }
+
+ public function getTarget():string
+ {
+ return $this->target;
+ }
+}
diff --git a/src/Configuration/GitignoreEntry.php b/src/Configuration/GitignoreEntry.php
new file mode 100644
index 0000000..50381f7
--- /dev/null
+++ b/src/Configuration/GitignoreEntry.php
@@ -0,0 +1,25 @@
+path = $path;
+ }
+
+ public function getPath(): string
+ {
+ return $this->path;
+ }
+}
diff --git a/src/Configuration/InstallerConfiguration.php b/src/Configuration/InstallerConfiguration.php
new file mode 100644
index 0000000..957296d
--- /dev/null
+++ b/src/Configuration/InstallerConfiguration.php
@@ -0,0 +1,51 @@
+files[] = $file;
+ return $this;
+ }
+
+ /**
+ * @return File[]
+ */
+ public function getFiles(): array
+ {
+ return $this->files;
+ }
+
+ public function addGitignoreEntry(GitignoreEntry $gitignoreEntry): self
+ {
+ $this->gitignoreEntries[] = $gitignoreEntry;
+ return $this;
+ }
+
+ /**
+ * @return GitignoreEntry[]
+ */
+ public function getGitignoreEntries(): array
+ {
+ return $this->gitignoreEntries;
+ }
+}
diff --git a/src/Factory/ConfigurationFactory.php b/src/Factory/ConfigurationFactory.php
new file mode 100644
index 0000000..a9e9c11
--- /dev/null
+++ b/src/Factory/ConfigurationFactory.php
@@ -0,0 +1,39 @@
+ $target) {
+ if (!is_string($target)) {
+ continue;
+ }
+ $source = is_int($source) ? $target : $source;
+ $installerConfiguration->addFile(new File($source, $target));
+ }
+ foreach ($data['gitignore'] ?? [] as $value) {
+ $installerConfiguration->addGitignoreEntry(new GitignoreEntry($value));
+ }
+ return $installerConfiguration;
+ }
+}
diff --git a/src/Handler/AbstractHandler.php b/src/Handler/AbstractHandler.php
new file mode 100644
index 0000000..d0e1c92
--- /dev/null
+++ b/src/Handler/AbstractHandler.php
@@ -0,0 +1,38 @@
+composer = $composer;
+ $this->io = $io;
+ $this->filesystem = $filesystem;
+ $this->initialize();
+ }
+
+ public function initialize()
+ {
+ }
+
+ abstract public function install(InstallerConfiguration $installerConfiguration, PackageInterface $package);
+ abstract public function remove(InstallerConfiguration $installerConfiguration, PackageInterface $package);
+}
diff --git a/src/Handler/FileHandler.php b/src/Handler/FileHandler.php
new file mode 100644
index 0000000..b2ef2b3
--- /dev/null
+++ b/src/Handler/FileHandler.php
@@ -0,0 +1,54 @@
+io->write(' Installing file links for ' . $package->getName() . '');
+ $packagePath = $this->getPackagePath($package);
+ foreach ($installerConfiguration->getFiles() as $file) {
+ $source = $this->filesystem->normalizePath($packagePath . '/' . $file->getSource());
+ $target = $this->filesystem->normalizePath(getcwd() . '/' . $file->getTarget());
+ if (!file_exists($source)) {
+ throw new \InvalidArgumentException('The source "' . $source . '" is not available.');
+ }
+ if (file_exists($target)) {
+ $this->filesystem->remove($target);
+ }
+ $this->filesystem->ensureDirectoryExists(dirname($target));
+ $this->filesystem->copy($source, $target);
+ $this->io->write(' ' . $source . ' -> ' . $target . '');
+ }
+ }
+
+ public function remove(InstallerConfiguration $installerConfiguration, PackageInterface $package)
+ {
+ $this->io->write(' Remove file links for ' . $package->getName() . '');
+ foreach ($installerConfiguration->getFiles() as $file) {
+ $target = $this->filesystem->normalizePath(realpath($file->getTarget()));
+ $this->io->write(' Remove ' . $target . '');
+ $this->filesystem->unlink($target);
+ }
+ }
+
+ private function getPackagePath(PackageInterface $package): string
+ {
+ $vendorDir = rtrim($this->composer->getConfig()->get('vendor-dir'), '/');
+ $basePath = ($vendorDir ? $vendorDir . '/' : '') . $package->getPrettyName();
+ $targetDir = $package->getTargetDir();
+
+ return $basePath . ($targetDir ? '/' . $targetDir : '');
+ }
+}
diff --git a/src/Handler/GitignoreHandler.php b/src/Handler/GitignoreHandler.php
new file mode 100644
index 0000000..2b3d1ed
--- /dev/null
+++ b/src/Handler/GitignoreHandler.php
@@ -0,0 +1,40 @@
+gitignore = new GitignoreService();
+ }
+
+ public function install(InstallerConfiguration $installerConfiguration, PackageInterface $package)
+ {
+ foreach ($installerConfiguration->getGitignoreEntries() as $entry) {
+ $this->gitignore->addEntry($entry->getPath());
+ }
+ $this->gitignore->write();
+ }
+
+ public function remove(InstallerConfiguration $installerConfiguration, PackageInterface $package)
+ {
+ foreach ($installerConfiguration->getGitignoreEntries() as $entry) {
+ $this->gitignore->removeEntry($entry->getPath());
+ }
+ $this->gitignore->write();
+ }
+}
diff --git a/src/Installer/ConfigurationInstaller.php b/src/Installer/ConfigurationInstaller.php
new file mode 100644
index 0000000..d3a7161
--- /dev/null
+++ b/src/Installer/ConfigurationInstaller.php
@@ -0,0 +1,94 @@
+handler = [
+ 'files' => Handler\FileHandler::class,
+ 'gitignore' => Handler\GitignoreHandler::class
+ ];
+ }
+
+ public function getHandler($key)
+ {
+ if (!isset($this->handler[$key])) {
+ throw new \InvalidArgumentException(sprintf('Unknown handler "%s".', $key));
+ }
+ if (isset($this->cache[$key])) {
+ return $this->cache[$key];
+ }
+ $class = $this->handler[$key];
+ return $this->cache[$key] = new $class($this->composer, $this->io, $this->filesystem);
+ }
+
+ protected function installCode(PackageInterface $package)
+ {
+ parent::installCode($package);
+ $this->processInstallPackage($package);
+ }
+
+ protected function updateCode(PackageInterface $initial, PackageInterface $target)
+ {
+ $this->processRemovePackage($initial);
+ parent::updateCode($initial, $target);
+ $this->processInstallPackage($target);
+ }
+
+ protected function removeCode(PackageInterface $package)
+ {
+ $this->processRemovePackage($package);
+ parent::removeCode($package);
+ }
+
+ public function processInstallPackage(PackageInterface $package)
+ {
+ $installerConfiguration = $this->getInstallerConfiguration($package);
+ foreach (array_keys($this->handler) as $handler) {
+ $this->getHandler($handler)->install($installerConfiguration, $package);
+ }
+ }
+
+ public function processRemovePackage(PackageInterface $package)
+ {
+ $installerConfiguration = $this->getInstallerConfiguration($package);
+ foreach (array_keys($this->handler) as $handler) {
+ $this->getHandler($handler)->remove($installerConfiguration, $package);
+ }
+ }
+
+ public function getInstallerConfiguration(PackageInterface $package): InstallerConfiguration
+ {
+ $directory = $this->getInstallPath($package);
+ $configurationManifestPath = $this->filesystem->normalizePath($directory . DIRECTORY_SEPARATOR . 'manifest.json');
+ $importedConfiguration = [];
+ if (\file_exists($configurationManifestPath)) {
+ $importedConfiguration = json_decode(file_get_contents($configurationManifestPath), true, 512, JSON_THROW_ON_ERROR);
+ }
+
+ return ConfigurationFactory::fromArray($importedConfiguration);
+ }
+}
diff --git a/src/Installer/Plugin.php b/src/Installer/Plugin.php
new file mode 100644
index 0000000..49ccc33
--- /dev/null
+++ b/src/Installer/Plugin.php
@@ -0,0 +1,37 @@
+getInstallationManager()->addInstaller($installer);
+ }
+
+ public function deactivate(
+ Composer $composer,
+ IOInterface $io
+ ) {
+ }
+
+ public function uninstall(
+ Composer $composer,
+ IOInterface $io
+ ) {
+ }
+}
diff --git a/src/Service/GitignoreService.php b/src/Service/GitignoreService.php
new file mode 100644
index 0000000..26bc527
--- /dev/null
+++ b/src/Service/GitignoreService.php
@@ -0,0 +1,110 @@
+ensureGitignoreExists();
+ $this->lines = $this->removeDuplicates(file($this->gitignore, FILE_IGNORE_NEW_LINES));
+ }
+
+ public function addEntry(string $entry): void
+ {
+ $entry = $this->prependSlashIfNotExist($entry);
+ if (!in_array($entry, $this->lines)) {
+ $this->lines[] = $entry;
+ }
+ $this->hasChanges = true;
+ }
+
+ public function removeEntry(string $entry): void
+ {
+ $entry = $this->prependSlashIfNotExist($entry);
+ $key = array_search($entry, $this->lines);
+ if (false !== $key) {
+ unset($this->lines[$key]);
+ $this->hasChanges = true;
+ // renumber array
+ $this->lines = array_values($this->lines);
+ }
+ }
+
+ public function getEntries(): array
+ {
+ return $this->lines;
+ }
+
+ public function write(): void
+ {
+ if ($this->hasChanges) {
+ file_put_contents($this->gitignore, implode("\n", $this->lines) . "\n");
+ }
+ }
+
+ private function prependSlashIfNotExist(string $file): string
+ {
+ return sprintf('/%s', ltrim($file, '/'));
+ }
+
+ private function removeDuplicates(array $lines): array
+ {
+ // remove empty lines
+ $duplicates = array_filter($lines);
+ // remove comments
+ $duplicates = array_filter($duplicates, function ($line) {
+ return strpos($line, '#') !== 0;
+ });
+ // check if duplicates exist
+ if (count($duplicates) !== count(array_unique($duplicates))) {
+ $duplicates = array_filter(array_count_values($duplicates), function ($count) {
+ return $count > 1;
+ });
+ // search from bottom to top
+ $lines = array_reverse($lines);
+ foreach ($duplicates as $duplicate => $count) {
+ // remove all duplicates, except the first one
+ for ($i = 1; $i < $count; $i++) {
+ $key = array_search($duplicate, $lines);
+ unset($lines[$key]);
+ }
+ }
+ // restore original order
+ $lines = array_values(array_reverse($lines));
+ }
+ return $lines;
+ }
+
+ private function ensureGitignoreExists()
+ {
+ if (!file_exists($this->gitignore)) {
+ file_put_contents($this->gitignore, "\n");
+ }
+ }
+}
diff --git a/src/bootstrap.php b/src/bootstrap.php
new file mode 100644
index 0000000..acfb4b6
--- /dev/null
+++ b/src/bootstrap.php
@@ -0,0 +1,21 @@
+createConfigurationInstaller();
+ $this->assertTrue($installer->supports('project-configuration'));
+ }
+
+ public function testConfigurationInstallerSupportConfigurationTypeFromPackage()
+ {
+ $installer = $this->createConfigurationInstaller();
+ $package = $this->createPackageMock('bk2k/configuration-test', 'project-configuration');
+ $this->assertTrue($installer->supports($package->getType()));
+ }
+
+ public function testConfigurationInstallerIsNotSupportedWithOtherPackageType()
+ {
+ $installer = $this->createConfigurationInstaller();
+ $package = $this->createPackageMock('bk2k/configuration-test', 'configuration');
+ $this->assertFalse($installer->supports($package->getType()));
+ }
+
+ /**
+ * @dataProvider provideFilesForTesting
+ */
+ public function testFilesCreated(array $packageFiles, array $expectedResult)
+ {
+ $installer = $this->createConfigurationInstaller();
+ $package = $this->createPackageMockWithConfigurationFiles($installer, $packageFiles);
+ $installer->install($this->repository, $package);
+
+ foreach ($expectedResult['files'] as $file) {
+ $this->assertFileExists($this->rootDirectory . DIRECTORY_SEPARATOR . $file);
+ }
+ }
+
+ /**
+ * @dataProvider provideFilesForTesting
+ * @depends testFilesCreated
+ */
+ public function testFilesRemoved(array $packageFiles, array $expectedResult)
+ {
+ $installer = $this->createConfigurationInstaller();
+ $package = $this->createPackageMockWithConfigurationFiles($installer, $packageFiles);
+ $installer->install($this->repository, $package);
+
+ $this->repository
+ ->expects($this->exactly(1))
+ ->method('hasPackage')
+ ->with($package)
+ ->willReturn(true)
+ ;
+
+ $installer->uninstall($this->repository, $package);
+
+ foreach ($expectedResult['files'] as $file) {
+ $this->assertFileDoesNotExist($this->rootDirectory . DIRECTORY_SEPARATOR . $file);
+ }
+ }
+
+ /**
+ * @dataProvider provideFilesForTesting
+ */
+ public function testGitignoreCreatedIfNotPresent(array $packageFiles, array $expectedResult)
+ {
+ $installer = $this->createConfigurationInstaller();
+ $package = $this->createPackageMockWithConfigurationFiles($installer, $packageFiles);
+ $this->assertFileDoesNotExist($this->rootDirectory . DIRECTORY_SEPARATOR . '.gitignore');
+
+ $installer->install($this->repository, $package);
+ $this->assertFileExists($this->rootDirectory . DIRECTORY_SEPARATOR . '.gitignore');
+ }
+
+ /**
+ * @dataProvider provideFilesForTesting
+ * @depends testGitignoreCreatedIfNotPresent
+ */
+ public function testGitignoreEntriesUpdated(array $packageFiles, array $expectedResult)
+ {
+ $installer = $this->createConfigurationInstaller();
+ $package = $this->createPackageMockWithConfigurationFiles($installer, $packageFiles);
+ $installer->install($this->repository, $package);
+
+ $gitignore = new GitignoreService($this->rootDirectory . DIRECTORY_SEPARATOR . '.gitignore');
+ $entries = $gitignore->getEntries();
+
+ foreach ($expectedResult['gitignore'] as $entry) {
+ $this->assertTrue(in_array($entry, $entries));
+ }
+ }
+
+ /**
+ * @dataProvider provideFilesForTesting
+ * @depends testGitignoreCreatedIfNotPresent
+ * @depends testGitignoreEntriesUpdated
+ */
+ public function testGitignoreEntriesRemoved(array $packageFiles, array $expectedResult)
+ {
+ $installer = $this->createConfigurationInstaller();
+ $package = $this->createPackageMockWithConfigurationFiles($installer, $packageFiles);
+ $installer->install($this->repository, $package);
+
+ $this->repository
+ ->expects($this->exactly(1))
+ ->method('hasPackage')
+ ->with($package)
+ ->willReturn(true)
+ ;
+
+ $installer->uninstall($this->repository, $package);
+ $gitignore = new GitignoreService($this->rootDirectory . DIRECTORY_SEPARATOR . '.gitignore');
+ $entries = $gitignore->getEntries();
+
+ foreach ($expectedResult['gitignore'] as $entry) {
+ $this->assertFalse(in_array($entry, $entries));
+ }
+ }
+
+ public function provideFilesForTesting()
+ {
+ return [
+ 'basic usage' => [
+ [
+ 'README.md' => 'readme content',
+ 'manifest.json' => \json_encode([
+ 'files' => [
+ 'README.md' => 'README.md',
+ ],
+ 'gitignore' =>[
+ '/README.md',
+ ]
+ ])
+ ],
+ [
+ 'files' => [
+ 'README.md'
+ ],
+ 'gitignore' => [
+ '/README.md'
+ ]
+ ]
+ ],
+ 'files without target' => [
+ [
+ 'README.md' => 'readme content',
+ 'LICENSE' => 'license content',
+ 'manifest.json' => \json_encode([
+ 'files' => [
+ 'README.md',
+ 'LICENSE'
+ ],
+ 'gitignore' =>[
+ '/README.md',
+ '/LICENSE',
+ ]
+ ])
+ ],
+ [
+ 'files' => [
+ 'README.md',
+ 'LICENSE'
+ ],
+ 'gitignore' => [
+ '/README.md',
+ '/LICENSE',
+ ]
+ ]
+ ],
+ 'files in folders' => [
+ [
+ 'src/file1.md' => 'file1 content',
+ 'src/file2.md' => 'file2 content',
+ 'manifest.json' => \json_encode([
+ 'files' => [
+ 'src/file1.md',
+ 'src/file2.md' => 'README.md'
+ ],
+ 'gitignore' =>[
+ '/src/file1.md',
+ '/README.md',
+ ]
+ ])
+ ],
+ [
+ 'files' => [
+ 'src/file1.md',
+ 'README.md'
+ ],
+ 'gitignore' => [
+ '/src/file1.md',
+ '/README.md',
+ ]
+ ]
+ ],
+ ];
+ }
+}
diff --git a/tests/Installer/InstallerTestCase.php b/tests/Installer/InstallerTestCase.php
new file mode 100644
index 0000000..dd5cadc
--- /dev/null
+++ b/tests/Installer/InstallerTestCase.php
@@ -0,0 +1,173 @@
+previousDirectory = getcwd();
+ $this->rootDirectory = TestCase::getUniqueTmpDirectory();
+ chdir($this->rootDirectory);
+
+ $this->composer = new Composer();
+ $this->composer->setConfig($this->createComposerConfig());
+
+ /** @var InstallationManager */
+ $installationManager = $this->createMock(InstallationManager::class);
+ $this->composer->setInstallationManager($installationManager);
+
+ /** @var DownloadManager */
+ $downloadManager = $this->getMockBuilder(DownloadManager::class)->disableOriginalConstructor()->getMock();
+ $this->composer->setDownloadManager($downloadManager);
+
+ /** @var RootPackage|\PHPUnit_Framework_MockObject_MockObject $package */
+ $package = $this->createMock(RootPackageInterface::class);
+ $this->composer->setPackage($package);
+
+ $this->repository = $this->createMock(InstalledRepositoryInterface::class);
+ $this->io = $this->createMock(IOInterface::class);
+ }
+
+ protected function tearDown(): void
+ {
+ chdir($this->previousDirectory);
+ if (is_dir($this->rootDirectory)) {
+ $filesystem = new Filesystem;
+ $filesystem->removeDirectory($this->rootDirectory);
+ }
+ }
+
+ protected function createPackageMock(string $prettyName, $type = 'library', array $extra = []): Package
+ {
+ /** @var Package|\PHPUnit_Framework_MockObject_MockObject $package */
+ $package = $this->getMockBuilder(Package::class)
+ ->setConstructorArgs([md5(uniqid()), 'dev-develop', 'dev-develop'])
+ ->getMock()
+ ;
+ $package
+ ->expects($this->any())
+ ->method('getType')
+ ->willReturn($type)
+ ;
+ $package
+ ->expects($this->any())
+ ->method('getPrettyName')
+ ->willReturn($prettyName)
+ ;
+ $package
+ ->expects($this->any())
+ ->method('getPrettyVersion')
+ ->willReturn('dev-develop')
+ ;
+ $package
+ ->expects($this->any())
+ ->method('getVersion')
+ ->willReturn('dev-develop')
+ ;
+ $package
+ ->expects($this->any())
+ ->method('getInstallationSource')
+ ->willReturn('source')
+ ;
+ $package
+ ->expects($this->any())
+ ->method('getExtra')
+ ->willReturn($extra)
+ ;
+
+ return $package;
+ }
+
+ protected function createPackageMockWithConfigurationFiles(InstallerInterface $installer, array $files = []): Package
+ {
+ $package = $this->createPackageMock(
+ 'bk2k/configuration-test',
+ 'project-configuration'
+ );
+
+ $packageInstallationPath = $installer->getInstallPath($package);
+ $filesystem = new Filesystem;
+ $filesystem->ensureDirectoryExists($packageInstallationPath);
+
+ if (count($files) > 0) {
+ foreach ($files as $filename => $fileContent) {
+ $path = $filesystem->normalizePath($packageInstallationPath . DIRECTORY_SEPARATOR . $filename);
+ $filesystem->ensureDirectoryExists(dirname($path));
+ file_put_contents($path, $fileContent);
+ }
+ }
+
+ return $package;
+ }
+
+ protected function createConfigurationInstaller(): InstallerInterface
+ {
+ return new ConfigurationInstaller($this->io, $this->composer);
+ }
+
+ protected function createComposerConfig(): Config
+ {
+ $config = new Config();
+ $config->merge([
+ 'config' => [
+ 'vendor-dir' => $this->rootDirectory . DIRECTORY_SEPARATOR . 'vendor',
+ 'bin-dir' => $this->rootDirectory . DIRECTORY_SEPARATOR . 'bin',
+ ],
+ 'repositories' => ['packagist' => false],
+ ]);
+
+ return $config;
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..ad17b01
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,31 @@
+add('BK2K\ConfigurationInstallerTest\Installer', __DIR__);