diff options
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | main.go | 28 | ||||
| -rw-r--r-- | main_test.go | 22 |
3 files changed, 52 insertions, 1 deletions
@@ -98,7 +98,8 @@ All standard `html/template` actions and functions remain available, including ## Git ignore management `-gitignore` atomically replaces a marked block in the site-root `.gitignore` -with exact, root-anchored paths for the current generated outputs. Content +with exact, root-anchored paths for the current generated outputs. Outputs below +symlinked directories are omitted because Git does not traverse them. Content outside the block is preserved, and stale output entries are removed. Without the flag, Weft does not touch `.gitignore`. @@ -858,12 +858,40 @@ func (s *site) gitignore(outputs []string) ([]byte, error) { if strings.ContainsAny(output, "\r\n") { return nil, fmt.Errorf("cannot add output to .gitignore: %q", output) } + symlinked, err := s.hasSymlinkedParent(output) + if err != nil { + return nil, err + } + if symlinked { + continue + } kept.WriteString("/" + escapeGitignore(output) + "\n") } kept.WriteString(gitignoreEnd + "\n") return []byte(kept.String()), nil } +func (s *site) hasSymlinkedParent(output string) (bool, error) { + current := s.root + for _, part := range strings.Split(path.Dir(output), "/") { + if part == "." { + continue + } + current = filepath.Join(current, part) + info, err := os.Lstat(current) + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + if err != nil { + return false, err + } + if info.Mode()&os.ModeSymlink != 0 { + return true, nil + } + } + return false, nil +} + func escapeGitignore(value string) string { return strings.NewReplacer( `\`, `\\`, " ", `\ `, "#", `\#`, "!", `\!`, diff --git a/main_test.go b/main_test.go index 66d793f..f6c943e 100644 --- a/main_test.go +++ b/main_test.go @@ -354,6 +354,28 @@ func TestGitignoreTracksOnlyCurrentGeneratedOutputs(t *testing.T) { } } +func TestGitignoreOmitsOutputsBelowSymlinks(t *testing.T) { + root := t.TempDir() + vault := t.TempDir() + writeTestFile(t, root, ".gitignore", gitignoreStart+"\n/notes/note.html\n"+gitignoreEnd+"\n") + writeTestFile(t, root, "layouts.tmpl", testLayouts()) + writeTestFile(t, root, "local.md", "# Local\n") + writeTestFile(t, vault, "note.md", "# Note\n") + if err := os.Symlink(vault, filepath.Join(root, "notes")); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + if err := buildWithFallback(root, "https://example.com", "page", true); err != nil { + t.Fatal(err) + } + want := gitignoreStart + "\n/local.html\n" + gitignoreEnd + "\n" + if raw, _ := os.ReadFile(filepath.Join(root, ".gitignore")); string(raw) != want { + t.Fatalf(".gitignore = %q, want %q", raw, want) + } + if raw, _ := os.ReadFile(filepath.Join(root, manifestName)); !strings.Contains(string(raw), `"notes/note.html"`) { + t.Fatalf("manifest does not own symlinked output: %s", raw) + } +} + func TestSortingFilteringSlicing(t *testing.T) { a := &Page{Title: "A", Meta: map[string]any{"post_date": "2024-01-01", "kind": "x"}, ModTime: time.Unix(1, 0)} b := &Page{Title: "B", Meta: map[string]any{"post_date": "2025-01-01", "kind": "y"}, ModTime: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)} |
