瀏覽代碼

Merge pull request #1567 from naderman/feature-dist

Distributions/Archives
Nils Adermann 12 年之前
父節點
當前提交
78c250da19
共有 28 個文件被更改,包括 1420 次插入3 次删除
  1. 2 0
      .travis.yml
  2. 25 0
      doc/04-schema.md
  3. 10 0
      res/composer-schema.json
  4. 123 0
      src/Composer/Command/ArchiveCommand.php
  5. 1 0
      src/Composer/Console/Application.php
  6. 19 0
      src/Composer/Factory.php
  7. 4 0
      src/Composer/Package/AliasPackage.php
  8. 77 0
      src/Composer/Package/Archiver/ArchivableFilesFinder.php
  9. 147 0
      src/Composer/Package/Archiver/ArchiveManager.php
  10. 45 0
      src/Composer/Package/Archiver/ArchiverInterface.php
  11. 141 0
      src/Composer/Package/Archiver/BaseExcludeFilter.php
  12. 31 0
      src/Composer/Package/Archiver/ComposerExcludeFilter.php
  13. 80 0
      src/Composer/Package/Archiver/GitExcludeFilter.php
  14. 104 0
      src/Composer/Package/Archiver/HgExcludeFilter.php
  15. 65 0
      src/Composer/Package/Archiver/PharArchiver.php
  16. 4 0
      src/Composer/Package/Dumper/ArrayDumper.php
  17. 4 0
      src/Composer/Package/Loader/ArrayLoader.php
  18. 19 0
      src/Composer/Package/Package.php
  19. 7 0
      src/Composer/Package/PackageInterface.php
  20. 2 3
      tests/Composer/Test/AllFunctionalTest.php
  21. 206 0
      tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php
  22. 99 0
      tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php
  23. 67 0
      tests/Composer/Test/Package/Archiver/ArchiverTest.php
  24. 42 0
      tests/Composer/Test/Package/Archiver/HgExcludeFilterTest.php
  25. 82 0
      tests/Composer/Test/Package/Archiver/PharArchiverTest.php
  26. 8 0
      tests/Composer/Test/Package/Dumper/ArrayDumperTest.php
  27. 3 0
      tests/Composer/Test/Package/Loader/ArrayLoaderTest.php
  28. 3 0
      tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php

+ 2 - 0
.travis.yml

@@ -9,5 +9,7 @@ php:
 before_script: 
     - echo '' > ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
     - composer install --dev --prefer-source
+    - git config --global user.name travis-ci
+    - git config --global user.email travis@example.com
 
 script: ./vendor/bin/phpunit -c tests/complete.phpunit.xml

+ 25 - 0
doc/04-schema.md

@@ -656,4 +656,29 @@ See [Vendor Binaries](articles/vendor-binaries.md) for more details.
 
 Optional.
 
+### archive
+
+A set of options for creating package archives.
+
+The following options are supported:
+
+* **exclude:** Allows configuring a list of patterns for excluded paths. The
+  pattern syntax matches .gitignore files. A leading exclamation mark (!) will
+  result in any matching files to be included even if a previous pattern
+  excluded them. A leading slash will only match at the beginning of the project
+  relative path. An asterisk will not expand to a directory separator.
+
+Example:
+
+    {
+        "archive": {
+            "exclude": ["/foo/bar", "baz", "/*.test", "!/foo/bar/baz"]
+        }
+    }
+
+The example will include `/dir/foo/bar/file`, `/foo/bar/baz`, `/file.php`,
+`/foo/my.test` but it will exclude `/foo/bar/any`, `/foo/baz`, and `/my.test`.
+
+Optional.
+
 ← [Command-line interface](03-cli.md)  |  [Repositories](05-repositories.md) →

+ 10 - 0
res/composer-schema.json

@@ -202,6 +202,16 @@
                 }
             }
         },
