From 8f7bb3497d61cb519880f4b6c882a454eb531f28 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 23 Jul 2025 17:33:05 +0000 Subject: [PATCH 01/13] fix(agent/agentcontainers): respect ignore files --- agent/agentcontainers/api.go | 22 ++++++++ agent/agentcontainers/api_test.go | 40 ++++++++++++++ agent/agentcontainers/ignore/dir.go | 68 ++++++++++++++++++++++++ agent/agentcontainers/ignore/dir_test.go | 46 ++++++++++++++++ go.mod | 7 ++- go.sum | 2 + 6 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 agent/agentcontainers/ignore/dir.go create mode 100644 agent/agentcontainers/ignore/dir_test.go diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 10020e4ec5c30..5297e7ffbf10a 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -21,11 +21,13 @@ import ( "github.com/fsnotify/fsnotify" "github.com/go-chi/chi/v5" + "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/google/uuid" "github.com/spf13/afero" "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/coder/v2/agent/agentcontainers/ignore" "github.com/coder/coder/v2/agent/agentcontainers/watcher" "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/agent/usershell" @@ -469,13 +471,33 @@ func (api *API) discoverDevcontainerProjects() error { } func (api *API) discoverDevcontainersInProject(projectPath string) error { + patterns, err := ignore.ReadPatterns(api.fs, projectPath) + if err != nil { + return fmt.Errorf("read git ignore patterns: %w", err) + } + + matcher := gitignore.NewMatcher(patterns) + devcontainerConfigPaths := []string{ "/.devcontainer/devcontainer.json", "/.devcontainer.json", } return afero.Walk(api.fs, projectPath, func(path string, info fs.FileInfo, _ error) error { + pathParts := ignore.FilePathToParts(path) + + // We know that a directory entry cannot be a `devcontainer.json` file, so we + // always skip processing directories. If the directory happens to be ignored + // by git then we'll make sure to ignore all of the children of that directory. if info.IsDir() { + if matcher.Match(pathParts, true) { + return fs.SkipDir + } + + return nil + } + + if matcher.Match(pathParts, false) { return nil } diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 7387d9a17aba9..362a12d5bcf22 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -3345,6 +3345,46 @@ func TestDevcontainerDiscovery(t *testing.T) { }, }, }, + { + name: "RespectGitIgnore", + agentDir: "/home/coder", + fs: map[string]string{ + "/home/coder/coder/.git/HEAD": "", + "/home/coder/coder/.gitignore": "y/", + "/home/coder/coder/.devcontainer.json": "", + "/home/coder/coder/x/y/.devcontainer.json": "", + }, + expected: []codersdk.WorkspaceAgentDevcontainer{ + { + WorkspaceFolder: "/home/coder/coder", + ConfigPath: "/home/coder/coder/.devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, + }, + }, + }, + { + name: "RespectNestedGitIgnore", + agentDir: "/home/coder", + fs: map[string]string{ + "/home/coder/coder/.git/HEAD": "", + "/home/coder/coder/.devcontainer.json": "", + "/home/coder/coder/y/.devcontainer.json": "", + "/home/coder/coder/x/.gitignore": "y/", + "/home/coder/coder/x/y/.devcontainer.json": "", + }, + expected: []codersdk.WorkspaceAgentDevcontainer{ + { + WorkspaceFolder: "/home/coder/coder", + ConfigPath: "/home/coder/coder/.devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, + }, + { + WorkspaceFolder: "/home/coder/coder/y", + ConfigPath: "/home/coder/coder/y/.devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, + }, + }, + }, } initFS := func(t *testing.T, files map[string]string) afero.Fs { diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go new file mode 100644 index 0000000000000..e9e32f5f9a933 --- /dev/null +++ b/agent/agentcontainers/ignore/dir.go @@ -0,0 +1,68 @@ +package ignore + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5/plumbing/format/gitignore" + "github.com/spf13/afero" +) + +func FilePathToParts(path string) []string { + components := []string{} + + if path == "" { + return []string{} + } + + for segment := range strings.SplitSeq(filepath.Clean(path), "/") { + if segment != "" { + components = append(components, segment) + } + } + + return components +} + +func readIgnoreFile(fs afero.Fs, path string) ([]gitignore.Pattern, error) { + var ps []gitignore.Pattern + + data, err := afero.ReadFile(fs, filepath.Join(path, ".gitignore")) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, err + } + + for s := range strings.SplitSeq(string(data), "\n") { + if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 { + ps = append(ps, gitignore.ParsePattern(s, FilePathToParts(path))) + } + } + + return ps, nil +} + +func ReadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { + ps, _ := readIgnoreFile(fileSystem, path) + + if err := afero.Walk(fileSystem, path, func(path string, info fs.FileInfo, err error) error { + if !info.IsDir() { + return nil + } + + subPs, err := readIgnoreFile(fileSystem, path) + if err != nil { + return err + } + + ps = append(ps, subPs...) + + return nil + }); err != nil { + return nil, err + } + + return ps, nil +} diff --git a/agent/agentcontainers/ignore/dir_test.go b/agent/agentcontainers/ignore/dir_test.go new file mode 100644 index 0000000000000..dad0bce25a657 --- /dev/null +++ b/agent/agentcontainers/ignore/dir_test.go @@ -0,0 +1,46 @@ +package ignore_test + +import ( + "fmt" + "testing" + + "github.com/coder/coder/v2/agent/agentcontainers/ignore" + "github.com/stretchr/testify/require" +) + +func TestFilePathToParts(t *testing.T) { + t.Parallel() + + tests := []struct { + path string + expected []string + }{ + {"/foo/bar/baz", []string{"foo", "bar", "baz"}}, + {"foo/bar/baz", []string{"foo", "bar", "baz"}}, + {"/foo", []string{"foo"}}, + {"foo", []string{"foo"}}, + {"/", []string{}}, + {"", []string{}}, + {"./foo/bar", []string{"foo", "bar"}}, + {"../foo/bar", []string{"..", "foo", "bar"}}, + {"/foo//bar///baz", []string{"foo", "bar", "baz"}}, + {"foo/bar/", []string{"foo", "bar"}}, + {"/foo/bar/", []string{"foo", "bar"}}, + {"foo/../bar", []string{"bar"}}, + {"./foo/../bar", []string{"bar"}}, + {"/foo/../bar", []string{"bar"}}, + {"foo/./bar", []string{"foo", "bar"}}, + {"/foo/./bar", []string{"foo", "bar"}}, + {"a/b/c/d/e", []string{"a", "b", "c", "d", "e"}}, + {"/a/b/c/d/e", []string{"a", "b", "c", "d", "e"}}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("`%s`", tt.path), func(t *testing.T) { + t.Parallel() + + parts := ignore.FilePathToParts(tt.path) + require.Equal(t, tt.expected, parts) + }) + } +} diff --git a/go.mod b/go.mod index bf367187d488c..bb6d9f259d68c 100644 --- a/go.mod +++ b/go.mod @@ -122,7 +122,7 @@ require ( github.com/fergusstrange/embedded-postgres v1.31.0 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa github.com/gen2brain/beeep v0.11.1 - github.com/gliderlabs/ssh v0.3.4 + github.com/gliderlabs/ssh v0.3.8 github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.15.0 @@ -512,10 +512,14 @@ require ( github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/esiqveland/notify v0.13.3 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.16.2 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/go-getter v1.7.8 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/jackmordaunt/icns/v3 v3.0.1 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect @@ -535,5 +539,6 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect google.golang.org/genai v1.12.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect ) diff --git a/go.sum b/go.sum index ff5c603c3db18..cc7e255612582 100644 --- a/go.sum +++ b/go.sum @@ -1102,6 +1102,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= +github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= From 5cb9e5c6741d48a80562734f7c14793873645aee Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 23 Jul 2025 17:37:34 +0000 Subject: [PATCH 02/13] chore: run `go mod tidy` --- go.mod | 2 +- go.sum | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bb6d9f259d68c..b6090694010d5 100644 --- a/go.mod +++ b/go.mod @@ -484,6 +484,7 @@ require ( github.com/coder/aisdk-go v0.0.9 github.com/coder/preview v1.0.3-0.20250714153828-a737d4750448 github.com/fsnotify/fsnotify v1.9.0 + github.com/go-git/go-git/v5 v5.16.2 github.com/mark3labs/mcp-go v0.34.0 ) @@ -514,7 +515,6 @@ require ( github.com/esiqveland/notify v0.13.3 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-git/go-git/v5 v5.16.2 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/go-getter v1.7.8 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect diff --git a/go.sum b/go.sum index cc7e255612582..867d5478530f1 100644 --- a/go.sum +++ b/go.sum @@ -1100,8 +1100,6 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= -github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ= -github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= From e39edf29f15824f3c366b2073dd697284c9de58d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 23 Jul 2025 17:40:39 +0000 Subject: [PATCH 03/13] chore: appease linter --- agent/agentcontainers/api.go | 2 +- agent/agentcontainers/ignore/dir.go | 6 +++--- agent/agentcontainers/ignore/dir_test.go | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 5297e7ffbf10a..bace76181b7b0 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -473,7 +473,7 @@ func (api *API) discoverDevcontainerProjects() error { func (api *API) discoverDevcontainersInProject(projectPath string) error { patterns, err := ignore.ReadPatterns(api.fs, projectPath) if err != nil { - return fmt.Errorf("read git ignore patterns: %w", err) + return xerrors.Errorf("read git ignore patterns: %w", err) } matcher := gitignore.NewMatcher(patterns) diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go index e9e32f5f9a933..d5e3800846396 100644 --- a/agent/agentcontainers/ignore/dir.go +++ b/agent/agentcontainers/ignore/dir.go @@ -27,10 +27,10 @@ func FilePathToParts(path string) []string { return components } -func readIgnoreFile(fs afero.Fs, path string) ([]gitignore.Pattern, error) { +func readIgnoreFile(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { var ps []gitignore.Pattern - data, err := afero.ReadFile(fs, filepath.Join(path, ".gitignore")) + data, err := afero.ReadFile(fileSystem, filepath.Join(path, ".gitignore")) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } @@ -47,7 +47,7 @@ func readIgnoreFile(fs afero.Fs, path string) ([]gitignore.Pattern, error) { func ReadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { ps, _ := readIgnoreFile(fileSystem, path) - if err := afero.Walk(fileSystem, path, func(path string, info fs.FileInfo, err error) error { + if err := afero.Walk(fileSystem, path, func(path string, info fs.FileInfo, _ error) error { if !info.IsDir() { return nil } diff --git a/agent/agentcontainers/ignore/dir_test.go b/agent/agentcontainers/ignore/dir_test.go index dad0bce25a657..560c5ca7b10c7 100644 --- a/agent/agentcontainers/ignore/dir_test.go +++ b/agent/agentcontainers/ignore/dir_test.go @@ -4,8 +4,9 @@ import ( "fmt" "testing" - "github.com/coder/coder/v2/agent/agentcontainers/ignore" "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/agent/agentcontainers/ignore" ) func TestFilePathToParts(t *testing.T) { From a56c82752722a9375100874f5403813a445d29c2 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 23 Jul 2025 17:53:16 +0000 Subject: [PATCH 04/13] fix: test on windows --- agent/agentcontainers/ignore/dir.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go index d5e3800846396..3244e3152795f 100644 --- a/agent/agentcontainers/ignore/dir.go +++ b/agent/agentcontainers/ignore/dir.go @@ -18,7 +18,7 @@ func FilePathToParts(path string) []string { return []string{} } - for segment := range strings.SplitSeq(filepath.Clean(path), "/") { + for segment := range strings.SplitSeq(filepath.Clean(path), string(filepath.Separator)) { if segment != "" { components = append(components, segment) } From f5d16eae5dd6367ca4f0d3ba7914cfd581fe2c21 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 23 Jul 2025 18:45:04 +0000 Subject: [PATCH 05/13] chore: remove initial call the initial call duplicates work done in Walk --- agent/agentcontainers/ignore/dir.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go index 3244e3152795f..e6a6c12ed5614 100644 --- a/agent/agentcontainers/ignore/dir.go +++ b/agent/agentcontainers/ignore/dir.go @@ -45,7 +45,7 @@ func readIgnoreFile(fileSystem afero.Fs, path string) ([]gitignore.Pattern, erro } func ReadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { - ps, _ := readIgnoreFile(fileSystem, path) + var ps []gitignore.Pattern if err := afero.Walk(fileSystem, path, func(path string, info fs.FileInfo, _ error) error { if !info.IsDir() { From e134b47f7929cf5df59035d8425a87e8c36fff32 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 24 Jul 2025 08:36:30 +0000 Subject: [PATCH 06/13] chore: `[]string{}` -> `components` --- agent/agentcontainers/ignore/dir.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go index e6a6c12ed5614..e3c2c2ce94208 100644 --- a/agent/agentcontainers/ignore/dir.go +++ b/agent/agentcontainers/ignore/dir.go @@ -15,7 +15,7 @@ func FilePathToParts(path string) []string { components := []string{} if path == "" { - return []string{} + return components } for segment := range strings.SplitSeq(filepath.Clean(path), string(filepath.Separator)) { From 8e6c3f3e431b6aca95e0d51e57d737f22976959d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 24 Jul 2025 08:36:49 +0000 Subject: [PATCH 07/13] chore: remove duplicated test cases for `TestFilePathToParts` --- agent/agentcontainers/ignore/dir_test.go | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/agent/agentcontainers/ignore/dir_test.go b/agent/agentcontainers/ignore/dir_test.go index 560c5ca7b10c7..2af54cf63930d 100644 --- a/agent/agentcontainers/ignore/dir_test.go +++ b/agent/agentcontainers/ignore/dir_test.go @@ -16,24 +16,15 @@ func TestFilePathToParts(t *testing.T) { path string expected []string }{ - {"/foo/bar/baz", []string{"foo", "bar", "baz"}}, - {"foo/bar/baz", []string{"foo", "bar", "baz"}}, - {"/foo", []string{"foo"}}, - {"foo", []string{"foo"}}, - {"/", []string{}}, {"", []string{}}, + {"/", []string{}}, + {"foo", []string{"foo"}}, + {"/foo", []string{"foo"}}, {"./foo/bar", []string{"foo", "bar"}}, {"../foo/bar", []string{"..", "foo", "bar"}}, - {"/foo//bar///baz", []string{"foo", "bar", "baz"}}, - {"foo/bar/", []string{"foo", "bar"}}, - {"/foo/bar/", []string{"foo", "bar"}}, + {"foo/bar/baz", []string{"foo", "bar", "baz"}}, + {"/foo/bar/baz", []string{"foo", "bar", "baz"}}, {"foo/../bar", []string{"bar"}}, - {"./foo/../bar", []string{"bar"}}, - {"/foo/../bar", []string{"bar"}}, - {"foo/./bar", []string{"foo", "bar"}}, - {"/foo/./bar", []string{"foo", "bar"}}, - {"a/b/c/d/e", []string{"a", "b", "c", "d", "e"}}, - {"/a/b/c/d/e", []string{"a", "b", "c", "d", "e"}}, } for _, tt := range tests { From d787bf6e1d404251afde8c423d30fa7269bbb94d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 24 Jul 2025 08:46:20 +0000 Subject: [PATCH 08/13] fix: respect `.git/info/exclude` file --- agent/agentcontainers/api_test.go | 17 +++++++++++++++++ agent/agentcontainers/ignore/dir.go | 18 +++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 362a12d5bcf22..65d405eabc6a2 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -3385,6 +3385,23 @@ func TestDevcontainerDiscovery(t *testing.T) { }, }, }, + { + name: "RespectGitInfoExclude", + agentDir: "/home/coder", + fs: map[string]string{ + "/home/coder/coder/.git/HEAD": "", + "/home/coder/coder/.git/info/exclude": "y/", + "/home/coder/coder/.devcontainer.json": "", + "/home/coder/coder/x/y/.devcontainer.json": "", + }, + expected: []codersdk.WorkspaceAgentDevcontainer{ + { + WorkspaceFolder: "/home/coder/coder", + ConfigPath: "/home/coder/coder/.devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, + }, + }, + }, } initFS := func(t *testing.T, files map[string]string) afero.Fs { diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go index e3c2c2ce94208..093d7fa5cd893 100644 --- a/agent/agentcontainers/ignore/dir.go +++ b/agent/agentcontainers/ignore/dir.go @@ -11,6 +11,11 @@ import ( "github.com/spf13/afero" ) +const ( + gitignoreFile = ".gitignore" + gitInfoExcludeFile = ".git/info/exclude" +) + func FilePathToParts(path string) []string { components := []string{} @@ -27,10 +32,10 @@ func FilePathToParts(path string) []string { return components } -func readIgnoreFile(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { +func readIgnoreFile(fileSystem afero.Fs, path, ignore string) ([]gitignore.Pattern, error) { var ps []gitignore.Pattern - data, err := afero.ReadFile(fileSystem, filepath.Join(path, ".gitignore")) + data, err := afero.ReadFile(fileSystem, filepath.Join(path, ignore)) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } @@ -47,12 +52,19 @@ func readIgnoreFile(fileSystem afero.Fs, path string) ([]gitignore.Pattern, erro func ReadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { var ps []gitignore.Pattern + subPs, err := readIgnoreFile(fileSystem, path, gitInfoExcludeFile) + if err != nil { + return nil, err + } + + ps = append(ps, subPs...) + if err := afero.Walk(fileSystem, path, func(path string, info fs.FileInfo, _ error) error { if !info.IsDir() { return nil } - subPs, err := readIgnoreFile(fileSystem, path) + subPs, err := readIgnoreFile(fileSystem, path, gitignoreFile) if err != nil { return err } From 0a438a87ce7192b68bbd1773090a67a5b42a3ba1 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 24 Jul 2025 09:08:16 +0000 Subject: [PATCH 09/13] fix: respect `~/.gitconfig`'s `core.excludesFile` option --- agent/agentcontainers/api.go | 4 +++- agent/agentcontainers/api_test.go | 28 +++++++++++++++++++++++- agent/agentcontainers/ignore/dir.go | 34 +++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index bace76181b7b0..1d216ed1ce4e1 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -471,12 +471,14 @@ func (api *API) discoverDevcontainerProjects() error { } func (api *API) discoverDevcontainersInProject(projectPath string) error { + globalPatterns, err := ignore.LoadGlobalPatterns(api.fs) + patterns, err := ignore.ReadPatterns(api.fs, projectPath) if err != nil { return xerrors.Errorf("read git ignore patterns: %w", err) } - matcher := gitignore.NewMatcher(patterns) + matcher := gitignore.NewMatcher(append(globalPatterns, patterns...)) devcontainerConfigPaths := []string{ "/.devcontainer/devcontainer.json", diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 65d405eabc6a2..3f852411e4457 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -9,6 +9,7 @@ import ( "net/http/httptest" "os" "os/exec" + "path/filepath" "runtime" "slices" "strings" @@ -3211,6 +3212,9 @@ func TestDevcontainerDiscovery(t *testing.T) { // repositories to find any `.devcontainer/devcontainer.json` // files. These tests are to validate that behavior. + homeDir, err := os.UserHomeDir() + require.NoError(t, err) + tests := []struct { name string agentDir string @@ -3402,6 +3406,28 @@ func TestDevcontainerDiscovery(t *testing.T) { }, }, }, + { + name: "RespectHomeGitConfig", + agentDir: homeDir, + fs: map[string]string{ + "/tmp/.gitignore": "node_modules/", + filepath.Join(homeDir, ".gitconfig"): ` + [core] + excludesFile = /tmp/.gitignore + `, + + filepath.Join(homeDir, ".git/HEAD"): "", + filepath.Join(homeDir, ".devcontainer.json"): "", + filepath.Join(homeDir, "node_modules/y/.devcontainer.json"): "", + }, + expected: []codersdk.WorkspaceAgentDevcontainer{ + { + WorkspaceFolder: homeDir, + ConfigPath: filepath.Join(homeDir, ".devcontainer.json"), + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, + }, + }, + }, } initFS := func(t *testing.T, files map[string]string) afero.Fs { @@ -3454,7 +3480,7 @@ func TestDevcontainerDiscovery(t *testing.T) { err := json.NewDecoder(rec.Body).Decode(&got) require.NoError(t, err) - return len(got.Devcontainers) == len(tt.expected) + return len(got.Devcontainers) >= len(tt.expected) }, testutil.WaitShort, testutil.IntervalFast, "dev containers never found") // Now projects have been discovered, we'll allow the updater loop diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go index 093d7fa5cd893..298a2b8cf6ca9 100644 --- a/agent/agentcontainers/ignore/dir.go +++ b/agent/agentcontainers/ignore/dir.go @@ -1,17 +1,21 @@ package ignore import ( + "bytes" "errors" "io/fs" "os" "path/filepath" "strings" + "github.com/go-git/go-git/v5/plumbing/format/config" "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/spf13/afero" + "golang.org/x/xerrors" ) const ( + gitconfigFile = ".gitconfig" gitignoreFile = ".gitignore" gitInfoExcludeFile = ".git/info/exclude" ) @@ -78,3 +82,33 @@ func ReadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) return ps, nil } + +func loadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { + data, err := afero.ReadFile(fileSystem, path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, err + } + + decoder := config.NewDecoder(bytes.NewBuffer(data)) + + conf := config.New() + if err := decoder.Decode(conf); err != nil { + return nil, xerrors.Errorf("decode config: %w", err) + } + + excludes := conf.Section("core").Options.Get("excludesfile") + if excludes == "" { + return nil, nil + } + + return readIgnoreFile(fileSystem, "", excludes) +} + +func LoadGlobalPatterns(fileSystem afero.Fs) ([]gitignore.Pattern, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + return loadPatterns(fileSystem, filepath.Join(home, gitconfigFile)) +} From e344d814fd78b2f1d5afa0ae2c9707696af58cb9 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 24 Jul 2025 09:11:46 +0000 Subject: [PATCH 10/13] chore: oops, forgot my error handling --- agent/agentcontainers/api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 1d216ed1ce4e1..617c3d8a68f72 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -472,6 +472,9 @@ func (api *API) discoverDevcontainerProjects() error { func (api *API) discoverDevcontainersInProject(projectPath string) error { globalPatterns, err := ignore.LoadGlobalPatterns(api.fs) + if err != nil { + return xerrors.Errorf("read global git ignore patterns: %w", err) + } patterns, err := ignore.ReadPatterns(api.fs, projectPath) if err != nil { From 27a735e75b44f1e755991215004d6b6cb8ecac55 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 24 Jul 2025 09:36:18 +0000 Subject: [PATCH 11/13] test: ensure we ignore nonsense dev container names --- agent/agentcontainers/api_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 3f852411e4457..5714027960a7b 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -3428,6 +3428,34 @@ func TestDevcontainerDiscovery(t *testing.T) { }, }, }, + { + name: "IgnoreNonsenseDevcontainerNames", + agentDir: "/home/coder", + fs: map[string]string{ + "/home/coder/.git/HEAD": "", + + "/home/coder/.devcontainer/devcontainer.json.bak": "", + "/home/coder/.devcontainer/devcontainer.json.old": "", + "/home/coder/.devcontainer/devcontainer.json~": "", + "/home/coder/.devcontainer/notdevcontainer.json": "", + "/home/coder/.devcontainer/devcontainer.json.swp": "", + + "/home/coder/foo/.devcontainer.json.bak": "", + "/home/coder/foo/.devcontainer.json.old": "", + "/home/coder/foo/.devcontainer.json~": "", + "/home/coder/foo/.notdevcontainer.json": "", + "/home/coder/foo/.devcontainer.json.swp": "", + + "/home/coder/bar/.devcontainer.json": "", + }, + expected: []codersdk.WorkspaceAgentDevcontainer{ + { + WorkspaceFolder: "/home/coder/bar", + ConfigPath: "/home/coder/bar/.devcontainer.json", + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, + }, + }, + }, } initFS := func(t *testing.T, files map[string]string) afero.Fs { From 7d8a7966153a3d5432e2050f1897b759092c5f83 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 24 Jul 2025 09:42:12 +0000 Subject: [PATCH 12/13] chore: add some error logging --- agent/agentcontainers/api.go | 19 +++++++++++++++---- agent/agentcontainers/ignore/dir.go | 13 +++++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 617c3d8a68f72..4f9287713fcfc 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -471,12 +471,16 @@ func (api *API) discoverDevcontainerProjects() error { } func (api *API) discoverDevcontainersInProject(projectPath string) error { + logger := api.logger. + Named("project-discovery"). + With(slog.F("project_path", projectPath)) + globalPatterns, err := ignore.LoadGlobalPatterns(api.fs) if err != nil { return xerrors.Errorf("read global git ignore patterns: %w", err) } - patterns, err := ignore.ReadPatterns(api.fs, projectPath) + patterns, err := ignore.ReadPatterns(api.ctx, logger, api.fs, projectPath) if err != nil { return xerrors.Errorf("read git ignore patterns: %w", err) } @@ -488,7 +492,14 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error { "/.devcontainer.json", } - return afero.Walk(api.fs, projectPath, func(path string, info fs.FileInfo, _ error) error { + return afero.Walk(api.fs, projectPath, func(path string, info fs.FileInfo, err error) error { + if err != nil { + logger.Error(api.ctx, "encountered error while walking for dev container projects", + slog.F("path", path), + slog.Error(err)) + return nil + } + pathParts := ignore.FilePathToParts(path) // We know that a directory entry cannot be a `devcontainer.json` file, so we @@ -513,11 +524,11 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error { workspaceFolder := strings.TrimSuffix(path, relativeConfigPath) - api.logger.Debug(api.ctx, "discovered dev container project", slog.F("workspace_folder", workspaceFolder)) + logger.Debug(api.ctx, "discovered dev container project", slog.F("workspace_folder", workspaceFolder)) api.mu.Lock() if _, found := api.knownDevcontainers[workspaceFolder]; !found { - api.logger.Debug(api.ctx, "adding dev container project", slog.F("workspace_folder", workspaceFolder)) + logger.Debug(api.ctx, "adding dev container project", slog.F("workspace_folder", workspaceFolder)) dc := codersdk.WorkspaceAgentDevcontainer{ ID: uuid.New(), diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go index 298a2b8cf6ca9..1bbbb34fe702c 100644 --- a/agent/agentcontainers/ignore/dir.go +++ b/agent/agentcontainers/ignore/dir.go @@ -2,12 +2,14 @@ package ignore import ( "bytes" + "context" "errors" "io/fs" "os" "path/filepath" "strings" + "cdr.dev/slog" "github.com/go-git/go-git/v5/plumbing/format/config" "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/spf13/afero" @@ -53,7 +55,7 @@ func readIgnoreFile(fileSystem afero.Fs, path, ignore string) ([]gitignore.Patte return ps, nil } -func ReadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { +func ReadPatterns(ctx context.Context, logger slog.Logger, fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) { var ps []gitignore.Pattern subPs, err := readIgnoreFile(fileSystem, path, gitInfoExcludeFile) @@ -63,7 +65,14 @@ func ReadPatterns(fileSystem afero.Fs, path string) ([]gitignore.Pattern, error) ps = append(ps, subPs...) - if err := afero.Walk(fileSystem, path, func(path string, info fs.FileInfo, _ error) error { + if err := afero.Walk(fileSystem, path, func(path string, info fs.FileInfo, err error) error { + if err != nil { + logger.Error(ctx, "encountered error while walking for git ignore files", + slog.F("path", path), + slog.Error(err)) + return nil + } + if !info.IsDir() { return nil } From cd3be6cfb152b8d4a991d21886b3bed465ee29a7 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 24 Jul 2025 09:50:55 +0000 Subject: [PATCH 13/13] chore: appease the linter --- agent/agentcontainers/ignore/dir.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/agentcontainers/ignore/dir.go b/agent/agentcontainers/ignore/dir.go index 1bbbb34fe702c..d97e2ef2235a3 100644 --- a/agent/agentcontainers/ignore/dir.go +++ b/agent/agentcontainers/ignore/dir.go @@ -9,11 +9,12 @@ import ( "path/filepath" "strings" - "cdr.dev/slog" "github.com/go-git/go-git/v5/plumbing/format/config" "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/spf13/afero" "golang.org/x/xerrors" + + "cdr.dev/slog" ) const ( 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