Skip to content

Commit d49fa9e

Browse files
jongioCopilot
andcommitted
address review: cross-file negation test, stat fallback test, error wrap, docs
- Add TestNewMatcher_CrossFileNegationCannotOverride to codify that .gitignore negation cannot un-ignore .azdxignore matches (W1/New 2) - Add TestGetFileChanges_DeleteIgnoredDirFallback to test the os.Stat failure re-check with isDir=true for dir-only patterns (W4/New 3) - Wrap filepath.Abs() error with context in NewMatcher (New 4) - Add Long description to watch cobra command documenting .azdxignore usage with example (New 1) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 73e9e2c commit d49fa9e

4 files changed

Lines changed: 88 additions & 1 deletion

File tree

cli/azd/extensions/microsoft.azd.extensions/internal/cmd/watch.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ func newWatchCommand() *cobra.Command {
3030
watchCmd := &cobra.Command{
3131
Use: "watch",
3232
Short: "Watches the azd extension project for file changes and rebuilds it.",
33+
Long: `Watches the azd extension project for file changes and rebuilds it.
34+
35+
Place a .azdxignore file in your project root to exclude paths from triggering
36+
rebuilds. It uses standard gitignore syntax (https://git-scm.com/docs/gitignore).
37+
Patterns from .gitignore are also respected. Both files are additive — a path is
38+
ignored if it matches either file.
39+
40+
Example .azdxignore:
41+
dist/
42+
build/
43+
*.tmp
44+
coverage/
45+
!.vscode/launch.json`,
3346
RunE: func(cmd *cobra.Command, args []string) error {
3447
internal.WriteCommandHeader(
3548
"Watch and azd extension (azd x watch)",

cli/azd/pkg/ignore/ignore.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package ignore
66
import (
77
"bytes"
88
"errors"
9+
"fmt"
910
"io/fs"
1011
"os"
1112
"path/filepath"
@@ -38,7 +39,7 @@ type Matcher struct {
3839
func NewMatcher(root string) (*Matcher, error) {
3940
absRoot, err := filepath.Abs(root)
4041
if err != nil {
41-
return nil, err
42+
return nil, fmt.Errorf("resolving root path: %w", err)
4243
}
4344

4445
m := &Matcher{root: absRoot}

cli/azd/pkg/ignore/ignore_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,26 @@ func TestNewMatcher_BlankAndWhitespaceLines(t *testing.T) {
239239
require.True(t, m.IsIgnored("temp", true))
240240
require.False(t, m.IsIgnored("main.go", false))
241241
}
242+
243+
func TestNewMatcher_CrossFileNegationCannotOverride(t *testing.T) {
244+
dir := t.TempDir()
245+
246+
// .azdxignore ignores all .log files.
247+
writeFile(t, dir, AzdxIgnoreFile, "*.log\n")
248+
// .gitignore tries to un-ignore important.log via negation.
249+
writeFile(t, dir, GitIgnoreFile, "!important.log\n")
250+
251+
m, err := NewMatcher(dir)
252+
require.NoError(t, err)
253+
254+
// important.log is STILL ignored — .gitignore negation cannot override
255+
// .azdxignore matches because each file is parsed independently (union semantics).
256+
require.True(t, m.IsIgnored("important.log", false),
257+
".gitignore negation must not un-ignore paths matched by .azdxignore")
258+
259+
// Regular .log files are also ignored.
260+
require.True(t, m.IsIgnored("debug.log", false))
261+
262+
// Non-log files are unaffected.
263+
require.False(t, m.IsIgnored("main.go", false))
264+
}

cli/azd/pkg/watch/watch_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,53 @@ func TestGetFileChanges_RenameFile(t *testing.T) {
354354
return false
355355
}, 2*time.Second, 50*time.Millisecond, "expected new.txt after rename")
356356
}
357+
358+
func TestGetFileChanges_DeleteIgnoredDirFallback(t *testing.T) {
359+
// Tests the os.Stat failure fallback: when a directory matching a dir-only
360+
// ignore pattern (trailing /) is deleted, os.Stat fails and isDir defaults
361+
// to false. The watcher re-checks with isDir=true so that the Remove event
362+
// is still filtered by the directory-only pattern.
363+
dir := t.TempDir()
364+
t.Chdir(dir)
365+
366+
// .azdxignore uses a dir-only pattern (trailing slash).
367+
err := os.WriteFile(filepath.Join(dir, ".azdxignore"), []byte("tmpout/\n"), 0600)
368+
require.NoError(t, err)
369+
370+
// Pre-create the directory so watchRecursive skips it (ignored).
371+
err = os.MkdirAll(filepath.Join(dir, "tmpout"), 0700)
372+
require.NoError(t, err)
373+
374+
ctx, cancel := context.WithCancel(t.Context())
375+
defer cancel()
376+
377+
watcher, err := NewWatcher(ctx)
378+
require.NoError(t, err)
379+
380+
// Delete the ignored directory — the parent watcher fires a Remove event.
381+
// os.Stat will fail (path gone), so isDir defaults to false.
382+
// Without the fallback re-check (isDir=true), this would leak through
383+
// as a file deletion since "tmpout/" only matches directories.
384+
err = os.RemoveAll(filepath.Join(dir, "tmpout"))
385+
require.NoError(t, err)
386+
387+
// Write a tracked file as a positive signal.
388+
err = os.WriteFile(filepath.Join(dir, "tracked.go"), []byte("package main"), 0600)
389+
require.NoError(t, err)
390+
391+
// Verify tracked file appears and no tmpout path leaks through.
392+
require.Eventually(t, func() bool {
393+
changes := watcher.GetFileChanges()
394+
foundTracked := false
395+
for _, c := range changes {
396+
if filepath.Base(c.Path) == "tmpout" {
397+
return false // ignored dir leaked through — fail fast
398+
}
399+
if filepath.Base(c.Path) == "tracked.go" && c.ChangeType == FileCreated {
400+
foundTracked = true
401+
}
402+
}
403+
return foundTracked
404+
}, 2*time.Second, 50*time.Millisecond,
405+
"expected tracked.go created without tmpout directory delete leaking through")
406+
}

0 commit comments

Comments
 (0)