Skip to content

Commit ef1d2f2

Browse files
jubianchimnapoli
authored andcommitted
Read excluded directories from the .gitignore file of the repository (#165)
This allows users to have their git ignored files and directories automatically ignored by Couscous. To enable this feature, they will have to add the special `%gitignore%` entry to their exclude list. Why do we use a special entry? To keep the actual behavior unchanged and avoid unexpected generation errors. The actual implementation is a bit naïve: it will take each entry line from the `.gitignore` and add them to the exclude list of Couscous. So we might end up adding files and directories. It's hard to test every `.gitignore` entry to see if the are actual directories: paths might be relative or patterns, starting from the repository's root directory or not. Moreover, the implementation does not support complex pattern as git would.
1 parent 3a8087e commit ef1d2f2

File tree

5 files changed

+197
-4
lines changed

5 files changed

+197
-4
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ To be able to preview Couscous own website on your machine, you will need the fo
2727
- Less compiler:
2828
2929
```
30-
$ npm install -g less
30+
$ npm install -g less less-plugin-clean-css
3131
```
3232
3333
- Phar generation enabled in `php.ini`:

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ exclude:
2929
- vendor
3030
- website
3131
- some/dir
32+
# This special entry will ask Couscous to read the exluded directories from your ".gitignore" file
33+
- %gitignore%
3234

3335
scripts:
3436
# Scripts to execute before generating the website

src/Model/ExcludeList.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace Couscous\Model;
4+
5+
use Symfony\Component\Finder\Finder;
6+
7+
class ExcludeList
8+
{
9+
/**
10+
* @var string[]
11+
*/
12+
private $excluded;
13+
14+
public function __construct(array $exclude = [])
15+
{
16+
$this->excluded = $exclude;
17+
}
18+
19+
public function addEntry($entry)
20+
{
21+
$this->excluded[] = $entry;
22+
23+
return $this;
24+
}
25+
26+
public function addEntries(array $entries)
27+
{
28+
$this->excluded = array_merge($this->excluded, $entries);
29+
30+
return $this;
31+
}
32+
33+
public function contains($needle)
34+
{
35+
return in_array($needle, $this->excluded);
36+
}
37+
38+
public function toArray()
39+
{
40+
$excluded = $this->excluded;
41+
$excluded = array_filter($excluded, [$this, 'keepEntry']);
42+
$excluded = array_map([$this, 'sanitizeEntry'], $excluded);
43+
$excluded = array_map(function ($entry) {
44+
return trim($entry, '/');
45+
}, $excluded);
46+
47+
return array_values(array_unique($excluded));
48+
}
49+
50+
public function excludeFromFinder(Finder $finder)
51+
{
52+
$finder->exclude($this->toArray());
53+
54+
return $this;
55+
}
56+
57+
private function keepEntry($entry)
58+
{
59+
switch (true) {
60+
case !is_string($entry) && !is_numeric($entry):
61+
case $entry === '':
62+
case preg_match('/^[#!]/', $entry) > 0:
63+
case strpos($entry, '*') !== false:
64+
return false;
65+
66+
default:
67+
return true;
68+
}
69+
}
70+
71+
private function sanitizeEntry($entry)
72+
{
73+
return preg_replace(
74+
'/\\\(\s)$/',
75+
'$1',
76+
preg_replace(
77+
'/(?<!\\\)(\s)$/',
78+
'',
79+
$entry
80+
)
81+
);
82+
}
83+
}

src/Model/Project.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,21 @@ public function sourceFiles()
120120
});
121121
}
122122

123-
$excludedDirectories = $this->metadata['exclude'] ? $this->metadata['exclude'] : [];
123+
$excludedDirectories = new ExcludeList($this->metadata['exclude'] ? $this->metadata['exclude'] : []);
124+
125+
if (is_file($this->sourceDirectory.'/.gitignore')) {
126+
$excludedDirectories->addEntries(file($this->sourceDirectory.'/.gitignore'));
127+
}
124128

