Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix(agent/agentcontainers): respect ignore files#19016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
DanielleMaywood merged 13 commits intomainfromdanielle/respect-ignore-files
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
13 commits
Select commitHold shift + click to select a range
8f7bb34
fix(agent/agentcontainers): respect ignore files
DanielleMaywoodJul 23, 2025
5cb9e5c
chore: run `go mod tidy`
DanielleMaywoodJul 23, 2025
e39edf2
chore: appease linter
DanielleMaywoodJul 23, 2025
a56c827
fix: test on windows
DanielleMaywoodJul 23, 2025
f5d16ea
chore: remove initial call
DanielleMaywoodJul 23, 2025
e134b47
chore: `[]string{}` -> `components`
DanielleMaywoodJul 24, 2025
8e6c3f3
chore: remove duplicated test cases for `TestFilePathToParts`
DanielleMaywoodJul 24, 2025
d787bf6
fix: respect `.git/info/exclude` file
DanielleMaywoodJul 24, 2025
0a438a8
fix: respect `~/.gitconfig`'s `core.excludesFile` option
DanielleMaywoodJul 24, 2025
e344d81
chore: oops, forgot my error handling
DanielleMaywoodJul 24, 2025
27a735e
test: ensure we ignore nonsense dev container names
DanielleMaywoodJul 24, 2025
7d8a796
chore: add some error logging
DanielleMaywoodJul 24, 2025
cd3be6c
chore: appease the linter
DanielleMaywoodJul 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletionsagent/agentcontainers/api.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
Expand DownExpand Up@@ -469,13 +471,49 @@ 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.ctx, logger, api.fs, projectPath)
if err != nil {
return xerrors.Errorf("read git ignore patterns: %w", err)
}

matcher := gitignore.NewMatcher(append(globalPatterns, patterns...))

devcontainerConfigPaths := []string{
"/.devcontainer/devcontainer.json",
"/.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
// 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
}

Expand All@@ -486,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(),
Expand Down
113 changes: 112 additions & 1 deletionagent/agentcontainers/api_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ import (
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -3345,6 +3349,113 @@ 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,
},
},
},
{
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,
},
},
},
{
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,
},
},
},
{
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 {
Expand DownExpand Up@@ -3397,7 +3508,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
Expand Down
124 changes: 124 additions & 0 deletionsagent/agentcontainers/ignore/dir.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
package ignore

import (
"bytes"
"context"
"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"

"cdr.dev/slog"
)

const (
gitconfigFile = ".gitconfig"
gitignoreFile = ".gitignore"
gitInfoExcludeFile = ".git/info/exclude"
)

func FilePathToParts(path string) []string {
components := []string{}

if path == "" {
return components
}

for segment := range strings.SplitSeq(filepath.Clean(path), string(filepath.Separator)) {
if segment != "" {
components = append(components, segment)
}
}

return components
}

func readIgnoreFile(fileSystem afero.Fs, path, ignore string) ([]gitignore.Pattern, error) {
var ps []gitignore.Pattern

data, err := afero.ReadFile(fileSystem, filepath.Join(path, ignore))
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(ctx context.Context, logger slog.Logger, 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, 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
}

subPs, err := readIgnoreFile(fileSystem, path, gitignoreFile)
if err != nil {
return err
}

ps = append(ps, subPs...)

return nil
}); err != nil {
return nil, err
}

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))
}
38 changes: 38 additions & 0 deletionsagent/agentcontainers/ignore/dir_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
package ignore_test

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/agent/agentcontainers/ignore"
)

func TestFilePathToParts(t *testing.T) {
t.Parallel()

tests := []struct {
path string
expected []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/baz", []string{"foo", "bar", "baz"}},
{"foo/../bar", []string{"bar"}},
}

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)
})
}
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp