Architecture

March 26, 2026 · View on GitHub

gitconfig follows the Hexagonal Architecture (Ports & Adapters) pattern. The goal is to keep the domain logic completely decoupled from I/O concerns (filesystem, CLI, external processes).


Directory layout

gitconfig/
├── cmd/                            # Entry point — wires everything together
│   └── main.go
└── internal/
    ├── config/                     # App-wide constants (e.g. home dir name)
    ├── templates/                  # Default file content templates
    ├── home/                       # Resolve & ensure app home directory

    ├── domain/                     # ── DOMAIN LAYER ──────────────────────
    │   ├── location/               # Location entity, factory, Service port
    │   │   ├── location.go
    │   │   ├── location_test.go
    │   │   └── service.go          # ← Port (interface)
    │   └── gitconfig/              # GitConfig value object (read/write .gitconfig)
    │       ├── gitconfig.go
    │       └── gitconfig_test.go

    ├── application/                # ── APPLICATION LAYER ─────────────────
    │   └── locations/
    │       ├── manager.go          # LocationManager — implements location.Service
    │       └── manager_test.go

    └── adapter/                    # ── ADAPTER LAYER (Infrastructure) ────
        ├── cli/                    # Primary adapter: Cobra CLI
        │   ├── root.go
        │   ├── locations.go
        │   ├── location.go
        │   ├── location_new.go
        │   ├── location_delete.go
        │   └── config.go
        ├── filesystem/             # afero wrapper (OS + in-memory)
        │   ├── filesystem.go
        │   ├── file.go
        │   └── wd.go
        └── git/                    # Secondary adapter: git process execution
            ├── git.go
            └── commander/
                ├── commander.go    # Run processes, capture output
                └── builder/
                    └── builder.go  # Fluent exec.Cmd builder

Layers

Domain layer (internal/domain/)

Contains pure business logic with no I/O dependencies.

PackageResponsibility
locationLocation entity, NewLocation factory, Service interface (port)
gitconfigGitConfig struct — read/write a single .gitconfig file via an injected afero.Afero

The location.Service interface is the primary port. It defines the operations the application exposes to the outside world:

type Service interface {
    GetLocations() ([]Location, error)
    FindLocationByKey(key string) (*Location, error)
    SaveLocation(key string, location string) error
    DeleteLocation(key string) error
}

Application layer (internal/application/)

Orchestrates domain objects to fulfil use-cases. LocationManager implements location.Service — it coordinates GitConfig (reads/writes .gitconfig) and afero.Afero (manages individual location config files).

Adapter layer (internal/adapter/)

AdapterDirectionResponsibility
cliPrimary (driving)Cobra command tree — translates CLI input into location.Service calls
filesystemSecondary (driven)afero factory helpers — used by domain & application layers
gitSecondary (driven)Wraps the git config binary via commander and builder

Entry point (cmd/main.go)

Calls cli.RootCmd(version) which internally wires defaultLocationServiceFactoryLocationManagerGitConfig + OS filesystem.


Dependency injection & testability

The CLI adapter receives a LocationServiceFactory function:

type LocationServiceFactory func(gitconfigPath string) location.Service

In production, defaultLocationServiceFactory returns a real LocationManager backed by the OS filesystem. In tests, RootCmdWithDeps accepts a factory that returns a LocationManager backed by an afero.MemMapFs.

This means the entire CLI can be exercised in memory without touching disk, spawning processes, or requiring a real ~/.gitconfig.


Data flow — example: gitconfig location new --key work

CLI args
  └─► cobra (adapter/cli/location_new.go)
        └─► LocationServiceFactory(gitconfigPath) → LocationManager
              ├─► GitConfig.GetContent()       reads .gitconfig (afero)
              ├─► LocationManager.SaveLocation()
              │     ├─► location.NewLocation()  builds entity
              │     ├─► location.ToSection()    renders template
              │     └─► GitConfig.AppendSection() / WriteContent()
              └─► GitConfig (writes updated content back to .gitconfig)

Key design decisions

DecisionRationale
Hexagonal structureKeeps domain logic free of I/O; enables in-memory tests
afero for filesystemSwappable between OS and in-memory without changing application code
LocationServiceFactory patternAllows CLI integration tests without global state or build tags
No panic in domain codeErrors propagate up naturally; panic would break testability
TrimSuffix("/") on parsed pathsPrevents double-slash when the regex captures trailing slash from .gitconfig