This document describes the architecture and design of the gitconfig library.
gitconfig is a Go library for parsing and manipulating git configuration files without depending on the git CLI tool. The library maintains the structure of the original config file (including comments and whitespace) while allowing programmatic access and modification.
Git config has a hierarchical scope system. Each scope corresponds to a different level of configuration:
Priority (highest to lowest):
Environment Variables (GIT_CONFIG_*)
↓
Per-Worktree Config (.git/config.worktree)
↓
Local/Repository Config (.git/config)
↓
Global/User Config (~/.gitconfig or ~/.config/git/config)
↓
System-wide Config (/etc/gitconfig)
↓
Presets (built-in defaults)
When a key is requested, the library searches through scopes in priority order and returns the first match found. This allows settings at higher-priority scopes to override lower ones.
Git config keys follow a hierarchical structure:
section.key → Simple value
section.subsection.key → Value in a subsection
Keys are normalized according to git rules:
- Section names: case-insensitive, typically lowercase
- Subsection names: case-sensitive
- Key names: case-insensitive, typically lowercase
Git config files follow an INI-like format:
[section]
key = value
[section "subsection"]
key = value
; Comments
# Another comment
[section]
multivalue = first
multivalue = secondSpecial considerations:
- Subsections in quotes preserve case
- Multiple values for same key are supported
- Comments and whitespace are preserved during modifications
- Boolean values can be implicit (presence indicates true)
Purpose: Represents a single configuration file (one scope)
File: config.go
Key Responsibilities:
- Parse a single config file
- Maintain both parsed representation (
varsmap) and raw text representation - Support reading (Get, GetAll, IsSet)
- Support writing (Set, Unset)
- Preserve formatting during modifications
Design Pattern: Round-Trip Preservation
The Config struct maintains two parallel representations:
-
Parsed representation (
vars map[string][]string)- Fast lookups: O(1)
- Ordered values for multi-value keys
- Structure:
section.subsection.key→[]string
-
Raw text representation (strings.Builder)
- Original file content
- Preserves comments and whitespace
- Modified in-place on write operations
When reading, the library uses the parsed vars map. When writing, it modifies the raw text representation to maintain the original file structure.
type Config struct {
vars raw string // Original file content
// Internal parsed structure
}Write Algorithm:
- Find existing key location in raw text
- Update value in-place if exists, append if new
- Reconstruct raw text preserving all other content
- Flush to disk
Complexity Analysis:
- Get: O(1)
- Set: O(n) where n = file size (due to raw text rewriting)
- Unset: O(n)
Purpose: Unified interface for all configuration scopes
File: configs.go
Key Responsibilities:
- Load and manage multiple Config objects (one per scope)
- Implement scope precedence/priority
- Provide unified Get/Set/Unset interface
- Route writes to specific scopes
- Handle scope-aware operations
Design Pattern: Scope Delegation
Configs acts as a facade over multiple Config objects:
type Configs struct {
env Config // Environment variables
worktree Config // Worktree-specific
local Config // Repository-specific
global Config // User-specific
system Config // System-wide
preset Config // Built-in defaults
}Hierarchy Implementation:
When calling Get(key):
- Check environment variables
- Check worktree config
- Check local config
- Check global config
- Check system config
- Check presets
- Return first match or error
This is implemented as a simple linear search with early termination. Optimization considerations were examined but deemed unnecessary given typical config module sizes (< 10KB).
Write API:
SetLocal()→ writes to local scopeSetGlobal()→ writes to global scopeSetWorktree()→ writes to worktree scopeSet()→ writes to local scope (default)
This prevents silent surprises where Set() might write to unexpected scope based on environment state.
Purpose: Common parsing and matching operations
File: utils.go
Key Functions:
| Function | Purpose | Implementation |
|---|---|---|
splitKey() |
Parse key into section, subsection, key | String splitting logic |
canonicalizeKey() |
Normalize key per git rules | Case normalization |
globMatch() |
Pattern matching for includes | gobwas/glob wrapper |
parseLineForComment() |
Handle quoted strings in values | State machine parser |
trim() |
Whitespace handling | Standard library wrapper |
Purpose: Handle differences between operating systems
Files:
gitconfig.go- Common functionsgitconfig_windows.go- Windows-specific pathsgitconfig_others.go- Unix/Linux/macOS paths
Key Differences:
- Home directory detection (environment variables)
- Path separators (\ vs /)
- Config file locations (Windows vs Unix conventions)
- Permission handling
Decision: Minimize external dependencies
Reasoning:
- Improves portability and cross-compilation
- Reduces build complexity
- Avoids dependency version conflicts
- Easier to maintain long-term
Exception - gobwas/glob:
- Required for include conditional pattern matching
- Minimal pure-Go library
- No dependencies of its own
Decision: Maintain original file formatting during modifications
Reasoning:
- Preserves user formatting intentions
- Retains comments which may contain important notes
- Matches git behavior of preserving structure
- Enables collaborative workflows where formatting matters
Trade-off:
- File write is O(n) instead of O(1) (n = file size)
- Acceptable because: config files are small (< 10KB typical), write frequency is low
Decision: Separate local/global/worktree in public API
Reasoning:
- Makes scope explicit (no hidden behavior)
- Prevents bugs where wrong scope is written
- Self-documenting code (SetLocal() clearly means local)
- Aligns with git subcommands (git config --local vs --global)
Trade-off:
- Slightly more verbose API
- Benefit: correctness and clarity outweigh verbosity
Decision: Get() returns single string, GetAll() for multiple values
Reasoning:
- Simpler common case (most keys have one value)
- Clear distinction between single and multi-value patterns
- Prevents accidental data loss
Trade-off:
- Extra method for multi-value keys
- Benefit: prevents silent data truncation bugs
Decision: Store parsed config as map[string][]string
Reasoning:
- Fast lookups: O(1)
- Simple structure
- Easy to reason about
- Compatible with standard library patterns
Trade-off:
- Loses structural hierarchy (flat namespace)
- Benefit: simplicity and performance
Current: The library is NOT thread-safe by default.
Reason:
- Config file format is relatively simple
- Most applications load config once at startup
- Proper synchronization is application responsibility
- Adds complexity for uncommon use case
Recommendation for concurrent use:
- Use sync.RWMutex to protect Config/Configs objects
- Serialize writes to prevent corruption
- Example:
type ThreadSafeConfig struct {
mu sync.RWMutex
cfg *gitconfig.Config
}
func (tc *ThreadSafeConfig) Get(key string) (string, bool) {
tc.mu.RLock()
defer tc.mu.RUnlock()
return tc.cfg.Get(key)
}| Operation | Complexity | Notes |
|---|---|---|
| Get(key) | O(1) | Map lookup |
| GetAll(key) | O(1) | Map lookup |
| Set(key) | O(n) | n = file size (rewrite required) |
| Unset(key) | O(n) | n = file size |
| IsSet(key) | O(1) | Map lookup |
| LoadAll() | O(m) | m = number of config files |
- Parsed representation: O(k) where k = number of keys
- Raw representation: O(f) where f = file size
- Total: O(k + f)
For typical git configs: < 10KB, so negligible impact.
On typical systems:
- Loading a config: < 1ms
- Reading a value: < 0.1ms
- Writing a value: 1-5ms
- Loading all scopes: 5-10ms
Performance is not a bottleneck for config operations since they typically happen at application startup.
gitconfig supports the [include] directive:
[include]
path = /path/to/common.confImplementation:
- Parser detects include directives
- Recursively loads included files
- Values from includes are merged into the same Config object
- Later values override earlier ones (path order matters)
Use Cases:
- Base configurations (DRY principle)
- Environment-specific overrides
- Team shared settings
- Machine-specific secrets (with .gitignore)
Git also supports conditional includes (gitconfig 2.13+):
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-workImplementation:
- Uses glob pattern matching (globMatch)
- Conditional evaluated at load time
- Only matching includes are processed
The core API is stable and unlikely to change significantly because:
- Strongly tied to git config semantics
- Already covers primary use cases
- Simple, minimal API reduces change surface area
Unit tests with a target coverage: > 80%