Move the CLI to urfave/cli v3 (closes #110)
check / check (push) Failing after 3s

Ported per the library's v2-to-v3 migration guide: the app is a root
cli.Command, actions take a context and the command, and flag environment
variables become value sources. -v, -q and the version flag are marked
local so that, as before, only the commands that define them accept them.
go mod tidy raises testify to v1.12.1, which v3 requires. The
urfave_cli_no_docs build tag is dropped from script/build and the
Dockerfile; v3 has no such tag. v3 no longer refuses a flag given under
two of its names, so -v --verbose now gives debug output, and the test
that pinned the refusal becomes a TestVerboseCount case. Help text follows
v3's templates.

Model: opus-5-5
This commit is contained in:
2026-10-04 18:16:03 +00:00
parent 9bb0ab3a03
commit 6680e5f066
15 changed files with 194 additions and 229 deletions
+21 -19
View File
@@ -14,7 +14,7 @@ import (
"github.com/dustin/go-humanize"
"github.com/multiformats/go-multihash"
"github.com/spf13/afero"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"sneak.berlin/go/mfer/internal/log"
"sneak.berlin/go/mfer/mfer"
)
@@ -202,12 +202,12 @@ func (s *freshenScanner) walk(path string, info fs.FileInfo, walkErr error) erro
// resolveFreshenManifestPath determines the manifest path from the CLI
// arguments, searching directories for a manifest where needed.
func (mfa *CLIApp) resolveFreshenManifestPath(ctx *cli.Context) (string, error) {
if ctx.Args().Len() == 0 {
func (mfa *CLIApp) resolveFreshenManifestPath(cmd *cli.Command) (string, error) {
if cmd.Args().Len() == 0 {
return findManifest(mfa.Fs, ".")
}
arg := ctx.Args().Get(0)
arg := cmd.Args().Get(0)
info, statErr := mfa.Fs.Stat(arg)
if statErr == nil && info.IsDir() {
@@ -338,14 +338,14 @@ func writeFreshenedManifest(
// newFreshenBuilder constructs the manifest builder configured from CLI
// flags.
func newFreshenBuilder(ctx *cli.Context) *mfer.Builder {
func newFreshenBuilder(cmd *cli.Command) *mfer.Builder {
builder := mfer.NewBuilder()
if ctx.Bool("include-timestamps") {
if cmd.Bool("include-timestamps") {
builder.SetIncludeTimestamps(true)
}
// Set up signing options if sign-key is provided
if signKey := ctx.String("sign-key"); signKey != "" {
if signKey := cmd.String("sign-key"); signKey != "" {
builder.SetSigningOptions(&mfer.SigningOptions{
KeyID: mfer.GPGKeyID(signKey),
})
@@ -358,13 +358,13 @@ func newFreshenBuilder(ctx *cli.Context) *mfer.Builder {
// freshenScan runs the scan phase against the loaded manifest entries
// and returns the populated scanner and the count of removed files.
func (mfa *CLIApp) freshenScan(
ctx *cli.Context, manifestPath, absBase string,
cmd *cli.Command, manifestPath, absBase string,
existingByPath map[string]*mfer.MFFilePath,
) (*freshenScanner, int64, error) {
log.Infof("scanning filesystem...")
startScan := time.Now()
showProgress := ctx.Bool("progress")
showProgress := cmd.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
@@ -382,8 +382,8 @@ func (mfa *CLIApp) freshenScan(
fs: mfa.Fs,
absBase: absBase,
excluded: excluded,
includeDotfiles: ctx.Bool("include-dotfiles"),
followSymlinks: ctx.Bool("follow-symlinks"),
includeDotfiles: cmd.Bool("include-dotfiles"),
followSymlinks: cmd.Bool("follow-symlinks"),
showProgress: showProgress,
existingByPath: existingByPath,
}
@@ -433,7 +433,7 @@ func hashTotals(entries []*freshenEntry) (int64, int64) {
// runFreshenHash processes every entry through the hasher, aborting if
// the context is canceled.
func runFreshenHash(
ctx *cli.Context, hasher *freshenHasher, entries []*freshenEntry,
ctx context.Context, hasher *freshenHasher, entries []*freshenEntry,
) error {
for _, e := range entries {
select {
@@ -479,14 +479,16 @@ func (mfa *CLIApp) loadExistingEntries(
return existingByPath, nil
}
func (mfa *CLIApp) freshenManifestOperation(ctx *cli.Context) error {
func (mfa *CLIApp) freshenManifestOperation(
ctx context.Context, cmd *cli.Command,
) error {
log.Debug("freshenManifestOperation()")
basePath := ctx.String("base")
showProgress := ctx.Bool("progress")
basePath := cmd.String("base")
showProgress := cmd.Bool("progress")
// Find manifest file
manifestPath, err := mfa.resolveFreshenManifestPath(ctx)
manifestPath, err := mfa.resolveFreshenManifestPath(cmd)
if err != nil {
return fmt.Errorf("freshen: %w", err)
}
@@ -502,7 +504,7 @@ func (mfa *CLIApp) freshenManifestOperation(ctx *cli.Context) error {
}
// Phase 1: Scan filesystem
scanner, removed, err := mfa.freshenScan(ctx, manifestPath, absBase,
scanner, removed, err := mfa.freshenScan(cmd, manifestPath, absBase,
existingByPath)
if err != nil {
return err
@@ -524,7 +526,7 @@ func (mfa *CLIApp) freshenManifestOperation(ctx *cli.Context) error {
totalHashBytes: totalHashBytes,
filesToHash: filesToHash,
startHash: time.Now(),
builder: newFreshenBuilder(ctx),
builder: newFreshenBuilder(cmd),
}
err = runFreshenHash(ctx, hasher, scanner.entries)
@@ -548,7 +550,7 @@ func (mfa *CLIApp) freshenManifestOperation(ctx *cli.Context) error {
}
// Write updated manifest atomically (write to temp, then rename)
err = writeFreshenedManifest(ctx.Context, mfa.Fs, hasher.builder, manifestPath)
err = writeFreshenedManifest(ctx, mfa.Fs, hasher.builder, manifestPath)
if err != nil {
return err
}