mirror of
https://github.com/go-task/task.git
synced 2026-06-11 09:51:50 +00:00
Compare commits
8 Commits
main
...
git-ignore
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d92de8e44 | ||
|
|
70fe29314f | ||
|
|
de99487b65 | ||
|
|
7705f922c1 | ||
|
|
7cea8e3364 | ||
|
|
13ef1b2dda | ||
|
|
ae3627c596 | ||
|
|
fe542d5418 |
3
go.mod
3
go.mod
@@ -92,7 +92,7 @@ require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
@@ -121,6 +121,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -181,8 +181,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -274,8 +274,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
|
||||
102
internal/fingerprint/gitignore.go
Normal file
102
internal/fingerprint/gitignore.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/gitignore"
|
||||
)
|
||||
|
||||
type gitignoreRule struct {
|
||||
dir string
|
||||
matcher gitignore.Matcher
|
||||
}
|
||||
|
||||
// loadGitignoreRules walks up from dir collecting .gitignore files.
|
||||
// Stops at the first .git (file or directory) found.
|
||||
// Returns nil if no .git is found (not in a git repo).
|
||||
func loadGitignoreRules(dir string) []gitignoreRule {
|
||||
dir, _ = filepath.Abs(dir)
|
||||
|
||||
var rules []gitignoreRule
|
||||
foundGit := false
|
||||
current := dir
|
||||
|
||||
for {
|
||||
lines := readGitignoreLines(filepath.Join(current, ".gitignore"))
|
||||
if len(lines) > 0 {
|
||||
patterns := make([]gitignore.Pattern, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
patterns = append(patterns, gitignore.ParsePattern(line, nil))
|
||||
}
|
||||
rules = append(rules, gitignoreRule{
|
||||
dir: current,
|
||||
matcher: gitignore.NewMatcher(patterns),
|
||||
})
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(current, ".git")); err == nil {
|
||||
foundGit = true
|
||||
break
|
||||
}
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
break
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
|
||||
if !foundGit {
|
||||
return nil
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func readGitignoreLines(path string) []string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimRight(scanner.Text(), "\r")
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// filterGitignored removes entries from the file map that match gitignore rules.
|
||||
func filterGitignored(files map[string]bool, dir string) map[string]bool {
|
||||
rules := loadGitignoreRules(dir)
|
||||
if len(rules) == 0 {
|
||||
return files
|
||||
}
|
||||
|
||||
for path := range files {
|
||||
for _, rule := range rules {
|
||||
relPath, err := filepath.Rel(rule.dir, path)
|
||||
if err != nil || strings.HasPrefix(relPath, "..") {
|
||||
continue
|
||||
}
|
||||
// Sources are files, not directories; pass isDir=false. Per the
|
||||
// gitignore spec this still matches files under an ignored dir
|
||||
// (e.g. "build/" matches build/out.txt).
|
||||
if rule.matcher.Match(strings.Split(filepath.ToSlash(relPath), "/"), false) {
|
||||
files[path] = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
112
internal/fingerprint/gitignore_test.go
Normal file
112
internal/fingerprint/gitignore_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func initGitRepo(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755))
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "included.txt"), []byte("included"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "ignored.log"), []byte("ignored"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "also-included.txt"), []byte("also included"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
filesWithout, err := Globs(dir, globs, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
filesWith, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
hasLog := false
|
||||
for _, f := range filesWithout {
|
||||
if filepath.Base(f) == "ignored.log" {
|
||||
hasLog = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, hasLog, "ignored.log should be present without gitignore filter")
|
||||
|
||||
hasLog = false
|
||||
for _, f := range filesWith {
|
||||
if filepath.Base(f) == "ignored.log" {
|
||||
hasLog = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.False(t, hasLog, "ignored.log should be excluded with gitignore filter")
|
||||
|
||||
txtCount := 0
|
||||
for _, f := range filesWith {
|
||||
if filepath.Ext(f) == ".txt" {
|
||||
txtCount++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 2, txtCount, "both .txt files should remain")
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreNested(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
initGitRepo(t, dir)
|
||||
|
||||
subDir := filepath.Join(dir, "sub")
|
||||
require.NoError(t, os.MkdirAll(subDir, 0o755))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, "keep.txt"), []byte("keep"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, "build.out"), []byte("build"), 0o644))
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.out\n"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
files, err := Globs(subDir, globs, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, f := range files {
|
||||
assert.NotEqual(t, "build.out", filepath.Base(f), "build.out should be excluded by nested .gitignore")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobsWithGitignoreNoRepo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Cannot use t.TempDir() here because it creates a dir inside the
|
||||
// go-task repo which has a .git parent, defeating the "no repo" test.
|
||||
dir, err := os.MkdirTemp("", "task-gitignore-norepo-*") //nolint:usetesting
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.RemoveAll(dir) })
|
||||
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "file.txt"), []byte("content"), 0o644))
|
||||
|
||||
globs := []*ast.Glob{
|
||||
{Glob: "./*"},
|
||||
}
|
||||
|
||||
files, err := Globs(dir, globs, true)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, files, 1)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func Globs(dir string, globs []*ast.Glob) ([]string, error) {
|
||||
func Globs(dir string, globs []*ast.Glob, useGitignore bool) ([]string, error) {
|
||||
resultMap := make(map[string]bool)
|
||||
for _, g := range globs {
|
||||
matches, err := glob(dir, g.Glob)
|
||||
@@ -21,6 +21,11 @@ func Globs(dir string, globs []*ast.Glob) ([]string, error) {
|
||||
resultMap[match] = !g.Negate
|
||||
}
|
||||
}
|
||||
|
||||
if useGitignore {
|
||||
resultMap = filterGitignored(resultMap, dir)
|
||||
}
|
||||
|
||||
return collectKeys(resultMap), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ func (*ChecksumChecker) Kind() string {
|
||||
}
|
||||
|
||||
func (c *ChecksumChecker) checksum(t *ast.Task) (string, error) {
|
||||
sources, err := Globs(t.Dir, t.Sources)
|
||||
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sources, err := Globs(t.Dir, t.Sources)
|
||||
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (checker *TimestampChecker) IsUpToDate(t *ast.Task) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
generates, err := Globs(t.Dir, t.Generates)
|
||||
generates, err := Globs(t.Dir, t.Generates, t.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func (checker *TimestampChecker) Kind() string {
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (checker *TimestampChecker) Value(t *ast.Task) (any, error) {
|
||||
sources, err := Globs(t.Dir, t.Sources)
|
||||
sources, err := Globs(t.Dir, t.Sources, t.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
201
internal/gitignore/LICENSE
Normal file
201
internal/gitignore/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Sourced Technologies, S.L.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
34
internal/gitignore/matcher.go
Normal file
34
internal/gitignore/matcher.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Vendored from go-git: github.com/go-git/go-git/v5 v5.19.1,
|
||||
// plumbing/format/gitignore/matcher.go. Licensed under the Apache License 2.0;
|
||||
// see the LICENSE file in this directory.
|
||||
|
||||
package gitignore
|
||||
|
||||
// Matcher defines a global multi-pattern matcher for gitignore patterns
|
||||
type Matcher interface {
|
||||
// Match matches patterns in the order of priorities. As soon as an inclusion or
|
||||
// exclusion is found, not further matching is performed.
|
||||
Match(path []string, isDir bool) bool
|
||||
}
|
||||
|
||||
// NewMatcher constructs a new global matcher. Patterns must be given in the order of
|
||||
// increasing priority. That is most generic settings files first, then the content of
|
||||
// the repo .gitignore, then content of .gitignore down the path or the repo and then
|
||||
// the content command line arguments.
|
||||
func NewMatcher(ps []Pattern) Matcher {
|
||||
return &matcher{ps}
|
||||
}
|
||||
|
||||
type matcher struct {
|
||||
patterns []Pattern
|
||||
}
|
||||
|
||||
func (m *matcher) Match(path []string, isDir bool) bool {
|
||||
n := len(m.patterns)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if match := m.patterns[i].Match(path, isDir); match > NoMatch {
|
||||
return match == Exclude
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
23
internal/gitignore/matcher_test.go
Normal file
23
internal/gitignore/matcher_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Test cases ported from go-git: github.com/go-git/go-git/v5 v5.19.1,
|
||||
// plumbing/format/gitignore/matcher_test.go. Licensed under the Apache
|
||||
// License 2.0; see LICENSE.
|
||||
|
||||
package gitignore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMatcher_Match(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := NewMatcher([]Pattern{
|
||||
ParsePattern("**/middle/v[uo]l?ano", nil),
|
||||
ParsePattern("!volcano", nil),
|
||||
})
|
||||
|
||||
if got := m.Match([]string{"head", "middle", "vulkano"}, false); got != true {
|
||||
t.Errorf("Match(vulkano) = %t, want true", got)
|
||||
}
|
||||
if got := m.Match([]string{"head", "middle", "volcano"}, false); got != false {
|
||||
t.Errorf("Match(volcano) = %t, want false (negated)", got)
|
||||
}
|
||||
}
|
||||
165
internal/gitignore/pattern.go
Normal file
165
internal/gitignore/pattern.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Package gitignore implements gitignore pattern matching.
|
||||
//
|
||||
// This package is vendored from go-git:
|
||||
//
|
||||
// github.com/go-git/go-git/v5 v5.19.1, plumbing/format/gitignore
|
||||
//
|
||||
// Only the pattern parsing and matching logic (pattern.go and matcher.go) is
|
||||
// copied; the file-walking helpers (dir.go) are omitted as they pull in
|
||||
// go-billy and other go-git internals. The original code is licensed under the
|
||||
// Apache License 2.0; see the LICENSE file in this directory.
|
||||
package gitignore
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MatchResult defines outcomes of a match, no match, exclusion or inclusion.
|
||||
type MatchResult int
|
||||
|
||||
const (
|
||||
// NoMatch defines the no match outcome of a match check
|
||||
NoMatch MatchResult = iota
|
||||
// Exclude defines an exclusion of a file as a result of a match check
|
||||
Exclude
|
||||
// Include defines an explicit inclusion of a file as a result of a match check
|
||||
Include
|
||||
)
|
||||
|
||||
const (
|
||||
inclusionPrefix = "!"
|
||||
zeroToManyDirs = "**"
|
||||
patternDirSep = "/"
|
||||
)
|
||||
|
||||
// Pattern defines a single gitignore pattern.
|
||||
type Pattern interface {
|
||||
// Match matches the given path to the pattern.
|
||||
Match(path []string, isDir bool) MatchResult
|
||||
}
|
||||
|
||||
type pattern struct {
|
||||
domain []string
|
||||
pattern []string
|
||||
inclusion bool
|
||||
dirOnly bool
|
||||
isGlob bool
|
||||
}
|
||||
|
||||
// ParsePattern parses a gitignore pattern string into the Pattern structure.
|
||||
func ParsePattern(p string, domain []string) Pattern {
|
||||
// storing domain, copy it to ensure it isn't changed externally
|
||||
domain = append([]string(nil), domain...)
|
||||
res := pattern{domain: domain}
|
||||
|
||||
if strings.HasPrefix(p, inclusionPrefix) {
|
||||
res.inclusion = true
|
||||
p = p[1:]
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(p, "\\ ") {
|
||||
p = strings.TrimRight(p, " ")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(p, patternDirSep) {
|
||||
res.dirOnly = true
|
||||
p = p[:len(p)-1]
|
||||
}
|
||||
|
||||
if strings.Contains(p, patternDirSep) {
|
||||
res.isGlob = true
|
||||
}
|
||||
|
||||
res.pattern = strings.Split(p, patternDirSep)
|
||||
return &res
|
||||
}
|
||||
|
||||
func (p *pattern) Match(path []string, isDir bool) MatchResult {
|
||||
if len(path) <= len(p.domain) {
|
||||
return NoMatch
|
||||
}
|
||||
for i, e := range p.domain {
|
||||
if path[i] != e {
|
||||
return NoMatch
|
||||
}
|
||||
}
|
||||
|
||||
path = path[len(p.domain):]
|
||||
if p.isGlob && !p.globMatch(path, isDir) {
|
||||
return NoMatch
|
||||
} else if !p.isGlob && !p.simpleNameMatch(path, isDir) {
|
||||
return NoMatch
|
||||
}
|
||||
|
||||
if p.inclusion {
|
||||
return Include
|
||||
} else {
|
||||
return Exclude
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pattern) simpleNameMatch(path []string, isDir bool) bool {
|
||||
for i, name := range path {
|
||||
if match, err := filepath.Match(p.pattern[0], name); err != nil {
|
||||
return false
|
||||
} else if !match {
|
||||
continue
|
||||
}
|
||||
if p.dirOnly && !isDir && i == len(path)-1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *pattern) globMatch(path []string, isDir bool) bool {
|
||||
matched := false
|
||||
canTraverse := false
|
||||
for i, pattern := range p.pattern {
|
||||
if pattern == "" {
|
||||
canTraverse = false
|
||||
continue
|
||||
}
|
||||
if pattern == zeroToManyDirs {
|
||||
if i == len(p.pattern)-1 {
|
||||
break
|
||||
}
|
||||
canTraverse = true
|
||||
continue
|
||||
}
|
||||
if strings.Contains(pattern, zeroToManyDirs) {
|
||||
return false
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return false
|
||||
}
|
||||
if canTraverse {
|
||||
canTraverse = false
|
||||
for len(path) > 0 {
|
||||
e := path[0]
|
||||
path = path[1:]
|
||||
if match, err := filepath.Match(pattern, e); err != nil {
|
||||
return false
|
||||
} else if match {
|
||||
matched = true
|
||||
break
|
||||
} else if len(path) == 0 {
|
||||
// if nothing left then fail
|
||||
matched = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if match, err := filepath.Match(pattern, path[0]); err != nil || !match {
|
||||
return false
|
||||
}
|
||||
matched = true
|
||||
path = path[1:]
|
||||
}
|
||||
}
|
||||
if matched && p.dirOnly && !isDir && len(path) == 0 {
|
||||
matched = false
|
||||
}
|
||||
return matched
|
||||
}
|
||||
80
internal/gitignore/pattern_test.go
Normal file
80
internal/gitignore/pattern_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Test cases ported from go-git: github.com/go-git/go-git/v5 v5.19.1,
|
||||
// plumbing/format/gitignore/pattern_test.go (originally written against
|
||||
// gopkg.in/check.v1; rewritten here as table-driven stdlib tests to avoid an
|
||||
// extra test dependency). Licensed under the Apache License 2.0; see LICENSE.
|
||||
|
||||
package gitignore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParsePattern_Match(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pattern string
|
||||
domain []string
|
||||
path []string
|
||||
isDir bool
|
||||
want MatchResult
|
||||
}{
|
||||
{"inclusion", "!vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Include},
|
||||
{"domainLonger_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle"}, false, NoMatch},
|
||||
{"domainSameLength_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle", "tail"}, false, NoMatch},
|
||||
{"domainMismatch_mismatch", "value", []string{"head", "middle", "tail"}, []string{"head", "middle", "_tail_", "value"}, false, NoMatch},
|
||||
{"withDomain", "middle/", []string{"value", "volcano"}, []string{"value", "volcano", "middle", "tail"}, false, Exclude},
|
||||
{"onlyMatchInDomain_mismatch", "volcano/", []string{"value", "volcano"}, []string{"value", "volcano", "tail"}, true, NoMatch},
|
||||
{"atStart", "value", nil, []string{"value", "tail"}, false, Exclude},
|
||||
{"inTheMiddle", "value", nil, []string{"head", "value", "tail"}, false, Exclude},
|
||||
{"atEnd", "value", nil, []string{"head", "value"}, false, Exclude},
|
||||
{"atStart_dirWanted", "value/", nil, []string{"value", "tail"}, false, Exclude},
|
||||
{"inTheMiddle_dirWanted", "value/", nil, []string{"head", "value", "tail"}, false, Exclude},
|
||||
{"atEnd_dirWanted", "value/", nil, []string{"head", "value"}, true, Exclude},
|
||||
{"atEnd_dirWanted_notADir_mismatch", "value/", nil, []string{"head", "value"}, false, NoMatch},
|
||||
{"mismatch", "value", nil, []string{"head", "val", "tail"}, false, NoMatch},
|
||||
{"valueLonger_mismatch", "val", nil, []string{"head", "value", "tail"}, false, NoMatch},
|
||||
{"withAsterisk", "v*o", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
|
||||
{"withQuestionMark", "vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
|
||||
{"magicChars", "v[ou]l[kc]ano", nil, []string{"value", "volcano"}, false, Exclude},
|
||||
{"wrongPattern_mismatch", "v[ou]l[", nil, []string{"value", "vol["}, false, NoMatch},
|
||||
{"glob_fromRootWithSlash", "/value/vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
|
||||
{"glob_withDomain", "middle/tail/", []string{"value", "volcano"}, []string{"value", "volcano", "middle", "tail"}, true, Exclude},
|
||||
{"glob_onlyMatchInDomain_mismatch", "volcano/tail", []string{"value", "volcano"}, []string{"value", "volcano", "tail"}, false, NoMatch},
|
||||
{"glob_fromRootWithoutSlash", "value/vul?ano", nil, []string{"value", "vulkano", "tail"}, false, Exclude},
|
||||
{"glob_fromRoot_mismatch", "value/vulkano", nil, []string{"value", "volcano"}, false, NoMatch},
|
||||
{"glob_fromRoot_tooShort_mismatch", "value/vul?ano", nil, []string{"value"}, false, NoMatch},
|
||||
{"glob_fromRoot_notAtRoot_mismatch", "/value/volcano", nil, []string{"value", "value", "volcano"}, false, NoMatch},
|
||||
{"glob_leadingAsterisks_atStart", "**/*lue/vol?ano", nil, []string{"value", "volcano", "tail"}, false, Exclude},
|
||||
{"glob_leadingAsterisks_notAtStart", "**/*lue/vol?ano", nil, []string{"head", "value", "volcano", "tail"}, false, Exclude},
|
||||
{"glob_leadingAsterisks_mismatch", "**/*lue/vol?ano", nil, []string{"head", "value", "Volcano", "tail"}, false, NoMatch},
|
||||
{"glob_leadingAsterisks_isDir", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano", "tail"}, false, Exclude},
|
||||
{"glob_leadingAsterisks_isDirAtEnd", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano"}, true, Exclude},
|
||||
{"glob_leadingAsterisks_isDir_mismatch", "**/*lue/vol?ano/", nil, []string{"head", "value", "Colcano"}, true, NoMatch},
|
||||
{"glob_leadingAsterisks_isDirNoDirAtEnd_mismatch", "**/*lue/vol?ano/", nil, []string{"head", "value", "volcano"}, false, NoMatch},
|
||||
{"glob_tailingAsterisks", "/*lue/vol?ano/**", nil, []string{"value", "volcano", "tail", "moretail"}, false, Exclude},
|
||||
{"glob_tailingAsterisks_exactMatch", "/*lue/vol?ano/**", nil, []string{"value", "volcano"}, false, Exclude},
|
||||
{"glob_middleAsterisks_emptyMatch", "/*lue/**/vol?ano", nil, []string{"value", "volcano"}, false, Exclude},
|
||||
{"glob_middleAsterisks_oneMatch", "/*lue/**/vol?ano", nil, []string{"value", "middle", "volcano"}, false, Exclude},
|
||||
{"glob_middleAsterisks_multiMatch", "/*lue/**/vol?ano", nil, []string{"value", "middle1", "middle2", "volcano"}, false, Exclude},
|
||||
{"glob_middleAsterisks_isDir_trailing", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano"}, true, Exclude},
|
||||
{"glob_middleAsterisks_isDir_trailing_mismatch", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano"}, false, NoMatch},
|
||||
{"glob_middleAsterisks_isDir", "/*lue/**/vol?ano/", nil, []string{"value", "middle1", "middle2", "volcano", "tail"}, false, Exclude},
|
||||
{"glob_wrongDoubleAsterisk_mismatch", "/*lue/**foo/vol?ano", nil, []string{"value", "foo", "volcano", "tail"}, false, NoMatch},
|
||||
{"glob_magicChars", "**/head/v[ou]l[kc]ano", nil, []string{"value", "head", "volcano"}, false, Exclude},
|
||||
{"glob_wrongPattern_noTraversal_mismatch", "**/head/v[ou]l[", nil, []string{"value", "head", "vol["}, false, NoMatch},
|
||||
{"glob_wrongPattern_onTraversal_mismatch", "/value/**/v[ou]l[", nil, []string{"value", "head", "vol["}, false, NoMatch},
|
||||
{"glob_issue_923", "**/android/**/GeneratedPluginRegistrant.java", nil, []string{"packages", "flutter_tools", "lib", "src", "android", "gradle.dart"}, false, NoMatch},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := ParsePattern(tt.pattern, tt.domain)
|
||||
if got := p.Match(tt.path, tt.isDir); got != tt.want {
|
||||
t.Errorf("ParsePattern(%q, %v).Match(%v, %t) = %v, want %v",
|
||||
tt.pattern, tt.domain, tt.path, tt.isDir, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
49
task_test.go
49
task_test.go
@@ -653,6 +653,55 @@ func TestStatusChecksumMissingGenerated(t *testing.T) { // nolint:paralleltest /
|
||||
require.NoError(t, err, "generated.txt should be recreated after third run")
|
||||
}
|
||||
|
||||
func TestGitignoreChecksum(t *testing.T) { //nolint:paralleltest // cannot run in parallel
|
||||
const dir = "testdata/gitignore"
|
||||
|
||||
// Clean up
|
||||
_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
|
||||
_ = os.Remove(filepathext.SmartJoin(dir, "generated.txt"))
|
||||
|
||||
var buff bytes.Buffer
|
||||
tempDir := task.TempDir{
|
||||
Remote: filepathext.SmartJoin(dir, ".task"),
|
||||
Fingerprint: filepathext.SmartJoin(dir, ".task"),
|
||||
}
|
||||
e := task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
task.WithStdout(&buff),
|
||||
task.WithStderr(&buff),
|
||||
task.WithTempDir(tempDir),
|
||||
)
|
||||
require.NoError(t, e.Setup())
|
||||
|
||||
// First run - should execute
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
|
||||
// Second run - should be up to date
|
||||
buff.Reset()
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
assert.Equal(t, "task: Task \"build\" is up to date\n", buff.String())
|
||||
|
||||
// Modify the ignored file - should still be up to date
|
||||
require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, "ignored.txt"), []byte("modified\n"), 0o644))
|
||||
buff.Reset()
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
assert.Equal(t, "task: Task \"build\" is up to date\n", buff.String())
|
||||
|
||||
// Modify the source file - should re-execute
|
||||
require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, "source.txt"), []byte("modified source\n"), 0o644))
|
||||
buff.Reset()
|
||||
require.NoError(t, e.Run(t.Context(), &task.Call{Task: "build"}))
|
||||
assert.NotEqual(t, "task: Task \"build\" is up to date\n", buff.String())
|
||||
|
||||
// Restore source file
|
||||
require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, "source.txt"), []byte("source content\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepathext.SmartJoin(dir, "ignored.txt"), []byte("ignored content\n"), 0o644))
|
||||
|
||||
// Clean up
|
||||
_ = os.RemoveAll(filepathext.SmartJoin(dir, ".task"))
|
||||
_ = os.Remove(filepathext.SmartJoin(dir, "generated.txt"))
|
||||
}
|
||||
|
||||
func TestStatusVariables(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ type Task struct {
|
||||
Method string
|
||||
Prefix string `hash:"ignore"`
|
||||
IgnoreError bool
|
||||
UseGitignore *bool
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
If string
|
||||
@@ -75,6 +76,12 @@ func (t *Task) IsSilent() bool {
|
||||
return t.Silent != nil && *t.Silent
|
||||
}
|
||||
|
||||
// ShouldUseGitignore returns true if gitignore filtering is enabled for the task.
|
||||
// Returns false if UseGitignore is nil or set to false.
|
||||
func (t *Task) ShouldUseGitignore() bool {
|
||||
return t.UseGitignore != nil && *t.UseGitignore
|
||||
}
|
||||
|
||||
// WildcardMatch will check if the given string matches the name of the Task and returns any wildcard values.
|
||||
func (t *Task) WildcardMatch(name string) (bool, []string) {
|
||||
names := append([]string{t.Task}, t.Aliases...)
|
||||
@@ -149,7 +156,8 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
Internal bool
|
||||
Method string
|
||||
Prefix string
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
UseGitignore *bool `yaml:"use_gitignore,omitempty"`
|
||||
Run string
|
||||
Platforms []*Platform
|
||||
If string
|
||||
@@ -190,6 +198,7 @@ func (t *Task) UnmarshalYAML(node *yaml.Node) error {
|
||||
t.Method = task.Method
|
||||
t.Prefix = task.Prefix
|
||||
t.IgnoreError = task.IgnoreError
|
||||
t.UseGitignore = deepcopy.Scalar(task.UseGitignore)
|
||||
t.Run = task.Run
|
||||
t.Platforms = task.Platforms
|
||||
t.If = task.If
|
||||
@@ -233,6 +242,7 @@ func (t *Task) DeepCopy() *Task {
|
||||
Method: t.Method,
|
||||
Prefix: t.Prefix,
|
||||
IgnoreError: t.IgnoreError,
|
||||
UseGitignore: deepcopy.Scalar(t.UseGitignore),
|
||||
Run: t.Run,
|
||||
IncludeVars: t.IncludeVars.DeepCopy(),
|
||||
IncludedTaskfileVars: t.IncludedTaskfileVars.DeepCopy(),
|
||||
|
||||
@@ -20,20 +20,21 @@ var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles c
|
||||
|
||||
// Taskfile is the abstract syntax tree for a Taskfile
|
||||
type Taskfile struct {
|
||||
Location string
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
Location string
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
UseGitignore bool `yaml:"use_gitignore"`
|
||||
}
|
||||
|
||||
// Merge merges the second Taskfile into the first
|
||||
@@ -76,19 +77,20 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
var taskfile struct {
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
Version *semver.Version
|
||||
Output Output
|
||||
Method string
|
||||
Includes *Includes
|
||||
Set []string
|
||||
Shopt []string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks *Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
Run string
|
||||
Interval time.Duration
|
||||
UseGitignore bool `yaml:"use_gitignore"`
|
||||
}
|
||||
if err := node.Decode(&taskfile); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
@@ -106,6 +108,7 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
|
||||
tf.Dotenv = taskfile.Dotenv
|
||||
tf.Run = taskfile.Run
|
||||
tf.Interval = taskfile.Interval
|
||||
tf.UseGitignore = taskfile.UseGitignore
|
||||
if tf.Includes == nil {
|
||||
tf.Includes = NewIncludes()
|
||||
}
|
||||
|
||||
1
testdata/gitignore/.gitignore
vendored
Normal file
1
testdata/gitignore/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ignored.txt
|
||||
25
testdata/gitignore/Taskfile.yml
vendored
Normal file
25
testdata/gitignore/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
version: '3'
|
||||
|
||||
use_gitignore: true
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- cp ./source.txt ./generated.txt
|
||||
sources:
|
||||
- ./*.txt
|
||||
- exclude: ./generated.txt
|
||||
generates:
|
||||
- ./generated.txt
|
||||
method: checksum
|
||||
|
||||
build-no-use_gitignore:
|
||||
use_gitignore: false
|
||||
cmds:
|
||||
- cp ./source.txt ./generated.txt
|
||||
sources:
|
||||
- ./*.txt
|
||||
- exclude: ./generated.txt
|
||||
generates:
|
||||
- ./generated.txt
|
||||
method: checksum
|
||||
1
testdata/gitignore/source.txt
vendored
Normal file
1
testdata/gitignore/source.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
source content
|
||||
16
variables.go
16
variables.go
@@ -59,6 +59,7 @@ func (e *Executor) CompiledTaskForTaskList(call *Call) (*ast.Task, error) {
|
||||
Env: nil,
|
||||
Dotenv: origTask.Dotenv,
|
||||
Silent: deepcopy.Scalar(origTask.Silent),
|
||||
UseGitignore: deepcopy.Scalar(origTask.UseGitignore),
|
||||
Interactive: origTask.Interactive,
|
||||
Internal: origTask.Internal,
|
||||
Method: origTask.Method,
|
||||
@@ -110,6 +111,11 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
||||
}
|
||||
}
|
||||
|
||||
gitignore := origTask.ShouldUseGitignore()
|
||||
if origTask.UseGitignore == nil {
|
||||
gitignore = e.Taskfile.UseGitignore
|
||||
}
|
||||
|
||||
new := ast.Task{
|
||||
Task: origTask.Task,
|
||||
Label: templater.Replace(origTask.Label, cache),
|
||||
@@ -126,6 +132,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
||||
Env: nil,
|
||||
Dotenv: templater.Replace(origTask.Dotenv, cache),
|
||||
Silent: deepcopy.Scalar(origTask.Silent),
|
||||
UseGitignore: &gitignore,
|
||||
Interactive: origTask.Interactive,
|
||||
Internal: origTask.Internal,
|
||||
Method: templater.Replace(origTask.Method, cache),
|
||||
@@ -219,7 +226,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
||||
continue
|
||||
}
|
||||
if cmd.For != nil {
|
||||
list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache)
|
||||
list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -268,7 +275,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
||||
continue
|
||||
}
|
||||
if dep.For != nil {
|
||||
list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, vars, origTask.Location, cache)
|
||||
list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, new.Generates, gitignore, vars, origTask.Location, cache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -339,6 +346,7 @@ func itemsFromFor(
|
||||
dir string,
|
||||
sources []*ast.Glob,
|
||||
generates []*ast.Glob,
|
||||
gitignore bool,
|
||||
vars *ast.Vars,
|
||||
location *ast.Location,
|
||||
cache *templater.Cache,
|
||||
@@ -361,7 +369,7 @@ func itemsFromFor(
|
||||
}
|
||||
// Get the list from the task sources
|
||||
if f.From == "sources" {
|
||||
glist, err := fingerprint.Globs(dir, sources)
|
||||
glist, err := fingerprint.Globs(dir, sources, gitignore)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -375,7 +383,7 @@ func itemsFromFor(
|
||||
}
|
||||
// Get the list from the task generates
|
||||
if f.From == "generates" {
|
||||
glist, err := fingerprint.Globs(dir, generates)
|
||||
glist, err := fingerprint.Globs(dir, generates, gitignore)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
2
watch.go
2
watch.go
@@ -205,7 +205,7 @@ func (e *Executor) collectSources(calls []*Call) ([]string, error) {
|
||||
var sources []string
|
||||
|
||||
err := e.traverse(calls, func(task *ast.Task) error {
|
||||
files, err := fingerprint.Globs(task.Dir, task.Sources)
|
||||
files, err := fingerprint.Globs(task.Dir, task.Sources, task.ShouldUseGitignore())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -177,6 +177,11 @@
|
||||
"enum": ["none", "checksum", "timestamp"],
|
||||
"default": "none"
|
||||
},
|
||||
"use_gitignore": {
|
||||
"description": "When set to true, files matching .gitignore rules will be excluded from sources and generates glob resolution. Overrides the global gitignore setting.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"prefix": {
|
||||
"description": "Defines a string to prefix the output of tasks running in parallel. Only used when the output mode is `prefixed`.",
|
||||
"type": "string"
|
||||
@@ -687,6 +692,11 @@
|
||||
"enum": ["none", "checksum", "timestamp"],
|
||||
"default": "checksum"
|
||||
},
|
||||
"use_gitignore": {
|
||||
"description": "When set to true, files matching .gitignore rules will be excluded from sources and generates glob resolution for all tasks. Can be overridden per task.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"includes": {
|
||||
"description": "Imports tasks from the specified taskfiles. The tasks described in the given Taskfiles will be available with the informed namespace.",
|
||||
"type": "object",
|
||||
|
||||
Reference in New Issue
Block a user