Implements configurable per-field merge strategies (hq-ew1mbr.11):
- Add FieldStrategy type with strategies: newest, max, union, manual
- Add conflict.fields config section for per-field overrides
- compaction_level defaults to "max" (highest value wins)
- estimated_minutes defaults to "manual" (flags for user resolution)
- labels defaults to "union" (set merge)
Manual conflicts are displayed during sync with resolution options:
bd sync --ours / --theirs, or bd resolve <id> <field> <value>
Config example:
conflict:
strategy: newest
fields:
compaction_level: max
estimated_minutes: manual
labels: union
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
268 lines
8.7 KiB
Go
268 lines
8.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Sync mode configuration values (from hq-ew1mbr.3)
|
|
// These control how Dolt syncs with JSONL/remotes.
|
|
|
|
// ConfigWarnings controls whether warnings are logged for invalid config values.
|
|
// Set to false to suppress warnings (useful for tests or scripts).
|
|
var ConfigWarnings = true
|
|
|
|
// ConfigWarningWriter is the destination for config warnings.
|
|
// Defaults to os.Stderr. Can be replaced for testing or custom logging.
|
|
var ConfigWarningWriter io.Writer = os.Stderr
|
|
|
|
// logConfigWarning logs a warning message if ConfigWarnings is enabled.
|
|
func logConfigWarning(format string, args ...interface{}) {
|
|
if ConfigWarnings && ConfigWarningWriter != nil {
|
|
_, _ = fmt.Fprintf(ConfigWarningWriter, format, args...)
|
|
}
|
|
}
|
|
|
|
// SyncMode represents the sync mode configuration
|
|
type SyncMode string
|
|
|
|
const (
|
|
// SyncModeGitPortable exports JSONL on push, imports on pull (default)
|
|
SyncModeGitPortable SyncMode = "git-portable"
|
|
// SyncModeRealtime exports JSONL on every change (legacy behavior)
|
|
SyncModeRealtime SyncMode = "realtime"
|
|
// SyncModeDoltNative uses Dolt remote directly (dolthub://, gs://, s3://)
|
|
SyncModeDoltNative SyncMode = "dolt-native"
|
|
// SyncModeBeltAndSuspenders uses Dolt remote + JSONL backup
|
|
SyncModeBeltAndSuspenders SyncMode = "belt-and-suspenders"
|
|
)
|
|
|
|
// validSyncModes is the set of allowed sync mode values
|
|
var validSyncModes = map[SyncMode]bool{
|
|
SyncModeGitPortable: true,
|
|
SyncModeRealtime: true,
|
|
SyncModeDoltNative: true,
|
|
SyncModeBeltAndSuspenders: true,
|
|
}
|
|
|
|
// ValidSyncModes returns the list of valid sync mode values.
|
|
func ValidSyncModes() []string {
|
|
return []string{
|
|
string(SyncModeGitPortable),
|
|
string(SyncModeRealtime),
|
|
string(SyncModeDoltNative),
|
|
string(SyncModeBeltAndSuspenders),
|
|
}
|
|
}
|
|
|
|
// IsValidSyncMode returns true if the given string is a valid sync mode.
|
|
func IsValidSyncMode(mode string) bool {
|
|
return validSyncModes[SyncMode(strings.ToLower(strings.TrimSpace(mode)))]
|
|
}
|
|
|
|
// ConflictStrategy represents the conflict resolution strategy
|
|
type ConflictStrategy string
|
|
|
|
const (
|
|
// ConflictStrategyNewest uses last-write-wins (default)
|
|
ConflictStrategyNewest ConflictStrategy = "newest"
|
|
// ConflictStrategyOurs prefers local changes
|
|
ConflictStrategyOurs ConflictStrategy = "ours"
|
|
// ConflictStrategyTheirs prefers remote changes
|
|
ConflictStrategyTheirs ConflictStrategy = "theirs"
|
|
// ConflictStrategyManual requires manual resolution
|
|
ConflictStrategyManual ConflictStrategy = "manual"
|
|
)
|
|
|
|
// FieldStrategy represents the merge strategy for a specific field
|
|
type FieldStrategy string
|
|
|
|
const (
|
|
// FieldStrategyNewest uses last-write-wins (default for scalar fields)
|
|
FieldStrategyNewest FieldStrategy = "newest"
|
|
// FieldStrategyMax takes the maximum value (for counters like compaction_level)
|
|
FieldStrategyMax FieldStrategy = "max"
|
|
// FieldStrategyUnion performs set union (for arrays like labels, waiters)
|
|
FieldStrategyUnion FieldStrategy = "union"
|
|
// FieldStrategyManual flags conflict for user resolution (for fields like estimated_minutes)
|
|
FieldStrategyManual FieldStrategy = "manual"
|
|
)
|
|
|
|
// validConflictStrategies is the set of allowed conflict strategy values
|
|
var validConflictStrategies = map[ConflictStrategy]bool{
|
|
ConflictStrategyNewest: true,
|
|
ConflictStrategyOurs: true,
|
|
ConflictStrategyTheirs: true,
|
|
ConflictStrategyManual: true,
|
|
}
|
|
|
|
// validFieldStrategies is the set of allowed per-field strategy values
|
|
var validFieldStrategies = map[FieldStrategy]bool{
|
|
FieldStrategyNewest: true,
|
|
FieldStrategyMax: true,
|
|
FieldStrategyUnion: true,
|
|
FieldStrategyManual: true,
|
|
}
|
|
|
|
// ValidConflictStrategies returns the list of valid conflict strategy values.
|
|
func ValidConflictStrategies() []string {
|
|
return []string{
|
|
string(ConflictStrategyNewest),
|
|
string(ConflictStrategyOurs),
|
|
string(ConflictStrategyTheirs),
|
|
string(ConflictStrategyManual),
|
|
}
|
|
}
|
|
|
|
// IsValidConflictStrategy returns true if the given string is a valid conflict strategy.
|
|
func IsValidConflictStrategy(strategy string) bool {
|
|
return validConflictStrategies[ConflictStrategy(strings.ToLower(strings.TrimSpace(strategy)))]
|
|
}
|
|
|
|
// ValidFieldStrategies returns the list of valid per-field strategy values.
|
|
func ValidFieldStrategies() []string {
|
|
return []string{
|
|
string(FieldStrategyNewest),
|
|
string(FieldStrategyMax),
|
|
string(FieldStrategyUnion),
|
|
string(FieldStrategyManual),
|
|
}
|
|
}
|
|
|
|
// IsValidFieldStrategy returns true if the given string is a valid per-field strategy.
|
|
func IsValidFieldStrategy(strategy string) bool {
|
|
return validFieldStrategies[FieldStrategy(strings.ToLower(strings.TrimSpace(strategy)))]
|
|
}
|
|
|
|
// Sovereignty represents the federation sovereignty tier
|
|
type Sovereignty string
|
|
|
|
const (
|
|
// SovereigntyNone means no sovereignty restriction (empty value)
|
|
SovereigntyNone Sovereignty = ""
|
|
// SovereigntyT1 is the most open tier (public repos)
|
|
SovereigntyT1 Sovereignty = "T1"
|
|
// SovereigntyT2 is organization-level
|
|
SovereigntyT2 Sovereignty = "T2"
|
|
// SovereigntyT3 is pseudonymous
|
|
SovereigntyT3 Sovereignty = "T3"
|
|
// SovereigntyT4 is anonymous
|
|
SovereigntyT4 Sovereignty = "T4"
|
|
)
|
|
|
|
// validSovereigntyTiers is the set of allowed sovereignty values (excluding empty)
|
|
var validSovereigntyTiers = map[Sovereignty]bool{
|
|
SovereigntyT1: true,
|
|
SovereigntyT2: true,
|
|
SovereigntyT3: true,
|
|
SovereigntyT4: true,
|
|
}
|
|
|
|
// ValidSovereigntyTiers returns the list of valid sovereignty tier values.
|
|
func ValidSovereigntyTiers() []string {
|
|
return []string{
|
|
string(SovereigntyT1),
|
|
string(SovereigntyT2),
|
|
string(SovereigntyT3),
|
|
string(SovereigntyT4),
|
|
}
|
|
}
|
|
|
|
// IsValidSovereignty returns true if the given string is a valid sovereignty tier.
|
|
// Empty string is valid (means no restriction).
|
|
func IsValidSovereignty(sovereignty string) bool {
|
|
if sovereignty == "" {
|
|
return true
|
|
}
|
|
return validSovereigntyTiers[Sovereignty(strings.ToUpper(strings.TrimSpace(sovereignty)))]
|
|
}
|
|
|
|
// GetSyncMode retrieves the sync mode configuration.
|
|
// Returns the configured mode, or SyncModeGitPortable (default) if not set or invalid.
|
|
// Logs a warning if an invalid value is configured (unless ConfigWarnings is false).
|
|
//
|
|
// Config key: sync.mode
|
|
// Valid values: git-portable, realtime, dolt-native, belt-and-suspenders
|
|
func GetSyncMode() SyncMode {
|
|
value := GetString("sync.mode")
|
|
if value == "" {
|
|
return SyncModeGitPortable // Default
|
|
}
|
|
|
|
mode := SyncMode(strings.ToLower(strings.TrimSpace(value)))
|
|
if !validSyncModes[mode] {
|
|
logConfigWarning("Warning: invalid sync.mode %q in config (valid: %s), using default 'git-portable'\n",
|
|
value, strings.Join(ValidSyncModes(), ", "))
|
|
return SyncModeGitPortable
|
|
}
|
|
|
|
return mode
|
|
}
|
|
|
|
// GetConflictStrategy retrieves the conflict resolution strategy configuration.
|
|
// Returns the configured strategy, or ConflictStrategyNewest (default) if not set or invalid.
|
|
// Logs a warning if an invalid value is configured (unless ConfigWarnings is false).
|
|
//
|
|
// Config key: conflict.strategy
|
|
// Valid values: newest, ours, theirs, manual
|
|
func GetConflictStrategy() ConflictStrategy {
|
|
value := GetString("conflict.strategy")
|
|
if value == "" {
|
|
return ConflictStrategyNewest // Default
|
|
}
|
|
|
|
strategy := ConflictStrategy(strings.ToLower(strings.TrimSpace(value)))
|
|
if !validConflictStrategies[strategy] {
|
|
logConfigWarning("Warning: invalid conflict.strategy %q in config (valid: %s), using default 'newest'\n",
|
|
value, strings.Join(ValidConflictStrategies(), ", "))
|
|
return ConflictStrategyNewest
|
|
}
|
|
|
|
return strategy
|
|
}
|
|
|
|
// GetSovereignty retrieves the federation sovereignty tier configuration.
|
|
// Returns the configured tier, or SovereigntyNone (empty, no restriction) if not set.
|
|
// Returns SovereigntyT1 and logs a warning if an invalid non-empty value is configured.
|
|
//
|
|
// Config key: federation.sovereignty
|
|
// Valid values: T1, T2, T3, T4 (empty means no restriction)
|
|
func GetSovereignty() Sovereignty {
|
|
value := GetString("federation.sovereignty")
|
|
if value == "" {
|
|
return SovereigntyNone // No restriction
|
|
}
|
|
|
|
// Normalize to uppercase for comparison (T1, T2, etc.)
|
|
tier := Sovereignty(strings.ToUpper(strings.TrimSpace(value)))
|
|
if !validSovereigntyTiers[tier] {
|
|
logConfigWarning("Warning: invalid federation.sovereignty %q in config (valid: %s, or empty for no restriction), using 'T1'\n",
|
|
value, strings.Join(ValidSovereigntyTiers(), ", "))
|
|
return SovereigntyT1
|
|
}
|
|
|
|
return tier
|
|
}
|
|
|
|
// String returns the string representation of the SyncMode.
|
|
func (m SyncMode) String() string {
|
|
return string(m)
|
|
}
|
|
|
|
// String returns the string representation of the ConflictStrategy.
|
|
func (s ConflictStrategy) String() string {
|
|
return string(s)
|
|
}
|
|
|
|
// String returns the string representation of the Sovereignty.
|
|
func (s Sovereignty) String() string {
|
|
return string(s)
|
|
}
|
|
|
|
// String returns the string representation of the FieldStrategy.
|
|
func (f FieldStrategy) String() string {
|
|
return string(f)
|
|
}
|