CLI tool for managing local backups using rsync as the engine. Built in Go with cobra for CLI, afero for filesystem abstraction, and YAML for configuration. Local-only — no remote rsync support.
Design principles: simple human-readable configuration, minimal interdependencies, flexible variable/macro substitution (${var}). Each backup job can be listed, simulated (dry-run), or run independently.
- Idiomatic Go (see
go.modfor version); format withgo fmt, lint withgolangci-lint(.golangci.yml) - All linters enabled by default — check
.golangci.ymlfor disabled ones - Packages:
cmd/for CLI wiring,internal/for core logic - Prefer dependency injection over global state; use interfaces at consumption boundaries
- Route all output through
cmd.OutOrStdout()or injectedio.Writer— never rawfmt.Printf - Dual logging: user-facing output via
io.Writer, file logging via*log.Loggerunderlogs/ - All commands use
RunEwith wrapped errors
backup/
main.go # Entrypoint — calls cmd.BuildRootCommand().Execute()
cmd/ # Cobra commands: list, run, simulate, config (show/validate), check-coverage, version
root.go # BuildRootCommand() / BuildRootCommandWithFs(fs) — injects afero.Fs
test/ # Cobra command integration tests
internal/ # Core logic: config, job execution, rsync wrapper, coverage checker
test/ # Unit tests + mockery-generated mocks
| Type | Kind | Description | Injected Dependencies |
|---|---|---|---|
Exec |
interface | Command execution abstraction (OsExec real, MockExec tests) |
— |
JobCommand |
interface | Run(job Job) JobStatus + GetVersionInfo() — implemented by ListCommand, SimulateCommand, SyncCommand |
Exec via constructors |
SharedCommand |
struct | Base for all commands — holds BinPath, BaseLogPath, Shell Exec, Output io.Writer |
io.Writer, Exec |
CoverageChecker |
struct | Analyzes path coverage | *log.Logger, afero.Fs |
Config |
struct | YAML config (Config, Mapping, Job, Path, ${var} substitution); custom UnmarshalYAML for job defaults |
*log.Logger in Apply() |
Additional injection points: afero.Fs into BuildRootCommandWithFs() → buildCheckCoverageCommand(fs); commands use cmd.OutOrStdout() for testable output.
make build # Build to dist/backup
make test # go test -race ./... -v
make test-integration # go test -race -tags=integration ./... -v
make lint # golangci-lint run ./...
make lint-fix # Auto-fix lint issues
make format # go fmt ./...
make tidy # gofmt -s + go mod tidy
make sanity-check # format + clean + tidy
make check-coverage # Fail if coverage < 98%
make report-coverage # Generate HTML coverage report- See
docs/testing-guide.mdfor patterns;docs/mockery-integration.mdfor mock setup - Mocks: Generated with mockery (config:
.mockery.yml)- Files:
internal/test/mock_<interface>_test.go; structs:Mock<Interface>
- Files:
- Assertions via
testify(require/assert); prefer table-driven tests - Test files live in
<package>/test/subdirectories - Use
afero.NewMemMapFs()— never hit the real filesystem - Use
bytes.Bufferorio.Discardfor output capture - Integration tests:
//go:build integrationtag, real rsync on temp directories
Runs on every push/PR to main (.github/workflows/go.yml):
- Sanity check (format + clean + mod tidy)
- Lint (
golangci-lint) - Build
- Test (with
-raceflag) - Integration test (real rsync,
-tags=integration) - Coverage threshold enforcement (98%)