+        "archive": {
+            "type": ["object"],
+            "description": "Options for creating package archives for distribution.",
+            "properties": {
+                "exclude": {
+                    "type": "array",
+                    "description": "A list of patterns for paths to exclude or include if prefixed with an exclamation mark."
+                }
+            }
+        },
         "repositories": {
             "type": ["object", "array"],
             "description": "A set of additional repositories where packages can be found.",

+ 123 - 0
src/Composer/Command/ArchiveCommand.php

@@ -0,0 +1,123 @@
+<?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\Command;
+
+use Composer\Factory;
+use Composer\IO\IOInterface;
+use Composer\DependencyResolver\Pool;
+use Composer\Package\LinkConstraint\VersionConstraint;
+use Composer\Repository\CompositeRepository;
+
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Creates an archive of a package for distribution.
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class ArchiveCommand extends Command
+{
+    protected function configure()
+    {
+        $this
+            ->setName('archive')
+            ->setDescription('Create an archive of this composer package')
+            ->setDefinition(array(
+                new InputArgument('package', InputArgument::OPTIONAL, 'The package to archive instead of the current project'),
+                new InputArgument('version', InputArgument::OPTIONAL, 'The package version to archive'),
+                new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the resulting archive: tar or zip', 'tar'),
+                new InputOption('dir', false, InputOption::VALUE_REQUIRED, 'Write the archive to this directory', '.'),
+            ))
+            ->setHelp(<<<EOT
+The <info>archive</info> command creates an archive of the specified format
+containing the files and directories of the Composer project or the specified
+package in the specified version and writes it to the specified directory.
+
+<info>php composer.phar archive [--format=zip] [--dir=/foo] [package [version]]</info>
+
+EOT
+            )
+        ;
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        return $this->archive(
+            $this->getIO(),
+            $input->getArgument('package'),
+            $input->getArgument('version'),
+            $input->getOption('format'),
+            $input->getOption('dir')
+        );
+    }
+
+    protected function archive(IOInterface $io, $packageName = null, $version = null, $format = 'tar', $dest = '.')
+    {
+        $config = Factory::createConfig();
+        $factory = new Factory;
+        $archiveManager = $factory->createArchiveManager($config);
+
+        if ($packageName) {
+            $package = $this->selectPackage($io, $packageName, $version);
+
+            if (!$package) {
+                return 1;
+            }
+        } else {
+            $package = $this->getComposer()->getPackage();
+        }
+
+        $io->write('<info>Creating the archive.</info>');
+        $archiveManager->archive($package, $format, $dest);
+
+        return 0;
+    }
+
+    protected function selectPackage(IOInterface $io, $packageName, $version = null)
+    {
+        $io->write('<info>Searching for the specified package.</info>');
+
+        if ($composer = $this->getComposer(false)) {
+            $localRepo = $composer->getRepositoryManager()->getLocalRepository();
+            $repos = new CompositeRepository(array_merge(array($localRepo), $composer->getRepositoryManager()->getRepositories()));
+        } else {
+            $defaultRepos = Factory::createDefaultRepositories($this->getIO());
+            $output->writeln('No composer.json found in the current directory, searching packages from ' . implode(', ', array_keys($defaultRepos)));
+            $repos = new CompositeRepository($defaultRepos);
+        }
+
+        $pool = new Pool();
+        $pool->addRepository($repos);
+
+        $constraint = ($version) ? new VersionConstraint('>=', $version) : null;
+        $packages = $pool->whatProvides($packageName, $constraint);
+
+        if (count($packages) > 1) {
+            $package = $packages[0];
+            $io->write('<info>Found multiple matches, selected '.$package->getPrettyString().'.</info>');
+            $io->write('Alternatives were '.implode(', ', array_map(function ($p) { return $p->getPrettyString(); }, $packages)).'.');
+            $io->write('<comment>Please use a more specific constraint to pick a different package.</comment>');
+        } elseif ($packages) {
+            $package = $packages[0];
+            $io->write('<info>Found an exact match '.$package->getPrettyString().'.</info>');
+        } else {
+            $io->write('<error>Could not find a package matching '.$packageName.'.</error>');
+            return false;
+        }
+
+        return $package;
+    }
+}

+ 1 - 0
src/Composer/Console/Application.php

@@ -194,6 +194,7 @@ class Application extends BaseApplication
         $commands[] = new Command\RequireCommand();
         $commands[] = new Command\DumpAutoloadCommand();
         $commands[] = new Command\StatusCommand();
+        $commands[] = new Command\ArchiveCommand();
 
         if ('phar:' === substr(__FILE__, 0, 5)) {
             $commands[] = new Command\SelfUpdateCommand();

+ 19 - 0
src/Composer/Factory.php

@@ -15,6 +15,7 @@ namespace Composer;
 use Composer\Config\JsonConfigSource;
 use Composer\Json\JsonFile;
 use Composer\IO\IOInterface;
+use Composer\Package\Archiver;
 use Composer\Repository\ComposerRepository;
 use Composer\Repository\RepositoryManager;
 use Composer\Util\ProcessExecutor;
@@ -317,6 +318,24 @@ class Factory
         return $dm;
     }
 
+    /**
+     * @param Config                     $config  The configuration
+     * @param Downloader\DownloadManager $dm      Manager use to download sources
+     *
+     * @return Archiver\ArchiveManager
+     */
+    public function createArchiveManager(Config $config, Downloader\DownloadManager $dm = null)
+    {
+        if (null === $dm) {
+            $dm = $this->createDownloadManager(new IO\NullIO(), $config);
+        }
+
+        $am = new Archiver\ArchiveManager($dm);
+        $am->addArchiver(new Archiver\PharArchiver);
+
+        return $am;
+    }
+
     /**
      * @return Installer\InstallationManager
      */

+ 4 - 0
src/Composer/Package/AliasPackage.php

@@ -311,6 +311,10 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
     {
         return $this->aliasOf->getNotificationUrl();
     }
+    public function getArchiveExcludes()
+    {
+        return $this->aliasOf->getArchiveExcludes();
+    }
     public function __toString()
     {
         return parent::__toString().' (alias of '.$this->aliasOf->getVersion().')';

+ 77 - 0
src/Composer/Package/Archiver/ArchivableFilesFinder.php

@@ -0,0 +1,77 @@
+<?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\Package\Archiver;
+
+use Composer\Package\BasePackage;
+use Composer\Package\PackageInterface;
+
+use Symfony\Component\Finder;
+
+/**
+ * A Symfony Finder wrapper which locates files that should go into archives
+ *
+ * Handles .gitignore, .gitattributes and .hgignore files as well as composer's
+ * own exclude rules from composer.json
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class ArchivableFilesFinder extends \FilterIterator
+{
+    /**
+     * @var Symfony\Component\Finder\Finder
+     */
+    protected $finder;
+
+    /**
+     * Initializes the internal Symfony Finder with appropriate filters
+     *
+     * @param string $sources Path to source files to be archived
+     * @param array $excludes Composer's own exclude rules from composer.json
+     */
+    public function __construct($sources, array $excludes)
+    {
+        $sources = realpath($sources);
+
+        $filters = array(
+            new HgExcludeFilter($sources),
+            new GitExcludeFilter($sources),
+            new ComposerExcludeFilter($sources, $excludes),
+        );
+
+        $this->finder = new Finder\Finder();
+        $this->finder
+            ->in($sources)
+            ->filter(function (\SplFileInfo $file) use ($sources, $filters) {
+                $relativePath = preg_replace(
+                    '#^'.preg_quote($sources, '#').'#',
+                    '',
+                    str_replace(DIRECTORY_SEPARATOR, '/', $file->getRealPath())
+                );
+
+                $exclude = false;
+                foreach ($filters as $filter) {
+                    $exclude = $filter->filter($relativePath, $exclude);
+                }
+                return !$exclude;
+            })
+            ->ignoreVCS(true)
+            ->ignoreDotFiles(false);
+
+        parent::__construct($this->finder->getIterator());
+    }
+
+    public function accept()
+    {
+        return !$this->getInnerIterator()->current()->isDir();
+    }
+}

+ 147 - 0
src/Composer/Package/Archiver/ArchiveManager.php

@@ -0,0 +1,147 @@
+<?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\Package\Archiver;
+
+use Composer\Downloader\DownloadManager;
+use Composer\Factory;
+use Composer\IO\NullIO;
+use Composer\Package\PackageInterface;
+use Composer\Package\RootPackage;
+use Composer\Util\Filesystem;
+
+/**
+ * @author Matthieu Moquet <matthieu@moquet.net>
+ * @author Till Klampaeckel <till@php.net>
+ */
+class ArchiveManager
+{
+    protected $downloadManager;
+
+    protected $archivers = array();
+
+    /**
+     * @var bool
+     */
+    protected $overwriteFiles = true;
+
+    /**
+     * @param DownloadManager $downloadManager A manager used to download package sources
+     */
+    public function __construct(DownloadManager $downloadManager)
+    {
+        $this->downloadManager = $downloadManager;
+    }
+
+    /**
+     * @param ArchiverInterface $archiver
+     */
+    public function addArchiver(ArchiverInterface $archiver)
+    {
+        $this->archivers[] = $archiver;
+    }
+
+    /**
+     * Set whether existing archives should be overwritten
+     *
+     * @param bool $overwriteFiles New setting
+     *
+     * @return $this
+     */
+    public function setOverwriteFiles($overwriteFiles)
+    {
+        $this->overwriteFiles = $overwriteFiles;
+        return $this;
+    }
+
+    /**
+     * Generate a distinct filename for a particular version of a package.
+     *
+     * @param PackageInterface $package The package to get a name for
+     *
+     * @return string A filename without an extension
+     */
+    public function getPackageFilename(PackageInterface $package)
+    {
+        $nameParts = array(preg_replace('#[^a-z0-9-_.]#i', '-', $package->getName()));
+
+        if (preg_match('{^[a-f0-9]{40}$}', $package->getDistReference())) {
+            $nameParts = array_merge($nameParts, array($package->getDistReference(), $package->getDistType()));
+        } else {
+            $nameParts = array_merge($nameParts, array($package->getPrettyVersion(), $package->getDistReference()));
+        }
+
+        if ($package->getSourceReference()) {
+            $nameParts[] = substr(sha1($package->getSourceReference()), 0, 6);
+        }
+
+        return implode('-', array_filter($nameParts, function ($p) {
+            return !empty($p);
+        }));
+    }
+
+    /**
+     * Create an archive of the specified package.
+     *
+     * @param PackageInterface $package   The package to archive
+     * @param string           $format    The format of the archive (zip, tar, ...)
+     * @param string           $targetDir The diretory where to build the archive
+     *
+     * @return string The path of the created archive
+     */
+    public function archive(PackageInterface $package, $format, $targetDir)
+    {
+        if (empty($format)) {
+            throw new \InvalidArgumentException('Format must be specified');
+        }
+
+        // Search for the most appropriate archiver
+        $usableArchiver = null;
+        foreach ($this->archivers as $archiver) {
+            if ($archiver->supports($format, $package->getSourceType())) {
+                $usableArchiver = $archiver;
+                break;
+            }
+        }
+
+        // Checks the format/source type are supported before downloading the package
+        if (null === $usableArchiver) {
+            throw new \RuntimeException(sprintf('No archiver found to support %s format', $format));
+        }
+
+        $filesystem = new Filesystem();
+        $packageName = $this->getPackageFilename($package);
+
+        // Archive filename
+        $filesystem->ensureDirectoryExists($targetDir);
+        $target = realpath($targetDir).'/'.$packageName.'.'.$format;
+        $filesystem->ensureDirectoryExists(dirname($target));
+
+        if (!$this->overwriteFiles && file_exists($target)) {
+            return $target;
+        }
+
+        if ($package instanceof RootPackage) {
+            $sourcePath = realpath('.');
+        } else {
+            // Directory used to download the sources
+            $sourcePath = sys_get_temp_dir().'/composer_archiver/'.$packageName;
+            $filesystem->ensureDirectoryExists($sourcePath);
+
+            // Download sources
+            $this->downloadManager->download($package, $sourcePath, true);
+        }
+
+        // Create the archive
+        return $usableArchiver->archive($sourcePath, $target, $format, $package->getArchiveExcludes());
+    }
+}

+ 45 - 0
src/Composer/Package/Archiver/ArchiverInterface.php

@@ -0,0 +1,45 @@
+<?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\Package\Archiver;
+
+use Composer\Package\PackageInterface;
+
+/**
+ * @author Till Klampaeckel <till@php.net>
+ * @author Matthieu Moquet <matthieu@moquet.net>
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+interface ArchiverInterface
+{
+    /**
+     * Create an archive from the sources.
+     *
+     * @param string $sources   The sources directory
+     * @param string $target    The target file
+     * @param string $format    The format used for archive
+     * @param array  $excludes  A list of patterns for files to exclude
+     *
+     * @return string The path to the written archive file
+     */
+    public function archive($sources, $target, $format, array $excludes = array());
+
+    /**
+     * Format supported by the archiver.
+     *
+     * @param string $format     The archive format
+     * @param string $sourceType The source type (git, svn, hg, etc.)
+     *
+     * @return boolean true if the format is supported by the archiver
+     */
+    public function supports($format, $sourceType);
+}

