Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
Merger
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
2 / 2
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 merge
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(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
19namespace FastForward\DevTools\GitIgnore;
20
21/**
22 * Merges, deduplicates, and sorts .gitignore entries.
23 *
24 * This service SHALL combine canonical and project-specific .gitignore
25 * definitions into a single normalized result. The resulting entry list MUST
26 * exclude blank lines and comment lines from the merged output, MUST remove
27 * duplicate entries, and MUST group directory entries before file entries.
28 * Directory and file groups SHALL be sorted independently in ascending string
29 * order to provide deterministic output.
30 */
31  class Merger implements MergerInterface
32{
33    /**
34     * Initializes the merger with a classifier implementation.
35     *
36     * The classifier MUST be capable of determining whether a normalized
37     * .gitignore entry represents a directory or a file pattern. When no
38     * classifier is provided, a default Classifier instance SHALL be used.
39     *
40     * @param ClassifierInterface $classifier the classifier responsible for
41     *                                        distinguishing directory entries
42     *                                        from file entries during merging
43     */
44    public function __construct(
45        private ClassifierInterface $classifier = new Classifier()
46    ) {}
47
48    /**
49     * Merges canonical and project .gitignore entries into a normalized result.
50     *
51     * The implementation MUST combine entries from both sources, MUST remove
52     * duplicates, and MUST ignore blank or commented entries after trimming.
53     * Entries identified as directories SHALL be collected separately from file
54     * entries. Each group MUST be sorted using string comparison, and directory
55     * entries MUST appear before file entries in the final result.
56     *
57     * The returned GitIgnore instance SHALL preserve the project path provided by
58     * the $project argument.
59     *
60     * @param GitIgnoreInterface $canonical The canonical .gitignore source whose
61     *                                      entries provide the shared baseline.
62     * @param GitIgnoreInterface $project The project-specific .gitignore source
63     *                                    whose path MUST be preserved in the
64     *                                    merged result.
65     *
66     * @return GitIgnoreInterface A new merged .gitignore representation containing
67     *                            normalized, deduplicated, and ordered entries.
68     */
69    public function merge(GitIgnoreInterface $canonical, GitIgnoreInterface $project): GitIgnoreInterface
70    {
71        $entries = array_unique(array_merge($canonical->entries(), $project->entries()));
72
73        $directories = [];
74        $files = [];
75
76        foreach ($entries as $entry) {
77            $trimmed = trim($entry);
78            if ('' === $trimmed) {
79                continue;
80            }
81
82            if (str_starts_with($trimmed, '#')) {
83                continue;
84            }
85
86            if ($this->classifier->isDirectory($trimmed)) {
87                $directories[] = $trimmed;
88            } else {
89                $files[] = $trimmed;
90            }
91        }
92
93        sort($directories, \SORT_STRING);
94        sort($files, \SORT_STRING);
95
96        $mergedEntries = array_merge($directories, $files);
97
98        return new GitIgnore($project->path(), array_values($mergedEntries));
99    }
100}