Skip to content
go-libfossil Artifact [9fece6d82d]
Login

Artifact [9fece6d82d]

Artifact 9fece6d82d3d57d7c9f91530ccc725246ecf2164b57fa2e6fcda343e82b07d01:


package manifest

import (
	"database/sql"
	"fmt"
	"strings"

	"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"
)

// ReparentCheckins applies every singleton `parent` tag by rewriting the
// tagged check-in's ancestry to the parents the tag names. It is fossil's
// manifest_crosslink_end reparent pass (src/manifest.c
// manifest_reparent_checkin): the check-in's plink and mlink rows are deleted
// and re-derived against the tag's parents instead of its own P-card.
//
// It runs after the candidate sweep and before tag propagation, as fossil
// does, so the propagation walk and the batch's leaf rechecks see the
// rewritten plink. Like fossil's, the rewrite schedules no leaf recheck of its
// own (draft-fossil-repo-state-01 Section 7.3): an old parent left without a
// child is rechecked only when a branch write in the batch reaches it, and an
// explicit rebuild (leaf.Rebuild) always reaches it. A parent tag whose value
// is not a well-formed list of artifact hashes is ignored, matching fossil's
// reparent_abort.
//
// Each repair round scans tagxref for parent tags and rewrites only the tagged
// check-ins, so the pass is O(reparented), not O(repository); a round with no
// parent tag does one indexed query and returns. This matches fossil's
// manifest_crosslink_end, which runs it in every transaction that links
// artifacts; here the repair gate every link transaction runs does (linkTx).
//
// A check-in whose plink rows already name the tag's parents, each of them a
// linked check-in, is not rewritten again: the rewrite would write the rows
// that are there. That keeps the pass cheap for the gate each link
// transaction runs (issue #432).
//
// The new primary parent of each rewritten check-in is recorded on parents,
// so the gate's propagation reaches the check-in under its new parent. This
// goes beyond fossil: manifest_reparent_checkin rewrites plink and mlink and
// propagates nothing, so in fossil a reparented check-in inherits its new
// parent's tags only when a later tag change or crosslink propagates from
// that parent. The whole-repository pass this gate replaced propagated from
// every parent, so it did the same as this does.
//
// Known divergence: a reparent never removes the tags the check-in and its
// descendants inherited from the old parent. Propagation from the new parent
// replaces a tag both parents carry (the branch tag) when the new parent's is
// the newer, but a propagating tag only the old parent carried stays. Fossil's manifest_reparent_checkin
// removes none either, and fossil's own rebuild is order-dependent here: the
// tags a reparented check-in ends with depend on whether the old parent's tag
// reached it before the rewrite. So an incremental crosslink that reparents
// can differ from a rebuild in such inherited tags, as fossil's can.
func ReparentCheckins(tx *db.Tx, parents *parentSet) error {
	if tx == nil {
		panic("manifest.ReparentCheckins: tx must not be nil")
	}
	if parents == nil {
		panic("manifest.ReparentCheckins: parents must not be nil")
	}
	type reparent struct {
		rid   libfossil.FslID
		value string
	}
	rows, err := tx.Query(`SELECT tagxref.rid, tagxref.value FROM tagxref
		 JOIN tag USING(tagid)
		 WHERE tag.tagname='parent' AND tagxref.tagtype=1`)
	if err != nil {
		return fmt.Errorf("manifest.ReparentCheckins query: %w", err)
	}
	var todo []reparent
	for rows.Next() {
		var r reparent
		var value sql.NullString
		if err := rows.Scan(&r.rid, &value); err != nil {
			rows.Close()
			return fmt.Errorf("manifest.ReparentCheckins scan: %w", err)
		}
		r.value = value.String
		todo = append(todo, r)
	}
	rows.Close()
	if err := rows.Err(); err != nil {
		return fmt.Errorf("manifest.ReparentCheckins rows: %w", err)
	}
	if len(todo) == 0 {
		return nil // the common case: no reparent, no cache to build
	}
	cache := content.NewCache(crosslinkCacheBytes)
	for _, r := range todo {
		if err := reparentCheckin(tx, cache, r.rid, r.value, parents); err != nil {
			return err
		}
	}
	return nil
}

