Compare commits

..
1 Commits
Author SHA1 Message Date
sneak 97875a82bc 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 now leave out of their own listing both the manifest they
write and a temp file an interrupted run left beside it, by file
identity (os.SameFile), so each is recognized however its path is
spelled, while an ordinary file of the same name elsewhere in the tree
is still listed.

Model: opus-5-5
2026-10-04 10:54:31 +00:00
6 changed files with 62 additions and 50 deletions
+9 -7
View File
@@ -28,6 +28,7 @@ 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"
@@ -682,7 +683,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, manifestTempPath(testOutput))
tmpExists, err := afero.Exists(fs, testOutputTmp)
require.NoError(t, err)
assert.False(t, tmpExists,
"temp file should not exist after successful generation")
@@ -714,7 +715,7 @@ func TestGenerateAtomicWriteOverwriteWithForce(t *testing.T) {
"manifest should be overwritten")
// Verify temp file does NOT exist
tmpExists, err := afero.Exists(fs, manifestTempPath(testOutput))
tmpExists, err := afero.Exists(fs, testOutputTmp)
require.NoError(t, err)
assert.False(t, tmpExists,
"temp file should not exist after successful generation")
@@ -821,8 +822,9 @@ func TestGenerateDefaultOutputLeftOutOfListing(t *testing.T) {
}
// TestGenerateLeavesLeftoverTempFileOutOfListing runs gen where an
// interrupted run left its temp file beside the output: the leftover is
// not listed, and gen does not fail when it overwrites it.
// 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.
func TestGenerateLeavesLeftoverTempFileOutOfListing(t *testing.T) {
t.Parallel()
@@ -831,7 +833,7 @@ func TestGenerateLeavesLeftoverTempFileOutOfListing(t *testing.T) {
fs := afero.NewOsFs()
writeTestFile(t, fs, filepath.Join(root, testFileTxt), "hello")
writeTestFile(t, fs, manifestTempPath(output), "part of a manifest")
writeTestFile(t, fs, output+".tmp", "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))
@@ -861,7 +863,7 @@ func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
exists, _ := afero.Exists(fs, testOutput)
assert.True(t, exists, "output file should exist")
tmpExists, _ := afero.Exists(fs, manifestTempPath(testOutput))
tmpExists, _ := afero.Exists(fs, testOutputTmp)
assert.False(t, tmpExists, "temp file should be cleaned up")
// Verify manifest is valid (not empty)
@@ -927,7 +929,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, manifestTempPath(testOutput))
tmpExists, _ := afero.Exists(baseFs, testOutputTmp)
assert.False(t, tmpExists,
"temp file should be cleaned up after failed generation")
}
+23 -17
View File
@@ -57,7 +57,8 @@ type freshenEntry struct {
type freshenScanner struct {
fs afero.Fs
absBase string
excluded []fs.FileInfo // files left out of the listing
manifestInfo fs.FileInfo // the manifest being freshened
tmpInfo fs.FileInfo // its leftover temp file; nil when there is none
includeDotfiles bool
followSymlinks bool
showProgress bool
@@ -181,10 +182,11 @@ func (s *freshenScanner) walk(path string, info fs.FileInfo, walkErr error) erro
info = realInfo
}
for _, excluded := range s.excluded {
if os.SameFile(info, excluded) {
return nil
}
// 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
}
s.scanCount++
@@ -309,7 +311,7 @@ func (h *freshenHasher) processEntry(e *freshenEntry) error {
func writeFreshenedManifest(
ctx context.Context, afs afero.Fs, builder *mfer.Builder, manifestPath string,
) error {
tmpPath := manifestTempPath(manifestPath)
tmpPath := manifestPath + ".tmp"
outFile, err := afs.Create(tmpPath)
if err != nil {
@@ -366,29 +368,33 @@ func (mfa *CLIApp) freshenScan(
startScan := time.Now()
showProgress := ctx.Bool("progress")
// 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
manifestInfo, err := mfa.Fs.Stat(manifestPath)
if err != nil {
return nil, 0, fmt.Errorf("freshen: %w", err)
}
for _, p := range []string{manifestPath, manifestTempPath(manifestPath)} {
info, err := mfa.Fs.Stat(p)
if err == nil {
excluded = append(excluded, info)
}
// 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
}
scanner := &freshenScanner{
fs: mfa.Fs,
absBase: absBase,
excluded: excluded,
manifestInfo: manifestInfo,
tmpInfo: tmpInfo,
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 beside it: the leftover is not
// listed.
// an interrupted run left its temp file, index.mf.tmp, 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, manifestTempPath(manifestPath), "part of a manifest")
writeTestFile(t, fs, manifestPath+".tmp", "part of a manifest")
// A new file gives freshen something to write.
writeTestFile(t, fs, filepath.Join(root, "added.txt"), "added")
+2 -5
View File
@@ -89,15 +89,12 @@ 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,
// Neither a manifest being replaced nor a temp file left by an
// interrupted run belongs in the new manifest.
ExcludePaths: []string{output, manifestTempPath(output)},
OutputPath: ctx.String("output"),
}
// Set seed for deterministic UUID if provided
@@ -221,7 +218,7 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
}
// Create temp file for atomic write
tmpPath := manifestTempPath(outputPath)
tmpPath := outputPath + ".tmp"
outFile, err := mfa.Fs.Create(tmpPath)
if err != nil {
-6
View File
@@ -32,12 +32,6 @@ 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")
+25 -12
View File
@@ -58,8 +58,15 @@ type ScannerOptions struct {
SigningOptions *SigningOptions
// Seed, if set, derives a deterministic UUID from this seed.
Seed string
// ExcludePaths lists files to leave out of the listing, matched with os.SameFile.
ExcludePaths []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
}
// FileEntry represents a file that has been enumerated.
@@ -78,7 +85,8 @@ type Scanner struct {
totalBytes FileSize // cached sum of all file sizes
options *ScannerOptions
fs afero.Fs
excluded []fs.FileInfo // the files named in ExcludePaths that exist
outputInfo fs.FileInfo // the existing output file; nil when there is none
tmpInfo fs.FileInfo // its leftover temp file; nil when there is none
}
// NewScanner creates a new Scanner with default options.
@@ -103,12 +111,17 @@ func NewScannerWithOptions(opts *ScannerOptions) *Scanner {
fs: fs,
}
// 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 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)
if err == nil {
s.excluded = append(s.excluded, info)
s.outputInfo = info
}
info, err = s.fs.Stat(opts.OutputPath + ".tmp")
if err == nil {
s.tmpInfo = info
}
}
@@ -450,10 +463,10 @@ func (s *Scanner) enumerateFileWithInfo(
info = realInfo
}
for _, excluded := range s.excluded {
if os.SameFile(info, excluded) {
return nil
}
// 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
}
entry := &FileEntry{