134 lines
3.4 KiB
Go
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)
|
|
}
|