Skip to content

Commit 0208f71

Browse files
feat: .leafwikiignore (#1263)
IgnoreFile struct wrapping github.com/sabhiram/go-gitignore with LoadFromDir(), Matches(path, isDir), and PatternCount() methods.
1 parent 326fe31 commit 0208f71

40 files changed

Lines changed: 1349 additions & 138 deletions

.golangci-lint

Lines changed: 0 additions & 14 deletions
This file was deleted.

.golangci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version: "2"
2+
linters:
3+
enable:
4+
- govet
5+
- unused
6+
- gocritic
7+
- staticcheck
8+
exclusions:
9+
generated: lax
10+
paths:
11+
- third_party$
12+
- builtin$
13+
- examples$
14+
formatters:
15+
enable:
16+
- gofmt
17+
- goimports
18+
exclusions:
19+
generated: lax
20+
paths:
21+
- third_party$
22+
- builtin$
23+
- examples$

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,63 @@ For most setups, prefer `--public-access` for read-only public access and the vi
545545

546546
---
547547

548+
## .leafwikiignore — Ignore Files
549+
550+
LeafWiki indexes every `.md` file it finds on disk. If you have files or directories you want to keep on disk but exclude from the wiki (draft pages, archive sections, private notes, imported markdown being organized), create a `.leafwikiignore` file at the root of your wiki's data directory.
551+
552+
**Location:** Anywhere under `root/`. A `.leafwikiignore` at the wiki root applies to the entire wiki; per-directory files apply to their directory and its children.
553+
554+
**Syntax:** Standard gitignore-style patterns:
555+
556+
| Pattern | Meaning |
557+
|---------|---------|
558+
| `#` | Comment |
559+
| `*` | Matches anything except `/` |
560+
| `?` | Matches any single char except `/` |
561+
| `**` | Matches zero or more directories |
562+
| Trailing `/` | Directory-only match |
563+
| Leading `/` | Anchored to wiki root |
564+
| `!` prefix | Negation (un-ignore) |
565+
566+
**Examples:**
567+
568+
```gitignore
569+
# Exclude all log files
570+
*.log
571+
572+
# Exclude entire directory
573+
drafts/
574+
575+
# Exclude everything except important.md
576+
*.md
577+
!important.md
578+
```
579+
580+
**Notes:**
581+
- Changes to any `.leafwikiignore` require a restart to take effect.
582+
- Ignored files are hidden completely — remove the ignore pattern to see them again.
583+
- Per-directory ignore files are supported: any directory under `root/` may contain its own `.leafwikiignore`. Patterns accumulate from root to leaf, and child patterns can negate parent patterns with `!`.
584+
585+
**Multi-level example:**
586+
587+
```gitignore
588+
# root/.leafwikiignore — applies everywhere
589+
*.md
590+
591+
# root/docs/.leafwikiignore — un-ignore specific files under docs/
592+
!important.md
593+
594+
# root/docs/archive/.leafwikiignore — re-ignore files under archive/
595+
*.md
596+
```
597+
598+
In this example:
599+
- All `.md` files are ignored by default (root rule).
600+
- `docs/important.md` is un-ignored (child negation).
601+
- `docs/archive/` is re-ignored (grandchild re-applies the restriction).
602+
603+
---
604+
548605
## Sorting Pages
549606

550607
Page order in LeafWiki is **explicit and manual** — it does not follow filename or alphabetical order automatically. By default, pages appear in the order they were created.

cmd/leafwiki/main.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/dustin/go-humanize"
2424
"github.com/gin-gonic/gin"
2525
"github.com/perber/wiki/internal/backup"
26+
"github.com/perber/wiki/internal/core/ignore"
2627
"github.com/perber/wiki/internal/core/tools"
2728
httpinternal "github.com/perber/wiki/internal/http"
2829
httpmetrics "github.com/perber/wiki/internal/http/metrics"
@@ -149,11 +150,12 @@ func printUsage() {
149150

150151
func setupLogger() {
151152
level := slog.LevelInfo
152-
if os.Getenv("LEAFWIKI_LOG_LEVEL") == "debug" {
153+
switch os.Getenv("LEAFWIKI_LOG_LEVEL") {
154+
case "debug":
153155
level = slog.LevelDebug
154-
} else if (os.Getenv("LEAFWIKI_LOG_LEVEL")) == "error" {
156+
case "error":
155157
level = slog.LevelError
156-
} else if (os.Getenv("LEAFWIKI_LOG_LEVEL")) == "warn" {
158+
case "warn":
157159
level = slog.LevelWarn
158160
}
159161

@@ -447,6 +449,15 @@ func main() {
447449
if err != nil {
448450
fail("Failed to initialize Wiki", "error", err)
449451
}
452+
453+
// Log .leafwikiignore status
454+
rootDir := filepath.Join(dataDir, "root")
455+
if ignoreFile, err := ignore.LoadFromDir(rootDir); err != nil {
456+
slog.Default().Warn("invalid .leafwikiignore", "error", err)
457+
} else if ignoreFile != nil {
458+
slog.Default().Info("loaded .leafwikiignore", "patterns", ignoreFile.PatternCount())
459+
}
460+
450461
defer func() {
451462
if err := w.Close(); err != nil {
452463
slog.Default().Error("Failed to close Wiki", "error", err)

cmd/leafwiki/main_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,9 @@ func TestRemoveStaleUnixSocket_RemovesExistingSocket(t *testing.T) {
347347
}
348348
dir := t.TempDir()
349349
socketPath := filepath.Join(dir, "leafwiki.sock")
350-
350+
if len(socketPath) >= 100 {
351+
t.Skipf("socket path too long (%d chars) for this platform", len(socketPath))
352+
}
351353
listener, err := net.Listen("unix", socketPath)
352354
if err != nil {
353355
t.Fatalf("listen unix: %v", err)
@@ -387,7 +389,9 @@ func TestListenOnUnixSocket_CreatesSocketWithExpectedPermissions(t *testing.T) {
387389
}
388390
dir := t.TempDir()
389391
socketPath := filepath.Join(dir, "leafwiki.sock")
390-
392+
if len(socketPath) >= 100 {
393+
t.Skipf("socket path too long (%d chars) for this platform", len(socketPath))
394+
}
391395
listener, err := listenOnUnixSocket(socketPath)
392396
if err != nil {
393397
t.Fatalf("listenOnUnixSocket() error = %v", err)

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ require (
6262
github.com/quic-go/qpack v0.6.0 // indirect
6363
github.com/quic-go/quic-go v0.59.1 // indirect
6464
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
65+
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
6566
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
6667
github.com/skeema/knownhosts v1.3.1 // indirect
6768
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
143143
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
144144
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
145145
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
146+
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
147+
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
146148
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
147149
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
148150
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -155,6 +157,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
155157
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
156158
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
157159
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
160+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
158161
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
159162
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
160163
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=

internal/backup/gitignore.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ func EnsureGitignore(repoDir string) error {
2626
}
2727
// os.WriteFile already respects the process umask — no manual umask needed
2828
return os.WriteFile(gitignorePath, []byte(gitignoreContent), 0644)
29-
}
29+
}

internal/backup/gitignore_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ func TestEnsureGitignore_DoesNotOverwrite(t *testing.T) {
4646
if string(content) != existingContent {
4747
t.Errorf("expected content %q, got %q", existingContent, string(content))
4848
}
49-
}
49+
}

internal/backup/repo.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ var errRemoteBranchNotFound = errors.New("remote branch not found")
4343

4444
// Repository wraps a git repository with backup-specific state.
4545
type Repository struct {
46-
mu sync.Mutex // serialises RunBackup and ForcePush so the HTTP handler can't race the scheduler goroutine
46+
mu sync.Mutex // serialises RunBackup and ForcePush so the HTTP handler can't race the scheduler goroutine
4747
cfg Config
4848
repoDir string
4949
repo *gogit.Repository
5050
status *Status
5151
looseObjsSinceGC int
52-
lastPushedHash plumbing.Hash // hash of the last commit successfully pushed; zero = never pushed
52+
lastPushedHash plumbing.Hash // hash of the last commit successfully pushed; zero = never pushed
5353
}
5454

5555
// Init opens an existing repo at repoDir or initialises a new one.
@@ -855,18 +855,19 @@ func (r *Repository) buildSSHAuth() (ssh.AuthMethod, error) {
855855
var err error
856856

857857
// Try SSHKey string first
858-
if r.cfg.SSHKey != "" {
858+
switch {
859+
case r.cfg.SSHKey != "":
859860
slog.Debug("buildSSHAuth: using inline SSH key")
860861
privateKey = []byte(r.cfg.SSHKey)
861-
} else if r.cfg.SSHKeyPath != "" {
862+
case r.cfg.SSHKeyPath != "":
862863
slog.Debug("buildSSHAuth: reading SSH key from file", "path", r.cfg.SSHKeyPath)
863864
privateKey, err = os.ReadFile(r.cfg.SSHKeyPath)
864865
if err != nil {
865866
slog.Debug("buildSSHAuth: failed to read SSH key file", "path", r.cfg.SSHKeyPath, "error", err)
866867
return nil, fmt.Errorf("failed to read SSH key: %w", err)
867868
}
868869
slog.Debug("buildSSHAuth: SSH key file read successfully", "path", r.cfg.SSHKeyPath, "size", len(privateKey))
869-
} else {
870+
default:
870871
slog.Debug("buildSSHAuth: no SSH key configured (neither inline nor path)")
871872
return nil, fmt.Errorf("no SSH key provided")
872873
}

0 commit comments

Comments
 (0)