Skip to content

Commit 45a16de

Browse files
fix(agent/agentcontainers): respect ignore files
1 parent f41275e commit 45a16de

File tree

6 files changed

+184
-1
lines changed

6 files changed

+184
-1
lines changed

agent/agentcontainers/api.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import (
2121

2222
"github.com/fsnotify/fsnotify"
2323
"github.com/go-chi/chi/v5"
24+
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
2425
"github.com/google/uuid"
2526
"github.com/spf13/afero"
2627
"golang.org/x/xerrors"
2728

2829
"cdr.dev/slog"
30+
"github.com/coder/coder/v2/agent/agentcontainers/ignore"
2931
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
3032
"github.com/coder/coder/v2/agent/agentexec"
3133
"github.com/coder/coder/v2/agent/usershell"
@@ -469,13 +471,33 @@ func (api *API) discoverDevcontainerProjects() error {
469471
}
470472

471473
func (api *API) discoverDevcontainersInProject(projectPath string) error {
474+
patterns, err := ignore.ReadPatterns(api.fs, projectPath)
475+
if err != nil {
476+
return fmt.Errorf("read git ignore patterns: %w", err)
477+
}
478+
479+
matcher := gitignore.NewMatcher(patterns)
480+
472481
devcontainerConfigPaths := []string{
473482
"/.devcontainer/devcontainer.json",
474483
"/.devcontainer.json",
475484
}
476485

477486
return afero.Walk(api.fs, projectPath, func(path string, info fs.FileInfo, _ error) error {
487+
pathParts := ignore.FilePathToParts(path)
488+
489+
// We know that a directory entry cannot be a `devcontainer.json` file, so we
490+
// always skip processing directories. If the directory happens to be ignored
491+
// by git then we'll make sure to ignore all of the children of that directory.
478492
if info.IsDir() {
493+
if matcher.Match(pathParts, true) {
494+
return fs.SkipDir
495+
}
496+
497+
return nil
498+
}
499+
500+
if matcher.Match(pathParts, false) {
479501
return nil
480502
}
481503

agent/agentcontainers/api_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3345,6 +3345,46 @@ func TestDevcontainerDiscovery(t *testing.T) {
33453345
},
33463346
},
33473347
},
3348+
{
3349+
name: "RespectGitIgnore",
3350+
agentDir: "/home/coder",
3351+
fs: map[string]string{
3352+
"/home/coder/coder/.git/HEAD": "",
3353+
"/home/coder/coder/.gitignore": "y/",
3354+
"/home/coder/coder/.devcontainer.json": "",
3355+
"/home/coder/coder/x/y/.devcontainer.json": "",
3356+
},
3357+
expected: []codersdk.WorkspaceAgentDevcontainer{
3358+
{
3359+
WorkspaceFolder: "/home/coder/coder",
3360+
ConfigPath: "/home/coder/coder/.devcontainer.json",
3361+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3362+
},
3363+
},
3364+
},
3365+
{
3366+
name: "RespectNestedGitIgnore",
3367+
agentDir: "/home/coder",
3368+
fs: map[string]string{
3369+
"/home/coder/coder/.git/HEAD": "",
3370+
"/home/coder/coder/.devcontainer.json": "",
3371+
"/home/coder/coder/y/.devcontainer.json": "",
3372+
"/home/coder/coder/x/.gitignore": "y/",
3373+
"/home/coder/coder/x/y/.devcontainer.json": "",
3374+
},
3375+
expected: []codersdk.WorkspaceAgentDevcontainer{
3376+
{
3377+
WorkspaceFolder: "/home/coder/coder",
3378+
ConfigPath: "/home/coder/coder/.devcontainer.json",
3379+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3380+
},
3381+
{
3382+
WorkspaceFolder: "/home/coder/coder/y",
3383+
ConfigPath: "/home/coder/coder/y/.devcontainer.json",
3384+
Status: codersdk.WorkspaceAgentDevcontainerStatusStopped,
3385+
},
3386+
},
3387+
},
33483388
}
33493389

33503390
initFS := func(t *testing.T, files map[string]string) afero.Fs {

agent/agentcontainers/ignore/dir.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package ignore
2+
3+
import (
4+
"errors"
5+
"io/fs"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
11+
"github.com/spf13/afero"
12+
)
13+
14+
func FilePathToParts(path string) []string {
15+
components := []string{}
16+
17+
if path == "" {
18+
return []string{}
19+
}
20+
21+
for segment := range strings.SplitSeq(filepath.Clean(path), "/") {
22+
if segment != "" {
23+
components = append(components, segment)
24+
}
25+
}
26+
27+
return components
28+
}
29+
30+
func readIgnoreFile(fs afero.Fs, path string) ([]gitignore.Pattern, error) {
31+
var ps []gitignore.Pattern
32+
33+
data, err := afero.ReadFile(fs, filepath.Join(path, ".gitignore"))
34+
if err != nil && !errors.Is(err, os.ErrNotExist) {
35+
return nil, err
36+
}
37+
38+
for s := range strings.SplitSeq(string(data), "\n") {
39+
if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 {
40+
ps = append(ps, gitignore.ParsePattern(s, FilePathToParts(path)))
41+
}
42+
}
43+
44+
return ps, nil
45+
}
46+
47+
func ReadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) {
48+
ps, _ := readIgnoreFile(fileSystem, path)
49+
50+
if err := afero.Walk(fileSystem, path, func(path string, info fs.FileInfo, err error) error {
51+
if !info.IsDir() {
52+
return nil
53+
}
54+
55+
subPs, err := readIgnoreFile(fileSystem, path)
56+
if err != nil {
57+
return err
58+
}
59+
60+
ps = append(ps, subPs...)
61+
62+
return nil
63+
}); err != nil {
64+
return nil, err
65+
}
66+
67+
return ps, nil
68+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package ignore_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/coder/coder/v2/agent/agentcontainers/ignore"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestFilePathToParts(t *testing.T) {
12+
t.Parallel()
13+
14+
tests := []struct {
15+
path string
16+
expected []string
17+
}{
18+
{"/foo/bar/baz", []string{"foo", "bar", "baz"}},
19+
{"foo/bar/baz", []string{"foo", "bar", "baz"}},
20+
{"/foo", []string{"foo"}},
21+
{"foo", []string{"foo"}},
22+
{"/", []string{}},
23+
{"", []string{}},
24+
{"./foo/bar", []string{"foo", "bar"}},
25+
{"../foo/bar", []string{"..", "foo", "bar"}},
26+
{"/foo//bar///baz", []string{"foo", "bar", "baz"}},
27+
{"foo/bar/", []string{"foo", "bar"}},
28+
{"/foo/bar/", []string{"foo", "bar"}},
29+
{"foo/../bar", []string{"bar"}},
30+
{"./foo/../bar", []string{"bar"}},
31+
{"/foo/../bar", []string{"bar"}},
32+
{"foo/./bar", []string{"foo", "bar"}},
33+
{"/foo/./bar", []string{"foo", "bar"}},
34+
{"a/b/c/d/e", []string{"a", "b", "c", "d", "e"}},
35+
{"/a/b/c/d/e", []string{"a", "b", "c", "d", "e"}},
36+
}
37+
38+
for _, tt := range tests {
39+
t.Run(fmt.Sprintf("`%s`", tt.path), func(t *testing.T) {
40+
t.Parallel()
41+
42+
parts := ignore.FilePathToParts(tt.path)
43+
require.Equal(t, tt.expected, parts)
44+
})
45+
}
46+
}

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ require (
122122
github.com/fergusstrange/embedded-postgres v1.31.0
123123
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
124124
github.com/gen2brain/beeep v0.11.1
125-
github.com/gliderlabs/ssh v0.3.4
125+
github.com/gliderlabs/ssh v0.3.8
126126
github.com/go-chi/chi/v5 v5.2.2
127127
github.com/go-chi/cors v1.2.1
128128
github.com/go-chi/httprate v0.15.0
@@ -512,10 +512,14 @@ require (
512512
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
513513
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
514514
github.com/esiqveland/notify v0.13.3 // indirect
515+
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
516+
github.com/go-git/go-billy/v5 v5.6.2 // indirect
517+
github.com/go-git/go-git/v5 v5.16.2 // indirect
515518
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
516519
github.com/hashicorp/go-getter v1.7.8 // indirect
517520
github.com/hashicorp/go-safetemp v1.0.0 // indirect
518521
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
522+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
519523
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
520524
github.com/moby/sys/user v0.4.0 // indirect
521525
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
@@ -535,5 +539,6 @@ require (
535539
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
536540
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
537541
google.golang.org/genai v1.12.0 // indirect
542+
gopkg.in/warnings.v0 v0.1.2 // indirect
538543
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
539544
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
11021102
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
11031103
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
11041104
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
1105+
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
1106+
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
11051107
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
11061108
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
11071109
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=

0 commit comments

Comments
 (0)