1 Commits
Author SHA1 Message Date
sneak b8c699adee Never list index.mf at the top of the tree in gen and freshen (closes #233)
check / check (push) Waiting to run
fetch saves the manifest as index.mf at the top of the tree and refuses a
manifest that lists a file there. gen given --output, and freshen with its
manifest outside the base directory, listed such a file, so they wrote
manifests fetch would not take.

Both now leave out each file the manifest would list as index.mf: one at
the top of the tree, or a file given to gen by that name. The rule goes by
that path alone and looks up no file, so a symlink named index.mf never
hides the file it points to. index.mf in a subdirectory is still listed.
The scanner in package mfer gets an option to leave out files by the path
they would be listed by.

Model: opus-5-5
2026-10-15 22:22:04 +00:00
3 changed files with 1 additions and 158 deletions
-3
View File
@@ -304,9 +304,6 @@ are now tracked only in the [issues](https://git.eeqj.de/sneak/mfer/issues).
`--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
- never reports `index.mf` at the top of the base directory as a file the
manifest does not list, even when given another manifest or a URL, since
gen and fetch write the manifest there
- `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
+1 -8
View File
@@ -195,10 +195,7 @@ func countCheckFailures(
// findExtraFiles reports files present on disk but absent from the
// manifest, and anything the search cannot read: each is a failure, logged
// at error level, under --no-extra-files, otherwise a warning. index.mf at
// the top of the base directory is never reported as absent from the
// manifest, even when check is given another manifest or a URL, since gen
// and fetch write the manifest there.
// at error level, under --no-extra-files, otherwise a warning.
func findExtraFiles(
ctx context.Context, cmd *cli.Command, chk *mfer.Checker, failures *int64,
) error {
@@ -207,10 +204,6 @@ func findExtraFiles(
go func() {
for result := range extraResults {
if result.Status == mfer.StatusExtra && result.Path == defaultManifestName {
continue
}
if cmd.Bool("no-extra-files") {
*failures++
-147
View File
@@ -1191,99 +1191,6 @@ func TestCheckURLManifestWithoutTempDirectory(t *testing.T) {
assert.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
}
// TestCheckNeverReportsIndexMFAtTopOfBase checks a tree holding index.mf at
// its top, where gen and fetch write the manifest, against a manifest that
// does not list it, given by URL and by a path outside the tree: check never
// reports it, and passes with and without --no-extra-files. index.mf in a
// subdirectory, and the temp file fetch saves index.mf through, are still
// reported.
func TestCheckNeverReportsIndexMFAtTopOfBase(t *testing.T) {
t.Parallel()
files := map[string][]byte{testFileTxt: []byte("hello")}
manifest := manifestOf(t, files)
server := httptest.NewServer(fetchTestHandler(manifest, files))
t.Cleanup(server.Close)
manifestOutsideTree := filepath.Join(t.TempDir(), "other.mf")
require.NoError(t, os.WriteFile(manifestOutsideTree, manifest, 0o600))
for name, manifestArg := range map[string]string{
"manifest by URL": server.URL + "/" + defaultManifestName,
"manifest by path": manifestOutsideTree,
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
base := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(base, testFileTxt), files[testFileTxt], 0o600))
require.NoError(t, os.WriteFile(
filepath.Join(base, defaultManifestName), manifest, 0o600))
check := func(flags ...string) (int, string) {
opts := testOpts(slices.Concat(
[]string{testApp, cmdCheck}, flags,
[]string{testFlagBase, base, manifestArg},
), afero.NewOsFs())
return runCLI(opts), testStderr(t, opts)
}
exitCode, stderr := check()
assert.Equal(t, 0, exitCode, "stderr: %s", stderr)
assert.NotContains(t, stderr, "EXTRA: "+defaultManifestName)
exitCode, stderr = check(testFlagNoExtra)
assert.Equal(t, 0, exitCode, "stderr: %s", stderr)
assert.NotContains(t, stderr, "EXTRA: "+defaultManifestName)
reported := []string{
testSubdirManifest,
tempPathFor(defaultManifestName),
}
require.NoError(t, os.Mkdir(filepath.Join(base, testSubdir), 0o750))
for _, path := range reported {
require.NoError(t,
os.WriteFile(filepath.Join(base, path), manifest, 0o600))
}
exitCode, stderr = check(testFlagNoExtra)
assert.Equal(t, 1, exitCode, "stderr: %s", stderr)
assert.NotContains(t, stderr, "EXTRA: "+defaultManifestName)
for _, path := range reported {
assert.Contains(t, stderr, "EXTRA: "+path)
}
})
}
}
// TestCheckChecksListedIndexMF checks a tree against a manifest that lists
// index.mf at the top of the tree, where that file has changed since: check
// reports it like any other changed file.
func TestCheckChecksListedIndexMF(t *testing.T) {
t.Parallel()
manifestPath := filepath.Join(t.TempDir(), "other.mf")
require.NoError(t, os.WriteFile(manifestPath, manifestOf(t, map[string][]byte{
defaultManifestName: []byte("listed"),
}), 0o600))
base := t.TempDir()
require.NoError(t, os.WriteFile(
filepath.Join(base, defaultManifestName), []byte("edited"), 0o600))
opts := testOpts([]string{
testApp, cmdCheck, testFlagBase, base, manifestPath,
}, afero.NewOsFs())
assert.Equal(t, 1, runCLI(opts), "stderr: %s", testStderr(t, opts))
assert.Contains(t, testStderr(t, opts), "HASH_MISMATCH: "+defaultManifestName)
}
// unlistableDirFs is a filesystem on which one directory cannot be listed.
type unlistableDirFs struct {
afero.Fs
@@ -1332,60 +1239,6 @@ func TestCheckWithUnlistableDirectory(t *testing.T) {
assert.Contains(t, testStderr(t, opts), "unlisted.txt")
}
// TestCheckReportsUnreadableIndexMFAtTopOfBase has a directory named
// index.mf at the top of the base that cannot be listed. check leaves out
// only the report that index.mf is not in the manifest, so it reports the
// error and fails under --no-extra-files.
func TestCheckReportsUnreadableIndexMFAtTopOfBase(t *testing.T) {
t.Parallel()
unreadable := filepath.Join(testDir, defaultManifestName)
mem := afero.NewMemMapFs()
require.NoError(t, mem.MkdirAll(unreadable, 0o755))
writeTestFile(t, mem, testFile1, "hello")
opts := testOpts([]string{
testApp, cmdGenerate, "-q", "-o", testManifest, testDir,
}, mem)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
fs := unlistableDirFs{Fs: mem, dir: unreadable}
opts = testOpts([]string{
testApp, cmdCheck, testFlagNoExtra, testFlagBase, testDir, testManifest,
}, fs)
assert.Equal(t, 1, runCLI(opts), "stderr: %s", testStderr(t, opts))
assert.Contains(t, testStderr(t, opts), "ERROR: "+defaultManifestName)
}
// TestCheckReportsUpperCaseIndexMFAtTopOfBase checks a tree holding
// INDEX.MF at its top against a manifest that does not list it. Only the
// name index.mf, in exactly that letter case, is left out, so check reports
// INDEX.MF. The tree is in memory, where the two names are two files
// whatever the letter case rules of the host's filesystem.
func TestCheckReportsUpperCaseIndexMFAtTopOfBase(t *testing.T) {
t.Parallel()
upperCase := strings.ToUpper(defaultManifestName)
fs := afero.NewMemMapFs()
writeTestFile(t, fs, testFile1, "hello")
opts := testOpts([]string{
testApp, cmdGenerate, "-q", "-o", testManifest, testDir,
}, fs)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
writeTestFile(t, fs, filepath.Join(testDir, upperCase), "not listed")
opts = testOpts([]string{
testApp, cmdCheck, testFlagNoExtra, testFlagBase, testDir, testManifest,
}, fs)
assert.Equal(t, 1, runCLI(opts), "stderr: %s", testStderr(t, opts))
assert.Contains(t, testStderr(t, opts), "EXTRA: "+upperCase)
}
func TestGenerateAtomicWriteNoTempFileOnSuccess(t *testing.T) {
t.Parallel()