Signed-off-by: Dave Goodchild <buddhamagnet@gmail.com>
buddhamagnet authored on 2015/04/10 04:07:06... | ... |
@@ -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 |
+} |