Skip to content

Commit 7ae43b3

Browse files
authored
Merge pull request #27 from php-fast-forward/feature/13-gitattributes-sync
feat(sync): add .gitattributes export-ignore management to dev-tools:sync (#13)
2 parents 1787173 + 11f5186 commit 7ae43b3

32 files changed

+2838
-25
lines changed

.gitattributes

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
* text=auto
2-
/.github/ export-ignore
3-
/docs/ export-ignore
4-
/tests/ export-ignore
5-
/.gitattributes export-ignore
6-
/.gitignore export-ignore
7-
/.gitmodules export-ignore
8-
AGENTS.md export-ignore
1+
* text=auto
2+
/.github/ export-ignore
3+
/.vscode/ export-ignore
4+
/docs/ export-ignore
5+
/tests/ export-ignore
6+
/.gitattributes export-ignore
7+
/.gitmodules export-ignore
8+
/AGENTS.md export-ignore
9+
/context7.json export-ignore
10+
/README.md export-ignore

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ composer dev-tools skills
7777
# Merges and synchronizes .gitignore files
7878
composer dev-tools gitignore
7979

80+
# Manages .gitattributes export-ignore rules for leaner package archives
81+
composer dev-tools gitattributes
82+
8083
# Generates a LICENSE file from composer.json license information
8184
composer dev-tools license
8285

@@ -105,7 +108,8 @@ automation assets.
105108
| `composer dev-tools dependencies` | Reports missing and unused Composer dependencies. |
106109
| `composer dev-tools docs` | Builds the HTML documentation site from PSR-4 code and `docs/`. |
107110
| `composer dev-tools skills` | Creates or repairs packaged skill links in `.agents/skills`. |
108-
| `composer dev-tools:sync` | Updates scripts, workflow stubs, `.editorconfig`, `.gitignore`, wiki setup, and packaged skills. |
111+
| `composer dev-tools gitattributes` | Manages export-ignore rules in .gitattributes. |
112+
| `composer dev-tools:sync` | Updates scripts, workflow stubs, `.editorconfig`, `.gitignore`, `.gitattributes`, wiki setup, and packaged skills. |
109113

110114
## 🔌 Integration
111115

composer.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@
8585
"dev-main": "1.x-dev"
8686
},
8787
"class": "FastForward\\DevTools\\Composer\\Plugin",
88+
"gitattributes": {
89+
"keep-in-export": [
90+
"/.agents/",
91+
"/.editorconfig",
92+
"/.gitignore",
93+
"/.php-cs-fixer.dist.php",
94+
"/ecs.php",
95+
"/grumphp.yml",
96+
"/phpunit.xml",
97+
"/rector.php"
98+
]
99+
},
88100
"grumphp": {
89101
"config-default-path": "grumphp.yml"
90102
}

docs/running/specialized-commands.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,31 @@ Important details:
177177
missing.
178178
- it calls ``gitignore`` to merge the canonical .gitignore with the project's
179179
.gitignore;
180+
- it calls ``gitattributes`` to manage export-ignore rules in .gitattributes;
180181
- it calls ``skills`` so ``.agents/skills`` contains links to the packaged
181182
skill set.
182183

184+
``gitattributes``
185+
----------------
186+
187+
Manages .gitattributes export-ignore rules for leaner Composer package archives.
188+
189+
.. code-block:: bash
190+
191+
composer dev-tools gitattributes
192+
193+
Important details:
194+
195+
- it adds export-ignore entries for repository-only files and directories;
196+
- it only adds entries for paths that actually exist in the repository;
197+
- it respects the ``extra.gitattributes.keep-in-export`` configuration to
198+
keep specific paths in exported archives;
199+
- it preserves existing custom .gitattributes rules;
200+
- it deduplicates equivalent entries and sorts them with directories before
201+
files, then alphabetically;
202+
- it uses CandidateProvider, ExistenceChecker, ExportIgnoreFilter, Merger,
203+
Reader, and Writer components from the GitAttributes namespace.
204+
183205
``gitignore``
184206
-------------
185207

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of fast-forward/dev-tools.
7+
*
8+
* This source file is subject to the license bundled
9+
* with this source code in the file LICENSE.
10+
*
11+
* @copyright Copyright (c) 2026 Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
12+
* @license https://opensource.org/licenses/MIT MIT License
13+
*
14+
* @see https://github.com/php-fast-forward/dev-tools
15+
* @see https://github.com/php-fast-forward
16+
* @see https://datatracker.ietf.org/doc/html/rfc2119
17+
*/
18+
19+
namespace FastForward\DevTools\Command;
20+
21+
use FastForward\DevTools\GitAttributes\CandidateProvider;
22+
use FastForward\DevTools\GitAttributes\CandidateProviderInterface;
23+
use FastForward\DevTools\GitAttributes\ExistenceChecker;
24+
use FastForward\DevTools\GitAttributes\ExistenceCheckerInterface;
25+
use FastForward\DevTools\GitAttributes\ExportIgnoreFilter;
26+
use FastForward\DevTools\GitAttributes\ExportIgnoreFilterInterface;
27+
use FastForward\DevTools\GitAttributes\Merger;
28+
use FastForward\DevTools\GitAttributes\MergerInterface;
29+
use FastForward\DevTools\GitAttributes\Reader;
30+
use FastForward\DevTools\GitAttributes\ReaderInterface;
31+
use FastForward\DevTools\GitAttributes\Writer;
32+
use FastForward\DevTools\GitAttributes\WriterInterface;
33+
use Symfony\Component\Console\Input\InputInterface;
34+
use Symfony\Component\Console\Output\OutputInterface;
35+
use Symfony\Component\Filesystem\Filesystem;
36+
use Symfony\Component\Filesystem\Path;
37+
38+
/**
39+
* Provides functionality to manage .gitattributes export-ignore rules.
40+
*
41+
* This command adds export-ignore entries for repository-only files and directories
42+
* to keep them out of Composer package archives.
43+
*/
44+
final class GitAttributesCommand extends AbstractCommand
45+
{
46+
private const string EXTRA_NAMESPACE = 'gitattributes';
47+
48+
private const string EXTRA_KEEP_IN_EXPORT = 'keep-in-export';
49+
50+
private const string EXTRA_NO_EXPORT_IGNORE = 'no-export-ignore';
51+
52+
private readonly WriterInterface $writer;
53+
54+
/**
55+
* Creates a new GitAttributesCommand instance.
56+
*
57+
* @param Filesystem|null $filesystem the filesystem component
58+
* @param CandidateProviderInterface $candidateProvider the candidate provider
59+
* @param ExistenceCheckerInterface $existenceChecker the repository path existence checker
60+
* @param ExportIgnoreFilterInterface $exportIgnoreFilter the configured candidate filter
61+
* @param MergerInterface $merger the merger component
62+
* @param ReaderInterface $reader the reader component
63+
* @param WriterInterface|null $writer the writer component
64+
*/
65+
public function __construct(
66+
?Filesystem $filesystem = null,
67+
private readonly CandidateProviderInterface $candidateProvider = new CandidateProvider(),
68+
private readonly ExistenceCheckerInterface $existenceChecker = new ExistenceChecker(),
69+
private readonly ExportIgnoreFilterInterface $exportIgnoreFilter = new ExportIgnoreFilter(),
70+
private readonly MergerInterface $merger = new Merger(),
71+
private readonly ReaderInterface $reader = new Reader(),
72+
?WriterInterface $writer = null,
73+
) {
74+
parent::__construct($filesystem);
75+
$this->writer = $writer ?? new Writer($this->filesystem);
76+
}
77+
78+
/**
79+
* Configures the current command.
80+
*
81+
* This method MUST define the name, description, and help text for the command.
82+
*
83+
* @return void
84+
*/
85+
protected function configure(): void
86+
{
87+
$this
88+
->setName('gitattributes')
89+
->setDescription('Manages .gitattributes export-ignore rules for leaner package archives.')
90+
->setHelp(
91+
'This command adds export-ignore entries for repository-only files and directories '
92+
. 'to keep them out of Composer package archives. Only paths that exist in the '
93+
. 'repository are added, existing custom rules are preserved, and '
94+
. '"extra.gitattributes.keep-in-export" paths stay in exported archives.'
95+
);
96+
}
97+
98+
/**
99+
* Configures the current command.
100+
*
101+
* This method MUST define the name, description, and help text for the command.
102+
*
103+
* @param InputInterface $input
104+
* @param OutputInterface $output
105+
*/
106+
protected function execute(InputInterface $input, OutputInterface $output): int
107+
{
108+
$output->writeln('<info>Synchronizing .gitattributes export-ignore rules...</info>');
109+
110+
$basePath = $this->getCurrentWorkingDirectory();
111+
$keepInExportPaths = $this->configuredKeepInExportPaths();
112+
113+
$folderCandidates = $this->exportIgnoreFilter->filter($this->candidateProvider->folders(), $keepInExportPaths);
114+
$fileCandidates = $this->exportIgnoreFilter->filter($this->candidateProvider->files(), $keepInExportPaths);
115+
116+
$existingFolders = $this->existenceChecker->filterExisting($basePath, $folderCandidates);
117+
$existingFiles = $this->existenceChecker->filterExisting($basePath, $fileCandidates);
118+
119+
$entries = [...$existingFolders, ...$existingFiles];
120+
121+
if ([] === $entries) {
122+
$output->writeln(
123+
'<comment>No candidate paths found in repository. Skipping .gitattributes sync.</comment>'
124+
);
125+
126+
return self::SUCCESS;
127+
}
128+
129+
$gitattributesPath = Path::join($basePath, '.gitattributes');
130+
$existingContent = $this->reader->read($gitattributesPath);
131+
$content = $this->merger->merge($existingContent, $entries, $keepInExportPaths);
132+
$this->writer->write($gitattributesPath, $content);
133+
134+
$output->writeln(\sprintf(
135+
'<info>Added %d export-ignore entries to .gitattributes.</info>',
136+
\count($entries)
137+
));
138+
139+
return self::SUCCESS;
140+
}
141+
142+
/**
143+
* Resolves the consumer-defined paths that MUST stay in exported archives.
144+
*
145+
* The preferred configuration key is "extra.gitattributes.keep-in-export".
146+
* The alternate "extra.gitattributes.no-export-ignore" key remains
147+
* supported as a compatibility alias.
148+
*
149+
* @return list<string> the configured keep-in-export paths
150+
*/
151+
private function configuredKeepInExportPaths(): array
152+
{
153+
$extra = $this->requireComposer()
154+
->getPackage()
155+
->getExtra();
156+
157+
$gitattributesConfig = $extra[self::EXTRA_NAMESPACE] ?? null;
158+
159+
if (! \is_array($gitattributesConfig)) {
160+
return [];
161+
}
162+
163+
$configuredPaths = [];
164+
165+
foreach ([self::EXTRA_KEEP_IN_EXPORT, self::EXTRA_NO_EXPORT_IGNORE] as $key) {
166+
$values = $gitattributesConfig[$key] ?? [];
167+
168+
if (\is_string($values)) {
169+
$values = [$values];
170+
}
171+
172+
if (! \is_array($values)) {
173+
continue;
174+
}
175+
176+
foreach ($values as $value) {
177+
if (\is_string($value)) {
178+
$configuredPaths[] = $value;
179+
}
180+
}
181+
}
182+
183+
return array_values(array_unique($configuredPaths));
184+
}
185+
}

src/Command/SyncCommand.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ protected function configure(): void
4747
$this
4848
->setName('dev-tools:sync')
4949
->setDescription(
50-
'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, and .editorconfig in the root project.'
50+
'Installs and synchronizes dev-tools scripts, GitHub Actions workflows, .editorconfig, and .gitattributes in the root project.'
5151
)
5252
->setHelp(
53-
'This command adds or updates dev-tools scripts in composer.json, copies reusable GitHub Actions workflows, and ensures .editorconfig is present and up to date.'
53+
'This command adds or updates dev-tools scripts in composer.json, copies reusable GitHub Actions workflows, ensures .editorconfig is present and up to date, and manages .gitattributes export-ignore rules.'
5454
);
5555
}
5656

@@ -75,6 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7575
$this->copyDependabotConfig();
7676
$this->addRepositoryWikiGitSubmodule();
7777
$this->runCommand('gitignore', $output);
78+
$this->runCommand('gitattributes', $output);
7879
$this->runCommand('skills', $output);
7980
$this->runCommand('license', $output);
8081

src/Composer/Capability/DevToolsCommandProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use FastForward\DevTools\Command\CopyLicenseCommand;
2525
use FastForward\DevTools\Command\DependenciesCommand;
2626
use FastForward\DevTools\Command\DocsCommand;
27+
use FastForward\DevTools\Command\GitAttributesCommand;
2728
use FastForward\DevTools\Command\GitIgnoreCommand;
2829
use FastForward\DevTools\Command\PhpDocCommand;
2930
use FastForward\DevTools\Command\RefactorCommand;
@@ -62,6 +63,7 @@ public function getCommands()
6263
new WikiCommand(),
6364
new SyncCommand(),
6465
new GitIgnoreCommand(),
66+
new GitAttributesCommand(),
6567
new SkillsCommand(),
6668
new CopyLicenseCommand(),
6769
];

0 commit comments

Comments
 (0)