Skip to content
go-libfossil Artifact [7acf1cf20d]
Login

Artifact [7acf1cf20d]

Artifact 7acf1cf20d55e45cdd0869b3c48502951496c4cb43a6211f95417a2dfb4dcb0f:


package checkout

import (
	"database/sql"
	"errors"
	"fmt"

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

// BranchLeaves returns the open leaf RIDs for a named branch: a leaf whose
// active branch tag is branch and that carries no 'closed' tag, as fossil's
// `leaves` lists them (src/leaf.c leaf_is_closed_sql). A branch with >1 open
// leaf is forked. An empty branch name queries trunk.
func BranchLeaves(r *repo.Repo, branch string) ([]libfossil.FslID, error) {
	if r == nil {
		panic("checkout.BranchLeaves: r must not be nil")
	}
	if branch == "" {
		branch = "trunk"
	}
	rows, err := r.DB().Query(`
		SELECT l.rid FROM leaf l
		JOIN tagxref tx ON tx.rid = l.rid
		JOIN tag t ON t.tagid = tx.tagid
		WHERE t.tagname = 'branch'
		  AND tx.value = ?
		  AND tx.tagtype > 0
		  AND NOT EXISTS (
		    SELECT 1 FROM tagxref cx
		    JOIN tag ct ON ct.tagid = cx.tagid
		    WHERE ct.tagname = 'closed'
		      AND cx.rid = l.rid
		      AND cx.tagtype > 0
		  )
	`, branch)
	if err != nil {
		return nil, fmt.Errorf("checkout.BranchLeaves: %w", err)
	}
	defer rows.Close()
	var leaves []libfossil.FslID
	for rows.Next() {
		var rid int64
		if err := rows.Scan(&rid); err != nil {
			return nil, fmt.Errorf("checkout.BranchLeaves scan: %w", err)
		}
		leaves = append(leaves, libfossil.FslID(rid))
	}
	return leaves, rows.Err()
}

// WouldFork reports whether committing on the checkout's version would
// create a fork: the version already has a child on its own branch, so a new
// check-in would become a second leaf beside it. This is fossil's commit-time
// refusal (src/checkin.c "Would fork", taken when !is_a_leaf(vid)). Another
// open leaf elsewhere on the branch does not make a commit fork -- the commit
// extends this leaf -- and is reported by LeafAmbiguous instead. A checkout
// with no version (an empty repository) cannot fork.
func (c *Checkout) WouldFork() (bool, error) {
	if c == nil {
		panic("checkout.WouldFork: nil *Checkout")
	}
	rid, _, err := c.Version()
	if err != nil {
		return false, fmt.Errorf("checkout.WouldFork: %w", err)
	}
	if rid <= 0 {
		return false, nil
	}
	return hasSameBranchChild(c.repo, rid)
}

// hasSameBranchChild reports whether rid has a child check-in on the same
// branch, computed from plink as fossil's is_a_leaf (src/leaf.c) does, with
// no branch tag meaning trunk. Reading plink rather than the leaf table keeps
// the commit check correct even if the leaf table has drifted.
func hasSameBranchChild(r *repo.Repo, rid libfossil.FslID) (bool, error) {
	if rid <= 0 {
		panic("checkout.hasSameBranchChild: rid must be positive")
	}
	var one int
	err := r.DB().QueryRow(`
		SELECT 1 FROM plink
		WHERE pid = ?
		  AND coalesce((SELECT tx.value FROM tagxref tx
		                JOIN tag t ON t.tagid = tx.tagid
		                WHERE t.tagname = 'branch' AND tx.rid = plink.pid
		                  AND tx.tagtype > 0), 'trunk')
		    = coalesce((SELECT tx.value FROM tagxref tx
		                JOIN tag t ON t.tagid = tx.tagid
		                WHERE t.tagname = 'branch' AND tx.rid = plink.cid
		                  AND tx.tagtype > 0), 'trunk')
		LIMIT 1
	`, int64(rid)).Scan(&one)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("checkout.hasSameBranchChild: %w", err)
	}
	return true, nil
}

// LeafAmbiguous is fossil's leaf_ambiguity (src/leaf.c) and the repo-state
// draft's Section 7.5 ambiguity test: rid must itself be an open leaf, and
// another open leaf must share its branch. It answers whether propagating
// from rid is underdetermined, not whether a commit on rid would fork (see
// WouldFork).
func LeafAmbiguous(r *repo.Repo, rid libfossil.FslID) (bool, error) {
	if r == nil {
		panic("checkout.LeafAmbiguous: r must not be nil")
	}
	if rid <= 0 {
		panic("checkout.LeafAmbiguous: rid must be positive")
	}
	open, err := isOpenLeaf(r, rid)
	if err != nil {
		return false, err
	}
	if !open {
		return false, nil
	}
	branch, err := branchOf(r, rid)
	if err != nil {
		return false, err
	}
	leaves, err := BranchLeaves(r, branch)
	if err != nil {
		return false, err
	}
	for _, leaf := range leaves {
		if leaf != rid {
			return true, nil
		}
	}
	return false, nil
}

// isOpenLeaf reports whether rid is in the leaf table and carries no 'closed'
// tag -- fossil's is_a_leaf(rid) AND NOT leaf_is_closed(rid), the precondition
// LeafAmbiguous checks before looking for a sibling.
func isOpenLeaf(r *repo.Repo, rid libfossil.FslID) (bool, error) {
	var one int
	err := r.DB().QueryRow(`
		SELECT 1 FROM leaf
		WHERE rid = ?
		  AND NOT EXISTS (
		    SELECT 1 FROM tagxref cx
		    JOIN tag ct ON ct.tagid = cx.tagid
		    WHERE ct.tagname = 'closed'
		      AND cx.rid = leaf.rid
		      AND cx.tagtype > 0
		  )
	`, int64(rid)).Scan(&one)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("checkout.isOpenLeaf: %w", err)
	}
	return true, nil
}

// branchOf returns the branch name for the given RID, "trunk" when the
// check-in carries no branch tag. A real query error is returned rather than
// masked as "trunk", so LeafAmbiguous surfaces it.
func branchOf(r *repo.Repo, rid libfossil.FslID) (string, error) {
	var branch string
	err := r.DB().QueryRow(`
		SELECT tx.value FROM tagxref tx
		JOIN tag t ON t.tagid = tx.tagid
		WHERE t.tagname = 'branch'
		  AND tx.rid = ?
		  AND tx.tagtype > 0
		ORDER BY tx.mtime DESC
		LIMIT 1
	`, int64(rid)).Scan(&branch)
	if errors.Is(err, sql.ErrNoRows) {
		return "trunk", nil // no branch tag → trunk
	}
	if err != nil {
		return "", fmt.Errorf("checkout.branchOf: %w", err)
	}
	return branch, nil
}