Shows path, type, status, mode, hash, timestamps, encryption, lock state, and targeting labels for a single tracked file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
4.0 KiB
Go
159 lines
4.0 KiB
Go
package garden
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// FileInfo holds detailed information about a single tracked entry.
|
|
type FileInfo struct {
|
|
Path string // tilde path from manifest
|
|
Type string // "file", "link", or "directory"
|
|
State string // "ok", "modified", "drifted", "missing", "skipped"
|
|
Mode string // octal file mode from manifest
|
|
Hash string // blob hash from manifest (files only)
|
|
PlaintextHash string // plaintext hash (encrypted files only)
|
|
CurrentHash string // SHA-256 of current file on disk (files only, empty if missing)
|
|
Encrypted bool
|
|
Locked bool
|
|
Updated string // manifest timestamp (RFC 3339)
|
|
DiskModTime string // filesystem modification time (RFC 3339, empty if missing)
|
|
Target string // symlink target (links only)
|
|
CurrentTarget string // current symlink target on disk (links only, empty if missing)
|
|
Only []string // targeting: only these labels
|
|
Never []string // targeting: never these labels
|
|
BlobStored bool // whether the blob exists in the store
|
|
}
|
|
|
|
// Info returns detailed information about a tracked file.
|
|
func (g *Garden) Info(path string) (*FileInfo, error) {
|
|
abs, err := resolvePath(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tilded := toTildePath(abs)
|
|
|
|
entry := g.findEntry(tilded)
|
|
if entry == nil {
|
|
// Also try the path as given (it might already be a tilde path).
|
|
entry = g.findEntry(path)
|
|
if entry == nil {
|
|
return nil, fmt.Errorf("not tracked: %s", path)
|
|
}
|
|
}
|
|
|
|
fi := &FileInfo{
|
|
Path: entry.Path,
|
|
Type: entry.Type,
|
|
Mode: entry.Mode,
|
|
Hash: entry.Hash,
|
|
PlaintextHash: entry.PlaintextHash,
|
|
Encrypted: entry.Encrypted,
|
|
Locked: entry.Locked,
|
|
Target: entry.Target,
|
|
Only: entry.Only,
|
|
Never: entry.Never,
|
|
}
|
|
|
|
if !entry.Updated.IsZero() {
|
|
fi.Updated = entry.Updated.Format("2006-01-02 15:04:05 UTC")
|
|
}
|
|
|
|
// Check blob existence for files.
|
|
if entry.Type == "file" && entry.Hash != "" {
|
|
fi.BlobStored = g.store.Exists(entry.Hash)
|
|
}
|
|
|
|
// Determine state and filesystem info.
|
|
labels := g.Identity()
|
|
applies, err := EntryApplies(entry, labels)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !applies {
|
|
fi.State = "skipped"
|
|
return fi, nil
|
|
}
|
|
|
|
entryAbs, err := ExpandTildePath(entry.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("expanding path %s: %w", entry.Path, err)
|
|
}
|
|
|
|
info, err := os.Lstat(entryAbs)
|
|
if os.IsNotExist(err) {
|
|
fi.State = "missing"
|
|
return fi, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat %s: %w", entryAbs, err)
|
|
}
|
|
|
|
fi.DiskModTime = info.ModTime().UTC().Format("2006-01-02 15:04:05 UTC")
|
|
|
|
switch entry.Type {
|
|
case "file":
|
|
hash, err := HashFile(entryAbs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("hashing %s: %w", entryAbs, err)
|
|
}
|
|
fi.CurrentHash = hash
|
|
|
|
compareHash := entry.Hash
|
|
if entry.Encrypted && entry.PlaintextHash != "" {
|
|
compareHash = entry.PlaintextHash
|
|
}
|
|
if hash != compareHash {
|
|
if entry.Locked {
|
|
fi.State = "drifted"
|
|
} else {
|
|
fi.State = "modified"
|
|
}
|
|
} else {
|
|
fi.State = "ok"
|
|
}
|
|
|
|
case "link":
|
|
target, err := os.Readlink(entryAbs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading symlink %s: %w", entryAbs, err)
|
|
}
|
|
fi.CurrentTarget = target
|
|
if target != entry.Target {
|
|
fi.State = "modified"
|
|
} else {
|
|
fi.State = "ok"
|
|
}
|
|
|
|
case "directory":
|
|
fi.State = "ok"
|
|
}
|
|
|
|
return fi, nil
|
|
}
|
|
|
|
// resolvePath resolves a user-provided path to an absolute path, handling
|
|
// tilde expansion and relative paths.
|
|
func resolvePath(path string) (string, error) {
|
|
if path == "~" || strings.HasPrefix(path, "~/") {
|
|
return ExpandTildePath(path)
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// If it looks like a tilde path already, just expand it.
|
|
if strings.HasPrefix(path, home) {
|
|
return path, nil
|
|
}
|
|
abs, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !strings.HasPrefix(path, "/") {
|
|
path = abs + "/" + path
|
|
}
|
|
return path, nil
|
|
}
|