Artifact b1de0bbdacf906923be4e22db0ccf8835cc88de7fd8bd9288208825e083a7683:
- File
internal/checkout/perm.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: 4518) [annotate] [blame] [check-ins using] [more...]
package checkout import ( "errors" "fmt" "os" "runtime" ) // filePerm is a file's permission as fossil records it: fossil's PERM_REG, // PERM_EXE and PERM_LNK. type filePerm int const ( permReg filePerm = iota permExe permLnk ) // recordedPerm is the permission vfile records for a row, as fossil's scan // reads it: isexe wins over islink. func recordedPerm(isexe, islink int64) filePerm { if isexe != 0 { return permExe } if islink != 0 { return permLnk } return permReg } // manifestPerm is the F-card permission for p. func (p filePerm) manifestPerm() string { switch p { case permExe: return "x" case permLnk: return "l" } return "" } // linkStorage is the optional storage half that has file modes and symbolic // links. Storages without them (memory, browser) do not have it. type linkStorage interface { Lstat(path string) (os.FileInfo, error) Readlink(path string) (string, error) } // diskView is how a checkout sees files on disk, as fossil's RepoFILE does. type diskView struct { // links is the storage's link half, nil when permissions are not read // from disk: a storage without modes, or Windows, where fossil keeps the // recorded permissions. links linkStorage // symlinks is fossil's allow-symlinks setting where links are read: // a symlink is then seen as itself, and otherwise as what it points to. symlinks bool } // newDiskView reads the view for the checkout's storage and repository. func (c *Checkout) newDiskView() (diskView, error) { links, ok := c.env.Storage.(linkStorage) if !ok || runtime.GOOS == "windows" { return diskView{}, nil } on, err := c.boolSetting("allow-symlinks", false) if err != nil { return diskView{}, err } return diskView{links: links, symlinks: on}, nil } // stat is fossil's file stat for a checkout file: it does not follow a // symlink while allow-symlinks is on. A missing file returns nil info and no // error. func (v diskView) stat(c *Checkout, fullPath string) (os.FileInfo, error) { var info os.FileInfo var err error if v.symlinks { info, err = v.links.Lstat(fullPath) } else { info, err = c.env.Storage.Stat(fullPath) } if errors.Is(err, os.ErrNotExist) { return nil, nil } if err != nil { return nil, fmt.Errorf("stat %s: %w", fullPath, err) } return info, nil } // isFileOrLink is fossil's file_isfile_or_link on info from stat. func isFileOrLink(info os.FileInfo) bool { return info != nil && (info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0) } // perm is fossil's file_perm on info from stat, or recorded where // permissions are not read from disk. A missing file is permReg. func (v diskView) perm(info os.FileInfo, recorded filePerm) filePerm { if v.links == nil { return recorded } if info == nil { return permReg } if modeIsExecutable(info.Mode()) { return permExe } if v.symlinks && info.Mode()&os.ModeSymlink != 0 { return permLnk } return permReg } // read returns a checkout file's content as fossil reads it: a symlink seen // as itself holds the name of its target. func (v diskView) read(c *Checkout, fullPath string, info os.FileInfo) ([]byte, error) { if info != nil && info.Mode()&os.ModeSymlink != 0 { if v.links == nil { panic("checkout.diskView.read: a symlink seen without link support") } target, err := v.links.Readlink(fullPath) if err != nil { return nil, fmt.Errorf("readlink %s: %w", fullPath, err) } return []byte(target), nil } data, err := c.env.Storage.ReadFile(fullPath) if err != nil { return nil, fmt.Errorf("read %s: %w", fullPath, err) } return data, nil } // nextPermState is the permission half of fossil's vfile_check_signature: // the chnged value its scan stores given the content half's verdict chnged // and the file's recorded and current permission. Becoming or ceasing to be // a symlink overrides any other change; a change of the executable bit is // recorded only on a file not otherwise changed, and a mode change that has // been undone returns to 0. func nextPermState(chnged int64, orig, current filePerm) int64 { if chnged < 0 { panic("checkout.nextPermState: negative chnged") } switch { case orig != permLnk && current == permLnk: return 7 case orig == permLnk && current != permLnk: return 9 case chnged != 0 && (chnged < 6 || chnged > 9): return chnged case orig == current: return 0 case current == permExe: return 6 case orig == permExe: return 8 } panic("checkout.nextPermState: permissions differ with neither a symlink nor exec") }