Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions boot/deps/lock/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ func NewCheckReplacementPathError(path string, cause error) apierror.Error {
return apierror.New(apierror.Internal, fmt.Sprintf("failed to check replacement path %q", path)).WithCause(cause)
}

func NewReadOverlayFileError(cause error) apierror.Error {
return apierror.New(apierror.Internal, "failed to read overlay lock file").WithCause(cause)
}

func NewUnmarshalOverlayError(cause error) apierror.Error {
return apierror.New(apierror.Invalid, "failed to parse overlay lock file (wippy.lock.local may only contain replacements)").WithCause(cause)
}

func NewInvalidFormatError(name string) apierror.Error {
return apierror.New(apierror.Invalid, fmt.Sprintf("invalid format (expected org/module, got %q)", name))
}
Expand Down
98 changes: 90 additions & 8 deletions boot/deps/lock/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
package lock

import (
"bytes"
"errors"
"io"
"os"
"path/filepath"
"sort"
Expand All @@ -14,10 +17,16 @@ import (
// DefaultFilename is the standard name for wippy lock files.
const DefaultFilename = "wippy.lock"

// OverlaySuffix is appended to the lock file path to locate the local,
// gitignored overlay (e.g. wippy.lock -> wippy.lock.local) that carries
// developer-local replacements without touching the tracked lock.
const OverlaySuffix = ".local"

// Lock represents a lock file with operations for reading, writing, and querying.
type Lock struct {
path string
data File
path string
overlay []Replacement
data File
}

// New creates a new Lock instance from the given path.
Expand Down Expand Up @@ -48,9 +57,70 @@ func New(path string) (*Lock, error) {
return nil, NewStatLockFileError(err)
}

if err := lock.readOverlay(); err != nil {
return nil, err
}

return lock, nil
}

// readOverlay loads developer-local replacements from the "<path>.local" overlay
// when present. The overlay may only contain a replacements section; any other
// key is rejected. Overlay replacements are kept separate from the tracked data
// so they are never written back into the lock file.
func (l *Lock) readOverlay() error {
overlayPath := l.path + OverlaySuffix

data, err := os.ReadFile(overlayPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return NewReadOverlayFileError(err)
}

var overlay OverlayFile
dec := yaml.NewDecoder(bytes.NewReader(data))
dec.KnownFields(true)
if err := dec.Decode(&overlay); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return NewUnmarshalOverlayError(err)
}

l.overlay = overlay.Replacements
return nil
}

// effectiveReplacements returns the tracked replacements overlaid with the local
// overlay, where overlay entries win by "from". Order is preserved: tracked
// entries first (updated in place when overridden), then overlay-only additions.
func (l *Lock) effectiveReplacements() []Replacement {
if len(l.overlay) == 0 {
return l.data.Replacements
}

index := make(map[string]int, len(l.data.Replacements)+len(l.overlay))
merged := make([]Replacement, 0, len(l.data.Replacements)+len(l.overlay))

for _, r := range l.data.Replacements {
index[r.From] = len(merged)
merged = append(merged, r)
}

for _, r := range l.overlay {
if i, ok := index[r.From]; ok {
merged[i] = r
continue
}
index[r.From] = len(merged)
merged = append(merged, r)
}

return merged
}

// Read loads the lock file from disk.
func (l *Lock) Read() error {
data, err := os.ReadFile(l.path)
Expand Down Expand Up @@ -158,10 +228,11 @@ func (l *Lock) GetModules() []Module {
return l.data.Modules
}

// GetReplacement retrieves a replacement by from field.
// GetReplacement retrieves a replacement by from field from the effective set
// (tracked replacements overlaid with the local overlay).
// Returns the replacement and true if found, zero value and false otherwise.
func (l *Lock) GetReplacement(from string) (Replacement, bool) {
for _, r := range l.data.Replacements {
for _, r := range l.effectiveReplacements() {
if r.From == from {
return r, true
}
Expand Down Expand Up @@ -192,8 +263,16 @@ func (l *Lock) RemoveReplacement(from string) {
l.data.Replacements = filtered
}

// GetReplacements returns all replacements.
// GetReplacements returns the effective replacements: the tracked replacements
// overlaid with the local overlay (overlay entries win by "from").
func (l *Lock) GetReplacements() []Replacement {
return l.effectiveReplacements()
}

// GetTrackedReplacements returns only the replacements persisted in the lock
// file, excluding any local overlay. Used where the overlay must never
// participate, such as regenerating the tracked lock during update.
func (l *Lock) GetTrackedReplacements() []Replacement {
return l.data.Replacements
}

Expand Down Expand Up @@ -314,15 +393,18 @@ type ModuleLoadPath struct {
// Replacement paths carry Module from replacement "from" and empty Version.
func (l *Lock) GetModuleLoadPaths() []ModuleLoadPath {
lockDir := filepath.Dir(l.path)
paths := make([]ModuleLoadPath, 0, 1+len(l.data.Replacements)+len(l.data.Modules))
replacements := l.effectiveReplacements()
paths := make([]ModuleLoadPath, 0, 1+len(replacements)+len(l.data.Modules))

if l.data.Directories.Src != "" {
paths = append(paths, ModuleLoadPath{
Path: ResolveLockPath(lockDir, l.data.Directories.Src),
})
}

for _, repl := range l.data.Replacements {
replaced := make(map[string]struct{}, len(replacements))
for _, repl := range replacements {
replaced[repl.From] = struct{}{}
if repl.To != "" {
root := ResolveLockPath(lockDir, repl.To)
path := moduleEntryLoadPath(root)
Expand All @@ -338,7 +420,7 @@ func (l *Lock) GetModuleLoadPaths() []ModuleLoadPath {
fullVendorDir := ResolveLockPath(lockDir, vendorDir)

for _, mod := range l.data.Modules {
if _, hasReplacement := l.GetReplacement(mod.Name); hasReplacement {
if _, hasReplacement := replaced[mod.Name]; hasReplacement {
continue
}

Expand Down
Loading