rename becomes its own binary
This commit is contained in:
parent
b080679ec9
commit
3e7e625051
15
README.md
15
README.md
|
|
@ -103,6 +103,21 @@ Middle-click `Put` to write all metadata changes. This will rename files and, wh
|
|||
|
||||
Reload all notes from disk, discarding any uncommitted changes in the 9P metadata. Middle-click `Get` to do this. This is useful when notes are modified outside of Acme or when you want to discard metadata changes.
|
||||
|
||||
### Drename
|
||||
|
||||
Update note metadata (title, tags, signature) from any note window:
|
||||
|
||||
```
|
||||
Drename 'new title' tag1,tag2 ==newsig
|
||||
```
|
||||
|
||||
This command works for all files in the denote directory:
|
||||
- **Regular notes** (.md, .org, .txt): Updates frontmatter in window + renames file
|
||||
- **Binary files** (PDFs, images): Just renames file
|
||||
- **Encrypted files** (.gpg): Updates frontmatter in window + renames file
|
||||
|
||||
The window must be saved (Put/CryptPut) after Drename to persist changes to disk.
|
||||
|
||||
## File Format
|
||||
|
||||
By default notes are markdown files with YAML frontmatter:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"denote/pkg/metadata"
|
||||
p9client "denote/internal/p9/client"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"9fans.net/go/acme"
|
||||
"9fans.net/go/plan9/client"
|
||||
)
|
||||
|
||||
var denoteDir = os.Getenv("HOME") + "/doc"
|
||||
|
||||
func main() {
|
||||
// Check if running in service mode (called by OnRename callback)
|
||||
if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "--id=") {
|
||||
identifier := strings.TrimPrefix(os.Args[1], "--id=")
|
||||
if err := serviceMode(identifier); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "drename: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Window mode (called by user from acme window)
|
||||
if err := windowMode(os.Args[1:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "drename: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// serviceMode handles rename when called by OnRename callback
|
||||
// This replicates the logic from HandleRenameEvent
|
||||
func serviceMode(identifier string) error {
|
||||
return p9client.With9P(func(f *client.Fsys) error {
|
||||
// Get new path from 9P server
|
||||
newPath, err := p9client.ReadFile(f, "n/"+identifier+"/path")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read path: %w", err)
|
||||
}
|
||||
|
||||
if newPath == "" {
|
||||
// Path is not set yet. This can happen during note creation.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find old file on disk (matches with or without signature)
|
||||
pattern := filepath.Join(denoteDir, identifier+"*")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find file: %w", err)
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
// File doesn't exist on disk. Nothing to rename.
|
||||
return nil
|
||||
}
|
||||
oldPath := matches[0]
|
||||
|
||||
// Rename if different
|
||||
if oldPath != newPath {
|
||||
newPath = filepath.Clean(newPath)
|
||||
absNew, err := filepath.Abs(newPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve new path: %w", err)
|
||||
}
|
||||
absBase, err := filepath.Abs(denoteDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve base directory: %w", err)
|
||||
}
|
||||
if !strings.HasPrefix(absNew, absBase) {
|
||||
return fmt.Errorf("path traversal attempt: path outside base")
|
||||
}
|
||||
|
||||
if err := os.Rename(oldPath, newPath); err != nil {
|
||||
return fmt.Errorf("failed to rename file from %s to %s: %w", oldPath, newPath, err)
|
||||
}
|
||||
|
||||
// Update window tag for renamed file
|
||||
if wins, err := acme.Windows(); err == nil {
|
||||
for _, w := range wins {
|
||||
if err := updateWindowName(w.ID, oldPath, newPath); err != nil {
|
||||
log.Printf("failed to update window %d: %v", w.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// windowMode handles rename when called by user from acme window
|
||||
func windowMode(args []string) error {
|
||||
// Get $winid
|
||||
winidStr := os.Getenv("winid")
|
||||
if winidStr == "" {
|
||||
return fmt.Errorf("$winid not set - must be called from acme window")
|
||||
}
|
||||
winid, err := strconv.Atoi(winidStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid $winid: %w", err)
|
||||
}
|
||||
|
||||
// Open the window
|
||||
win, err := acme.Open(winid, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open window: %w", err)
|
||||
}
|
||||
defer win.CloseFiles()
|
||||
|
||||
// Read window name from tag
|
||||
tag, err := win.ReadAll("tag")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read window tag: %w", err)
|
||||
}
|
||||
windowName := strings.Fields(string(tag))[0]
|
||||
|
||||
// Extract identifier from window name
|
||||
identifierPattern := regexp.MustCompile(`(\d{8}T\d{6})`)
|
||||
matches := identifierPattern.FindStringSubmatch(windowName)
|
||||
if len(matches) == 0 {
|
||||
return fmt.Errorf("no denote identifier found in window name: %s", windowName)
|
||||
}
|
||||
identifier := matches[1]
|
||||
|
||||
// Parse arguments: 'Title' ==signature tag1,tag2,...
|
||||
input := strings.Join(args, " ")
|
||||
|
||||
// Extract title (must be single-quoted)
|
||||
if !strings.HasPrefix(input, "'") {
|
||||
return fmt.Errorf("title must be single-quoted")
|
||||
}
|
||||
|
||||
closeQuote := strings.Index(input[1:], "'")
|
||||
if closeQuote == -1 {
|
||||
return fmt.Errorf("title must be single-quoted (missing closing quote)")
|
||||
}
|
||||
|
||||
title := input[1 : closeQuote+1]
|
||||
if title == "" {
|
||||
return fmt.Errorf("title cannot be empty")
|
||||
}
|
||||
|
||||
// Extract signature and tags (everything after closing quote)
|
||||
remainder := strings.TrimSpace(input[closeQuote+2:])
|
||||
var signature string
|
||||
var tags []string
|
||||
|
||||
if remainder != "" {
|
||||
// Check if signature is present (starts with ==)
|
||||
if strings.HasPrefix(remainder, "==") {
|
||||
// Find end of signature (space before tags)
|
||||
spaceIdx := strings.Index(remainder, " ")
|
||||
if spaceIdx == -1 {
|
||||
// No tags, just signature
|
||||
signature = remainder[2:] // Skip ==
|
||||
} else {
|
||||
signature = remainder[2:spaceIdx] // Skip ==
|
||||
remainder = strings.TrimSpace(remainder[spaceIdx+1:])
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tags if present
|
||||
if remainder != "" && !strings.HasPrefix(remainder, "==") {
|
||||
// Validate tags
|
||||
tagPattern := regexp.MustCompile(`^([\p{Ll}\p{Nd}]+,)*[\p{Ll}\p{Nd}]+$`)
|
||||
if !tagPattern.MatchString(remainder) {
|
||||
return fmt.Errorf("tags must be comma-delimited lowercase unicode words (no spaces)")
|
||||
}
|
||||
tags = strings.Split(remainder, ",")
|
||||
}
|
||||
}
|
||||
|
||||
// Read window body to check for frontmatter
|
||||
body, err := win.ReadAll("body")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read window body: %w", err)
|
||||
}
|
||||
|
||||
// Detect file type from window name
|
||||
ext := strings.ToLower(filepath.Ext(windowName))
|
||||
supportsFrontmatter := ext == ".org" || ext == ".md" || ext == ".txt"
|
||||
|
||||
// Update frontmatter if applicable
|
||||
if supportsFrontmatter && len(body) > 0 {
|
||||
// Create new frontmatter
|
||||
fm := &metadata.FrontMatter{
|
||||
Title: title,
|
||||
Tags: tags,
|
||||
Signature: signature,
|
||||
}
|
||||
|
||||
// Update frontmatter in content
|
||||
newContent, err := metadata.UpdateFrontMatter(string(body), fm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update frontmatter: %w", err)
|
||||
}
|
||||
|
||||
// Write updated content back to window
|
||||
if err := win.Addr("0,$"); err != nil {
|
||||
return fmt.Errorf("failed to select window content: %w", err)
|
||||
}
|
||||
if _, err := win.Write("data", []byte(newContent)); err != nil {
|
||||
return fmt.Errorf("failed to write updated content: %w", err)
|
||||
}
|
||||
if err := win.Ctl("dirty"); err != nil {
|
||||
return fmt.Errorf("failed to mark window dirty: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write to 9P metadata (triggers OnUpdate and OnRename callbacks)
|
||||
return p9client.With9P(func(f *client.Fsys) error {
|
||||
// Write title
|
||||
if err := p9client.WriteFile(f, "n/"+identifier+"/title", title); err != nil {
|
||||
return fmt.Errorf("failed to write title: %w", err)
|
||||
}
|
||||
|
||||
// Write keywords
|
||||
keywords := strings.Join(tags, ",")
|
||||
if err := p9client.WriteFile(f, "n/"+identifier+"/keywords", keywords); err != nil {
|
||||
return fmt.Errorf("failed to write keywords: %w", err)
|
||||
}
|
||||
|
||||
// Write signature
|
||||
if err := p9client.WriteFile(f, "n/"+identifier+"/signature", signature); err != nil {
|
||||
return fmt.Errorf("failed to write signature: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// updateWindowName updates the window tag name from oldPath to newPath
|
||||
func updateWindowName(id int, oldPath, newPath string) error {
|
||||
win, err := acme.Open(id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer win.CloseFiles()
|
||||
|
||||
tag, err := win.ReadAll("tag")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(string(tag), oldPath) {
|
||||
return nil // Window doesn't have this path, skip
|
||||
}
|
||||
|
||||
if err := win.Ctl("name " + newPath); err != nil {
|
||||
return fmt.Errorf("failed to rename window: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package disk
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"denote/internal/metadata"
|
||||
"denote/pkg/metadata"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
package disk
|
||||
|
||||
import (
|
||||
"denote/internal/metadata"
|
||||
"denote/pkg/metadata"
|
||||
p9client "denote/internal/p9/client"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"9fans.net/go/acme"
|
||||
"9fans.net/go/plan9/client"
|
||||
)
|
||||
|
||||
|
|
@ -100,61 +100,13 @@ func HandleUpdateEvent(f *client.Fsys, identifier, denoteDir string) error {
|
|||
}
|
||||
|
||||
// HandleRenameEvent handles 'r' events from the 9P server.
|
||||
// When path is updated in 9P, rename the file on the filesystem.
|
||||
// Delegates to Drename binary to perform the actual rename.
|
||||
func HandleRenameEvent(f *client.Fsys, identifier, denoteDir string) error {
|
||||
// Get new path from 9P server.
|
||||
newPath, err := p9client.ReadFile(f, "n/"+identifier+"/path")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read path: %w", err)
|
||||
cmd := exec.Command("Drename", "--id="+identifier)
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("rename failed: %w", err)
|
||||
}
|
||||
|
||||
if newPath == "" {
|
||||
// Path is not set yet. This can happen during note creation.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find old file on disk (matches with or without signature).
|
||||
pattern := filepath.Join(denoteDir, identifier+"*")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find file: %w", err)
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
// File doesn't exist on disk. Nothing to rename.
|
||||
return nil
|
||||
}
|
||||
oldPath := matches[0]
|
||||
|
||||
// Rename if different.
|
||||
if oldPath != newPath {
|
||||
newPath = filepath.Clean(newPath)
|
||||
absNew, err := filepath.Abs(newPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve new path: %w", err)
|
||||
}
|
||||
absBase, err := filepath.Abs(denoteDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve base directory: %w", err)
|
||||
}
|
||||
if !strings.HasPrefix(absNew, absBase) {
|
||||
return fmt.Errorf("path traversal attempt: path outside base")
|
||||
}
|
||||
|
||||
if err := os.Rename(oldPath, newPath); err != nil {
|
||||
return fmt.Errorf("failed to rename file from %s to %s: %w", oldPath, newPath, err)
|
||||
}
|
||||
|
||||
// Update window tag for renamed file
|
||||
if wins, err := acme.Windows(); err == nil {
|
||||
for _, w := range wins {
|
||||
if err := updateWindowName(w.ID, oldPath, newPath); err != nil {
|
||||
log.Printf("failed to update window %d: %v", w.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -194,27 +146,3 @@ func HandleDeleteEvent(f *client.Fsys, identifier, denoteDir string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateWindowName updates the window tag name from oldPath to newPath
|
||||
func updateWindowName(id int, oldPath, newPath string) error {
|
||||
win, err := acme.Open(id, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer win.CloseFiles()
|
||||
|
||||
tag, err := win.ReadAll("tag")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(string(tag), oldPath) {
|
||||
return nil // Window doesn't have this path, skip
|
||||
}
|
||||
|
||||
if err := win.Ctl("name " + newPath); err != nil {
|
||||
return fmt.Errorf("failed to rename window: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package disk
|
||||
|
||||
import (
|
||||
"denote/internal/metadata"
|
||||
"denote/pkg/metadata"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ the file name.
|
|||
package disk
|
||||
|
||||
import (
|
||||
"denote/internal/metadata"
|
||||
"denote/pkg/metadata"
|
||||
p9client "denote/internal/p9/client"
|
||||
"fmt"
|
||||
"log"
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ package fs
|
|||
|
||||
import (
|
||||
"denote/internal/disk"
|
||||
"denote/internal/metadata"
|
||||
"denote/pkg/metadata"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -2,7 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"denote/internal/disk"
|
||||
"denote/internal/metadata"
|
||||
"denote/pkg/metadata"
|
||||
p9client "denote/internal/p9/client"
|
||||
p9server "denote/internal/p9/server"
|
||||
"fmt"
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
|
|
@ -552,7 +553,7 @@ func applyIndexChanges(f *client.Fsys, current, updated metadata.Results) error
|
|||
|
||||
// Check if anything changed
|
||||
titleChanged := orig.Title != upd.Title
|
||||
tagsChanged := !metadata.SlicesEqual(orig.Tags, upd.Tags)
|
||||
tagsChanged := !slices.Equal(orig.Tags, upd.Tags)
|
||||
|
||||
if titleChanged || tagsChanged {
|
||||
// Write both title and keywords
|
||||
|
|
|
|||
3
mkfile
3
mkfile
|
|
@ -1,7 +1,8 @@
|
|||
install:V:
|
||||
go build -o $HOME/bin/Denote .
|
||||
go build -o $HOME/bin/Drename cmd/Drename/main.go
|
||||
cp scripts/Djournal $HOME/bin/Djournal
|
||||
cp scripts/Dmerge $HOME/bin/Dmerge
|
||||
|
||||
clean:V:
|
||||
rm -f $HOME/bin/Denote $HOME/bin/Djournal $HOME/bin/Dmerge
|
||||
rm -f $HOME/bin/Denote $HOME/bin/Drename $HOME/bin/Djournal $HOME/bin/Dmerge
|
||||
|
|
@ -44,9 +44,9 @@ signature: %s
|
|||
`,
|
||||
}
|
||||
|
||||
// FileExtensions contains the list of file extensions
|
||||
// fileExtensions contains the list of file extensions
|
||||
// for which Denote should add front matter.
|
||||
var FileExtensions = map[string]string{
|
||||
var fileExtensions = map[string]string{
|
||||
"org": ".org",
|
||||
"md-yaml": ".md",
|
||||
"md-toml": ".md",
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
|
@ -346,7 +347,7 @@ identifier: 20240101T120000
|
|||
t.Errorf("ParseFrontMatter().Title = %q, want %q", got.Title, tt.wantTitle)
|
||||
}
|
||||
|
||||
if !SlicesEqual(got.Tags, tt.wantTags) {
|
||||
if !slices.Equal(got.Tags, tt.wantTags) {
|
||||
t.Errorf("ParseFrontMatter().Tags = %v, want %v", got.Tags, tt.wantTags)
|
||||
}
|
||||
|
||||
|
|
@ -608,12 +609,12 @@ func TestFileExtensions(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.fileType, func(t *testing.T) {
|
||||
got, ok := FileExtensions[tt.fileType]
|
||||
got, ok := fileExtensions[tt.fileType]
|
||||
if !ok {
|
||||
t.Errorf("FileExtensions[%q] not found", tt.fileType)
|
||||
t.Errorf("fileExtensions[%q] not found", tt.fileType)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("FileExtensions[%q] = %q, want %q", tt.fileType, got, tt.want)
|
||||
t.Errorf("fileExtensions[%q] = %q, want %q", tt.fileType, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -632,7 +633,7 @@ func TestFrontMatterRoundTrip(t *testing.T) {
|
|||
content := Generate(title, "", tags, fileType, identifier)
|
||||
|
||||
// Parse it back
|
||||
ext := FileExtensions[fileType]
|
||||
ext := fileExtensions[fileType]
|
||||
fm, err := ParseFrontMatter(content, ext)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFrontMatter() error = %v", err)
|
||||
|
|
@ -642,7 +643,7 @@ func TestFrontMatterRoundTrip(t *testing.T) {
|
|||
if fm.Title != title {
|
||||
t.Errorf("Parsed title = %q, want %q", fm.Title, title)
|
||||
}
|
||||
if !SlicesEqual(fm.Tags, tags) {
|
||||
if !slices.Equal(fm.Tags, tags) {
|
||||
t.Errorf("Parsed tags = %v, want %v", fm.Tags, tags)
|
||||
}
|
||||
if fm.Identifier != identifier {
|
||||
|
|
@ -672,7 +673,7 @@ func TestFrontMatterRoundTrip(t *testing.T) {
|
|||
if fm2.Title != newTitle {
|
||||
t.Errorf("After update title = %q, want %q", fm2.Title, newTitle)
|
||||
}
|
||||
if !SlicesEqual(fm2.Tags, newTags) {
|
||||
if !slices.Equal(fm2.Tags, newTags) {
|
||||
t.Errorf("After update tags = %v, want %v", fm2.Tags, newTags)
|
||||
}
|
||||
})
|
||||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -81,12 +80,6 @@ func (es Results) FromString(data string) (Results, error) {
|
|||
return results, nil
|
||||
}
|
||||
|
||||
// SlicesEqual compares two string slices for equality.
|
||||
// Deprecated: Use slices.Equal from standard library instead.
|
||||
func SlicesEqual(a, b []string) bool {
|
||||
return slices.Equal(a, b)
|
||||
}
|
||||
|
||||
type SortBy string
|
||||
|
||||
const (
|
||||
|
|
@ -247,7 +240,7 @@ func BuildFilename(identifier, signature, title string, keywords []string, ext s
|
|||
// Returns (path, content).
|
||||
func GenerateNote(dir, title, signature string, keywords []string, fileType string) (string, string) {
|
||||
identifier := GenerateIdentifier()
|
||||
ext := FileExtensions[fileType]
|
||||
ext := fileExtensions[fileType]
|
||||
filename := BuildFilename(identifier, signature, title, keywords, ext)
|
||||
path := filepath.Join(dir, filename)
|
||||
content := Generate(title, signature, keywords, fileType, identifier)
|
||||
|
|
@ -2,6 +2,7 @@ package metadata
|
|||
|
||||
import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -396,7 +397,7 @@ func TestParseFilename(t *testing.T) {
|
|||
tt.path, got.Title, tt.wantTitle)
|
||||
}
|
||||
|
||||
if !SlicesEqual(got.Tags, tt.wantTags) {
|
||||
if !slices.Equal(got.Tags, tt.wantTags) {
|
||||
t.Errorf("ParseFilename(%q).Tags = %v, want %v",
|
||||
tt.path, got.Tags, tt.wantTags)
|
||||
}
|
||||
|
|
@ -764,7 +765,7 @@ func TestResultsFromString(t *testing.T) {
|
|||
if got[i].Title != tt.want[i].Title {
|
||||
t.Errorf("Result[%d].Title = %q, want %q", i, got[i].Title, tt.want[i].Title)
|
||||
}
|
||||
if !SlicesEqual(got[i].Tags, tt.want[i].Tags) {
|
||||
if !slices.Equal(got[i].Tags, tt.want[i].Tags) {
|
||||
t.Errorf("Result[%d].Tags = %v, want %v", i, got[i].Tags, tt.want[i].Tags)
|
||||
}
|
||||
}
|
||||
|
|
@ -772,63 +773,6 @@ func TestResultsFromString(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestSlicesEqual validates slice comparison
|
||||
func TestSlicesEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a []string
|
||||
b []string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal slices",
|
||||
a: []string{"a", "b", "c"},
|
||||
b: []string{"a", "b", "c"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different length",
|
||||
a: []string{"a", "b"},
|
||||
b: []string{"a", "b", "c"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different content",
|
||||
a: []string{"a", "b", "c"},
|
||||
b: []string{"a", "x", "c"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "both nil",
|
||||
a: nil,
|
||||
b: nil,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "both empty",
|
||||
a: []string{},
|
||||
b: []string{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// slices.Equal treats nil and empty slices as equal (both have len 0)
|
||||
name: "nil vs empty",
|
||||
a: nil,
|
||||
b: []string{},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := SlicesEqual(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("SlicesEqual(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenerateNote validates note generation (integration test)
|
||||
func TestGenerateNote(t *testing.T) {
|
||||
t.Run("without signature", func(t *testing.T) {
|
||||
Loading…
Reference in New Issue