Skip to content

Commit e78bbda

Browse files
committed
feat: add .gitignore sync to dev-tools:sync
- Add GitIgnore namespace with Reader, Classifier, Merger, Writer classes - Reader reads .gitignore entries from both package and project - Classifier classifies entries as directories or files - Merger merges, deduplicates, and sorts entries - Writer writes normalized .gitignore file - SyncCommand now syncs .gitignore entries automatically - Entries are sorted: directories first, then files, alphabetical within each group Implements: #12
1 parent 9cecf52 commit e78bbda

File tree

7 files changed

+308
-1
lines changed

7 files changed

+308
-1
lines changed

.github/wiki

Submodule wiki updated from 2c546cc to cfd1257

src/Command/SyncCommand.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
namespace FastForward\DevTools\Command;
2020

2121
use Composer\Factory;
22+
use FastForward\DevTools\GitIgnore\Classifier;
23+
use FastForward\DevTools\GitIgnore\Merger;
24+
use FastForward\DevTools\GitIgnore\Reader;
25+
use FastForward\DevTools\GitIgnore\Writer;
2226
use Composer\Json\JsonManipulator;
2327
use Symfony\Component\Console\Input\InputInterface;
2428
use Symfony\Component\Console\Output\OutputInterface;
@@ -74,6 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7478
$this->copyEditorConfig();
7579
$this->copyDependabotConfig();
7680
$this->addRepositoryWikiGitSubmodule();
81+
$this->syncGitIgnore();
7782

7883
return self::SUCCESS;
7984
}
@@ -233,4 +238,29 @@ private function getGitRepositoryUrl(): string
233238

234239
return trim($process->getOutput());
235240
}
241+
242+
/**
243+
* Synchronizes .gitignore entries from dev-tools into the target project.
244+
*
245+
* This method merges canonical .gitignore entries from the dev-tools package
246+
* with the target project's existing .gitignore entries, then writes the merged result.
247+
*
248+
* @return void
249+
*/
250+
private function syncGitIgnore(): void
251+
{
252+
$packagePath = parent::getDevToolsFile('');
253+
$projectPath = $this->getCurrentWorkingDirectory();
254+
$targetPath = $projectPath . '/.gitignore';
255+
256+
$canonicalEntries = Reader::readFromPackage($packagePath);
257+
$projectEntries = Reader::readFromProject($projectPath);
258+
259+
$classifier = new Classifier();
260+
$merger = new Merger($classifier);
261+
$mergedEntries = $merger->merge($canonicalEntries, $projectEntries);
262+
263+
$writer = new Writer($this->filesystem);
264+
$writer->write($mergedEntries, $targetPath);
265+
}
236266
}

src/GitIgnore/Classifier.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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\GitIgnore;
20+
21+
use function Safe\preg_match;
22+
23+
/**
24+
* Classifies .gitignore entries as directories or files.
25+
*/
26+
final class Classifier
27+
{
28+
/**
29+
* Classifies a .gitignore entry as directory or file pattern.
30+
*
31+
* @param string $entry the .gitignore entry
32+
*
33+
* @return 'directory'|'file' the classification
34+
*/
35+
public function classify(string $entry): string
36+
{
37+
$entry = trim($entry);
38+
39+
if ('' === $entry) {
40+
return 'file';
41+
}
42+
43+
if (str_starts_with($entry, '#')) {
44+
return 'file';
45+
}
46+
47+
if (str_ends_with($entry, '/')) {
48+
return 'directory';
49+
}
50+
51+
if (1 === preg_match('/^[^.*]+[\/*]+$/', $entry)) {
52+
return 'directory';
53+
}
54+
55+
if (str_contains($entry, '*/')) {
56+
return 'directory';
57+
}
58+
59+
if (str_starts_with($entry, '**/')) {
60+
return 'directory';
61+
}
62+
63+
return 'file';
64+
}
65+
66+
/**
67+
* Checks if an entry is a directory pattern.
68+
*
69+
* @param string $entry
70+
*/
71+
public function isDirectory(string $entry): bool
72+
{
73+
return 'directory' === $this->classify($entry);
74+
}
75+
76+
/**
77+
* Checks if an entry is a file pattern.
78+
*
79+
* @param string $entry
80+
*/
81+
public function isFile(string $entry): bool
82+
{
83+
return 'file' === $this->classify($entry);
84+
}
85+
}

