Artifact 08dc935375bc1f5d01b94cc976816b52504437f49b51978fa5efbf0a52ff24c1:
- File
internal/vfile/vfile.go
— part of check-in
[2d2e62d420]
at
2026-10-18 22:16:06
on branch feat/submanifest-reader
— fix(checkout): scan missing files and mode changes as fossil does
Port the remaining branches of fossil's vfile_check_signature. A missing file is marked changed with mtime -1, a removed file is marked changed, and the permission half sets chnged 6-9 for exec-bit and symlink changes (returning to 0 when undone), honouring the allow-symlinks setting: off, a symlink is seen as what it points to; on, as itself, its content its target's name.
Status reports the mode changes as fossil's changes command labels them (executable, symlink, unexec, unlink). Commit records the permission it finds on disk, l included, with a symlink's target as its content. Where the storage has no file modes (MemStorage) or on Windows, the recorded permissions are kept, as fossil on Windows keeps them. ScanENotFile now refuses a non-file where a tracked file belongs, and merge scans with it as fossil's does. Extract still writes a missing file without Force.
Interop tests run the same edits through fossil changes and go-libfossil status on one checkout, in both orders and with allow-symlinks on and off, and compare a commit's F cards with fossil's.
Tracked in #449 (user: [email protected], size: 5174) [annotate] [blame] [check-ins using] [more...]
// Package vfile reads a checkout's vfile table and says what each row means. // // The vfile table is fossil's record of a checkout: one row per tracked file. // Its columns encode state in ways that are easy to misread: rid=0 marks a // pending add, origname equals pathname on a row that was never renamed, // mrid differs from rid while a merge is pending, and chnged is not a flag: // 1 is an edit, 2 through 5 are merge and integrate outcomes, and 6 through 9 // are changes to the executable or symlink bit. Every caller that // needs to know what a row means asks this package instead of decoding the // columns itself, so fossil's encoding is interpreted in exactly one place. package vfile import ( "database/sql" "fmt" "github.com/danmestas/go-libfossil/db" ) // Change is how a tracked file differs from the checked-out version. type Change int const ( ChangeNone Change = iota ChangeAdded ChangeRemoved ChangeMissing ChangeRenamed ChangeModified ChangeExecutable // became executable (chnged 6) ChangeSymlink // became a symlink (chnged 7) ChangeUnexec // lost the executable bit (chnged 8) ChangeUnlink // was a symlink, now is not (chnged 9) ) // modeChanges maps chnged 6 through 9 to their change, in order. var modeChanges = [...]Change{ChangeExecutable, ChangeSymlink, ChangeUnexec, ChangeUnlink} // Row is one vfile row. type Row struct { ID int64 Pathname string Origname sql.NullString Chnged int64 Deleted int64 RID int64 MRID int64 // the artifact last written for the file; differs from RID mid-merge IsExe int64 IsLink int64 } // IsAdded reports a pending add: a file with no committed version (rid=0). // Fossil records an add with chnged left 0, so rid alone decides. func (r Row) IsAdded() bool { return r.RID == 0 } // IsRemoved reports a pending removal. func (r Row) IsRemoved() bool { return r.Deleted > 0 } // RenamedFrom returns the row's prior pathname when it records a rename, or // "". Fossil stores origname==pathname on rows that were never renamed. func (r Row) RenamedFrom() string { if !r.Origname.Valid { return "" } if r.Origname.String == r.Pathname { return "" } return r.Origname.String } // ContentChanged reports whether the file's content is not its committed // version: a pending add, an edit, or a merge outcome. func (r Row) ContentChanged() bool { if r.IsAdded() { return true } return r.Chnged > 0 } // PendingMerge reports a merge fossil has applied to the file but not yet // committed: a merge or integrate outcome (chnged 2 through 5), or merged-in // content (mrid differs from rid), which outlives the outcome code once the // file is edited on top of the merge. func (r Row) PendingMerge() bool { if r.Chnged >= 2 && r.Chnged <= 5 { return true } return r.MRID != r.RID } // ModeChanged reports a pending change to the executable or symlink bit // (chnged 6 through 9). func (r Row) ModeChanged() bool { return r.Chnged >= 6 && r.Chnged <= 9 } // Classify maps a row to its change type, in fossil's priority order (the // changes command): removed > missing > added > renamed > a change to the // executable or symlink bit > modified. missing reports that the file is // absent from disk; callers that have not looked pass false. One difference // from fossil: a renamed file that is also edited or has its mode changed is // Renamed, so callers that see only the change type keep the rename. func Classify(r Row, missing bool) Change { if r.Pathname == "" { panic("vfile.Classify: empty pathname") } if r.Chnged < 0 { panic("vfile.Classify: negative chnged for " + r.Pathname) } if r.IsRemoved() { return ChangeRemoved } if missing { return ChangeMissing } if r.IsAdded() { return ChangeAdded } if r.RenamedFrom() != "" { return ChangeRenamed } if r.ModeChanged() { return modeChanges[r.Chnged-6] } if r.Chnged > 0 { return ChangeModified } return ChangeNone } // Load reads every row of version vid, ordered by pathname as fossil's // changes command lists them. A NULL rid reads as 0 (a pending add) and a // NULL mrid as rid, the way the checkin code always read them. Rows are // collected up front so no cursor is held open while callers read files or // update rows. func Load(q db.Querier, vid int64) ([]Row, error) { if q == nil { panic("vfile.Load: nil querier") } if vid < 0 { panic("vfile.Load: negative vid") } rows, err := q.Query(` SELECT id, pathname, origname, CAST(chnged AS INTEGER), CAST(deleted AS INTEGER), coalesce(rid, 0), coalesce(mrid, rid, 0), CAST(isexe AS INTEGER), CAST(islink AS INTEGER) FROM vfile WHERE vid = ? ORDER BY pathname`, vid) if err != nil { return nil, fmt.Errorf("vfile.Load: query: %w", err) } defer rows.Close() var out []Row for rows.Next() { var r Row if err := rows.Scan( &r.ID, &r.Pathname, &r.Origname, &r.Chnged, &r.Deleted, &r.RID, &r.MRID, &r.IsExe, &r.IsLink, ); err != nil { return nil, fmt.Errorf("vfile.Load: scan: %w", err) } out = append(out, r) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("vfile.Load: iterate: %w", err) } return out, nil }