Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
114ba0a29f |
+1
-1
@@ -8,7 +8,7 @@ vendor.tzst
|
|||||||
modcache.tzst
|
modcache.tzst
|
||||||
|
|
||||||
# Generated manifest files
|
# Generated manifest files
|
||||||
/index.mf
|
.index.mf
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ linters:
|
|||||||
- depguard # Dependency allow/block lists
|
- depguard # Dependency allow/block lists
|
||||||
- godot # Requires comments to end with periods
|
- godot # Requires comments to end with periods
|
||||||
- wsl # Deprecated, replaced by wsl_v5
|
- wsl # Deprecated, replaced by wsl_v5
|
||||||
|
- gomodguard # Deprecated, replaced by gomodguard_v2
|
||||||
- wrapcheck # Too verbose for internal packages
|
- wrapcheck # Too verbose for internal packages
|
||||||
- varnamelen # Short names like db, id are idiomatic Go
|
- varnamelen # Short names like db, id are idiomatic Go
|
||||||
settings:
|
settings:
|
||||||
|
|||||||
@@ -75,7 +75,5 @@ RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
|||||||
RUN ldd /mfer 2>&1 | grep -q 'not a dynamic executable'
|
RUN ldd /mfer 2>&1 | grep -q 'not a dynamic executable'
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
# scratch has no CA certificates; fetch needs them to verify HTTPS servers.
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=builder /mfer /mfer
|
COPY --from=builder /mfer /mfer
|
||||||
ENTRYPOINT ["/mfer"]
|
ENTRYPOINT ["/mfer"]
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ Generate a manifest for a directory tree, verify it later, and fetch a published
|
|||||||
tree by URL:
|
tree by URL:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Write index.mf, a manifest of the files under the current directory.
|
# Write .index.mf, a manifest of the files under the current directory.
|
||||||
bin/mfer gen .
|
bin/mfer gen .
|
||||||
|
|
||||||
# Verify the files on disk against the manifest. Exits nonzero if any file
|
# Verify the files on disk against the manifest. Exits nonzero if any file
|
||||||
# is missing or corrupted.
|
# is missing or corrupted.
|
||||||
bin/mfer check index.mf
|
bin/mfer check .index.mf
|
||||||
|
|
||||||
# Download and cryptographically verify a tree published over HTTP: mfer
|
# Download and cryptographically verify a tree published over HTTP: mfer
|
||||||
# fetches <url>/index.mf, then downloads every file it lists.
|
# fetches <url>/index.mf, then downloads every file it lists.
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ require (
|
|||||||
github.com/pterm/pterm v0.12.35
|
github.com/pterm/pterm v0.12.35
|
||||||
github.com/spf13/afero v1.8.0
|
github.com/spf13/afero v1.8.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/urfave/cli/v2 v2.27.7
|
github.com/urfave/cli/v2 v2.23.6
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atomicgo/cursor v0.0.1 // indirect
|
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/fatih/color v1.7.0 // indirect
|
github.com/fatih/color v1.7.0 // indirect
|
||||||
github.com/gookit/color v1.4.2 // indirect
|
github.com/gookit/color v1.4.2 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
@@ -34,7 +34,7 @@ require (
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
||||||
golang.org/x/sys v0.1.0 // indirect
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -231,12 +231,12 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj
|
|||||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
github.com/urfave/cli/v2 v2.23.6 h1:iWmtKD+prGo1nKUtLO0Wg4z9esfBM4rAV4QRLQiEmJ4=
|
||||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
github.com/urfave/cli/v2 v2.23.6/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
|||||||
+15
-12
@@ -75,22 +75,25 @@ func safeRateUint64(rate float64) uint64 {
|
|||||||
return uint64(rate)
|
return uint64(rate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findManifest returns the path of the manifest with the default name in
|
// findManifest looks for a manifest file in the given directory.
|
||||||
// dir, or an error if there is none.
|
// It checks for index.mf and .index.mf, returning the first one found.
|
||||||
func findManifest(fs afero.Fs, dir string) (string, error) {
|
func findManifest(fs afero.Fs, dir string) (string, error) {
|
||||||
path := filepath.Join(dir, defaultManifestName)
|
candidates := []string{"index.mf", ".index.mf"}
|
||||||
|
for _, name := range candidates {
|
||||||
|
path := filepath.Join(dir, name)
|
||||||
|
|
||||||
exists, err := afero.Exists(fs, path)
|
exists, err := afero.Exists(fs, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
return "", fmt.Errorf(
|
||||||
return "", fmt.Errorf("%w in %s (looked for %s)",
|
"%w in %s (looked for index.mf and .index.mf)", errNoManifestFound, dir)
|
||||||
errNoManifestFound, dir, defaultManifestName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchManifestToTemp downloads a manifest URL to a temporary file and
|
// fetchManifestToTemp downloads a manifest URL to a temporary file and
|
||||||
|
|||||||
+5
-178
@@ -8,9 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -33,7 +30,6 @@ const (
|
|||||||
testFlagBase = "--base"
|
testFlagBase = "--base"
|
||||||
testFlagNoExtra = "--no-extra-files"
|
testFlagNoExtra = "--no-extra-files"
|
||||||
testFlagVersion = "--version"
|
testFlagVersion = "--version"
|
||||||
testFlagVerbose = "--verbose"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var errSimulatedWrite = errors.New("simulated write failure")
|
var errSimulatedWrite = errors.New("simulated write failure")
|
||||||
@@ -46,32 +42,20 @@ var errSimulatedWrite = errors.New("simulated write failure")
|
|||||||
var runMu sync.Mutex
|
var runMu sync.Mutex
|
||||||
|
|
||||||
// runCLI invokes RunWithOptions while holding runMu so parallel tests
|
// runCLI invokes RunWithOptions while holding runMu so parallel tests
|
||||||
// capture their own output, and returns its exit code.
|
// capture their own output. Before releasing the lock it points the
|
||||||
|
// process-global logger at io.Discard: other tests log outside the lock
|
||||||
|
// (manifest loads, scans), and those lines must not land in this run's
|
||||||
|
// buffers once it has returned and its test is reading them.
|
||||||
func runCLI(opts *RunOptions) int {
|
func runCLI(opts *RunOptions) int {
|
||||||
exitCode, _ := runCLIWithLevel(opts)
|
|
||||||
|
|
||||||
return exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCLIWithLevel is runCLI that also returns the log level the run left
|
|
||||||
// set, read while runMu still keeps other runs from changing it. Each run
|
|
||||||
// starts at the default level, as a new process does. Before releasing the
|
|
||||||
// lock it points the process-global logger at io.Discard: other tests log
|
|
||||||
// outside the lock (manifest loads, scans), and those lines must not land in
|
|
||||||
// this run's buffers once it has returned and its test is reading them.
|
|
||||||
func runCLIWithLevel(opts *RunOptions) (int, log.Level) {
|
|
||||||
runMu.Lock()
|
runMu.Lock()
|
||||||
defer runMu.Unlock()
|
defer runMu.Unlock()
|
||||||
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
|
|
||||||
exitCode := RunWithOptions(opts)
|
exitCode := RunWithOptions(opts)
|
||||||
level := log.GetLevel()
|
|
||||||
|
|
||||||
log.SetOutput(io.Discard, io.Discard)
|
log.SetOutput(io.Discard, io.Discard)
|
||||||
log.Init()
|
log.Init()
|
||||||
|
|
||||||
return exitCode, level
|
return exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
@@ -251,90 +235,6 @@ func TestRootVerbosityFlags(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// commandsTakingVerbose returns the command lines -v can follow: the root and
|
|
||||||
// the generate, check, freshen and fetch subcommands.
|
|
||||||
func commandsTakingVerbose() [][]string {
|
|
||||||
return [][]string{
|
|
||||||
{testApp},
|
|
||||||
{testApp, cmdGenerate},
|
|
||||||
{testApp, cmdCheck},
|
|
||||||
{testApp, cmdFreshen},
|
|
||||||
{testApp, cmdFetch},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVerboseCount asserts that one -v or --verbose gives verbose output and
|
|
||||||
// two -v give debug output (issue #125). urfave/cli before v2.25.5 counted a
|
|
||||||
// flag given by its alias twice, so one -v gave debug output.
|
|
||||||
func TestVerboseCount(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
flags []string
|
|
||||||
want log.Level
|
|
||||||
}{
|
|
||||||
{[]string{"-v"}, log.VerboseLevel},
|
|
||||||
{[]string{testFlagVerbose}, log.VerboseLevel},
|
|
||||||
{[]string{"-v", "-v"}, log.DebugLevel},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, command := range commandsTakingVerbose() {
|
|
||||||
for _, tc := range cases {
|
|
||||||
args := slices.Concat(command, tc.flags)
|
|
||||||
|
|
||||||
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, level := runCLIWithLevel(testOpts(args, afero.NewMemMapFs()))
|
|
||||||
assert.Equal(t, tc.want, level)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCombinedShortVerboseRefused asserts that -vv is refused (issue #125):
|
|
||||||
// single-letter flags do not combine, so the -v help text says -v -v.
|
|
||||||
func TestCombinedShortVerboseRefused(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, command := range commandsTakingVerbose() {
|
|
||||||
args := slices.Concat(command, []string{"-vv"})
|
|
||||||
|
|
||||||
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
opts := testOpts(args, afero.NewMemMapFs())
|
|
||||||
exitCode, level := runCLIWithLevel(opts)
|
|
||||||
|
|
||||||
assert.Equal(t, 1, exitCode)
|
|
||||||
assert.Contains(t, testStderr(t, opts), "flag provided but not defined: -vv")
|
|
||||||
assert.Equal(t, log.InfoLevel, level)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestShortAndLongVerboseRefused asserts that -v and --verbose given together
|
|
||||||
// are refused (issue #125): urfave/cli v2 refuses a flag given under two of its
|
|
||||||
// names, and one flag with an alias keeps help and parsing simple.
|
|
||||||
func TestShortAndLongVerboseRefused(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, command := range commandsTakingVerbose() {
|
|
||||||
args := slices.Concat(command, []string{"-v", testFlagVerbose})
|
|
||||||
|
|
||||||
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
opts := testOpts(args, afero.NewMemMapFs())
|
|
||||||
exitCode, level := runCLIWithLevel(opts)
|
|
||||||
|
|
||||||
assert.Equal(t, 1, exitCode)
|
|
||||||
assert.Contains(t, testStderr(t, opts), "Cannot use two forms of the same flag")
|
|
||||||
assert.Equal(t, log.InfoLevel, level)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHelpCommand(t *testing.T) {
|
func TestHelpCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -738,79 +638,6 @@ func TestGenerateFailsWithoutForceWhenOutputExists(t *testing.T) {
|
|||||||
assert.Equal(t, "existing", string(content), "original file should be preserved")
|
assert.Equal(t, "existing", string(content), "original file should be preserved")
|
||||||
}
|
}
|
||||||
|
|
||||||
// manifestPaths returns the file paths listed by the manifest at path.
|
|
||||||
func manifestPaths(t *testing.T, fs afero.Fs, path string) []string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
manifest, err := mfer.NewManifestFromFile(fs, path)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
paths := make([]string, 0, len(manifest.Files()))
|
|
||||||
for _, f := range manifest.Files() {
|
|
||||||
paths = append(paths, f.GetPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGenerateLeavesOutputOutOfListing overwrites an output file inside the
|
|
||||||
// scanned tree with --force: the old file is not listed, even when the tree
|
|
||||||
// is named through a symlink, while a file named index.mf in a subdirectory
|
|
||||||
// still is. The output file is recognized by file identity, which needs
|
|
||||||
// the real filesystem.
|
|
||||||
func TestGenerateLeavesOutputOutOfListing(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Paths are relative to a temp dir holding data/tree and link, a
|
|
||||||
// symlink to data.
|
|
||||||
for name, tc := range map[string]struct{ input, output string }{
|
|
||||||
"default name": {"data/tree", "data/tree/index.mf"},
|
|
||||||
"other name in a subdirectory": {"data/tree", "data/tree/sub/listing.mf"},
|
|
||||||
"tree named through a symlink": {"link/tree", "data/tree/index.mf"},
|
|
||||||
} {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
tree := filepath.Join(root, "data", "tree")
|
|
||||||
output := filepath.Join(root, tc.output)
|
|
||||||
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
require.NoError(t, fs.MkdirAll(filepath.Join(tree, "sub"), 0o750))
|
|
||||||
require.NoError(t, os.Symlink(filepath.Join(root, "data"), filepath.Join(root, "link")))
|
|
||||||
writeTestFile(t, fs, filepath.Join(tree, "readme.txt"), "hello")
|
|
||||||
writeTestFile(t, fs, filepath.Join(tree, "sub", "index.mf"), "an ordinary file")
|
|
||||||
writeTestFile(t, fs, output, "previous manifest")
|
|
||||||
|
|
||||||
opts := testOpts([]string{
|
|
||||||
testApp, cmdGenerate, "-q", "--force", "-o", output, filepath.Join(root, tc.input),
|
|
||||||
}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, []string{"readme.txt", "sub/index.mf"},
|
|
||||||
manifestPaths(t, fs, output))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGenerateDefaultOutputLeftOutOfListing runs gen with --force and no
|
|
||||||
// other arguments, so it scans the current directory and writes the
|
|
||||||
// relative path index.mf: the index.mf already there is not listed.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the process-global working directory
|
|
||||||
func TestGenerateDefaultOutputLeftOutOfListing(t *testing.T) {
|
|
||||||
chdirTemp(t)
|
|
||||||
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
writeTestFile(t, fs, "readme.txt", "hello")
|
|
||||||
writeTestFile(t, fs, "index.mf", "previous manifest")
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdGenerate, "-q", "--force"}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"readme.txt"}, manifestPaths(t, fs, "index.mf"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
|
func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func TestNoManifestFoundMessage(t *testing.T) {
|
|||||||
_, err := findManifest(afero.NewMemMapFs(), "/tmp/x")
|
_, err := findManifest(afero.NewMemMapFs(), "/tmp/x")
|
||||||
require.ErrorIs(t, err, errNoManifestFound)
|
require.ErrorIs(t, err, errNoManifestFound)
|
||||||
assert.EqualError(t, err,
|
assert.EqualError(t, err,
|
||||||
"no manifest found in /tmp/x (looked for index.mf)")
|
"no manifest found in /tmp/x (looked for index.mf and .index.mf)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyRequiredSignerMessages(t *testing.T) {
|
func TestVerifyRequiredSignerMessages(t *testing.T) {
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ func checkNoSymlinks(p string) error {
|
|||||||
|
|
||||||
// resolveManifestURL takes a URL and returns the manifest URL.
|
// resolveManifestURL takes a URL and returns the manifest URL.
|
||||||
// If the URL already ends with .mf, it's returned as-is.
|
// If the URL already ends with .mf, it's returned as-is.
|
||||||
// Otherwise, the default manifest name is appended.
|
// Otherwise, index.mf is appended.
|
||||||
func resolveManifestURL(inputURL string) (string, error) {
|
func resolveManifestURL(inputURL string) (string, error) {
|
||||||
parsed, err := url.Parse(inputURL)
|
parsed, err := url.Parse(inputURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -330,7 +330,8 @@ func resolveManifestURL(inputURL string) (string, error) {
|
|||||||
parsed.Path += "/"
|
parsed.Path += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed.Path += defaultManifestName
|
// Append index.mf
|
||||||
|
parsed.Path += "index.mf"
|
||||||
|
|
||||||
return parsed.String(), nil
|
return parsed.String(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-15
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ type freshenEntry struct {
|
|||||||
type freshenScanner struct {
|
type freshenScanner struct {
|
||||||
fs afero.Fs
|
fs afero.Fs
|
||||||
absBase string
|
absBase string
|
||||||
manifestInfo fs.FileInfo // the manifest being freshened
|
manifestBase string
|
||||||
includeDotfiles bool
|
includeDotfiles bool
|
||||||
followSymlinks bool
|
followSymlinks bool
|
||||||
showProgress bool
|
showProgress bool
|
||||||
@@ -157,6 +156,11 @@ func (s *freshenScanner) walk(path string, info fs.FileInfo, walkErr error) erro
|
|||||||
"freshen: failed to compute relative path for %s: %w", path, err)
|
"freshen: failed to compute relative path for %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip the manifest file itself
|
||||||
|
if relPath == s.manifestBase || relPath == "."+s.manifestBase {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Handle dotfiles
|
// Handle dotfiles
|
||||||
if !s.includeDotfiles && mfer.IsHiddenPath(filepath.ToSlash(relPath)) {
|
if !s.includeDotfiles && mfer.IsHiddenPath(filepath.ToSlash(relPath)) {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
@@ -181,12 +185,6 @@ func (s *freshenScanner) walk(path string, info fs.FileInfo, walkErr error) erro
|
|||||||
info = realInfo
|
info = realInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip the manifest file itself, however its path is spelled; gen
|
|
||||||
// leaves out its output file the same way.
|
|
||||||
if os.SameFile(info, s.manifestInfo) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s.scanCount++
|
s.scanCount++
|
||||||
|
|
||||||
// Check against existing manifest
|
// Check against existing manifest
|
||||||
@@ -366,22 +364,17 @@ func (mfa *CLIApp) freshenScan(
|
|||||||
startScan := time.Now()
|
startScan := time.Now()
|
||||||
showProgress := ctx.Bool("progress")
|
showProgress := ctx.Bool("progress")
|
||||||
|
|
||||||
manifestInfo, err := mfa.Fs.Stat(manifestPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, fmt.Errorf("freshen: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := &freshenScanner{
|
scanner := &freshenScanner{
|
||||||
fs: mfa.Fs,
|
fs: mfa.Fs,
|
||||||
absBase: absBase,
|
absBase: absBase,
|
||||||
manifestInfo: manifestInfo,
|
manifestBase: filepath.Base(manifestPath),
|
||||||
includeDotfiles: ctx.Bool("include-dotfiles"),
|
includeDotfiles: ctx.Bool("include-dotfiles"),
|
||||||
followSymlinks: ctx.Bool("follow-symlinks"),
|
followSymlinks: ctx.Bool("follow-symlinks"),
|
||||||
showProgress: showProgress,
|
showProgress: showProgress,
|
||||||
existingByPath: existingByPath,
|
existingByPath: existingByPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = afero.Walk(mfa.Fs, absBase, scanner.walk)
|
err := afero.Walk(mfa.Fs, absBase, scanner.walk)
|
||||||
|
|
||||||
if showProgress {
|
if showProgress {
|
||||||
log.ProgressDone()
|
log.ProgressDone()
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ func (s stubFileInfo) IsDir() bool { return false }
|
|||||||
func (s stubFileInfo) Sys() any { return nil }
|
func (s stubFileInfo) Sys() any { return nil }
|
||||||
|
|
||||||
// setupFreshenDir populates /testdir with two files, scans it, and
|
// setupFreshenDir populates /testdir with two files, scans it, and
|
||||||
// writes the resulting manifest to /testdir/index.mf.
|
// writes the resulting manifest to /testdir/.index.mf.
|
||||||
func setupFreshenDir(t *testing.T, fs afero.Fs) {
|
func setupFreshenDir(t *testing.T, fs afero.Fs) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ func setupFreshenDir(t *testing.T, fs afero.Fs) {
|
|||||||
|
|
||||||
// Write manifest to filesystem
|
// Write manifest to filesystem
|
||||||
require.NoError(t,
|
require.NoError(t,
|
||||||
afero.WriteFile(fs, "/testdir/index.mf", manifestBuf.Bytes(), 0o644))
|
afero.WriteFile(fs, "/testdir/.index.mf", manifestBuf.Bytes(), 0o644))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFreshenUnchanged(t *testing.T) {
|
func TestFreshenUnchanged(t *testing.T) {
|
||||||
@@ -59,7 +58,7 @@ func TestFreshenUnchanged(t *testing.T) {
|
|||||||
setupFreshenDir(t, fs)
|
setupFreshenDir(t, fs)
|
||||||
|
|
||||||
// Parse manifest to verify
|
// Parse manifest to verify
|
||||||
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/index.mf")
|
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, manifest.Files(), 2)
|
assert.Len(t, manifest.Files(), 2)
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,7 @@ func TestFreshenWithChanges(t *testing.T) {
|
|||||||
setupFreshenDir(t, fs)
|
setupFreshenDir(t, fs)
|
||||||
|
|
||||||
// Verify initial manifest has 2 files
|
// Verify initial manifest has 2 files
|
||||||
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/index.mf")
|
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, manifest.Files(), 2)
|
assert.Len(t, manifest.Files(), 2)
|
||||||
|
|
||||||
@@ -96,34 +95,6 @@ func TestFreshenWithChanges(t *testing.T) {
|
|||||||
assert.Equal(t, "modified content2", string(content))
|
assert.Equal(t, "modified content2", string(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFreshenLeavesManifestOutOfListing freshens a manifest kept in a
|
|
||||||
// subdirectory of the tree it lists: the manifest is not listed, while an
|
|
||||||
// ordinary file of the same name at the top of the tree is. The manifest
|
|
||||||
// is recognized by file identity, which needs the real filesystem.
|
|
||||||
func TestFreshenLeavesManifestOutOfListing(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
manifestPath := filepath.Join(root, "sub", "listing.mf")
|
|
||||||
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
require.NoError(t, fs.MkdirAll(filepath.Join(root, "sub"), 0o750))
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, "readme.txt"), "hello")
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, "listing.mf"), "an ordinary file")
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", manifestPath, root}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
// A new file gives freshen something to write.
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, "new.txt"), "new")
|
|
||||||
|
|
||||||
opts = testOpts([]string{testApp, "freshen", "-q", testFlagBase, root, manifestPath}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, []string{"readme.txt", "listing.mf", "new.txt"},
|
|
||||||
manifestPaths(t, fs, manifestPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFreshenRecordEntryMtimePresence pins the behavior of recordEntry
|
// TestFreshenRecordEntryMtimePresence pins the behavior of recordEntry
|
||||||
// with respect to MFFilePath.Mtime, which is a message pointer with
|
// with respect to MFFilePath.Mtime, which is a message pointer with
|
||||||
// proto3 field presence and may legitimately be absent.
|
// proto3 field presence and may legitimately be absent.
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ func (mfa *CLIApp) buildScannerOptions(ctx *cli.Context) *mfer.ScannerOptions {
|
|||||||
FollowSymLinks: ctx.Bool("follow-symlinks"),
|
FollowSymLinks: ctx.Bool("follow-symlinks"),
|
||||||
IncludeTimestamps: ctx.Bool("include-timestamps"),
|
IncludeTimestamps: ctx.Bool("include-timestamps"),
|
||||||
Fs: mfa.Fs,
|
Fs: mfa.Fs,
|
||||||
OutputPath: ctx.String("output"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set seed for deterministic UUID if provided
|
// Set seed for deterministic UUID if provided
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (mfa *CLIApp) openManifestReader(pathOrURL string) (io.ReadCloser, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveManifestArg resolves the manifest path from CLI arguments.
|
// resolveManifestArg resolves the manifest path from CLI arguments.
|
||||||
// HTTP(S) URLs are returned as-is. Directories are searched for index.mf.
|
// HTTP(S) URLs are returned as-is. Directories are searched for index.mf/.index.mf.
|
||||||
// If no argument is given, the current directory is searched.
|
// If no argument is given, the current directory is searched.
|
||||||
func (mfa *CLIApp) resolveManifestArg(ctx *cli.Context) (string, error) {
|
func (mfa *CLIApp) resolveManifestArg(ctx *cli.Context) (string, error) {
|
||||||
if ctx.Args().Len() > 0 {
|
if ctx.Args().Len() > 0 {
|
||||||
|
|||||||
+8
-16
@@ -17,7 +17,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
cmdGenerate = "generate"
|
cmdGenerate = "generate"
|
||||||
cmdCheck = "check"
|
cmdCheck = "check"
|
||||||
cmdFreshen = "freshen"
|
|
||||||
cmdExport = "export"
|
cmdExport = "export"
|
||||||
cmdFetch = "fetch"
|
cmdFetch = "fetch"
|
||||||
cmdVersion = "version"
|
cmdVersion = "version"
|
||||||
@@ -25,11 +24,6 @@ const (
|
|||||||
flagProgress = "progress"
|
flagProgress = "progress"
|
||||||
|
|
||||||
manifestArgsUsage = "[manifest file]"
|
manifestArgsUsage = "[manifest file]"
|
||||||
|
|
||||||
// defaultManifestName is the filename gen writes by default, the one
|
|
||||||
// looked for when a command is given a directory, and the one fetch
|
|
||||||
// appends to a directory URL.
|
|
||||||
defaultManifestName = "index.mf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// errUnknownCommand indicates an unrecognized command argument.
|
// errUnknownCommand indicates an unrecognized command argument.
|
||||||
@@ -124,7 +118,7 @@ func commonFlags() []cli.Flag {
|
|||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "verbose",
|
Name: "verbose",
|
||||||
Aliases: []string{"v"},
|
Aliases: []string{"v"},
|
||||||
Usage: "Increase verbosity (-v for verbose, -v -v for debug)",
|
Usage: "Increase verbosity (-v for verbose, -vv for debug)",
|
||||||
Count: new(int),
|
Count: new(int),
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
@@ -137,10 +131,9 @@ func commonFlags() []cli.Flag {
|
|||||||
|
|
||||||
func (mfa *CLIApp) generateCommand() *cli.Command {
|
func (mfa *CLIApp) generateCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: cmdGenerate,
|
Name: cmdGenerate,
|
||||||
Aliases: []string{"gen"},
|
Aliases: []string{"gen"},
|
||||||
Usage: "Generate manifest file",
|
Usage: "Generate manifest file",
|
||||||
ArgsUsage: "[path ...]",
|
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
mfa.setVerbosity(c)
|
mfa.setVerbosity(c)
|
||||||
mfa.printBanner()
|
mfa.printBanner()
|
||||||
@@ -161,7 +154,7 @@ func (mfa *CLIApp) generateCommand() *cli.Command {
|
|||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "output",
|
Name: "output",
|
||||||
Value: defaultManifestName,
|
Value: "./.index.mf",
|
||||||
Aliases: []string{"o"},
|
Aliases: []string{"o"},
|
||||||
Usage: "Specify output filename",
|
Usage: "Specify output filename",
|
||||||
},
|
},
|
||||||
@@ -234,7 +227,7 @@ func (mfa *CLIApp) checkCommand() *cli.Command {
|
|||||||
|
|
||||||
func (mfa *CLIApp) freshenCommand() *cli.Command {
|
func (mfa *CLIApp) freshenCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: cmdFreshen,
|
Name: "freshen",
|
||||||
Usage: "Update manifest with changed, new, and removed files",
|
Usage: "Update manifest with changed, new, and removed files",
|
||||||
ArgsUsage: manifestArgsUsage,
|
ArgsUsage: manifestArgsUsage,
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
@@ -331,9 +324,8 @@ func (mfa *CLIApp) listCommand() *cli.Command {
|
|||||||
|
|
||||||
func (mfa *CLIApp) fetchCommand() *cli.Command {
|
func (mfa *CLIApp) fetchCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: cmdFetch,
|
Name: cmdFetch,
|
||||||
Usage: "fetch manifest and referenced files",
|
Usage: "fetch manifest and referenced files",
|
||||||
ArgsUsage: "URL",
|
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
mfa.setVerbosity(c)
|
mfa.setVerbosity(c)
|
||||||
mfa.printBanner()
|
mfa.printBanner()
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ func TestFindExtraFilesSkipsManifestAndDotfiles(t *testing.T) {
|
|||||||
manifestFiles := map[string][]byte{
|
manifestFiles := map[string][]byte{
|
||||||
testFile1: []byte("in manifest"),
|
testFile1: []byte("in manifest"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, "/data/index.mf", manifestFiles)
|
createTestManifest(t, fs, "/data/.index.mf", manifestFiles)
|
||||||
createFilesOnDisk(t, fs, map[string][]byte{
|
createFilesOnDisk(t, fs, map[string][]byte{
|
||||||
testFile1: []byte("in manifest"),
|
testFile1: []byte("in manifest"),
|
||||||
})
|
})
|
||||||
@@ -371,7 +371,7 @@ func TestFindExtraFilesSkipsManifestAndDotfiles(t *testing.T) {
|
|||||||
require.NoError(t, fs.MkdirAll("/data", 0o755))
|
require.NoError(t, fs.MkdirAll("/data", 0o755))
|
||||||
require.NoError(t, afero.WriteFile(fs, "/data/extra.txt", []byte("extra"), 0o644))
|
require.NoError(t, afero.WriteFile(fs, "/data/extra.txt", []byte("extra"), 0o644))
|
||||||
|
|
||||||
chk, err := NewChecker("/data/index.mf", "/data", fs)
|
chk, err := NewChecker("/data/.index.mf", "/data", fs)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -383,7 +383,7 @@ func TestFindExtraFilesSkipsManifestAndDotfiles(t *testing.T) {
|
|||||||
extras = append(extras, r)
|
extras = append(extras, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should only report extra.txt, not .hidden, .config/settings, or index.mf
|
// Should only report extra.txt, not .hidden, .config/settings, or .index.mf
|
||||||
for _, e := range extras {
|
for _, e := range extras {
|
||||||
t.Logf("extra: %s", e.Path)
|
t.Logf("extra: %s", e.Path)
|
||||||
}
|
}
|
||||||
@@ -528,7 +528,7 @@ func TestFindExtraFilesSkipsDotfiles(t *testing.T) {
|
|||||||
files := map[string][]byte{
|
files := map[string][]byte{
|
||||||
testFile1: []byte("in manifest"),
|
testFile1: []byte("in manifest"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, "/data/index.mf", files)
|
createTestManifest(t, fs, "/data/.index.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
// Add dotfiles and manifest file on disk
|
// Add dotfiles and manifest file on disk
|
||||||
@@ -537,7 +537,7 @@ func TestFindExtraFilesSkipsDotfiles(t *testing.T) {
|
|||||||
require.NoError(t,
|
require.NoError(t,
|
||||||
afero.WriteFile(fs, "/data/.git/config", []byte("git config"), 0o644))
|
afero.WriteFile(fs, "/data/.git/config", []byte("git config"), 0o644))
|
||||||
|
|
||||||
chk, err := NewChecker("/data/index.mf", "/data", fs)
|
chk, err := NewChecker("/data/.index.mf", "/data", fs)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
|
|||||||
+1
-27
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -58,14 +57,6 @@ type ScannerOptions struct {
|
|||||||
SigningOptions *SigningOptions
|
SigningOptions *SigningOptions
|
||||||
// Seed, if set, derives a deterministic UUID from this seed.
|
// Seed, if set, derives a deterministic UUID from this seed.
|
||||||
Seed string
|
Seed string
|
||||||
// OutputPath, if set, is the file the manifest will be written to.
|
|
||||||
// If that file exists when the scanner is created, enumeration leaves
|
|
||||||
// it out however its path is spelled (through a symlink, a hard link,
|
|
||||||
// or in different letter case), so a manifest being replaced is never
|
|
||||||
// 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.
|
// FileEntry represents a file that has been enumerated.
|
||||||
@@ -84,7 +75,6 @@ type Scanner struct {
|
|||||||
totalBytes FileSize // cached sum of all file sizes
|
totalBytes FileSize // cached sum of all file sizes
|
||||||
options *ScannerOptions
|
options *ScannerOptions
|
||||||
fs afero.Fs
|
fs afero.Fs
|
||||||
outputInfo fs.FileInfo // the existing output file; nil when there is none
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScanner creates a new Scanner with default options.
|
// NewScanner creates a new Scanner with default options.
|
||||||
@@ -103,22 +93,11 @@ func NewScannerWithOptions(opts *ScannerOptions) *Scanner {
|
|||||||
fs = afero.NewOsFs()
|
fs = afero.NewOsFs()
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Scanner{
|
return &Scanner{
|
||||||
files: make([]*FileEntry, 0),
|
files: make([]*FileEntry, 0),
|
||||||
options: opts,
|
options: opts,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.OutputPath != "" {
|
|
||||||
// If the output file cannot be stat'd, normally because it does
|
|
||||||
// not exist yet, there is nothing to leave out.
|
|
||||||
info, err := s.fs.Stat(opts.OutputPath)
|
|
||||||
if err == nil {
|
|
||||||
s.outputInfo = info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnumerateFile adds a single file to the scanner, calling stat() to get metadata.
|
// EnumerateFile adds a single file to the scanner, calling stat() to get metadata.
|
||||||
@@ -456,11 +435,6 @@ func (s *Scanner) enumerateFileWithInfo(
|
|||||||
info = realInfo
|
info = realInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// The manifest being written is not one of the files it lists.
|
|
||||||
if s.outputInfo != nil && os.SameFile(info, s.outputInfo) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := &FileEntry{
|
entry := &FileEntry{
|
||||||
Path: RelFilePath(cleanPath),
|
Path: RelFilePath(cleanPath),
|
||||||
AbsPath: AbsFilePath(absPath),
|
AbsPath: AbsFilePath(absPath),
|
||||||
|
|||||||
Reference in New Issue
Block a user