src/GitIgnore/Merger.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\GitIgnore;
20+
21+
/**
22+
* Merges, deduplicates, and sorts .gitignore entries.
23+
*/
24+
final readonly class Merger
25+
{
26+
/**
27+
* @param Classifier $classifier
28+
*/
29+
public function __construct(
30+
private Classifier $classifier
31+
) {}
32+
33+
/**
34+
* Merges canonical and project entries, removes duplicates, and sorts.
35+
*
36+
* @param array<int, string> $canonical the canonical .gitignore entries from dev-tools
37+
* @param array<int, string> $project the project-specific .gitignore entries
38+
*
39+
* @return array<int, string> the merged and sorted entries
40+
*/
41+
public function merge(array $canonical, array $project): array
42+
{
43+
$entries = array_unique(array_merge($canonical, $project));
44+
45+
$directories = [];
46+
$files = [];
47+
48+
foreach ($entries as $entry) {
49+
$trimmed = trim($entry);
50+
if ('' === $trimmed) {
51+
continue;
52+
}
53+
if (str_starts_with($trimmed, '#')) {
54+
continue;
55+
}
56+
57+
if ($this->classifier->isDirectory($trimmed)) {
58+
$directories[] = $trimmed;
59+
} else {
60+
$files[] = $trimmed;
61+
}
62+
}
63+
64+
sort($directories, \SORT_STRING);
65+
sort($files, \SORT_STRING);
66+
67+
return array_merge($directories, $files);
68+
}
69+
}

src/GitIgnore/Reader.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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\GitIgnore;
20+
21+
/**
22+
* Loads canonical .gitignore entries from the dev-tools package.
23+
*/
24+
final class Reader
25+
{
26+
/**
27+
* Reads the canonical .gitignore entries from dev-tools package.
28+
*
29+
* @param string $packagePath the path to the dev-tools package
30+
*
31+
* @return array<int, string> the lines from .gitignore
32+
*/
33+
public static function readFromPackage(string $packagePath): array
34+
{
35+
$gitignorePath = $packagePath . '/.gitignore';
36+
37+
if (! file_exists($gitignorePath)) {
38+
return [];
39+
}
40+
41+
$content = file_get_contents($gitignorePath);
42+
$lines = explode("\n", $content);
43+
44+
return array_values(array_filter($lines, static fn(string $line): bool => '' !== trim($line)));
45+
}
46+
47+
/**
48+
* Reads the target project's .gitignore entries.
49+
*
50+
* @param string $projectPath the path to the target project
51+
*
52+
* @return array<int, string> the lines from .gitignore
53+
*/
54+
public static function readFromProject(string $projectPath): array
55+
{
56+
$gitignorePath = $projectPath . '/.gitignore';
57+
58+
if (! file_exists($gitignorePath)) {
59+
return [];
60+
}
61+
62+
$content = file_get_contents($gitignorePath);
63+
$lines = explode("\n", $content);
64+
65+
return array_values(array_filter($lines, static fn(string $line): bool => '' !== trim($line)));
66+
}
67+
}

src/GitIgnore/Writer.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\GitIgnore;
20+
21+
use Symfony\Component\Filesystem\Filesystem;
22+
23+
/**
24+
* Renders and writes the normalized .gitignore file.
25+
*/
26+
final readonly class Writer
27+
{
28+
/**
29+
* @param Filesystem $filesystem
30+
*/
31+
public function __construct(
32+
private Filesystem $filesystem
33+
) {}
34+
35+
/**
36+
* Writes the normalized .gitignore entries to the target file.
37+
*
38+
* @param array<int, string> $entries the sorted .gitignore entries
39+
* @param string $targetPath the target .gitignore file path
40+
*/
41+
public function write(array $entries, string $targetPath): void
42+
{
43+
$content = implode("\n", $entries) . "\n";
44+
45+
$this->filesystem->dumpFile($targetPath, $content);
46+
}
47+
}

tests/Command/SyncCommandTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,21 @@
1919
namespace FastForward\DevTools\Tests\Command;
2020

2121
use FastForward\DevTools\Command\SyncCommand;
22+
use FastForward\DevTools\GitIgnore\Classifier;
23+
use FastForward\DevTools\GitIgnore\Merger;
24+
use FastForward\DevTools\GitIgnore\Reader;
25+
use FastForward\DevTools\GitIgnore\Writer;
2226
use PHPUnit\Framework\Attributes\CoversClass;
2327
use PHPUnit\Framework\Attributes\Test;
28+
use PHPUnit\Framework\Attributes\UsesClass;
2429
use Prophecy\Argument;
2530
use Prophecy\PhpUnit\ProphecyTrait;
2631

2732
#[CoversClass(SyncCommand::class)]
33+
#[UsesClass(Reader::class)]
34+
#[UsesClass(Classifier::class)]
35+
#[UsesClass(Merger::class)]
36+
#[UsesClass(Writer::class)]
2837
final class SyncCommandTest extends AbstractCommandTestCase
2938
{
3039
use ProphecyTrait;

0 commit comments

Comments
 (0)