125129
$finder = new Finder();
126130
$finder->files()
127131
->followLinks()
128132
->in(!empty($includedDirectories) ? $includedDirectories : $this->sourceDirectory)
129-
->ignoreDotFiles(true)
130-
->exclude(array_merge($excludedDirectories, ['.couscous']));
133+
->ignoreDotFiles(true);
134+
135+
$excludedDirectories
136+
->addEntry('.couscous')
137+
->excludeFromFinder($finder);
131138

132139
return $finder;
133140
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
namespace Couscous\Tests\UnitTest\Model;
4+
5+
use Couscous\Model\ExcludeList;
6+
7+
/**
8+
* @covers \Couscous\Model\ExcludeList
9+
*/
10+
class ExcludeListTest extends \PHPUnit_Framework_TestCase
11+
{
12+
/**
13+
* @test
14+
*/
15+
public function it_should_init_with_excluded_entries()
16+
{
17+
$excluded = new ExcludeList(['foo', 'bar']);
18+
19+
$this->assertSame(['foo', 'bar'], $excluded->toArray());
20+
}
21+
22+
/**
23+
* @test
24+
*/
25+
public function it_should_store_additional_entry()
26+
{
27+
$excluded = new ExcludeList(['foo', 'bar']);
28+
$excluded->addEntry('baz');
29+
30+
$this->assertSame(['foo', 'bar', 'baz'], $excluded->toArray());
31+
}
32+
33+
/**
34+
* @test
35+
*/
36+
public function it_should_store_additional_entries()
37+
{
38+
$excluded = new ExcludeList(['foo', 'bar']);
39+
$excluded->addEntries(['baz', 'boo']);
40+
41+
$this->assertSame(['foo', 'bar', 'baz', 'boo'], $excluded->toArray());
42+
}
43+
44+
/**
45+
* @test
46+
*/
47+
public function it_should_dedupe_entries()
48+
{
49+
$excluded = new ExcludeList(['foo', 'bar', 'baz']);
50+
$excluded->addEntry('foo');
51+
$excluded->addEntries(['baz', 'boo']);
52+
53+
$this->assertSame(['foo', 'bar', 'baz', 'boo'], $excluded->toArray());
54+
}
55+
56+
/**
57+
* @test
58+
*/
59+
public function it_should_filter_invalid_entries()
60+
{
61+
$excluded = new ExcludeList(['foo', '', 'bar']);
62+
$excluded->addEntry('');
63+
$excluded->addEntries(['baz', '', null, true, false, 1337]);
64+
65+
$this->assertSame(['foo', 'bar', 'baz', '1337'], $excluded->toArray());
66+
}
67+
68+
/**
69+
* @test
70+
*/
71+
public function it_should_filter_special_entries()
72+
{
73+
$excluded = new ExcludeList(['foo', '#foo', 'bar', '!bar', '*.php', 'foo/**/bar.php']);
74+
75+
$this->assertSame(['foo', 'bar'], $excluded->toArray());
76+
}
77+
78+
/**
79+
* @test
80+
*/
81+
public function it_should_sanitize_entries()
82+
{
83+
$excluded = new ExcludeList(['foo', 'bar ', "baz\t", 'boo\ ']);
84+
85+
$this->assertSame(['foo', 'bar', 'baz', 'boo '], $excluded->toArray());
86+
}
87+
88+
/**
89+
* @test
90+
*/
91+
public function it_should_exclude_from_finder()
92+
{
93+
$finder = $this->getMockBuilder('Symfony\Component\Finder\Finder')
94+
->disableOriginalConstructor()
95+
->getMock();
96+
$finder->expects($this->once())->method('exclude')->with(['foo', 'bar', '1337']);
97+
98+
$excluded = new ExcludeList(['foo', '#foo', 'bar', '!bar', '', 1337, null]);
99+
$excluded->excludeFromFinder($finder);
100+
}
101+
}

0 commit comments

Comments
 (0)