Add the Cobra-based command surface and the flag/env/config-file resolution layer that all commands share. Configuration (internal/config): - Load() resolves settings with the documented precedence flags > env > config file > defaults, via viper: GITFLOW_-prefixed env vars with dot-to-underscore mapping, plus ~/.gitflow.yaml (or $GITFLOW_CONFIG) - RegisterFlags/NewFlagSet own the flag definitions so every command and the tests share a single source of truth - Config/Validate/Dump cover dir, interval, format (table/json/compact), exclude globs, max depth, worker count, and the AI block (enabled, provider, model, api_key_env, base_url); ConfigFile records the loaded path; Dump renders the effective config as human-readable YAML with the interval as a duration string App orchestration (internal/app): - New() validates the configuration at the boundary (fail fast) - ScanOnce() runs discovery then a concurrent status scan, warning on stderr and continuing when discovery is only partially successful (e.g. permission-denied subtrees), and bundles everything into a ScanResult CLI (cmd/gitflow): - root command with scan / config / version subcommands - scan: resolves config, prompts for the parent directory when stdin is a TTY and --dir was not given (per the README), runs a single pass, and renders the result — interim plain/JSON output until phase 3 lands the presenter package - config: prints the effective configuration - version: prints the build version (ldflags-injectable) - signalContext() wires SIGINT/SIGTERM into a cancellable context for graceful shutdown Testing: - config: defaults, flag overrides, env overrides, flag-beats-env precedence, config file loading (including duration and slice values), GITFLOW_CONFIG path override, validation failures, and Dump output - app: config validation on New, end-to-end ScanOnce over a real temp repo, and missing-directory errors Verified: go build, go vet, go test -race, gofmt clean; manual smoke of `gitflow version`, `gitflow config`, and `gitflow scan -d <dir>` against a scratch directory with a dirty repo.
104 lines
2.4 KiB
Go
104 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"gitea.oblak.solutions/dimitar/gitFlow/internal/app"
|
|
"gitea.oblak.solutions/dimitar/gitFlow/internal/config"
|
|
"gitea.oblak.solutions/dimitar/gitFlow/pkg/status"
|
|
)
|
|
|
|
// newScanCmd runs a single scan pass and renders the result.
|
|
func newScanCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "scan",
|
|
Short: "Scan repositories under a directory once",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
cfg, err := config.Load(cmd.Flags())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// The README asks for the parent directory interactively;
|
|
// prompt only when nothing was provided and stdin is a TTY.
|
|
if !cmd.Flags().Changed("dir") && isTerminal(os.Stdin) {
|
|
if d, err := promptDir(cfg.Dir); err == nil {
|
|
cfg.Dir = d
|
|
}
|
|
}
|
|
|
|
ctx, stop := signalContext()
|
|
defer stop()
|
|
|
|
a, err := app.New(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result, err := a.ScanOnce(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Interim rendering; phase 3 routes this through the
|
|
// presenter package (table/json/compact).
|
|
switch cfg.Format {
|
|
case "json":
|
|
return renderJSON(result)
|
|
default:
|
|
renderTableLines(result)
|
|
return nil
|
|
}
|
|
},
|
|
}
|
|
config.RegisterFlags(cmd.Flags())
|
|
return cmd
|
|
}
|
|
|
|
// newConfigCmd prints the effective configuration.
|
|
func newConfigCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "config",
|
|
Short: "Show the effective configuration",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
cfg, err := config.Load(cmd.Flags())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := cfg.Dump()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Print(out)
|
|
return nil
|
|
},
|
|
}
|
|
config.RegisterFlags(cmd.Flags())
|
|
return cmd
|
|
}
|
|
|
|
// renderTableLines is the interim text renderer (replaced in phase 3).
|
|
func renderTableLines(result status.ScanResult) {
|
|
for _, repo := range result.Repos {
|
|
mark := "ok"
|
|
if repo.Status.NeedsAttention() {
|
|
mark = "!!"
|
|
}
|
|
fmt.Fprintf(os.Stdout, "%2s %-10s %s (branch %s)\n", mark, repo.Status, repo.Path, repo.Branch)
|
|
}
|
|
s := result.Summary()
|
|
fmt.Fprintf(os.Stdout, "\n%d repos | %d clean | %d need attention | %d errors\n",
|
|
s.Total, s.Clean, s.Attention, s.Errored)
|
|
}
|
|
|
|
// renderJSON serializes the scan result as indented JSON.
|
|
func renderJSON(result status.ScanResult) error {
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(result)
|
|
}
|