GitAttributes for PHP Composer Projects

Published On2020-09-16

GitAttributes for PHP Composer projects
.gitattributes and .gitignore files can further fine tune how Git should treat files and directories inside a directory.

.gitignore files are used to specify which files, directories, and path patterns should be ignored by Git. If a path matches, Git will stop looking for changes in those files.

.gitattributes file, on the other hand, can be used to specify several Git features to files and directories. Majority of Composer packages are hosted on Git repositories, and a properly configured .gitattributes file can help reduce the package download size, enhance diff checks and patch workflows, and bring other benefits.

.gitattributes file

.gitattributes files can specify files and other options for files and sub directories within the directory the .gitattributes file is placed.

Create a .gitattributes file at the root of the Git repository, and that is where we will be specifying the rules.

Smaller Package Downloads

Using a .gitattributes file function called export-ignore, it is possible to dramatically reduce the file size of a Composer package download.

When Composer downloads a package, a zip/tar file is created using git archive command. This behavior can be changed, but by default, Composer downloads an archive of the specified package. When using GitHub, GitLab, or BitBucket, Composer can directly download the zip file, and all those services will imitate a git archive command output to generate the downloadable archive.

git archive command looks up for files matching an export-ignore rule, and if matched, it will exclude those files from the archive.

With export-ignore rules in .gitattributes file, Composer packages can opt to exclude build files and test files (such as Travis CI configuration file, PHPUnit test and configuration files, documentation, etc.) when the package is downloaded by Composer.

All these excluded files will remain in the repository when it is cloned/forked, and are otherwise available to those who download the package source files; The export-ignore rule will exclude them in the zip file downloads, thus reducing the download size and time when the packages are consumed.

A typical export-ignore rule is quite simple as you might expect:

/tests       export-ignore
/phpunit.xml export-ignore

A .gitattributes file with contents above will exclude tests directory and phpunit.xml file from the zip file Composer downloads. On most projects, this can easily cut down the package download size and time in half.

PHPUnit package, for examples, has a .gitattributes file with several export-ignore rules to fine to the downloadable package, and it reduces the package size from ~2,000 KB (without bundled binaries) or ~8,000 KB to ~394 KB. This is a cherry-picking example of course, but a simple .gitattributes file can make a difference in the overall package download size specially on packages that are downloaded several thousands of times a day.

export-ignore is different from .gitignore files. Files ignored by .gitignore file are not touched by Git all. They will not be pushed, checked out, or otherwise changed by Git. export-ignore rules in .gitattributes files, on the other hand, only exclude the files when an archive is created.

Diffs with Context

.gitattributes files can fine tune how Git generates diff outputs. Git already has support for PHP built-in, and it can help generate slightly nicer patch/diff outputs.

*.php diff=php

.gitattributes directive above specifies Git to use its PHP diff capabilities to display diff output for *.php files.

A typical Git diff looks like this:

git diff HEAD~2..HEAD
diff --git a/test.php b/test.php
index f353a29..84b957d 100644
--- a/test.php
+++ b/test.php
@@ -118,6 +118,7 @@ class Factory
$this->registerDefaultComparator(new MockObjectComparator);
$this->registerDefaultComparator(new DateTimeComparator);
$this->registerDefaultComparator(new DOMNodeComparator);
+ $this->registerDefaultComparator(new SplObjectStorageComparator);
}

See the @@ -118,6 +118,7 @@ class Factory part in the diff above? That is called Hunk Header. With diff=php configured, Git now knows to provide more context in the diff file.

With diff=php applied, the same diff would contain a more meaningful hunk header:

...
- @@ -118,6 +118,7 @@ class Factory
+ @@ -118,6 +118,7 @@ private function registerDefaultComparators()
...

It is now clear which function/method we make this change in, and makes it easier to quickly scan through the changes in a code review.

You can add this to the user-specific .gitattributes file at ~/.gitattributes file on your computer, and it will be effective in all PHP files Git comes across.

Drupal users might need to use some extra rules:

*.php     diff=php
*.inc     diff=php
*.module  diff=php
*.install diff=php
*.test    diff=php

Exclude binary files from Diffs

Binary files, such as Phar files, can be marked as binary files, so changes in generated Phar files will not produce massive and messy diff outputs:

*.phar -diff

Diff outputs with changed Phar contents will now only show Binary files differ.


Fine-tune Composer's use of source and dist packages

Composer downloads dist packages by default. This means when Composer downloads a package, the archive file already excludes files marked as export-ignore.

This behavior can be further tuned with CLI flags and/or composer.json configuration.

CLI

Prefer-source
When using --prefer-source CLI flag, Composer downloads the entire repository for the specified package and all its dependencies. This will include files that are marked as export-ignore in the .gitattributes files.

This will result in a longer operation time and download/storage size. For projects that use Git, this means the package itself and all its dependencies will be cloned.

A simple phpunit/phpunit installation in --prefer-source results in a whopping ~742 MB vendor directory compared to ~4.54 MB in --prefer-dist.

composer require foo/bar --prefer-source

Prefer-dist
Composer uses dist packages by default (evaluating export-ignore rules). However, if for reason, you are using source packages throughout the project, it is possible to force Composer to use dist packages for the specified package and all its dependencies.

composer require foo/bar --prefer-dist

Composer.json

The preferred-install configuration directive set in the composer.json file can be used to specify the type package to download.

This can be set in the Composer home config.json file as well:

{
    "config": {
        "preferred-install": {
            "foo/bar": "dist",
            "example/*": "source",
            "bar/*": "auto",
            "*": "dist"
        }
    }
}

Composer will evaluate the specified rules from top to bottom. In the example above, the last line ("*": "dist") makes Composer always use dist packages unless otherwise mentioned in the prior rules.


Complete example

Following is a suggested .gitattributes file that tries to follow conventions of most projects. It covers PHPUnit test/configuration files, Travis CI files, PHPCS configuration files, GitHub actions, issue/PR template files, documentation files, etc.

# https://php.watch/articles/composer-gitattributes

# Exclude build/test files from archive
/.github          export-ignore
/.phive           export-ignore
/.psalm           export-ignore
/build            export-ignore
/docs             export-ignore
/examples         export-ignore
/phpstan          export-ignore
/tests            export-ignore
/.editorconfig    export-ignore
/.gitattributes   export-ignore
/.gitignore       export-ignore
/.php_cs          export-ignore
/.php_cs.dist     export-ignore
/.travis.yml      export-ignore
/phpunit.xml      export-ignore
/phpunit.xml.dist export-ignore

# Configure diff output for .php and .phar files.
*.php diff=php
*.phar -diff

composer fund command
Removing ./github/funding.yml file will not stop composer fund command from working.

Rules for files that do not exist
It is completely fine to have a .gitattributes rule does not match any file. Git will silently ignore that rule.

Add composer.lock file?
It is OK to include composer.lock file as an export-ignore rule, if the package is not meant to be used as a root level project.

Composer only uses lock files when using a package as the root project, or when creating a project using composer create-project command. When used as a dependency for other packages, only composer.json file is used. In the example above, composer.lock is not included to prevent accidental copy-pastes on projects that are meant to be used as a root level project, such as a full Laravel project.

Do not export-ignore license files
Pretty much every license requires the license file to be included when distributing.

Recent Articles on PHP.Watch

All Articles β€’ Feed
PHP 8.0 Thanks ❀

PHP 8.0 Thanks ❀

PHP 8.0.0 is released today πŸŽ‰πŸΎπŸŽŠ. Thank you all of you for your amazing efforts ❀.
PHP's resource to object transformation

PHP's resource to object transformation

A summary of PHP's long-term progress in `resource` objects to class objects
PHP Hash Algorithm Benchmark

PHP Hash Algorithm Benchmark

Benchmarks the performance of hashing algorithms supported in PHP, including MurmurHash in PHP 8.1.
Subscribe to PHP.Watch newsletter for monthly updates

You will receive an email on last Saturday of every month and on major PHP releases with new articles related to PHP, upcoming changes, new features and what's changing in the language. No marketing emails, no selling of your contacts, no click-tracking, and one-click instant unsubscribe from any email you receive.