hugin/cli/internal/lock/lock.go
2026-10-27 08:33:21 +01:00

134 lines
3.4 KiB
Go

// 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"`
// "<datei>#<block>" → 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)
}