Browse code

add support for exclusion rules in dockerignore

Signed-off-by: Dave Goodchild <buddhamagnet@gmail.com>

buddhamagnet authored on 2015/04/10 04:07:06
Showing 7 changed files
... ...
@@ -32,13 +32,14 @@ ephemeral as possible. By “ephemeral,” we mean that it can be stopped and
32 32
 destroyed and a new one built and put in place with an absolute minimum of
33 33
 set-up and configuration.
34 34
 
35
-### Use [a .dockerignore file](https://docs.docker.com/reference/builder/#the-dockerignore-file)
36
-
37
-For faster uploading and efficiency during `docker build`, you should use
38
-a `.dockerignore` file to exclude files or directories from the build
39
-context and final image. For example, unless`.git` is needed by your build
40
-process or scripts, you should add it to `.dockerignore`, which can save many
41
-megabytes worth of upload time.
35
+### Use a .dockerignore file
36
+
37
+In most cases, it's best to put each Dockerfile in an empty directory. Then,
38
+add to that directory only the files needed for building the Dockerfile. To
39
+increase the build's performance, you can exclude files and directories by
40
+adding a `.dockerignore` file to that directory as well. This file supports 
41
+exclusion patterns similar to `.gitignore` files. For information on creating one,
42
+see the [.dockerignore file](../../reference/builder/#dockerignore-file).
42 43
 
43 44
 ### Avoid installing unnecessary packages
44 45
 
... ...
@@ -41,10 +41,11 @@ whole context must be transferred to the daemon. The Docker CLI reports
41 41
 > repository, the entire contents of your hard drive will get sent to the daemon (and
42 42
 > thus to the machine running the daemon). You probably don't want that.
43 43
 
44
-In most cases, it's best to put each Dockerfile in an empty directory, and then add only
45
-the files needed for building that Dockerfile to that directory. To further speed up the
46
-build, you can exclude files and directories by adding a `.dockerignore` file to the same
47
-directory.
44
+In most cases, it's best to put each Dockerfile in an empty directory. Then,
45
+only add the files needed for building the Dockerfile to the directory. To
46
+increase the build's performance, you can exclude files and directories by
47
+adding a `.dockerignore` file to the directory.  For information about how to
48
+[create a `.dockerignore` file](#the-dockerignore-file) on this page.
48 49
 
49 50
 You can specify a repository and tag at which to save the new image if
50 51
 the build succeeds:
... ...
@@ -169,43 +170,67 @@ will result in `def` having a value of `hello`, not `bye`.  However,
169 169
 `ghi` will have a value of `bye` because it is not part of the same command
170 170
 that set `abc` to `bye`.
171 171
 
172
-## The `.dockerignore` file
172
+### .dockerignore file
173 173
 
174
-If a file named `.dockerignore` exists in the source repository, then it
175
-is interpreted as a newline-separated list of exclusion patterns.
176
-Exclusion patterns match files or directories relative to the source repository
177
-that will be excluded from the context. Globbing is done using Go's
174
+If a file named `.dockerignore` exists in the root of `PATH`, then Docker
175
+interprets it as a newline-separated list of exclusion patterns. Docker excludes
176
+files or directories relative to `PATH` that match these exclusion patterns. If
177
+there are any `.dockerignore` files in `PATH` subdirectories, Docker treats
178
+them as normal files. 
179
+
180
+Filepaths in `.dockerignore` are absolute with the current directory as the
181
+root. Wildcards are allowed but the search is not recursive. Globbing (file name
182
+expansion) is done using Go's
178 183
 [filepath.Match](http://golang.org/pkg/path/filepath#Match) rules.
179 184
 
180
-> **Note**:
181
-> The `.dockerignore` file can even be used to ignore the `Dockerfile` and
182
-> `.dockerignore` files. This might be useful if you are copying files from
183
-> the root of the build context into your new container but do not want to 
184
-> include the `Dockerfile` or `.dockerignore` files (e.g. `ADD . /someDir/`).
185
+You can specify exceptions to exclusion rules. To do this, simply prefix a
186
+pattern with an `!` (exclamation mark) in the same way you would in a
187
+`.gitignore` file.  Currently there is no support for regular expressions.
188
+Formats like `[^temp*]` are ignored. 
185 189
 
186
-The following example shows the use of the `.dockerignore` file to exclude the
187
-`.git` directory from the context. Its effect can be seen in the changed size of
188
-the uploaded context.
190
+The following is an example `.dockerignore` file:
191
+
192
+```
193
+    */temp*
194
+    */*/temp*
195
+    temp?
196
+    *.md
197
+    !LICENCSE.md
198
+```
199
+
200
+This file causes the following build behavior:
201
+
202
+| Rule           | Behavior                                                                                                                                                                     |
203
+|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
204
+| `*/temp*`      | Exclude all files with names starting with`temp` in any subdirectory below the root directory. For example, a file named`/somedir/temporary.txt` is ignored.              |
205
+| `*/*/temp*`    | Exclude files starting with name `temp` from any subdirectory that is two levels below the root directory. For example, the file `/somedir/subdir/temporary.txt` is ignored. |
206
+| `temp?`        | Exclude the files that match the pattern in the root directory. For example, the files `tempa`, `tempb` in the root directory are ignored.                               |
207
+| `*.md `        | Exclude all markdown files.                                                                                                                                                  |
208
+| `!LICENSE.md` | Exception to the exclude all Markdown files is this file,  `LICENSE.md`, include this file in the build.                                                                     |
209
+
210
+The placement of  `!` exception rules influences the matching algorithm; the
211
+last line of the `.dockerignore` that matches a particular file determines
212
+whether it is included or excluded. In the above example, the `LICENSE.md` file
213
+matches both the  `*.md` and `!LICENSE.md` rule. If you reverse the lines in the
214
+example:
215
+
216
+```
217
+    */temp*
218
+    */*/temp*
219
+    temp?
220
+    !LICENCSE.md
221
+    *.md
222
+```
223
+
224
+The build would exclude `LICENSE.md` because the last `*.md` rule adds all
225
+Markdown files back onto the ignore list. The `!LICENSE.md` rule has no effect
226
+because the subsequent `*.md` rule overrides it.
227
+
228
+You can even use the  `.dockerignore` file to ignore the `Dockerfile` and
229
+`.dockerignore` files. This is useful if you are copying files from the root of
230
+the build context into your new container but do not want to include the
231
+`Dockerfile` or `.dockerignore` files (e.g. `ADD . /someDir/`).
189 232
 
190
-    $ docker build .
191
-    Uploading context 18.829 MB
192
-    Uploading context
193
-    Step 0 : FROM busybox
194
-     ---> 769b9341d937
195
-    Step 1 : CMD echo Hello World
196
-     ---> Using cache
197
-     ---> 99cc1ad10469
198
-    Successfully built 99cc1ad10469
199
-    $ echo ".git" > .dockerignore
200
-    $ docker build .
201
-    Uploading context  6.76 MB
202
-    Uploading context
203
-    Step 0 : FROM busybox
204
-     ---> 769b9341d937
205
-    Step 1 : CMD echo Hello World
206
-     ---> Using cache
207
-     ---> 99cc1ad10469
208
-    Successfully built 99cc1ad10469
209 233
 
210 234
 ## FROM
211 235
 
... ...
@@ -653,6 +653,26 @@ If you use STDIN or specify a `URL`, the system places the contents into a
653 653
 file called `Dockerfile`, and any `-f`, `--file` option is ignored. In this
654 654
 scenario, there is no context.
655 655
 
656
+By default the `docker build` command will look for a `Dockerfile` at the
657
+root of the build context. The `-f`, `--file`, option lets you specify
658
+the path to an alternative file to use instead.  This is useful
659
+in cases where the same set of files are used for multiple builds. The path
660
+must be to a file within the build context. If a relative path is specified
661
+then it must to be relative to the current directory.
662
+
663
+In most cases, it's best to put each Dockerfile in an empty directory. Then, add
664
+to that directory only the files needed for building the Dockerfile. To increase
665
+the build's performance, you can exclude files and directories by adding a
666
+`.dockerignore` file to that directory as well. For information on creating one,
667
+see the [.dockerignore file](../../reference/builder/#dockerignore-file).
668
+
669
+If the Docker client loses connection to the daemon, the build is canceled.
670
+This happens if you interrupt the Docker client with `ctrl-c` or if the Docker
671
+client is killed for any reason.
672
+
673
+> **Note:** Currently only the "run" phase of the build can be canceled until
674
+> pull cancelation is implemented).
675
+
656 676
 ### Return code
657 677
 
658 678
 On a successful build, a return code of success `0` will be returned.
... ...
@@ -673,55 +693,11 @@ INFO[0000] The command [/bin/sh -c exit 13] returned a non-zero code: 13
673 673
 $ echo $?
674 674
 1
675 675
 ```
676
-
677
-### .dockerignore file
678
-
679
-If a file named `.dockerignore` exists in the root of `PATH` then it
680
-is interpreted as a newline-separated list of exclusion patterns.
681
-Exclusion patterns match files or directories relative to `PATH` that
682
-will be excluded from the context. Globbing is done using Go's
683
-[filepath.Match](http://golang.org/pkg/path/filepath#Match) rules.
684
-
685
-Please note that `.dockerignore` files in other subdirectories are
686
-considered as normal files. Filepaths in `.dockerignore` are absolute with
687
-the current directory as the root. Wildcards are allowed but the search
688
-is not recursive.
689
-
690
-#### Example .dockerignore file
691
-    */temp*
692
-    */*/temp*
693
-    temp?
694
-
695
-The first line above `*/temp*`, would ignore all files with names starting with
696
-`temp` from any subdirectory below the root directory. For example, a file named
697
-`/somedir/temporary.txt` would be ignored. The second line `*/*/temp*`, will
698
-ignore files starting with name `temp` from any subdirectory that is two levels
699
-below the root directory. For example, the file `/somedir/subdir/temporary.txt`
700
-would get ignored in this case. The last line in the above example `temp?`
701
-will ignore the files that match the pattern from the root directory.
702
-For example, the files `tempa`, `tempb` are ignored from the root directory.
703
-Currently there is no support for regular expressions. Formats
704
-like `[^temp*]` are ignored.
705
-
706
-By default the `docker build` command will look for a `Dockerfile` at the
707
-root of the build context. The `-f`, `--file`, option lets you specify
708
-the path to an alternative file to use instead.  This is useful
709
-in cases where the same set of files are used for multiple builds. The path
710
-must be to a file within the build context. If a relative path is specified
711
-then it must to be relative to the current directory.
712
-
713
-If the Docker client loses connection to the daemon, the build is canceled.
714
-This happens if you interrupt the Docker client with `ctrl-c` or if the Docker
715
-client is killed for any reason.
716
-
717
-> **Note:** Currently only the "run" phase of the build can be canceled until
718
-> pull cancelation is implemented).
719
-
720 676
 See also:
721 677
 
722 678
 [*Dockerfile Reference*](/reference/builder).
723 679
 
724
-#### Examples
680
+### Examples
725 681
 
726 682
     $ docker build .
727 683
     Uploading context 10240 bytes
... ...
@@ -790,7 +766,8 @@ affect the build cache.
790 790
 
791 791
 This example shows the use of the `.dockerignore` file to exclude the `.git`
792 792
 directory from the context. Its effect can be seen in the changed size of the
793
-uploaded context.
793
+uploaded context. The builder reference contains detailed information on
794
+[creating a .dockerignore file](../../builder/#dockerignore-file)
794 795
 
795 796
     $ docker build -t vieux/apache:2.0 .
796 797
 
... ...
@@ -3427,20 +3427,29 @@ func (s *DockerSuite) TestBuildDockerignore(c *check.C) {
3427 3427
 		RUN [[ ! -e /bla/src/_vendor ]]
3428 3428
 		RUN [[ ! -e /bla/.gitignore ]]
3429 3429
 		RUN [[ ! -e /bla/README.md ]]
3430
+		RUN [[ ! -e /bla/dir/foo ]]
3431
+		RUN [[ ! -e /bla/foo ]]
3430 3432
 		RUN [[ ! -e /bla/.git ]]`
3431 3433
 	ctx, err := fakeContext(dockerfile, map[string]string{
3432 3434
 		"Makefile":         "all:",
3433 3435
 		".git/HEAD":        "ref: foo",
3434 3436
 		"src/x.go":         "package main",
3435 3437
 		"src/_vendor/v.go": "package main",
3438
+		"dir/foo":          "",
3436 3439
 		".gitignore":       "",
3437 3440
 		"README.md":        "readme",
3438
-		".dockerignore":    ".git\npkg\n.gitignore\nsrc/_vendor\n*.md",
3441
+		".dockerignore": `
3442
+.git
3443
+pkg
3444
+.gitignore
3445
+src/_vendor
3446
+*.md
3447
+dir`,
3439 3448
 	})
3440
-	defer ctx.Close()
3441 3449
 	if err != nil {
3442 3450
 		c.Fatal(err)
3443 3451
 	}
3452
+	defer ctx.Close()
3444 3453
 	if _, err := buildImageFromContext(name, ctx, true); err != nil {
3445 3454
 		c.Fatal(err)
3446 3455
 	}
... ...
@@ -3467,6 +3476,55 @@ func (s *DockerSuite) TestBuildDockerignoreCleanPaths(c *check.C) {
3467 3467
 	}
3468 3468
 }
3469 3469
 
3470
+func (s *DockerSuite) TestBuildDockerignoreExceptions(c *check.C) {
3471
+	name := "testbuilddockerignoreexceptions"
3472
+	defer deleteImages(name)
3473
+	dockerfile := `
3474
+        FROM busybox
3475
+        ADD . /bla
3476
+		RUN [[ -f /bla/src/x.go ]]
3477
+		RUN [[ -f /bla/Makefile ]]
3478
+		RUN [[ ! -e /bla/src/_vendor ]]
3479
+		RUN [[ ! -e /bla/.gitignore ]]
3480
+		RUN [[ ! -e /bla/README.md ]]
3481
+		RUN [[  -e /bla/dir/dir/foo ]]
3482
+		RUN [[ ! -e /bla/dir/foo1 ]]
3483
+		RUN [[ -f /bla/dir/e ]]
3484
+		RUN [[ -f /bla/dir/e-dir/foo ]]
3485
+		RUN [[ ! -e /bla/foo ]]
3486
+		RUN [[ ! -e /bla/.git ]]`
3487
+	ctx, err := fakeContext(dockerfile, map[string]string{
3488
+		"Makefile":         "all:",
3489
+		".git/HEAD":        "ref: foo",
3490
+		"src/x.go":         "package main",
3491
+		"src/_vendor/v.go": "package main",
3492
+		"dir/foo":          "",
3493
+		"dir/foo1":         "",
3494
+		"dir/dir/f1":       "",
3495
+		"dir/dir/foo":      "",
3496
+		"dir/e":            "",
3497
+		"dir/e-dir/foo":    "",
3498
+		".gitignore":       "",
3499
+		"README.md":        "readme",
3500
+		".dockerignore": `
3501
+.git
3502
+pkg
3503
+.gitignore
3504
+src/_vendor
3505
+*.md
3506
+dir
3507
+!dir/e*
3508
+!dir/dir/foo`,
3509
+	})
3510
+	if err != nil {
3511
+		c.Fatal(err)
3512
+	}
3513
+	defer ctx.Close()
3514
+	if _, err := buildImageFromContext(name, ctx, true); err != nil {
3515
+		c.Fatal(err)
3516
+	}
3517
+}
3518
+
3470 3519
 func (s *DockerSuite) TestBuildDockerignoringDockerfile(c *check.C) {
3471 3520
 	name := "testbuilddockerignoredockerfile"
3472 3521
 	dockerfile := `
... ...
@@ -3607,6 +3665,7 @@ func (s *DockerSuite) TestBuildDockerignoringWholeDir(c *check.C) {
3607 3607
 	ctx, err := fakeContext(dockerfile, map[string]string{
3608 3608
 		"Dockerfile":    "FROM scratch",
3609 3609
 		"Makefile":      "all:",
3610
+		".gitignore":    "",
3610 3611
 		".dockerignore": ".*\n",
3611 3612
 	})
3612 3613
 	defer ctx.Close()
... ...
@@ -391,6 +391,13 @@ func Tar(path string, compression Compression) (io.ReadCloser, error) {
391 391
 // TarWithOptions creates an archive from the directory at `path`, only including files whose relative
392 392
 // paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`.
393 393
 func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) {
394
+
395
+	patterns, patDirs, exceptions, err := fileutils.CleanPatterns(options.ExcludePatterns)
396
+
397
+	if err != nil {
398
+		return nil, err
399
+	}
400
+
394 401
 	pipeReader, pipeWriter := io.Pipe()
395 402
 
396 403
 	compressWriter, err := CompressStream(pipeWriter, options.Compression)
... ...
@@ -441,7 +448,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
441 441
 				// is asking for that file no matter what - which is true
442 442
 				// for some files, like .dockerignore and Dockerfile (sometimes)
443 443
 				if include != relFilePath {
444
-					skip, err = fileutils.Matches(relFilePath, options.ExcludePatterns)
444
+					skip, err = fileutils.OptimizedMatches(relFilePath, patterns, patDirs)
445 445
 					if err != nil {
446 446
 						logrus.Debugf("Error matching %s", relFilePath, err)
447 447
 						return err
... ...
@@ -449,7 +456,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
449 449
 				}
450 450
 
451 451
 				if skip {
452
-					if f.IsDir() {
452
+					if !exceptions && f.IsDir() {
453 453
 						return filepath.SkipDir
454 454
 					}
455 455
 					return nil
... ...
@@ -1,33 +1,120 @@
1 1
 package fileutils
2 2
 
3 3
 import (
4
+	"errors"
4 5
 	"fmt"
5 6
 	"io"
6 7
 	"io/ioutil"
7 8
 	"os"
8 9
 	"path/filepath"
10
+	"strings"
9 11
 
10 12
 	"github.com/Sirupsen/logrus"
11 13
 )
12 14
 
13
-// Matches returns true if relFilePath matches any of the patterns
14
-func Matches(relFilePath string, patterns []string) (bool, error) {
15
-	for _, exclude := range patterns {
16
-		matched, err := filepath.Match(exclude, relFilePath)
15
+func Exclusion(pattern string) bool {
16
+	return pattern[0] == '!'
17
+}
18
+
19
+func Empty(pattern string) bool {
20
+	return pattern == ""
21
+}
22
+
23
+// Cleanpatterns takes a slice of patterns returns a new
24
+// slice of patterns cleaned with filepath.Clean, stripped
25
+// of any empty patterns and lets the caller know whether the
26
+// slice contains any exception patterns (prefixed with !).
27
+func CleanPatterns(patterns []string) ([]string, [][]string, bool, error) {
28
+	// Loop over exclusion patterns and:
29
+	// 1. Clean them up.
30
+	// 2. Indicate whether we are dealing with any exception rules.
31
+	// 3. Error if we see a single exclusion marker on it's own (!).
32
+	cleanedPatterns := []string{}
33
+	patternDirs := [][]string{}
34
+	exceptions := false
35
+	for _, pattern := range patterns {
36
+		// Eliminate leading and trailing whitespace.
37
+		pattern = strings.TrimSpace(pattern)
38
+		if Empty(pattern) {
39
+			continue
40
+		}
41
+		if Exclusion(pattern) {
42
+			if len(pattern) == 1 {
43
+				logrus.Errorf("Illegal exclusion pattern: %s", pattern)
44
+				return nil, nil, false, errors.New("Illegal exclusion pattern: !")
45
+			}
46
+			exceptions = true
47
+		}
48
+		pattern = filepath.Clean(pattern)
49
+		cleanedPatterns = append(cleanedPatterns, pattern)
50
+		if Exclusion(pattern) {
51
+			pattern = pattern[1:]
52
+		}
53
+		patternDirs = append(patternDirs, strings.Split(pattern, "/"))
54
+	}
55
+
56
+	return cleanedPatterns, patternDirs, exceptions, nil
57
+}
58
+
59
+// Matches returns true if file matches any of the patterns
60
+// and isn't excluded by any of the subsequent patterns.
61
+func Matches(file string, patterns []string) (bool, error) {
62
+	file = filepath.Clean(file)
63
+
64
+	if file == "." {
65
+		// Don't let them exclude everything, kind of silly.
66
+		return false, nil
67
+	}
68
+
69
+	patterns, patDirs, _, err := CleanPatterns(patterns)
70
+	if err != nil {
71
+		return false, err
72
+	}
73
+
74
+	return OptimizedMatches(file, patterns, patDirs)
75
+}
76
+
77
+// Matches is basically the same as fileutils.Matches() but optimized for archive.go.
78
+// It will assume that the inputs have been preprocessed and therefore the function
79
+// doen't need to do as much error checking and clean-up. This was done to avoid
80
+// repeating these steps on each file being checked during the archive process.
81
+// The more generic fileutils.Matches() can't make these assumptions.
82
+func OptimizedMatches(file string, patterns []string, patDirs [][]string) (bool, error) {
83
+	matched := false
84
+	parentPath := filepath.Dir(file)
85
+	parentPathDirs := strings.Split(parentPath, "/")
86
+
87
+	for i, pattern := range patterns {
88
+		negative := false
89
+
90
+		if Exclusion(pattern) {
91
+			negative = true
92
+			pattern = pattern[1:]
93
+		}
94
+
95
+		match, err := filepath.Match(pattern, file)
17 96
 		if err != nil {
18
-			logrus.Errorf("Error matching: %s (pattern: %s)", relFilePath, exclude)
97
+			logrus.Errorf("Error matching: %s (pattern: %s)", file, pattern)
19 98
 			return false, err
20 99
 		}
21
-		if matched {
22
-			if filepath.Clean(relFilePath) == "." {
23
-				logrus.Errorf("Can't exclude whole path, excluding pattern: %s", exclude)
24
-				continue
100
+
101
+		if !match && parentPath != "." {
102
+			// Check to see if the pattern matches one of our parent dirs.
103
+			if len(patDirs[i]) <= len(parentPathDirs) {
104
+				match, _ = filepath.Match(strings.Join(patDirs[i], "/"),
105
+					strings.Join(parentPathDirs[:len(patDirs[i])], "/"))
25 106
 			}
26
-			logrus.Debugf("Skipping excluded path: %s", relFilePath)
27
-			return true, nil
28 107
 		}
108
+
109
+		if match {
110
+			matched = !negative
111
+		}
112
+	}
113
+
114
+	if matched {
115
+		logrus.Debugf("Skipping excluded path: %s", file)
29 116
 	}
30
-	return false, nil
117
+	return matched, nil
31 118
 }
32 119
 
33 120
 func CopyFile(src, dst string) (int64, error) {
... ...
@@ -79,3 +79,142 @@ func TestReadSymlinkedDirectoryToFile(t *testing.T) {
79 79
 		t.Errorf("failed to remove symlink: %s", err)
80 80
 	}
81 81
 }
82
+
83
+func TestWildcardMatches(t *testing.T) {
84
+	match, _ := Matches("fileutils.go", []string{"*"})
85
+	if match != true {
86
+		t.Errorf("failed to get a wildcard match, got %v", match)
87
+	}
88
+}
89
+
90
+// A simple pattern match should return true.
91
+func TestPatternMatches(t *testing.T) {
92
+	match, _ := Matches("fileutils.go", []string{"*.go"})
93
+	if match != true {
94
+		t.Errorf("failed to get a match, got %v", match)
95
+	}
96
+}
97
+
98
+// An exclusion followed by an inclusion should return true.
99
+func TestExclusionPatternMatchesPatternBefore(t *testing.T) {
100
+	match, _ := Matches("fileutils.go", []string{"!fileutils.go", "*.go"})
101
+	if match != true {
102
+		t.Errorf("failed to get true match on exclusion pattern, got %v", match)
103
+	}
104
+}
105
+
106
+// A folder pattern followed by an exception should return false.
107
+func TestPatternMatchesFolderExclusions(t *testing.T) {
108
+	match, _ := Matches("docs/README.md", []string{"docs", "!docs/README.md"})
109
+	if match != false {
110
+		t.Errorf("failed to get a false match on exclusion pattern, got %v", match)
111
+	}
112
+}
113
+
114
+// A folder pattern followed by an exception should return false.
115
+func TestPatternMatchesFolderWithSlashExclusions(t *testing.T) {
116
+	match, _ := Matches("docs/README.md", []string{"docs/", "!docs/README.md"})
117
+	if match != false {
118
+		t.Errorf("failed to get a false match on exclusion pattern, got %v", match)
119
+	}
120
+}
121
+
122
+// A folder pattern followed by an exception should return false.
123
+func TestPatternMatchesFolderWildcardExclusions(t *testing.T) {
124
+	match, _ := Matches("docs/README.md", []string{"docs/*", "!docs/README.md"})
125
+	if match != false {
126
+		t.Errorf("failed to get a false match on exclusion pattern, got %v", match)
127
+	}
128
+}
129
+
130
+// A pattern followed by an exclusion should return false.
131
+func TestExclusionPatternMatchesPatternAfter(t *testing.T) {
132
+	match, _ := Matches("fileutils.go", []string{"*.go", "!fileutils.go"})
133
+	if match != false {
134
+		t.Errorf("failed to get false match on exclusion pattern, got %v", match)
135
+	}
136
+}
137
+
138
+// A filename evaluating to . should return false.
139
+func TestExclusionPatternMatchesWholeDirectory(t *testing.T) {
140
+	match, _ := Matches(".", []string{"*.go"})
141
+	if match != false {
142
+		t.Errorf("failed to get false match on ., got %v", match)
143
+	}
144
+}
145
+
146
+// A single ! pattern should return an error.
147
+func TestSingleExclamationError(t *testing.T) {
148
+	_, err := Matches("fileutils.go", []string{"!"})
149
+	if err == nil {
150
+		t.Errorf("failed to get an error for a single exclamation point, got %v", err)
151
+	}
152
+}
153
+
154
+// A string preceded with a ! should return true from Exclusion.
155
+func TestExclusion(t *testing.T) {
156
+	exclusion := Exclusion("!")
157
+	if !exclusion {
158
+		t.Errorf("failed to get true for a single !, got %v", exclusion)
159
+	}
160
+}
161
+
162
+// An empty string should return true from Empty.
163
+func TestEmpty(t *testing.T) {
164
+	empty := Empty("")
165
+	if !empty {
166
+		t.Errorf("failed to get true for an empty string, got %v", empty)
167
+	}
168
+}
169
+
170
+func TestCleanPatterns(t *testing.T) {
171
+	cleaned, _, _, _ := CleanPatterns([]string{"docs", "config"})
172
+	if len(cleaned) != 2 {
173
+		t.Errorf("expected 2 element slice, got %v", len(cleaned))
174
+	}
175
+}
176
+
177
+func TestCleanPatternsStripEmptyPatterns(t *testing.T) {
178
+	cleaned, _, _, _ := CleanPatterns([]string{"docs", "config", ""})
179
+	if len(cleaned) != 2 {
180
+		t.Errorf("expected 2 element slice, got %v", len(cleaned))
181
+	}
182
+}
183
+
184
+func TestCleanPatternsExceptionFlag(t *testing.T) {
185
+	_, _, exceptions, _ := CleanPatterns([]string{"docs", "!docs/README.md"})
186
+	if !exceptions {
187
+		t.Errorf("expected exceptions to be true, got %v", exceptions)
188
+	}
189
+}
190
+
191
+func TestCleanPatternsLeadingSpaceTrimmed(t *testing.T) {
192
+	_, _, exceptions, _ := CleanPatterns([]string{"docs", "  !docs/README.md"})
193
+	if !exceptions {
194
+		t.Errorf("expected exceptions to be true, got %v", exceptions)
195
+	}
196
+}
197
+
198
+func TestCleanPatternsTrailingSpaceTrimmed(t *testing.T) {
199
+	_, _, exceptions, _ := CleanPatterns([]string{"docs", "!docs/README.md  "})
200
+	if !exceptions {
201
+		t.Errorf("expected exceptions to be true, got %v", exceptions)
202
+	}
203
+}
204
+
205
+func TestCleanPatternsErrorSingleException(t *testing.T) {
206
+	_, _, _, err := CleanPatterns([]string{"!"})
207
+	if err == nil {
208
+		t.Errorf("expected error on single exclamation point, got %v", err)
209
+	}
210
+}
211
+
212
+func TestCleanPatternsFolderSplit(t *testing.T) {
213
+	_, dirs, _, _ := CleanPatterns([]string{"docs/config/CONFIG.md"})
214
+	if dirs[0][0] != "docs" {
215
+		t.Errorf("expected first element in dirs slice to be docs, got %v", dirs[0][1])
216
+	}
217
+	if dirs[0][1] != "config" {
218
+		t.Errorf("expected first element in dirs slice to be config, got %v", dirs[0][1])
219
+	}
220
+}