Report only symlinks to regular files as extra files in check (closes #192)
check / check (push) Waiting to run

gen never lists a symlink to a directory or a broken one, and lists a
symlink to a regular file only with --follow-symlinks. check's search for
extra files reported every symlink, so on a tree holding a directory
symlink no manifest gen writes could pass check --no-extra-files. The
search now resolves a symlink and reports an entry only if it is a
regular file, which also covers the pipes, sockets and devices it already
left out. The README's check entry says what is reported.

Judgement call: one regular-file check replaces the separate test for
pipes, sockets and devices, so a file of unknown type is not reported
either; gen does not list one.

Model: opus-5-5
This commit is contained in:
2026-10-13 17:46:53 +00:00
parent 24496880c5
commit b4ab154f20
3 changed files with 66 additions and 13 deletions
+6 -3
View File
@@ -289,9 +289,12 @@ are now tracked only in the [issues](https://git.eeqj.de/sneak/mfer/issues).
else the directory holding the manifest, or the current directory for a
manifest given by URL. So `mfer check /media/drive` checks a drive against
the `index.mf` at its root, from any directory
- warns about each file under the base directory that the manifest does not
list, hidden files included; with `--no-extra-files` each one is a failure
instead
- warns about each regular file under the base directory that the manifest
does not list, hidden files included, and each symlink there to a regular
file, which gen lists only with `--follow-symlinks`; with
`--no-extra-files` each one is a failure instead. A named pipe, socket or
device, a symlink to a directory and a broken symlink are never reported,
since gen cannot list them
- `mfer freshen` / `mfer freshen .`
- rewrites `index.mf` to list the files now under the directory holding it,
or under the one `--base` names, hashing only the files that are new or
+47
View File
@@ -960,6 +960,53 @@ func TestCheckWarnsAboutUnlistedFiles(t *testing.T) {
}
}
// TestCheckReportsUnlistedSymlinkToFileOnly makes a tree holding a symlink
// to a directory, one to a file and a broken one. gen lists the symlink to
// the file only with --follow-symlinks, and never lists the other two, so
// check --no-extra-files passes on the manifest gen --follow-symlinks writes
// and fails only on the symlink to the file on the one plain gen writes.
func TestCheckReportsUnlistedSymlinkToFileOnly(t *testing.T) {
t.Parallel()
for name, tc := range map[string]struct {
genFlags []string
exitCode int
extras []string // what check reports as not in the manifest
}{
"gen --follow-symlinks": {[]string{"--follow-symlinks"}, 0, nil},
"plain gen": {nil, 1, []string{"filelink"}},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
fs := afero.NewOsFs()
writeTestTree(t, fs, dir)
require.NoError(t, os.Symlink(testSubdir, filepath.Join(dir, "dirlink")))
require.NoError(t, os.Symlink(testFileTxt, filepath.Join(dir, "filelink")))
require.NoError(t,
os.Symlink("nowhere", filepath.Join(dir, "brokenlink")))
opts := testOpts(slices.Concat(
[]string{testApp, cmdGenerate, "-q"}, tc.genFlags, []string{dir},
), fs)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
opts = testOpts([]string{testApp, cmdCheck, testFlagNoExtra, dir}, fs)
exitCode := runCLI(opts)
stderr := testStderr(t, opts)
assert.Equal(t, tc.exitCode, exitCode, "stderr: %s", stderr)
assert.Equal(t, len(tc.extras),
strings.Count(stderr, "not in manifest"), stderr)
for _, extra := range tc.extras {
assert.Contains(t, stderr, "EXTRA: "+extra)
}
})
}
}
// TestCheckNeverReportsManifest keeps the manifest inside the checked tree,
// under several names and path spellings, and names the tree both directly
// and through a symlink: the manifest is never reported, even under
+13 -10
View File
@@ -264,11 +264,12 @@ func (c *Checker) Check(
return nil
}
// FindExtraFiles walks the filesystem and reports files not in the manifest,
// hidden files and directories included. The manifest file itself is not
// reported. Anything the search cannot read, such as a directory that cannot
// be listed, is reported with StatusError and the search goes on. Results are
// sent to the results channel. The channel is closed when done.
// FindExtraFiles walks the filesystem and reports the regular files, and
// symlinks to them, that are not in the manifest, hidden files and
// directories included. The manifest file itself is not reported. Anything
// the search cannot read, such as a directory that cannot be listed, is
// reported with StatusError and the search goes on. Results are sent to the
// results channel. The channel is closed when done.
func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) error {
if results != nil {
defer close(results)
@@ -318,11 +319,6 @@ func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) err
return nil
}
// gen never lists a named pipe, a socket or a device.
if info.Mode()&(os.ModeNamedPipe|os.ModeSocket|os.ModeDevice) != 0 {
return nil
}
// A symlink is compared by what it points to, so a manifest reached
// through one is not reported either.
if info.Mode()&os.ModeSymlink != 0 {
@@ -332,6 +328,13 @@ func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) err
}
}
// gen lists regular files and symlinks to them only, so a named
// pipe, a socket, a device, a symlink to a directory and a broken
// symlink are never reported.
if !info.Mode().IsRegular() {
return nil
}
// Skip the manifest file itself, however its path is spelled
if os.SameFile(info, c.manifestInfo) {
return nil