// reparentCheckin rewrites one check-in's plink and mlink to the parents named
// in value, a space-separated list of artifact hashes. The rows are derived by
// the same builders a normal crosslink uses, over a deck whose P-card is the
// override: fossil reuses manifest_add_checkin_linkages the same way.
func reparentCheckin(
	tx *db.Tx, cache *content.Cache, rid libfossil.FslID, value string, parents *parentSet,
) error {
	hashes := strings.Fields(value)
	if len(hashes) == 0 {
		return nil
	}
	for _, p := range hashes {
		if !isHash(p) {
			return nil // malformed value: fossil leaves the plink rows untouched
		}
	}
	settled, err := reparentSettled(tx, rid, hashes)
	if err != nil {
		return err
	}
	if settled {
		return nil
	}
	data, err := expandManifestBytes(tx, cache, rid)
	if err != nil {
		return nil // unreadable check-in: nothing to rewrite
	}
	d, err := deck.Parse(data)
	if err != nil || d.Type != deck.Checkin {
		return nil
	}

	reparented := *d
	reparented.P = hashes

	if _, err := tx.Exec("DELETE FROM plink WHERE cid=?", rid); err != nil {
		return fmt.Errorf("manifest.reparentCheckin plink delete rid=%d: %w", rid, err)
	}
	if _, err := tx.Exec("DELETE FROM mlink WHERE mid=?", rid); err != nil {
		return fmt.Errorf("manifest.reparentCheckin mlink delete rid=%d: %w", rid, err)
	}
	if err := insertCheckinPlinks(tx, rid, &reparented, checkinBaseid(tx, d), parents); err != nil {
		return fmt.Errorf("manifest.reparentCheckin plink rid=%d: %w", rid, err)
	}
	if err := insertCheckinMlinks(tx, cache, rid, &reparented); err != nil {
		return fmt.Errorf("manifest.reparentCheckin mlink rid=%d: %w", rid, err)
	}
	return nil
}

// reparentSettled reports whether rid's plink rows name exactly the parents in
// hashes, the first one primary, and each parent is a linked check-in. A
// rewrite against a parent that is not linked yet derives mlink without that
// parent's manifest, so it is redone at every gate until the parent is linked,
// as it was before any rewrite was skipped.
func reparentSettled(q db.Querier, rid libfossil.FslID, hashes []string) (bool, error) {
	if rid <= 0 {
		panic("manifest.reparentSettled: rid must be positive")
	}
	rows, err := q.Query(`SELECT b.uuid, p.isprim,
	        EXISTS(SELECT 1 FROM event e WHERE e.objid=p.pid AND e.type='ci')
	   FROM plink p JOIN blob b ON b.rid=p.pid WHERE p.cid=?`, rid)
	if err != nil {
		return false, fmt.Errorf("manifest.reparentSettled rid=%d: %w", rid, err)
	}
	defer rows.Close()
	type edge struct{ prim, linked bool }
	have := make(map[string]edge, len(hashes))
	for rows.Next() {
		var uuid string
		var e edge
		if err := rows.Scan(&uuid, &e.prim, &e.linked); err != nil {
			return false, fmt.Errorf("manifest.reparentSettled rid=%d scan: %w", rid, err)
		}
		have[uuid] = e
	}
	if err := rows.Err(); err != nil {
		return false, fmt.Errorf("manifest.reparentSettled rid=%d rows: %w", rid, err)
	}
	if len(have) != len(hashes) {
		return false, nil
	}
	for i, h := range hashes {
		e, ok := have[h]
		if !ok || e.prim != (i == 0) || !e.linked {
			return false, nil
		}
	}
	return true, nil
}