Artifact ae2bf1989893f97e948bbda113d25322175a826154aeea022f92767eee5fa58d:
- File
internal/checkout/status.go
— part of check-in
[858c20561f]
at
2026-10-19 22:58:21
on branch feat/submanifest-reader
— fix(undo): save and restore symlinks as fossil's undo does
Move checkout's link-aware diskView into vfile.Disk, so checkout, undo and stash share one RepoFILE view: Stat, Read, Write (which deletes a link before writing, makes a symlink when allow-symlinks is on, creates the directory and sets the executable bit as file_setexe does), SetExe, MTime and Remove. The allow-symlinks setting is read by vfile.OpenDisk.
undo.Save, Undo, Redo, Swap, ReadFile and RestoreFiles now take a Disk: a symlink is read as its target's name and recorded with isLink, and is deleted and made a symlink again on restore, as undo_save and undo_one do, so Undo of an Update no longer writes through a link. (user: [email protected], size: 3619) [annotate] [blame] [check-ins using] [more...]
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 }