Skip to content

Commit 8f7bb34

Browse files
fix(agent/agentcontainers): respect ignore files
1 parent 28789d7 commit 8f7bb34

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy