Content-Length: 34054 | pFad | http://github.com/coder/coder/pull/19016.patch
thub.com
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//github.com/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//github.com/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 (
--- a PPN by Garber Painting Akron. With Image Size Reduction included!Fetched URL: http://github.com/coder/coder/pull/19016.patch
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy