Skip to content
go-libfossil Artifact [cfc33f3e1c]
Login

Artifact [cfc33f3e1c]

Artifact cfc33f3e1cfb3f036f921d2fbff6aa7bedd53d241bb07cd4e336f82b072b34ed:


package manifest

import (
	"fmt"
	"sort"

	"github.com/danmestas/go-libfossil/db"
	"github.com/danmestas/go-libfossil/internal/content"
	"github.com/danmestas/go-libfossil/internal/deck"
	libfossil "github.com/danmestas/go-libfossil/internal/fsltype"
	"github.com/danmestas/go-libfossil/internal/repo"
)

type FileEntry struct {
	Name string
	UUID string
	Perm string
}

func ListFiles(r *repo.Repo, rid libfossil.FslID) ([]FileEntry, error) {
	return ListFilesCached(r, rid, nil)
}

// ListFilesCached is ListFiles that serves the checkin manifest -- and, for a
// delta manifest, its baseline -- through cache. A nil cache is exactly
// ListFiles (content.Cache's nil receiver falls through to content.Expand).
// A live cache helps only a caller listing many overlapping revisions, such as
// annotate walking one file's history; see GetManifestCached.
func ListFilesCached(r *repo.Repo, rid libfossil.FslID, cache *content.Cache) ([]FileEntry, error) {
	d, err := GetManifestCached(r, rid, cache)
	if err != nil {
		return nil, fmt.Errorf("manifest.ListFiles: %w", err)
	}
	if d.B == "" {
		return fCardsToEntries(d.F), nil
	}
	baseDeck, err := loadBaseline(r.DB(), cache, d)
	if err != nil {
		return nil, fmt.Errorf("manifest.ListFiles: %w", err)
	}
	fileMap := make(map[string]FileEntry)
	for _, f := range baseDeck.F {
		// A named delta baseline keeps its own delete markers (-01 8.2);
		// they carry no file, so they must not enter the set.
		if f.UUID == "" {
			continue
		}
		fileMap[f.Name] = FileEntry{Name: f.Name, UUID: f.UUID, Perm: f.Perm}
	}
	for _, f := range d.F {
		if f.UUID == "" {
			delete(fileMap, f.Name)
		} else {
			fileMap[f.Name] = FileEntry{Name: f.Name, UUID: f.UUID, Perm: f.Perm}
		}
	}
	entries := make([]FileEntry, 0, len(fileMap))
	for _, e := range fileMap {
		entries = append(entries, e)
	}
	sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
	return entries, nil
}

// loadBaseline resolves the B card of delta manifest d to its baseline deck.
// The target must be a check-in (4.7.3, 8.3); a control artifact would hand
// back an empty file set as if it were whole. Per artifact-format-01 4.7.3 and
// fossil's manifest_get, the target MAY itself be a delta manifest: only its
// own F cards are used, its own B card is not followed (one level). -00 and
// #275 refused that case; -01 reverses it. Every reader of a B card goes
// through here, so the rule has one owner.
func loadBaseline(q db.Querier, cache *content.Cache, d *deck.Deck) (*deck.Deck, error) {
	if d.B == "" {
		panic("manifest.loadBaseline: d has no B card")
	}
	baseRid, ok := content.AvailableByUUID(q, d.B)
	if !ok {
		return nil, fmt.Errorf("baseline %s not found", d.B)
	}
	baseData, err := expandManifestBytes(q, cache, baseRid)
	if err != nil {
		return nil, fmt.Errorf("expand baseline: %w", err)
	}
	baseDeck, err := deck.Parse(baseData)
	if err != nil {
		return nil, fmt.Errorf("parse baseline: %w", err)
	}
	if baseDeck.Type != deck.Checkin {
		return nil, fmt.Errorf("baseline %s is not a check-in manifest", d.B)
	}
	return baseDeck, nil
}

func fCardsToEntries(fCards []deck.FileCard) []FileEntry {
	entries := make([]FileEntry, len(fCards))
	for i, f := range fCards {
		entries[i] = FileEntry{Name: f.Name, UUID: f.UUID, Perm: f.Perm}
	}
	sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
	return entries
}

