gitFlow/internal/app/app.go
dimitar d52e0715f2 feat: phase 2 — CLI commands and configuration resolution
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.
2026-08-02 08:38:55 +02:00

65 lines
1.7 KiB
Go

// Package app wires discovery, status scanning, and presentation into the
// runnable operations used by the CLI commands.
package app
import (
"context"
"fmt"
"os"
"time"
"gitea.oblak.solutions/dimitar/gitFlow/internal/config"
"gitea.oblak.solutions/dimitar/gitFlow/internal/scanner"
"gitea.oblak.solutions/dimitar/gitFlow/pkg/status"
)
// App coordinates the components built from the resolved configuration.
type App struct {
cfg *config.Config
discoverer *scanner.Discoverer
scanner *scanner.Scanner
warn func(format string, args ...any)
}
// New builds an App from a validated configuration.
func New(cfg *config.Config) (*App, error) {
if err := cfg.Validate(); err != nil {
return nil, err
}
return &App{
cfg: cfg,
discoverer: scanner.NewDiscoverer(
scanner.WithExclude(cfg.Exclude...),
scanner.WithMaxDepth(cfg.MaxDepth),
),
scanner: scanner.NewScanner(scanner.WithWorkers(cfg.Workers)),
warn: func(format string, args ...any) {
fmt.Fprintf(os.Stderr, "warning: "+format+"\n", args...)
},
}, nil
}
// ScanOnce discovers repositories under the configured directory and
// snapshots their status in a single pass.
func (a *App) ScanOnce(ctx context.Context) (status.ScanResult, error) {
repos, err := a.discoverer.Discover(ctx, a.cfg.Dir)
if err != nil {
if len(repos) == 0 {
return status.ScanResult{}, err
}
// Partial discovery (e.g. permission denied on some subtree):
// warn and continue with what was found.
a.warn("%v", err)
}
infos, err := a.scanner.Scan(ctx, repos)
if err != nil {
return status.ScanResult{}, err
}
return status.ScanResult{
ScannedAt: time.Now(),
ParentDir: a.cfg.Dir,
Repos: infos,
}, nil
}