// Package lock liest und schreibt hugin.lock (→ KONZEPT.md 11.3). // // Die Lock-Datei sagt, welche Dateien, JSON-Schlüssel und Blöcke Hugin gehören // und welchen Stand sie hatten. Daran erkennt sync Handänderungen und weiß, was // beim Abbestellen zu entfernen ist. package lock import ( "bytes" "errors" "io/fs" "os" "path/filepath" "slices" "gopkg.in/yaml.v3" ) const ( Filename = "hugin.lock" header = "# Von hugin sync erzeugt. Nicht von Hand ändern.\n" ) type Lock struct { Packages map[string]*Package `yaml:"packages"` Common Common `yaml:"common,omitempty"` } type Package struct { Version string `yaml:"version"` Checksum string `yaml:"checksum"` Files map[string]string `yaml:"files,omitempty"` Keys []Key `yaml:"keys,omitempty"` } // Key ist ein JSON-Schlüssel, den Hugin gesetzt hat. Bei Mode list_item // gehört Hugin nur der Eintrag Value in der Liste. type Key struct { File string `yaml:"file"` Path []string `yaml:"path,flow"` Mode string `yaml:"mode,omitempty"` Value any `yaml:"value,omitempty"` Sha256 string `yaml:"sha256,omitempty"` } // Common hält, was keinem einzelnen Paket gehört. type Common struct { Keys []Key `yaml:"keys,omitempty"` // "#" → Prüfsumme des Blockinhalts Blocks map[string]string `yaml:"blocks,omitempty"` // Dateien, die Hugin angelegt hat, z. B. CLAUDE.md oder opencode.json Created []string `yaml:"created,omitempty"` } func New() *Lock { return &Lock{Packages: map[string]*Package{}} } // Load liest hugin.lock aus repo; fehlt die Datei, ist das Ergebnis leer. func Load(repo string) (*Lock, error) { return LoadFrom(filepath.Join(repo, Filename)) } // LoadFrom liest eine Lock-Datei von path, z. B. .hugin/trial.lock. func LoadFrom(path string) (*Lock, error) { raw, err := os.ReadFile(path) if errors.Is(err, fs.ErrNotExist) { return New(), nil } if err != nil { return nil, err } l := New() if err := yaml.Unmarshal(raw, l); err != nil { return nil, err } if l.Packages == nil { l.Packages = map[string]*Package{} } return l, nil } func (l *Lock) Bytes() ([]byte, error) { var buffer bytes.Buffer buffer.WriteString(header) enc := yaml.NewEncoder(&buffer) enc.SetIndent(2) if err := enc.Encode(l); err != nil { return nil, err } return buffer.Bytes(), enc.Close() } // Files liefert alle Dateien aller Pakete: Pfad → Prüfsumme. func (l *Lock) Files() map[string]string { all := map[string]string{} for _, p := range l.Packages { for path, sha := range p.Files { all[path] = sha } } return all } // AllKeys liefert die Schlüssel aller Pakete und die gemeinsamen. func (l *Lock) AllKeys() []Key { var all []Key for _, p := range l.Packages { all = append(all, p.Keys...) } return append(all, l.Common.Keys...) } // Find sucht einen Schlüssel; bei list_item muss auch der Wert passen. func Find(items []Key, file string, path []string, mode string, value any) *Key { for i, s := range items { if s.File != file || !slices.Equal(s.Path, path) || s.Mode != mode { continue } if mode == "list_item" && !equal(s.Value, value) { continue } return &items[i] } return nil } func equal(a, b any) bool { ya, _ := yaml.Marshal(a) yb, _ := yaml.Marshal(b) return bytes.Equal(ya, yb) } func (l *Lock) Created(file string) bool { return slices.Contains(l.Common.Created, file) }