Skip to content
go-libfossil Artifact [dbc67bfe8d]
Login

Artifact [dbc67bfe8d]

Artifact dbc67bfe8d79f5b4f5293c18e89a03613c13fe16d78286819bf21ca05e20b5f6:


package checkout

import (
	"database/sql"
	"fmt"

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

// HasChanges returns true if the checkout has any modified, deleted, or renamed files.
// This is a DB-only check that does NOT scan the filesystem.
// Use ScanChanges first if you need to detect on-disk modifications.
//
// Panics if c is nil (TigerStyle precondition).
func (c *Checkout) HasChanges() (bool, error) {
	if c == nil {
		panic("checkout.HasChanges: nil *Checkout")
	}

	// Get current checkout version
	rid, _, err := c.Version()
	if err != nil {
		return false, fmt.Errorf("checkout.HasChanges: %w", err)
	}

	// Check for any changed, deleted, or renamed files. Fossil stores
	// origname==pathname on rows that were never renamed, so only a
	// different origname counts.
	var count int
	err = c.db.QueryRow(`
		SELECT count(*) FROM vfile
		WHERE (chnged > 0 OR deleted > 0 OR origname NOT IN ('', pathname)) AND vid = ?
	`, int64(rid)).Scan(&count)
	if err != nil {
		return false, fmt.Errorf("checkout.HasChanges: query: %w", err)
	}

	return count > 0, nil
}

// VisitChanges iterates over all changed files in the checkout, calling fn for each.
// If scan=true, calls ScanChanges(ScanHash) first to detect on-disk modifications.
// Otherwise, only reports files marked as changed in the vfile table.
//
// The function classifies each changed file:
// - deleted > 0 → ChangeRemoved
// - rid == 0 → ChangeAdded (newly managed file, not yet committed)
// - origname != "" and != pathname → ChangeRenamed
// - chnged > 0 → ChangeModified
//
// If fn returns a non-nil error, iteration stops and that error is returned.
//
// Panics if c is nil (TigerStyle precondition).
func (c *Checkout) VisitChanges(vid libfossil.FslID, scan bool, fn ChangeVisitor) error {
	if c == nil {
		panic("checkout.VisitChanges: nil *Checkout")
	}

	// Scan filesystem if requested
	if scan {
		if err := c.ScanChanges(ScanHash); err != nil {
			return fmt.Errorf("checkout.VisitChanges: %w", err)
		}
	}

	// Query all changed/deleted/renamed files
	rows, err := c.db.Query(`
		SELECT id, pathname, CAST(chnged AS INTEGER), CAST(deleted AS INTEGER),
		       CAST(isexe AS INTEGER), CAST(islink AS INTEGER), origname, rid
		FROM vfile
		WHERE vid = ? AND (chnged > 0 OR deleted > 0 OR origname NOT IN ('', pathname))
	`, int64(vid))
	if err != nil {
		return fmt.Errorf("checkout.VisitChanges: query: %w", err)
	}
	defer rows.Close()

	// Iterate over each changed file
	for rows.Next() {
		var id, chnged, deleted, isexe, islink, rid int64
		var pathname string
		var origname sql.NullString

		if err := rows.Scan(
			&id, &pathname, &chnged, &deleted,
			&isexe, &islink, &origname, &rid,
		); err != nil {
			return fmt.Errorf("checkout.VisitChanges: scan: %w", err)
		}

		changeType := classifyChange(pathname, origname, chnged, deleted, rid)
		if changeType == ChangeNone {
			// The WHERE clause and classifyChange disagree, which only a
			// malformed row can cause (e.g. a non-integer chnged).
			return fmt.Errorf("checkout.VisitChanges: malformed vfile row for %s", pathname)
		}

		// Build change entry
		entry := ChangeEntry{
			Name:     pathname,
			Change:   changeType,
			VFileID:  libfossil.FslID(id),
			IsExec:   isexe != 0,
			IsLink:   islink != 0,
			OrigName: origname.String,
		}

		// Call visitor function
		if err := fn(entry); err != nil {
			return fmt.Errorf("checkout.VisitChanges: visitor for %s: %w", pathname, err)
		}
	}

	if err := rows.Err(); err != nil {
		return fmt.Errorf("checkout.VisitChanges: iterate: %w", err)
	}

	return nil
}

// classifyChange maps a vfile row to its change type, in priority order
// deleted > added > renamed > modified. Fossil stores origname==pathname on
// rows that were never renamed, so only a different origname is a rename.
func classifyChange(
	pathname string, origname sql.NullString, chnged, deleted, rid int64,
) FileChange {
	if pathname == "" {
		panic("checkout.classifyChange: empty pathname")
	}
	if deleted > 0 {
		return ChangeRemoved
	}
	if rid == 0 {
		return ChangeAdded
	}
	if origname.Valid {
		if origname.String != "" && origname.String != pathname {
			return ChangeRenamed
		}
	}
	if chnged > 0 {
		return ChangeModified
	}
	return ChangeNone
}