Compare commits

..
1 Commits
Author SHA1 Message Date
sneak 0afcbae783 Default the manifest to index.mf and keep it out of its own listing (closes #100)
check / check (push) Failing after 1s
mfer gen now writes index.mf instead of .index.mf, the name fetch
requests and the README calls the standard filename. Given a
directory, check, freshen, list and export look only for index.mf;
.index.mf is no longer recognized. Because index.mf is not hidden, gen
and freshen leave out of their own listing the manifest they write and
a temp file an interrupted run left beside it, matched by file
identity (os.SameFile) however the path is spelled. The scanner takes
these as a plain ExcludePaths list; manifestTempPath alone names the
temp file, for both writes, both skips and the tests.

Model: opus-5-5
2026-10-04 11:20:19 +00:00
6 changed files with 50 additions and 62 deletions
+7 -9
View File
@@ -28,7 +28,6 @@ const (
testFile1 = "/testdir/file1.txt"
testMF = "/testdir/test.mf"
testOutput = "/output.mf"
testOutputTmp = "/output.mf.tmp"
testManifest = "/manifest.mf"
testFlagBase = "--base"
testFlagNoExtra = "--no-extra-files"
@@ -683,7 +682,7 @@ func TestGenerateAtomicWriteNoTempFileOnSuccess(t *testing.T) {
assert.True(t, exists, "output file should exist")
// Verify temp file does NOT exist
tmpExists, err := afero.Exists(fs, testOutputTmp)
tmpExists, err := afero.Exists(fs, manifestTempPath(testOutput))
require.NoError(t, err)
assert.False(t, tmpExists,
"temp file should not exist after successful generation")
@@ -715,7 +714,7 @@ func TestGenerateAtomicWriteOverwriteWithForce(t *testing.T) {
"manifest should be overwritten")
// Verify temp file does NOT exist
tmpExists, err := afero.Exists(fs, testOutputTmp)
tmpExists, err := afero.Exists(fs, manifestTempPath(testOutput))
require.NoError(t, err)
assert.False(t, tmpExists,
"temp file should not exist after successful generation")
@@ -822,9 +821,8 @@ func TestGenerateDefaultOutputLeftOutOfListing(t *testing.T) {
}
// TestGenerateLeavesLeftoverTempFileOutOfListing runs gen where an
// interrupted run left its temp file, index.mf.tmp, beside the output:
// the leftover is not listed, and gen does not fail when it overwrites
// it.
// interrupted run left its temp file beside the output: the leftover is
// not listed, and gen does not fail when it overwrites it.
func TestGenerateLeavesLeftoverTempFileOutOfListing(t *testing.T) {
t.Parallel()
@@ -833,7 +831,7 @@ func TestGenerateLeavesLeftoverTempFileOutOfListing(t *testing.T) {
fs := afero.NewOsFs()
writeTestFile(t, fs, filepath.Join(root, testFileTxt), "hello")
writeTestFile(t, fs, output+".tmp", "part of a manifest")
writeTestFile(t, fs, manifestTempPath(output), "part of a manifest")
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", output, root}, fs)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
@@ -863,7 +861,7 @@ func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
exists, _ := afero.Exists(fs, testOutput)
assert.True(t, exists, "output file should exist")
tmpExists, _ := afero.Exists(fs, testOutputTmp)
tmpExists, _ := afero.Exists(fs, manifestTempPath(testOutput))
assert.False(t, tmpExists, "temp file should be cleaned up")
// Verify manifest is valid (not empty)
@@ -929,7 +927,7 @@ func TestGenerateAtomicWriteCleansUpOnError(t *testing.T) {
"output file should not exist after failed generation (atomic write)")
// Temp file should also not exist
tmpExists, _ := afero.Exists(baseFs, testOutputTmp)
tmpExists, _ := afero.Exists(baseFs, manifestTempPath(testOutput))
assert.False(t, tmpExists,
"temp file should be cleaned up after failed generation")
}
+17 -23
View File
@@ -57,8 +57,7 @@ type freshenEntry struct {
type freshenScanner struct {
fs afero.Fs
absBase string
manifestInfo fs.FileInfo // the manifest being freshened
tmpInfo fs.FileInfo // its leftover temp file; nil when there is none
excluded []fs.FileInfo // files left out of the listing
includeDotfiles bool
followSymlinks bool
showProgress bool
@@ -182,11 +181,10 @@ func (s *freshenScanner) walk(path string, info fs.FileInfo, walkErr error) erro
info = realInfo
}
// Skip the manifest file itself and a temp file left beside it by an
// interrupted run, however their paths are spelled; gen leaves out its
// output file the same way. os.SameFile never matches a nil FileInfo.
if os.SameFile(info, s.manifestInfo) || os.SameFile(info, s.tmpInfo) {
return nil
for _, excluded := range s.excluded {
if os.SameFile(info, excluded) {
return nil
}
}
s.scanCount++
@@ -311,7 +309,7 @@ func (h *freshenHasher) processEntry(e *freshenEntry) error {
func writeFreshenedManifest(
ctx context.Context, afs afero.Fs, builder *mfer.Builder, manifestPath string,
) error {
tmpPath := manifestPath + ".tmp"
tmpPath := manifestTempPath(manifestPath)
outFile, err := afs.Create(tmpPath)
if err != nil {
@@ -368,33 +366,29 @@ func (mfa *CLIApp) freshenScan(
startScan := time.Now()
showProgress := ctx.Bool("progress")
manifestInfo, err := mfa.Fs.Stat(manifestPath)
if err != nil {
return nil, 0, fmt.Errorf("freshen: %w", err)
}
// Leave out the manifest and a temp file left by an interrupted run,
// as gen does. A path that cannot be stat'd, normally because no file
// is there, needs no leaving out.
var excluded []fs.FileInfo
// The temp file writeFreshenedManifest writes, if an interrupted run
// left one. One that cannot be stat'd, normally because it does not
// exist, needs no leaving out.
var tmpInfo fs.FileInfo
info, err := mfa.Fs.Stat(manifestPath + ".tmp")
if err == nil {
tmpInfo = info
for _, p := range []string{manifestPath, manifestTempPath(manifestPath)} {
info, err := mfa.Fs.Stat(p)
if err == nil {
excluded = append(excluded, info)
}
}
scanner := &freshenScanner{
fs: mfa.Fs,
absBase: absBase,
manifestInfo: manifestInfo,
tmpInfo: tmpInfo,
excluded: excluded,
includeDotfiles: ctx.Bool("include-dotfiles"),
followSymlinks: ctx.Bool("follow-symlinks"),
showProgress: showProgress,
existingByPath: existingByPath,
}
err = afero.Walk(mfa.Fs, absBase, scanner.walk)
err := afero.Walk(mfa.Fs, absBase, scanner.walk)
if showProgress {
log.ProgressDone()
+3 -3
View File
@@ -133,8 +133,8 @@ func TestFreshenLeavesManifestOutOfListing(t *testing.T) {
}
// TestFreshenLeavesLeftoverTempFileOutOfListing freshens a manifest where
// an interrupted run left its temp file, index.mf.tmp, beside it: the
// leftover is not listed.
// an interrupted run left its temp file beside it: the leftover is not
// listed.
func TestFreshenLeavesLeftoverTempFileOutOfListing(t *testing.T) {
t.Parallel()
@@ -147,7 +147,7 @@ func TestFreshenLeavesLeftoverTempFileOutOfListing(t *testing.T) {
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", manifestPath, root}, fs)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
writeTestFile(t, fs, manifestPath+".tmp", "part of a manifest")
writeTestFile(t, fs, manifestTempPath(manifestPath), "part of a manifest")
// A new file gives freshen something to write.
writeTestFile(t, fs, filepath.Join(root, "added.txt"), "added")
+5 -2
View File
@@ -89,12 +89,15 @@ func (mfa *CLIApp) collectInputPaths(args cli.Args) ([]string, error) {
// buildScannerOptions constructs scanner options from the CLI flags.
func (mfa *CLIApp) buildScannerOptions(ctx *cli.Context) *mfer.ScannerOptions {
output := ctx.String("output")
opts := &mfer.ScannerOptions{
IncludeDotfiles: ctx.Bool("include-dotfiles"),
FollowSymLinks: ctx.Bool("follow-symlinks"),
IncludeTimestamps: ctx.Bool("include-timestamps"),
Fs: mfa.Fs,
OutputPath: ctx.String("output"),
// Neither a manifest being replaced nor a temp file left by an
// interrupted run belongs in the new manifest.
ExcludePaths: []string{output, manifestTempPath(output)},
}
// Set seed for deterministic UUID if provided
@@ -218,7 +221,7 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
}
// Create temp file for atomic write
tmpPath := outputPath + ".tmp"
tmpPath := manifestTempPath(outputPath)
outFile, err := mfa.Fs.Create(tmpPath)
if err != nil {
+6
View File
@@ -32,6 +32,12 @@ const (
defaultManifestName = "index.mf"
)
// manifestTempPath returns the temp file gen and freshen write a manifest
// to before renaming it to out.
func manifestTempPath(out string) string {
return out + ".tmp"
}
// errUnknownCommand indicates an unrecognized command argument.
var errUnknownCommand = errors.New("unknown command")
+12 -25
View File
@@ -58,15 +58,8 @@ type ScannerOptions struct {
SigningOptions *SigningOptions
// Seed, if set, derives a deterministic UUID from this seed.
Seed string
// OutputPath, if set, is the file the manifest will be written to.
// Enumeration leaves out that file and OutputPath + ".tmp", the temp
// file mfer gen writes first, if they exist when the scanner is
// created, however their paths are spelled (through a symlink, a hard
// link, or in different letter case), so neither a manifest being
// replaced nor a temp file left by an interrupted run is ever listed.
// Files are matched with os.SameFile, which only recognizes files on
// the operating system's filesystem.
OutputPath string
// ExcludePaths lists files to leave out of the listing, matched with os.SameFile.
ExcludePaths []string
}
// FileEntry represents a file that has been enumerated.
@@ -85,8 +78,7 @@ type Scanner struct {
totalBytes FileSize // cached sum of all file sizes
options *ScannerOptions
fs afero.Fs
outputInfo fs.FileInfo // the existing output file; nil when there is none
tmpInfo fs.FileInfo // its leftover temp file; nil when there is none
excluded []fs.FileInfo // the files named in ExcludePaths that exist
}
// NewScanner creates a new Scanner with default options.
@@ -111,17 +103,12 @@ func NewScannerWithOptions(opts *ScannerOptions) *Scanner {
fs: fs,
}
if opts.OutputPath != "" {
// A file that cannot be stat'd, normally because it does not
// exist, needs no leaving out.
info, err := s.fs.Stat(opts.OutputPath)
// A path that cannot be stat'd, normally because no file is there,
// needs no leaving out.
for _, p := range opts.ExcludePaths {
info, err := s.fs.Stat(p)
if err == nil {
s.outputInfo = info
}
info, err = s.fs.Stat(opts.OutputPath + ".tmp")
if err == nil {
s.tmpInfo = info
s.excluded = append(s.excluded, info)
}
}
@@ -463,10 +450,10 @@ func (s *Scanner) enumerateFileWithInfo(
info = realInfo
}
// Neither the manifest being written nor its temp file is one of the
// files it lists. os.SameFile never matches a nil FileInfo.
if os.SameFile(info, s.outputInfo) || os.SameFile(info, s.tmpInfo) {
return nil
for _, excluded := range s.excluded {
if os.SameFile(info, excluded) {
return nil
}
}
entry := &FileEntry{