Add atomic writes, humanized sizes, debug logging, and -v/-q per-command

- Atomic writes for mfer gen: writes to temp file, renames on success,
  cleans up temp on error/interrupt. Prevents empty manifests on Ctrl-C.
- Humanized byte sizes using dustin/go-humanize (e.g., "10 MiB" not "10485760")
- Progress lines clear when done (using ANSI escape \r\033[K])
- Debug logging when files are added to manifest (mfer gen -vv)
- Move -v/-q flags from global to per-command for better UX
- Add tests for atomic write behavior with failing filesystem mock
This commit is contained in:
2025-12-17 15:57:20 -08:00
parent 444a4c8f45
commit c218fe56e9
12 changed files with 276 additions and 86 deletions

View File

@@ -57,14 +57,31 @@ func (mfa *CLIApp) VersionString() string {
return mfer.Version
}
func (mfa *CLIApp) setVerbosity(quiet bool, v int) {
func (mfa *CLIApp) setVerbosity(c *cli.Context) {
_, present := os.LookupEnv("MFER_DEBUG")
if present {
log.EnableDebugLogging()
} else if quiet {
} else if c.Bool("quiet") {
log.SetLevel(log.ErrorLevel)
} else {
log.SetLevelFromVerbosity(v)
log.SetLevelFromVerbosity(c.Count("verbose"))
}
}
// commonFlags returns the flags shared by most commands (-v, -q)
func commonFlags() []cli.Flag {
return []cli.Flag{
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Increase verbosity (-v for verbose, -vv for debug)",
Count: new(int),
},
&cli.BoolFlag{
Name: "quiet",
Aliases: []string{"q"},
Usage: "Suppress output except errors",
},
}
}
@@ -80,8 +97,6 @@ func (mfa *CLIApp) run(args []string) {
log.SetOutput(mfa.Stdout, mfa.Stderr)
log.Init()
var verbosity int
mfa.app = &cli.App{
Name: mfa.appname,
Usage: "Manifest generator",
@@ -96,30 +111,17 @@ func (mfa *CLIApp) run(args []string) {
mfa.printBanner()
return cli.ShowAppHelp(c)
},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "verbose",
Usage: "Verbosity level",
Aliases: []string{"v"},
Count: &verbosity,
},
&cli.BoolFlag{
Name: "quiet",
Usage: "don't produce output except on error",
Aliases: []string{"q"},
},
},
Commands: []*cli.Command{
{
Name: "generate",
Aliases: []string{"gen"},
Usage: "Generate manifest file",
Action: func(c *cli.Context) error {
mfa.setVerbosity(c.Bool("quiet"), verbosity)
mfa.setVerbosity(c)
mfa.printBanner()
return mfa.generateManifestOperation(c)
},
Flags: []cli.Flag{
Flags: append(commonFlags(),
&cli.BoolFlag{
Name: "FollowSymLinks",
Aliases: []string{"follow-symlinks"},
@@ -146,18 +148,18 @@ func (mfa *CLIApp) run(args []string) {
Aliases: []string{"P"},
Usage: "Show progress during enumeration and scanning",
},
},
),
},
{
Name: "check",
Usage: "Validate files using manifest file",
ArgsUsage: "[manifest file]",
Action: func(c *cli.Context) error {
mfa.setVerbosity(c.Bool("quiet"), verbosity)
mfa.setVerbosity(c)
mfa.printBanner()
return mfa.checkManifestOperation(c)
},
Flags: []cli.Flag{
Flags: append(commonFlags(),
&cli.StringFlag{
Name: "base",
Aliases: []string{"b"},
@@ -173,18 +175,18 @@ func (mfa *CLIApp) run(args []string) {
Name: "no-extra-files",
Usage: "Fail if files exist in base directory that are not in manifest",
},
},
),
},
{
Name: "freshen",
Usage: "Update manifest with changed, new, and removed files",
ArgsUsage: "[manifest file]",
Action: func(c *cli.Context) error {
mfa.setVerbosity(c.Bool("quiet"), verbosity)
mfa.setVerbosity(c)
mfa.printBanner()
return mfa.freshenManifestOperation(c)
},
Flags: []cli.Flag{
Flags: append(commonFlags(),
&cli.StringFlag{
Name: "base",
Aliases: []string{"b"},
@@ -206,7 +208,7 @@ func (mfa *CLIApp) run(args []string) {
Aliases: []string{"P"},
Usage: "Show progress during scanning and hashing",
},
},
),
},
{
Name: "version",
@@ -240,10 +242,11 @@ func (mfa *CLIApp) run(args []string) {
Name: "fetch",
Usage: "fetch manifest and referenced files",
Action: func(c *cli.Context) error {
mfa.setVerbosity(c.Bool("quiet"), verbosity)
mfa.setVerbosity(c)
mfa.printBanner()
return mfa.fetchManifestOperation(c)
},
Flags: commonFlags(),
},
},
}