- Notifications
You must be signed in to change notification settings - Fork1k
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
Changes fromall commits
8f7bb34
5cb9e5c
e39edf2
a56c827
f5d16ea
e134b47
8e6c3f3
d787bf6
0a438a8
e344d81
27a735e
7d8a796
cd3be6c
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
@@ -3345,6 +3349,113 @@ func TestDevcontainerDiscovery(t *testing.T) { | ||
}, | ||
}, | ||
}, | ||
{ | ||
name: "RespectGitIgnore", | ||
DanielleMaywood marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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", | ||
DanielleMaywood marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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 { | ||
@@ -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) | ||
}, testutil.WaitShort, testutil.IntervalFast, "dev containers never found") | ||
// Now projects have been discovered, we'll allow the updater loop | ||
Original file line number | Diff line number | Diff 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)) { | ||
DanielleMaywood marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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") { | ||
DanielleMaywood marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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)) | ||
} |
Original file line number | Diff line number | Diff 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) | ||
}) | ||
} | ||
} |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.