+ 141 - 0
src/Composer/Package/Archiver/BaseExcludeFilter.php

@@ -0,0 +1,141 @@
+<?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\Package\Archiver;
+
+use Symfony\Component\Finder;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+abstract class BaseExcludeFilter
+{
+    /**
+     * @var string
+     */
+    protected $sourcePath;
+
+    /**
+     * @var array
+     */
+    protected $excludePatterns;
+
+    /**
+     * @param string $sourcePath Directory containing sources to be filtered
+     */
+    public function __construct($sourcePath)
+    {
+        $this->sourcePath = $sourcePath;
+        $this->excludePatterns = array();
+    }
+
+    /**
+     * Checks the given path against all exclude patterns in this filter
+     *
+     * Negated patterns overwrite exclude decisions of previous filters.
+     *
+     * @param string $relativePath The file's path relative to the sourcePath
+     * @param bool $exclude Whether a previous filter wants to exclude this file
+     *
+     * @return bool Whether the file should be excluded
+     */
+    public function filter($relativePath, $exclude)
+    {
+        foreach ($this->excludePatterns as $patternData) {
+            list($pattern, $negate, $stripLeadingSlash) = $patternData;
+
+            if ($stripLeadingSlash) {
+                $path = substr($relativePath, 1);
+            } else {
+                $path = $relativePath;
+            }
+
+            if (preg_match($pattern, $path)) {
+                $exclude = !$negate;
+            }
+        }
+        return $exclude;
+    }
+
+    /**
+     * Processes a file containing exclude rules of different formats per line
+     *
+     * @param array $lines A set of lines to be parsed
+     * @param callback $lineParser The parser to be used on each line
+     *
+     * @return array Exclude patterns to be used in filter()
+     */
+    protected function parseLines(array $lines, $lineParser)
+    {
+        return array_filter(
+            array_map(
+                function ($line) use ($lineParser) {
+                    $line = trim($line);
+
+                    $commentHash = strpos($line, '#');
+                    if ($commentHash !== false) {
+                        $line = substr($line, 0, $commentHash);
+                    }
+
+                    if ($line) {
+                        return call_user_func($lineParser, $line);
+                    }
+                    return null;
+                }, $lines),
+            function ($pattern) {
+                return $pattern !== null;
+            }
+        );
+    }
+
+    /**
+     * Generates a set of exclude patterns for filter() from gitignore rules
+     *
+     * @param array $rules A list of exclude rules in gitignore syntax
+     *
+     * @return array Exclude patterns
+     */
+    protected function generatePatterns($rules)
+    {
+        $patterns = array();
+        foreach ($rules as $rule) {
+            $patterns[] = $this->generatePattern($rule);
+        }
+        return $patterns;
+    }
+
+    /**
+     * Generates an exclude pattern for filter() from a gitignore rule
+     *
+     * @param string An exclude rule in gitignore syntax
+     *
+     * @param array An exclude pattern
+     */
+    protected function generatePattern($rule)
+    {
+        $negate = false;
+        $pattern = '#';
+
+        if (strlen($rule) && $rule[0] === '!') {
+            $negate = true;
+            $rule = substr($rule, 1);
+        }
+
+        if (strlen($rule) && $rule[0] === '/') {
+            $pattern .= '^/';
+            $rule = substr($rule, 1);
+        }
+
+        $pattern .= substr(Finder\Glob::toRegex($rule), 2, -2);
+        return array($pattern . '#', $negate, false);
+    }
+}

+ 31 - 0
src/Composer/Package/Archiver/ComposerExcludeFilter.php

@@ -0,0 +1,31 @@
+<?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\Package\Archiver;
+
+/**
+ * An exclude filter which processes composer's own exclude rules
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class ComposerExcludeFilter extends BaseExcludeFilter
+{
+    /**
+     * @param string $sourcePath Directory containing sources to be filtered
+     * @param array $excludeRules An array of exclude rules from composer.json
+     */
+    public function __construct($sourcePath, array $excludeRules)
+    {
+        parent::__construct($sourcePath);
+        $this->excludePatterns = $this->generatePatterns($excludeRules);
+    }
+}

+ 80 - 0
src/Composer/Package/Archiver/GitExcludeFilter.php

@@ -0,0 +1,80 @@
+<?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\Package\Archiver;
+
+/**
+ * An exclude filter that processes gitignore and gitattributes
+ *
+ * It respects export-ignore git attributes
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class GitExcludeFilter extends BaseExcludeFilter
+{
+    /**
+     * Parses .gitignore and .gitattributes files if they exist
+     *
+     * @param string $sourcePath
+     */
+    public function __construct($sourcePath)
+    {
+        parent::__construct($sourcePath);
+
+        if (file_exists($sourcePath.'/.gitignore')) {
+            $this->excludePatterns = $this->parseLines(
+                file($sourcePath.'/.gitignore'),
+                array($this, 'parseGitIgnoreLine')
+            );
+        }
+        if (file_exists($sourcePath.'/.gitattributes')) {
+            $this->excludePatterns = array_merge(
+                $this->excludePatterns,
+                $this->parseLines(
+                    file($sourcePath.'/.gitattributes'),
+                    array($this, 'parseGitAttributesLine')
+            ));
+        }
+    }
+
+    /**
+     * Callback line parser which process gitignore lines
+     *
+     * @param string $line A line from .gitignore
+     *
+     * @return array An exclude pattern for filter()
+     */
+    public function parseGitIgnoreLine($line)
+    {
+        return $this->generatePattern($line);
+    }
+
+    /**
+     * Callback parser which finds export-ignore rules in git attribute lines
+     *
+     * @param string $line A line from .gitattributes
+     *
+     * @return array An exclude pattern for filter()
+     */
+    public function parseGitAttributesLine($line)
+    {
+        $parts = preg_split('#\s+#', $line);
+
+        if (count($parts) != 2) {
+            return null;
+        }
+
+        if ($parts[1] === 'export-ignore') {
+            return $this->generatePattern($parts[0]);
+        }
+    }
+}

+ 104 - 0
src/Composer/Package/Archiver/HgExcludeFilter.php

@@ -0,0 +1,104 @@
+<?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\Package\Archiver;
+
+use Symfony\Component\Finder;
+
+/**
+ * An exclude filter that processes hgignore files
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class HgExcludeFilter extends BaseExcludeFilter
+{
+    const HG_IGNORE_REGEX = 1;
+    const HG_IGNORE_GLOB = 2;
+
+    /**
+     * Either HG_IGNORE_REGEX or HG_IGNORE_GLOB
+     * @var integer
+     */
+    protected $patternMode;
+
+    /**
+     * Parses .hgignore file if it exist
+     *
+     * @param string $sourcePath
+     */
+    public function __construct($sourcePath)
+    {
+        parent::__construct($sourcePath);
+
+        $this->patternMode = self::HG_IGNORE_REGEX;
+
+        if (file_exists($sourcePath.'/.hgignore')) {
+            $this->excludePatterns = $this->parseLines(
+                file($sourcePath.'/.hgignore'),
+                array($this, 'parseHgIgnoreLine')
+            );
+        }
+    }
+
+    /**
+     * Callback line parser which process hgignore lines
+     *
+     * @param string $line A line from .hgignore
+     *
+     * @return array An exclude pattern for filter()
+     */
+    public function parseHgIgnoreLine($line)
+    {
+        if (preg_match('#^syntax\s*:\s*(glob|regexp)$#', $line, $matches)) {
+            if ($matches[1] === 'glob') {
+                $this->patternMode = self::HG_IGNORE_GLOB;
+            } else {
+                $this->patternMode = self::HG_IGNORE_REGEX;
+            }
+            return null;
+        }
+
+        if ($this->patternMode == self::HG_IGNORE_GLOB) {
+            return $this->patternFromGlob($line);
+        } else {
+            return $this->patternFromRegex($line);
+        }
+    }
+
+    /**
+     * Generates an exclude pattern for filter() from a hg glob expression
+     *
+     * @param string $line A line from .hgignore in glob mode
+     *
+     * @return array An exclude pattern for filter()
+     */
+    protected function patternFromGlob($line)
+    {
+        $pattern = '#'.substr(Finder\Glob::toRegex($line), 2, -1).'#';
+        $pattern = str_replace('[^/]*', '.*', $pattern);
+        return array($pattern, false, true);
+    }
+
+    /**
+     * Generates an exclude pattern for filter() from a hg regexp expression
+     *
+     * @param string $line A line from .hgignore in regexp mode
+     *
+     * @return array An exclude pattern for filter()
+     */
+    public function patternFromRegex($line)
+    {
+        // WTF need to escape the delimiter safely
+        $pattern = '#'.preg_replace('/((?:\\\\\\\\)*)(\\\\?)#/', '\1\2\2\\#', $line).'#';
+        return array($pattern, false, true);
+    }
+}

+ 65 - 0
src/Composer/Package/Archiver/PharArchiver.php

@@ -0,0 +1,65 @@
+<?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\Package\Archiver;
+
+use Composer\Package\BasePackage;
+use Composer\Package\PackageInterface;
+
+/**
+ * @author Till Klampaeckel <till@php.net>
+ * @author Nils Adermann <naderman@naderman.de>
+ * @author Matthieu Moquet <matthieu@moquet.net>
+ */
+class PharArchiver implements ArchiverInterface
+{
+    protected static $formats = array(
+        'zip' => \Phar::ZIP,
+        'tar' => \Phar::TAR,
+    );
+
+    /**
+     * {@inheritdoc}
+     */
+    public function archive($sources, $target, $format, array $excludes = array())
+    {
+        $sources = realpath($sources);
+
+        // Phar would otherwise load the file which we don't want
+        if (file_exists($target)) {
+            unlink($target);
+        }
+
+        try {
+            $phar = new \PharData($target, null, null, static::$formats[$format]);
+            $files = new ArchivableFilesFinder($sources, $excludes);
+            $phar->buildFromIterator($files, $sources);
+            return $target;
+        } catch (\UnexpectedValueException $e) {
+            $message = sprintf("Could not create archive '%s' from '%s': %s",
+                $target,
+                $sources,
+                $e->getMessage()
+            );
+
+            throw new \RuntimeException($message, $e->getCode(), $e);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function supports($format, $sourceType)
+    {
+        return isset(static::$formats[$format]);
+    }
+}

+ 4 - 0
src/Composer/Package/Dumper/ArrayDumper.php

@@ -58,6 +58,10 @@ class ArrayDumper
             $data['dist']['shasum'] = $package->getDistSha1Checksum();
         }
 
+        if ($package->getArchiveExcludes()) {
+            $data['archive']['exclude'] = $package->getArchiveExcludes();
+        }
+
         foreach (BasePackage::$supportedLinkTypes as $type => $opts) {
             if ($links = $package->{'get'.ucfirst($opts['method'])}()) {
                 foreach ($links as $link) {

+ 4 - 0
src/Composer/Package/Loader/ArrayLoader.php

@@ -150,6 +150,10 @@ class ArrayLoader implements LoaderInterface
             $package->setNotificationUrl($config['notification-url']);
         }
 
+        if (!empty($config['archive']['exclude'])) {
+            $package->setArchiveExcludes($config['archive']['exclude']);
+        }
+
         if ($package instanceof Package\CompletePackageInterface) {
             if (isset($config['scripts']) && is_array($config['scripts'])) {
                 foreach ($config['scripts'] as $event => $listeners) {

+ 19 - 0
src/Composer/Package/Package.php

@@ -51,6 +51,7 @@ class Package extends BasePackage
     protected $suggests = array();
     protected $autoload = array();
     protected $includePaths = array();
+    protected $archiveExcludes = array();
 
     /**
      * Creates a new in memory package.
@@ -525,4 +526,22 @@ class Package extends BasePackage
     {
         return $this->notificationUrl;
     }
+
+    /**
+     * Sets a list of patterns to be excluded from archives
+     *
+     * @param array $excludes
+     */
+    public function setArchiveExcludes(array $excludes)
+    {
+        $this->archiveExcludes = $excludes;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function getArchiveExcludes()
+    {
+        return $this->archiveExcludes;
+    }
 }

+ 7 - 0
src/Composer/Package/PackageInterface.php

@@ -308,4 +308,11 @@ interface PackageInterface
      * @return string
      */
     public function getPrettyString();
+
+    /**
+     * Returns a list of patterns to exclude from package archives
+     *
+     * @return array
+     */
+    public function getArchiveExcludes();
 }

+ 2 - 3
tests/Composer/Test/AllFunctionalTest.php

@@ -55,9 +55,8 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
         $fs->ensureDirectoryExists(dirname(self::$pharPath));
         chdir(dirname(self::$pharPath));
 
-        $proc = new Process('php '.escapeshellarg(__DIR__.'/../../../bin/compile'));
+        $proc = new Process('php '.escapeshellarg(__DIR__.'/../../../bin/compile'), dirname(self::$pharPath));
         $exitcode = $proc->run();
-
         if ($exitcode !== 0 || trim($proc->getOutput())) {
             $this->fail($proc->getOutput());
         }
@@ -76,7 +75,7 @@ class AllFunctionalTest extends \PHPUnit_Framework_TestCase
         putenv('COMPOSER_HOME='.$this->testDir.'home');
 
         $cmd = 'php '.escapeshellarg(self::$pharPath).' --no-ansi '.$testData['RUN'];
-        $proc = new Process($cmd);
+        $proc = new Process($cmd, __DIR__.'/Fixtures/functional');
         $exitcode = $proc->run();
 
         if (isset($testData['EXPECT'])) {

+ 206 - 0
tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php

@@ -0,0 +1,206 @@
+<?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\Test\Package\Archiver;
+
+use Composer\Package\Archiver\ArchivableFilesFinder;
+use Composer\Util\Filesystem;
+
+use Symfony\Component\Process\Process;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class ArchivableFilesFinderTest extends \PHPUnit_Framework_TestCase
+{
+    protected $sources;
+    protected $finder;
+
+    protected function setup()
+    {
+        $fs = new Filesystem;
+
+        $this->sources = sys_get_temp_dir().
+            '/composer_archiver_test'.uniqid(mt_rand(), true);
+
+        $fileTree = array(
+            'A/prefixA.foo',
+            'A/prefixB.foo',
+            'A/prefixC.foo',
+            'A/prefixD.foo',
+            'A/prefixE.foo',
+            'A/prefixF.foo',
+            'B/sub/prefixA.foo',
+            'B/sub/prefixB.foo',
+            'B/sub/prefixC.foo',
+            'B/sub/prefixD.foo',
+            'B/sub/prefixE.foo',
+            'B/sub/prefixF.foo',
+            'toplevelA.foo',
+            'toplevelB.foo',
+            'prefixA.foo',
+            'prefixB.foo',
+            'prefixC.foo',
+            'prefixD.foo',
+            'prefixE.foo',
+            'prefixF.foo',
+        );
+
+        foreach ($fileTree as $relativePath) {
+            $path = $this->sources.'/'.$relativePath;
+            $fs->ensureDirectoryExists(dirname($path));
+            file_put_contents($path, '');
+        }
+    }
+
+    protected function tearDown()
+    {
+        $fs = new Filesystem;
+        $fs->removeDirectory($this->sources);
+    }
+
+    public function testManualExcludes()
+    {
+        $excludes = array(
+            'prefixB.foo',
+            '!/prefixB.foo',
+            '/prefixA.foo',
+            'prefixC.*',
+            '!*/*/*/prefixC.foo'
+        );
+
+        $this->finder = new ArchivableFilesFinder($this->sources, $excludes);
+
+        $this->assertArchivableFiles(array(
+            '/A/prefixA.foo',
+            '/A/prefixD.foo',
+            '/A/prefixE.foo',
+            '/A/prefixF.foo',
+            '/B/sub/prefixA.foo',
+            '/B/sub/prefixC.foo',
+            '/B/sub/prefixD.foo',
+            '/B/sub/prefixE.foo',
+            '/B/sub/prefixF.foo',
+            '/prefixB.foo',
+            '/prefixD.foo',
+            '/prefixE.foo',
+            '/prefixF.foo',
+            '/toplevelA.foo',
+            '/toplevelB.foo',
+        ));
+    }
+
+    public function testGitExcludes()
+    {
+        file_put_contents($this->sources.'/.gitignore', implode("\n", array(
+            '# gitignore rules with comments and blank lines',
+            '',
+            'prefixE.foo',
+            '# and more',
+            '# comments',
+            '',
+            '!/prefixE.foo',
+            '/prefixD.foo',
+            'prefixF.*',
+            '!/*/*/prefixF.foo',
+            '',
+        )));
+
+        // git does not currently support negative git attributes
+        file_put_contents($this->sources.'/.gitattributes', implode("\n", array(
+            '',
+            '# gitattributes rules with comments and blank lines',
+            'prefixB.foo export-ignore',
+            //'!/prefixB.foo export-ignore',
+            '/prefixA.foo export-ignore',
+            'prefixC.* export-ignore',
+            //'!/*/*/prefixC.foo export-ignore'
+        )));
+
+        $this->finder = new ArchivableFilesFinder($this->sources, array());
+
+        $this->assertArchivableFiles($this->getArchivedFiles('git init && '.
+            'git add .git* && '.
+            'git commit -m "ignore rules" && '.
+            'git add . && '.
+            'git commit -m "init" && '.
+            'git archive --format=zip --prefix=archive/ -o archive.zip HEAD'
+        ));
+    }
+
+    public function testHgExcludes()
+    {
+        file_put_contents($this->sources.'/.hgignore', implode("\n", array(
+            '# hgignore rules with comments, blank lines and syntax changes',
+            '',
+            'pre*A.foo',
+            'prefixE.foo',
+            '# and more',
+            '# comments',
+            '',
+            '^prefixD.foo',
+            'syntax: glob',
+            'prefixF.*',
+            'B/*',
+        )));
+
+        $this->finder = new ArchivableFilesFinder($this->sources, array());
+
+        $expectedFiles = $this->getArchivedFiles('hg init && '.
+            'hg add && '.
+            'hg commit -m "init" && '.
+            'hg archive archive.zip'
+        );
+
+        array_shift($expectedFiles); // remove .hg_archival.txt
+
+        $this->assertArchivableFiles($expectedFiles);
+    }
+
+    protected function getArchivableFiles()
+    {
+        $files = array();
+        foreach ($this->finder as $file) {
+            if (!$file->isDir()) {
+                $files[] = preg_replace('#^'.preg_quote($this->sources, '#').'#', '', $file->getRealPath());
+            }
+        }
+
+        sort($files);
+
+        return $files;
+    }
+
+    protected function getArchivedFiles($command)
+    {
+        $process = new Process($command, $this->sources);
+        $process->run();
+
+        $archive = new \PharData($this->sources.'/archive.zip');
+        $iterator = new \RecursiveIteratorIterator($archive);
+
+        $files = array();
+        foreach ($iterator as $file) {
+            $files[] = preg_replace('#^phar://'.preg_quote($this->sources, '#').'/archive\.zip/archive#', '', $file);
+        }
+
+        unlink($this->sources.'/archive.zip');
+        return $files;
+    }
+
+    protected function assertArchivableFiles($expectedFiles)
+    {
+        $actualFiles = $this->getArchivableFiles();
+
+        $this->assertEquals($expectedFiles, $actualFiles);
+    }
+}

+ 99 - 0
tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php

@@ -0,0 +1,99 @@
+<?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\Test\Package\Archiver;
+
+use Composer\Factory;
+use Composer\IO\NullIO;
+use Composer\Package\Archiver;
+use Composer\Package\Archiver\ArchiveManager;
+use Composer\Package\PackageInterface;
+
+/**
+ * @author Till Klampaeckel <till@php.net>
+ * @author Matthieu Moquet <matthieu@moquet.net>
+ */
+class ArchiveManagerTest extends ArchiverTest
+{
+    protected $manager;
+    protected $targetDir;
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $factory = new Factory();
+        $this->manager = $factory->createArchiveManager($factory->createConfig());
+        $this->targetDir = $this->testDir.'/composer_archiver_tests';
+    }
+
+    public function testUnknownFormat()
+    {
+        $this->setExpectedException('RuntimeException');
+
+        $package = $this->setupPackage();
+
+        $this->manager->archive($package, '__unknown_format__', $this->targetDir);
+    }
+
+    public function testArchiveTar()
+    {
+        $this->setupGitRepo();
+
+        $package = $this->setupPackage();
+
+        $this->manager->archive($package, 'tar', $this->targetDir);
+
+        $target = $this->getTargetName($package, 'tar');
+        $this->assertFileExists($target);
+
+        unlink($target);
+    }
+
+    protected function getTargetName(PackageInterface $package, $format)
+    {
+        $packageName = $this->manager->getPackageFilename($package);
+        $target = $this->targetDir.'/'.$packageName.'.'.$format;
+
+        return $target;
+    }
+
+    /**
+     * Create local git repository to run tests against!
+     */
+    protected function setupGitRepo()
+    {
+        $currentWorkDir = getcwd();
+        chdir($this->testDir);
+
+        $output = null;
+        $result = $this->process->execute('git init -q', $output, $this->testDir);
+        if ($result > 0) {
+            chdir($currentWorkDir);
+            throw new \RuntimeException('Could not init: '.$this->process->getErrorOutput());
+        }
+
+        $result = file_put_contents('b', 'a');
+        if (false === $result) {
+            chdir($currentWorkDir);
+            throw new \RuntimeException('Could not save file.');
+        }
+
+        $result = $this->process->execute('git add b && git commit -m "commit b" -q', $output, $this->testDir);
+        if ($result > 0) {
+            chdir($currentWorkDir);
+            throw new \RuntimeException('Could not commit: '.$this->process->getErrorOutput());
+        }
+
+        chdir($currentWorkDir);
+    }
+}

+ 67 - 0
tests/Composer/Test/Package/Archiver/ArchiverTest.php

@@ -0,0 +1,67 @@
+<?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\Test\Package\Archiver;
+
+use Composer\Util\Filesystem;
+use Composer\Util\ProcessExecutor;
+use Composer\Package\Package;
+
+/**
+ * @author Till Klampaeckel <till@php.net>
+ * @author Matthieu Moquet <matthieu@moquet.net>
+ */
+abstract class ArchiverTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @var \Composer\Util\Filesystem
+     */
+    protected $filesystem;
+
+    /**
+     * @var \Composer\Util\ProcessExecutor
+     */
+    protected $process;
+
+    /**
+     * @var string
+     */
+    protected $testDir;
+
+    public function setUp()
+    {
+        $this->filesystem = new Filesystem();
+        $this->process    = new ProcessExecutor();
+        $this->testDir    = sys_get_temp_dir().'/composer_archiver_test_'.mt_rand();
+        $this->filesystem->ensureDirectoryExists($this->testDir);
+    }
+
+    public function tearDown()
+    {
+        $this->filesystem->removeDirectory($this->testDir);
+    }
+
+    /**
+     * Util method to quickly setup a package using the source path built.
+     *
+     * @return \Composer\Package\Package
+     */
+    protected function setupPackage()
+    {
+        $package = new Package('archivertest/archivertest', 'master', 'master');
+        $package->setSourceUrl(realpath($this->testDir));
+        $package->setSourceReference('master');
+        $package->setSourceType('git');
+
+        return $package;
+    }
+}

+ 42 - 0
tests/Composer/Test/Package/Archiver/HgExcludeFilterTest.php

@@ -0,0 +1,42 @@
+<?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\Test\Package\Archiver;
+
+use Composer\Package\Archiver\HgExcludeFilter;
+
+/**
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class HgExcludeFilterTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @dataProvider patterns
+     */
+    public function testPatternEscape($ignore, $expected)
+    {
+        $filter = new HgExcludeFilter('/');
+
+        $this->assertEquals($expected, $filter->patternFromRegex($ignore));
+    }
+
+    public function patterns()
+    {
+        return array(
+            array('.#', array('#.\\##', false, true)),
+            array('.\\#', array('#.\\\\\\##', false, true)),
+            array('\\.#', array('#\\.\\##', false, true)),
+            array('\\\\.\\\\\\\\#', array('#\\\\.\\\\\\\\\\##', false, true)),
+            array('.\\\\\\\\\\#', array('#.\\\\\\\\\\\\\\##', false, true)),
+        );
+    }
+}

+ 82 - 0
tests/Composer/Test/Package/Archiver/PharArchiverTest.php

@@ -0,0 +1,82 @@
+<?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\Test\Package\Archiver;
+
+use Composer\Package\Archiver\PharArchiver;
+
+/**
+ * @author Till Klampaeckel <till@php.net>
+ * @author Matthieu Moquet <matthieu@moquet.net>
+ */
+class PharArchiverTest extends ArchiverTest
+{
+    public function testTarArchive()
+    {
+        // Set up repository
+        $this->setupDummyRepo();
+        $package = $this->setupPackage();
+        $target  = sys_get_temp_dir().'/composer_archiver_test.tar';
+
+        // Test archive
+        $archiver = new PharArchiver();
+        $archiver->archive($package->getSourceUrl(), $target, 'tar', array('foo/bar', 'baz', '!/foo/bar/baz'));
+        $this->assertFileExists($target);
+
+        unlink($target);
+    }
+
+    public function testZipArchive()
+    {
+        // Set up repository
+        $this->setupDummyRepo();
+        $package = $this->setupPackage();
+        $target  = sys_get_temp_dir().'/composer_archiver_test.zip';
+
+        // Test archive
+        $archiver = new PharArchiver();
+        $archiver->archive($package->getSourceUrl(), $target, 'zip');
+        $this->assertFileExists($target);
+
+        unlink($target);
+    }
+
+    /**
+     * Create a local dummy repository to run tests against!
+     */
+    protected function setupDummyRepo()
+    {
+        $currentWorkDir = getcwd();
+        chdir($this->testDir);
+
+        $this->writeFile('file.txt', 'content', $currentWorkDir);
+        $this->writeFile('foo/bar/baz', 'content', $currentWorkDir);
+        $this->writeFile('foo/bar/ignoreme', 'content', $currentWorkDir);
+        $this->writeFile('x/baz', 'content', $currentWorkDir);
+        $this->writeFile('x/includeme', 'content', $currentWorkDir);
+
+        chdir($currentWorkDir);
+    }
+
+    protected function writeFile($path, $content, $currentWorkDir)
+    {
+        if (!file_exists(dirname($path))) {
+            mkdir(dirname($path), 0777, true);
+        }
+
+        $result = file_put_contents($path, 'a');
+        if (false === $result) {
+            chdir($currentWorkDir);
+            throw new \RuntimeException('Could not save file.');
+        }
+    }
+}

+ 8 - 0
tests/Composer/Test/Package/Dumper/ArrayDumperTest.php

@@ -130,6 +130,14 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase
                 'extra',
                 array('class' => 'MyVendor\\Installer')
             ),
+            array(
+                'archive',
+                array('/foo/bar', 'baz', '!/foo/bar/baz'),
+                'archiveExcludes',
+                array(
+                    'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'),
+                ),
+            ),
             array(
                 'require',
                 array(new Link('foo', 'foo/bar', new VersionConstraint('=', '1.0.0.0'), 'requires', '1.0.0')),

+ 3 - 0
tests/Composer/Test/Package/Loader/ArrayLoaderTest.php

@@ -114,6 +114,9 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase
             'target-dir' => 'some/prefix',
             'extra' => array('random' => array('things' => 'of', 'any' => 'shape')),
             'bin' => array('bin1', 'bin/foo'),
+            'archive' => array(
+                'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'),
+            ),
         );
 
         $package = $this->loader->load($config);

+ 3 - 0
tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php

@@ -123,6 +123,9 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase
                         'vendor-dir' => 'vendor',
                         'process-timeout' => 10000,
                     ),
+                    'archive' => array(
+                        'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'),
+                    ),
                     'scripts' => array(
                         'post-update-cmd' => 'Foo\\Bar\\Baz::doSomething',
                         'post-install-cmd' => array(