Skip to content
go-libfossil Artifact [ae2bf19898]
Login

Artifact [ae2bf19898]

Artifact ae2bf1989893f97e948bbda113d25322175a826154aeea022f92767eee5fa58d:


package checkout

import (
	"fmt"

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

// HasChanges reports whether any file of the current version is added,
// removed, renamed or modified. It reads only the checkout database: an edit
// on disk counts once a scan has recorded it, but adds, removals and renames
// are always seen. Missing files are not, because that needs the disk.
//
// Panics if c is nil (TigerStyle precondition).
func (c *Checkout) HasChanges() (bool, error) {
	if c == nil {
		panic("checkout.HasChanges: nil *Checkout")
	}

	rid, _, err := c.Version()
	if err != nil {
		return false, fmt.Errorf("checkout.HasChanges: %w", err)
	}
	if rid < 0 {
		panic("checkout.HasChanges: negative checkout version")
	}
	rows, err := c.managedRows(rid)
	if err != nil {
		return false, fmt.Errorf("checkout.HasChanges: %w", err)
	}
	for _, r := range rows {
		if vfile.Classify(r, false) != ChangeNone {
			return true, nil
		}
	}
	return false, nil
}

// VisitChanges calls fn for each changed file of version vid, classified by
// vfile.Classify. With scan=true it first runs ScanChanges(0), fossil's
// default scan, and checks each file on disk, so edits and missing files are
// reported; with scan=false it reports what the checkout database records.
//
// 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")
	}
	if fn == nil {
		panic("checkout.VisitChanges: nil visitor")
	}

	if scan {
		if err := c.ScanChanges(0); err != nil {
			return fmt.Errorf("checkout.VisitChanges: %w", err)
		}
	}
	rows, err := c.managedRows(vid)
	if err != nil {
		return fmt.Errorf("checkout.VisitChanges: %w", err)
	}
	disk, err := c.newDiskView()
	if err != nil {
		return fmt.Errorf("checkout.VisitChanges: %w", err)
	}
	for _, r := range rows {
		missing := false
		if scan {
			if missing, err = c.isMissingOn(disk, r); err != nil {
				return fmt.Errorf("checkout.VisitChanges: %w", err)
			}
		}
		change := vfile.Classify(r, missing)
		if change == ChangeNone {
			continue
		}
		entry := ChangeEntry{
			Name:     r.Pathname,
			Change:   change,
			VFileID:  libfossil.FslID(r.ID),
			IsExec:   r.IsExe != 0,
			IsLink:   r.IsLink != 0,
			OrigName: r.RenamedFrom(),
		}
		if err := fn(entry); err != nil {
			return fmt.Errorf("checkout.VisitChanges: visitor for %s: %w", r.Pathname, err)
		}
	}
	return nil
}

// isMissing reports whether a row's file is absent from disk: anything
// that is not a regular file, a symlink followed to what it points to,
// counts. A removed row is never missing; it is meant to be gone.
func (c *Checkout) isMissing(r vfile.Row) (bool, error) {
	return c.isMissingOn(vfile.NewDisk(c.env, false), r)
}

// isMissingOn is isMissing seen through disk, the way fossil's changes
// command decides MISSING: with allow-symlinks on, a symlink is not missing
// whatever it points to.
func (c *Checkout) isMissingOn(disk vfile.Disk, r vfile.Row) (bool, error) {
	if r.Pathname == "" {
		panic("checkout.isMissingOn: empty pathname")
	}
	if c.env == nil {
		panic("checkout.isMissingOn: nil env")
	}
	if r.Deleted > 0 {
		return false, nil
	}
	fullPath, err := c.safePath(r.Pathname)
	if err != nil {
		return false, fmt.Errorf("path traversal in %s: %w", r.Pathname, err)
	}
	info, err := disk.Stat(fullPath)
	if err != nil {
		return false, err
	}
	return !isFileOrLink(info), nil
}