// MergeParentFiles returns supplied with every file tracked at parentRID
// that is not already in supplied appended. Names in supplied win over the
// parent's entry; the parent's untouched files have their content expanded
// from the repo so the resulting slice is a full-tree set ready for Checkin.
//
// parentRID == 0 returns supplied unchanged.
func MergeParentFiles(r *repo.Repo, parentRID libfossil.FslID, supplied []File) ([]File, error) {
	if parentRID == 0 {
		return supplied, nil
	}
	parentFiles, err := ListFiles(r, parentRID)
	if err != nil {
		return nil, fmt.Errorf("manifest.MergeParentFiles: %w", err)
	}
	suppliedNames := make(map[string]struct{}, len(supplied))
	for _, f := range supplied {
		suppliedNames[f.Name] = struct{}{}
	}
	merged := supplied
	for _, pf := range parentFiles {
		if _, ok := suppliedNames[pf.Name]; ok {
			continue
		}
		baseRid, ok := content.AvailableByUUID(r.DB(), pf.UUID)
		if !ok {
			return nil, fmt.Errorf("manifest.MergeParentFiles: blob %s for %s not found", pf.UUID, pf.Name)
		}
		// Bare Expand: each untouched parent file is expanded once to build a
		// single commit's full-tree file set. Distinct blobs, no overlap.
		data, err := content.Expand(r.DB(), baseRid)
		if err != nil {
			return nil, fmt.Errorf("manifest.MergeParentFiles: expand %s: %w", pf.Name, err)
		}
		merged = append(merged, File{
			Name:    pf.Name,
			Content: data,
			Perm:    pf.Perm,
		})
	}
	return merged, nil
}

// LookupFile resolves one filename in a check-in's file set, as
// artifact-format-01 8.4 orders the search: a delta manifest's own F cards
// first, and only a name absent from them goes to the baseline. found is false
// when no file has the name or the delta deletes it; a delete marker answers
// without reading the baseline, so a missing baseline does not fail it. A name
// that needs the baseline fails when it cannot be resolved (8.3). The search
// uses the baseline's own F cards and does not follow a B card it carries.
func LookupFile(r *repo.Repo, rid libfossil.FslID, name string) (FileEntry, bool, error) {
	return LookupFileCached(r, rid, name, nil)
}

// LookupFileCached is LookupFile that serves the manifest and baseline through
// cache; a nil cache is exactly LookupFile. Like ListFilesCached it pays off
// only for a caller resolving one name across many overlapping revisions.
func LookupFileCached(
	r *repo.Repo, rid libfossil.FslID, name string, cache *content.Cache,
) (FileEntry, bool, error) {
	if r == nil {
		panic("manifest.LookupFile: r must not be nil")
	}
	if name == "" {
		panic("manifest.LookupFile: name must not be empty")
	}
	d, err := GetManifestCached(r, rid, cache)
	if err != nil {
		return FileEntry{}, false, fmt.Errorf("manifest.LookupFile: %w", err)
	}
	if card, ok := findFileCard(d.F, name); ok {
		return fileCardEntry(card)
	}
	if d.B == "" {
		return FileEntry{}, false, nil
	}
	baseDeck, err := loadBaseline(r.DB(), cache, d)
	if err != nil {
		return FileEntry{}, false, fmt.Errorf("manifest.LookupFile: %w", err)
	}
	// A delete marker in the baseline is not a file.
	if card, ok := findFileCard(baseDeck.F, name); ok {
		return fileCardEntry(card)
	}
	return FileEntry{}, false, nil
}

// findFileCard binary-searches cards, which a parsed deck keeps in comparator
// order (4.5.2), for name.
func findFileCard(cards []deck.FileCard, name string) (deck.FileCard, bool) {
	i := sort.Search(len(cards), func(i int) bool {
		return deck.Compare(cards[i].Name, name) >= 0
	})
	if i < len(cards) && cards[i].Name == name {
		return cards[i], true
	}
	return deck.FileCard{}, false
}

// fileCardEntry turns a found F card into LookupFile's answer: a delete
// marker (no hash) reads as not found.
func fileCardEntry(card deck.FileCard) (FileEntry, bool, error) {
	if card.UUID == "" {
		return FileEntry{}, false, nil
	}
	return FileEntry{Name: card.Name, UUID: card.UUID, Perm: card.Perm}, true, nil
}