config

package
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package config provides configuration management for gz-git CLI.

Overview

This package implements a flexible configuration system with profiles, global defaults, and project-specific overrides. It follows a 5-layer precedence system to resolve configuration values.

Precedence Order (Highest to Lowest)

  1. Command flags (e.g., --provider gitlab)
  2. Project config (.gz-git.yaml in current dir or parent)
  3. Active profile (~/.config/gz-git/profiles/{active}.yaml)
  4. Global config (~/.config/gz-git/config.yaml)
  5. Built-in defaults

File Locations

Global configuration directory: ~/.config/gz-git/

~/.config/gz-git/
├── config.yaml              # Global config
├── profiles/
│   ├── default.yaml        # Default profile
│   ├── work.yaml           # User profiles
│   └── personal.yaml
└── state/
    └── active-profile.txt  # Currently active profile

Project configuration file: .gz-git.yaml

This file is auto-detected by walking up the directory tree from the current working directory to the home directory.

Usage Example

Basic usage:

// Create a manager
mgr, err := config.NewManager()
if err != nil {
    log.Fatal(err)
}

// Initialize config directory
if err := mgr.Initialize(); err != nil {
    log.Fatal(err)
}

// Create a profile
profile := &config.Profile{
    Name:       "work",
    Provider:   "gitlab",
    BaseURL:    "https://gitlab.company.com",
    Token:      "${WORK_GITLAB_TOKEN}",
    CloneProto: "ssh",
    SSHPort:    2224,
    Parallel:   10,
}
if err := mgr.CreateProfile(profile); err != nil {
    log.Fatal(err)
}

// Set active profile
if err := mgr.SetActiveProfile("work"); err != nil {
    log.Fatal(err)
}

// Load configuration with precedence
loader, err := config.NewLoader()
if err != nil {
    log.Fatal(err)
}
if err := loader.Load(); err != nil {
    log.Fatal(err)
}

// Resolve effective config (merge flags if any)
flags := map[string]interface{}{
    "parallel": 20, // Override profile value
}
effective, err := loader.ResolveConfig(flags)
if err != nil {
    log.Fatal(err)
}

// Use effective config
fmt.Printf("Provider: %s (from %s)\n", effective.Provider, effective.GetSource("provider"))
fmt.Printf("Parallel: %d (from %s)\n", effective.Parallel, effective.GetSource("parallel"))

Environment Variables

Configuration files support environment variable expansion using ${VAR_NAME} syntax:

token: ${GITLAB_TOKEN}
baseURL: ${GITLAB_BASE_URL}

This is the recommended way to store sensitive values like API tokens.

Security

- Profile files are created with 0600 permissions (user read/write only) - Config directories are created with 0700 permissions (user access only) - Environment variable expansion is safe (no shell command execution) - Tokens should use ${ENV_VAR} syntax, not plain text

Validation

All configuration structures are validated on load: - Required fields are checked - Enum values (provider, clone protocol, etc.) are validated - Port numbers and counts are range-checked - Profile names must be alphanumeric with dash/underscore

Validation errors are returned with clear messages indicating the problem.

Package config provides configuration management for gz-git, including profile-based settings, global defaults, and project-specific overrides.

Configuration follows a 5-layer precedence system (highest to lowest):

  1. Command flags (e.g., --provider gitlab)
  2. Project config (.gz-git.yaml in current dir or parent)
  3. Active profile (~/.config/gz-git/profiles/{active}.yaml)
  4. Global config (~/.config/gz-git/config.yaml)
  5. Built-in defaults

Index

Constants

View Source
const (
	// IntegrationBranchSourceNone means no integration branch participates.
	IntegrationBranchSourceNone = "none"
	// IntegrationBranchSourceHeuristic means the remote-HEAD fallback won.
	IntegrationBranchSourceHeuristic = "heuristic"
	// IntegrationBranchSourceConfigPrefix prefixes a declared candidate source.
	IntegrationBranchSourceConfigPrefix = "config["
)
View Source
const (
	// ConfigDirName is the config directory name under XDG_CONFIG_HOME.
	ConfigDirName = "gz-git"

	// ProfilesDirName is the subdirectory for profile files.
	ProfilesDirName = "profiles"

	// StateDirName is the subdirectory for runtime state.
	StateDirName = "state"

	// GlobalConfigFileName is the base name for the main config file.
	GlobalConfigFileName = "config"

	// ProjectConfigFileName is the base name for the project-specific config file.
	ProjectConfigFileName = ".gz-git"

	// ActiveProfileFileName stores the active profile name.
	ActiveProfileFileName = "active-profile.txt"

	// DefaultProfileName is the default profile.
	DefaultProfileName = "default"
)
View Source
const (
	// PrepareProfileFamilybookEntV1 prepares Ent generated code.
	PrepareProfileFamilybookEntV1 = "familybook-ent-v1"
	// PrepareProfileFlowTaskchainLocalSubprojectsV1 prepares local taskchain subprojects.
	PrepareProfileFlowTaskchainLocalSubprojectsV1 = "flow-taskchain-local-subprojects-v1"
)
View Source
const AutoGeneratedMarker = "# AUTO-GENERATED"

AutoGeneratedMarker is the comment marker that identifies auto-generated configs.

View Source
const DefaultConfigFileName = ".gz-git.yaml"

DefaultConfigFileName is the default name for config files.

View Source
const ExampleConfig = `` /* 7893-byte string literal not displayed */

ExampleConfig is a documented example configuration showing all available options.

View Source
const (
	// MaxConfigDepth limits the recursion depth for config loading.
	// This prevents stack overflow from deeply nested or malformed configs.
	MaxConfigDepth = 10
)

Variables

View Source
var ErrKeyringUnavailable = fmt.Errorf("keyring unavailable")

ErrKeyringUnavailable is returned when the OS keychain cannot be used.

Functions

func CanonicalReadiness

func CanonicalReadiness(r Readiness) (string, error)

CanonicalReadiness is a stable, strict representation used to compare the target declaration to the source declaration before executing a runner.

func ClearWorkspaceAccessMarker

func ClearWorkspaceAccessMarker(ctx context.Context, repoPath string) error

ClearWorkspaceAccessMarker removes a previously persisted read-only contract after the owning workspace has successfully synced as read-write.

func CreateConfigSymlink(srcPath, targetDir, parentConfigDir string) error

CreateConfigSymlink creates a symlink from the workspace config location to a source config file.

Parameters:

  • srcPath: The source config file path. Supports:
  • Absolute paths: /path/to/config.yaml
  • Home-relative: ~/configs/myconfig.yaml
  • Relative to parent config: ./configs/myconfig.yaml
  • targetDir: The workspace directory where the symlink will be created as .gz-git.yaml
  • parentConfigDir: The directory containing the parent config (for resolving relative paths)

The symlink is created at {targetDir}/.gz-git.yaml → {resolved srcPath}

If a file already exists at the symlink location:

  • If it's a symlink: it will be removed and recreated
  • If it's a regular file: returns an error (use force=true to override)

func CreateConfigSymlinkForce

func CreateConfigSymlinkForce(srcPath, targetDir, parentConfigDir string) error

CreateConfigSymlinkForce creates a symlink, removing any existing file (not just symlinks). Use with caution - this will delete existing config files.

func DetectAllConfigFiles

func DetectAllConfigFiles(dir string) []string

DetectAllConfigFiles finds all config files in a directory. Returns list of found config file paths.

func DetectConfigFile

func DetectConfigFile(dir string) (string, error)

DetectConfigFile searches upward from the given directory to $HOME for a .gz-git config file and returns the full path to the nearest one. Searching upward (rather than the given directory alone) lets commands run from a workspace subdirectory resolve the workspace's existing config instead of re-initializing a second one in the wrong place. Returns an error when no config exists between dir and $HOME.

func FindConfigRecursive

func FindConfigRecursive(startPath, configFile string) (string, error)

FindConfigRecursive walks up from startPath to $HOME to find the nearest directory containing configFile. It delegates to the shared findConfigUpward core, so the $HOME ceiling is identical to FindProjectConfig/DetectConfigFile; the configFile parameter lets callers probe a non-default filename.

Parameters:

  • startPath: The directory to start searching from
  • configFile: The config file name to search for (e.g., ".gz-git.yaml")

Returns:

  • string: The directory containing the config file
  • error: Error if config file not found

Example:

// Find nearest .gz-git.yaml
configDir, err := FindConfigRecursive("/home/user/mydevbox/project", ".gz-git.yaml")
// Returns: "/home/user/mydevbox" if .gz-git.yaml exists there

func FindProjectConfig

func FindProjectConfig() (string, error)

FindProjectConfig walks up from the current working directory to $HOME and returns the path to the nearest .gz-git config file, or "" if none exists.

func GetAllProfiles

func GetAllProfiles(config *Config) map[string]*Profile

GetAllProfiles returns all inline profiles from the config.

func GetAllWorkspaces

func GetAllWorkspaces(config *Config) []struct {
	Name      string
	Workspace *Workspace
}

GetAllWorkspaces returns all workspaces as a slice with their names.

func GetConfigWorkspaces

func GetConfigWorkspaces(config *Config) map[string]*Workspace

GetConfigWorkspaces returns workspaces with type=config that have sync.recursive enabled. These are workspaces whose child config should be loaded and synced by the parent.

func GetForgeWorkspaces

func GetForgeWorkspaces(config *Config) map[string]*Workspace

GetForgeWorkspaces returns only workspaces that sync from a forge.

func GetGitWorkspaces

func GetGitWorkspaces(config *Config) map[string]*Workspace

GetGitWorkspaces returns only workspaces that are single git repositories (type=git with URL). These are workspaces with explicit URL that should be cloned/synced directly.

func GetProfileSource

func GetProfileSource(config *Config, name string) string

GetProfileSource returns the source location of a profile. Useful for debugging config precedence. Returns empty string if profile not found.

func GetSymlinkTarget

func GetSymlinkTarget(linkPath string) (string, error)

GetSymlinkTarget returns the target of a symlink, or empty string if not a symlink.

func HasInlineProfile

func HasInlineProfile(config *Config, name string) bool

HasInlineProfile checks if a profile exists in the inline profiles.

func IsConfigSymlink(configPath string) (bool, error)

IsConfigSymlink checks if the config file at the given path is a symlink.

func IsValidBaseURL

func IsValidBaseURL(s string) bool

IsValidBaseURL reports whether s is empty (optional) or a parseable http(s) URL with a host. Used to validate forge base URLs (GitHub Enterprise / GitLab / Gitea). Prefix-only values such as "https://" are rejected (fail-closed).

func IsValidCloneProto

func IsValidCloneProto(proto string) bool

IsValidCloneProto checks if a clone protocol is valid.

func IsValidProfileName

func IsValidProfileName(name string) bool

IsValidProfileName checks if a profile name is valid.

func IsValidProvider

func IsValidProvider(provider string) bool

IsValidProvider checks if a provider name is valid.

func IsValidSyncStrategy

func IsValidSyncStrategy(strategy string) bool

IsValidSyncStrategy checks if a sync strategy is valid.

func LoadWorkspaces

func LoadWorkspaces(path string, config *Config, mode DiscoveryMode) error

LoadWorkspaces loads workspaces based on discovery mode. This function is called after LoadConfigRecursive to optionally auto-discover workspaces.

Parameters:

  • path: The directory to search for workspaces
  • config: The config to append discovered workspaces to
  • mode: The discovery mode (explicit, auto, hybrid)

Returns:

  • error: Any error encountered during discovery

Example:

config, _ := LoadConfigRecursive("/home/user/mydevbox", ".gz-git.yaml")
err := LoadWorkspaces("/home/user/mydevbox", config, HybridMode)

func MatchTaskPattern

func MatchTaskPattern(name, pattern string) bool

MatchTaskPattern reports whether name falls inside a declared task-branch namespace. It delegates to pkg/repository, the single source of truth (see repository.MatchTaskPattern for why ownership sits there).

func MatchesAnyTaskPattern

func MatchesAnyTaskPattern(name string, patterns []string) bool

MatchesAnyTaskPattern reports whether name matches any declared pattern.

func NormalizeIntegrationBranchName

func NormalizeIntegrationBranchName(raw string, remotes []string) string

NormalizeIntegrationBranchName strips only a registered remote prefix.

func NormalizeName

func NormalizeName(raw string, remotes []string) string

NormalizeName is the concise form of NormalizeIntegrationBranchName.

func NormalizeProvider

func NormalizeProvider(provider string) string

NormalizeProvider converts provider name to lowercase.

func ParsePrepareProfileDocument

func ParsePrepareProfileDocument(data []byte, isJSON bool) (profile string, present bool, err error)

ParsePrepareProfileDocument extracts branch.prepareProfile from a repository-root config. It deliberately examines the source document rather than a merged config so duplicate keys and YAML multi-document payloads cannot be hidden by a permissive general-purpose decoder.

func ReadinessDigest

func ReadinessDigest(r Readiness) (string, error)

ReadinessDigest returns the SHA-256 digest of the canonical contract.

func RecordWorkspaceReadOnly

func RecordWorkspaceReadOnly(ctx context.Context, repoPath string) error

RecordWorkspaceReadOnly stores the resolved policy in repository-local Git metadata. This preserves an external absolute workspace's contract when a later push is invoked from outside the owning config tree.

func RejectMultiDocumentYAML

func RejectMultiDocumentYAML(data []byte) error

RejectMultiDocumentYAML reports an error when data carries more than one YAML document.

yaml.Unmarshal decodes the first document and discards the rest without a word. Every shape check written on top of it — the field allowlists below, the "nothing changed beyond branch.readiness" comparison in pkg/integrate — then inspects document 1 while documents 2..n travel with the commit unread. A config whose first document is an unremarkable, passing contract can carry a second document declaring anything at all, and the checks that exist to keep it off a protected branch never see it.

A leading "---" is not a second document. A trailing one is, because it opens an empty document after the content; both are rejected the same way. The contract is exactly one document, and no .gz-git.yaml in this family uses a document marker at all.

func ResolveDeclaredIntegrationBranch

func ResolveDeclaredIntegrationBranch(ctx context.Context, repoPath string) (string, error)

ResolveDeclaredIntegrationBranch returns the repository's declared canonical integration branch, resolved to a ref that actually exists.

It returns ("", nil) when .gz-git.yaml declares none. That is a distinct answer from an error and callers must keep it distinct: "this repository never said which branch is canonical" is a reason to do nothing, not a reason to fall back to a name heuristic.

func ResolveTokenFromEnv

func ResolveTokenFromEnv(provider string) (token, source string)

ResolveTokenFromEnv returns a token from environment variables for the provider. Checks provider-specific vars first, then GZ_GIT_TOKEN.

func SanitizeToken

func SanitizeToken(s string) string

SanitizeToken removes credentials from URLs for safe logging.

func SetTokenStore

func SetTokenStore(s TokenStore)

SetTokenStore replaces the default store (tests).

func SplitIntegrationRemoteBranch

func SplitIntegrationRemoteBranch(raw string, remotes []string) (remote, branch string, ok bool)

SplitIntegrationRemoteBranch separates a tracking ref using the longest registered remote prefix, preserving slash-containing remote and branch names.

func SplitRemoteBranch

func SplitRemoteBranch(raw string, remotes []string) (remote, branch string, ok bool)

SplitRemoteBranch is the concise form of SplitIntegrationRemoteBranch.

func UpstreamTargetsIntegration

func UpstreamTargetsIntegration(branch, upstream string, resolution Resolution, remotes []string) bool

UpstreamTargetsIntegration is the concise form of UpstreamTargetsIntegrationBranch.

func UpstreamTargetsIntegrationBranch

func UpstreamTargetsIntegrationBranch(branch, upstream string, resolution IntegrationBranchResolution, remotes []string) bool

UpstreamTargetsIntegrationBranch reports a non-integration branch tracking the integration branch itself.

func ValidatePrepareProfile

func ValidatePrepareProfile(profile string) error

ValidatePrepareProfile validates the closed set of repository preparation profiles. An empty value is valid only as an absent declaration; callers that parse a present declaration reject it before reaching this function.

func ValidateReadiness

func ValidateReadiness(r Readiness) error

ValidateReadiness validates the closed V1 contract.

Types

type AuditConfig

type AuditConfig struct {
	// Autofix overrides repository.DefaultAutofixPolicy per finding code. Keys
	// are finding codes (BRANCH_BEHIND_BASE, …); a code that is absent keeps its
	// built-in default, so a project states only where it disagrees.
	//
	// There are no tier names to choose from. A name like "aggressive" would
	// have to be decoded into behavior before anyone could trust it, and it
	// would force every code added later into a bucket someone else picked. A
	// per-code boolean is the same information without the indirection.
	//
	// Enabling a code here still cannot make an irreversible remediation
	// automatic: reversibility is checked after policy, so configuration widens
	// what is permitted but never what is safe.
	Autofix map[string]bool `yaml:"autofix,omitempty"`
}

AuditConfig holds `info --audit` defaults.

Example:

audit:
  autofix:
    BRANCH_BEHIND_BASE: false        # this project rebases by hand
    MERGED_BRANCH_NOT_RECLAIMED: true

type BranchConfig

type BranchConfig struct {
	DefaultBranch     BranchList `yaml:"defaultBranch,omitempty"`     // main, develop, master (string or list)
	ProtectedBranches []string   `yaml:"protectedBranches,omitempty"` // Branches to protect

	// IntegrationBranch is the ordered list of integration-branch names.
	// Consumers must read it from the repo-root file (LoadRepoRootTaskPattern),
	// not from the 5-layer merger.
	IntegrationBranch BranchList `yaml:"integrationBranch,omitempty"`

	// TaskPattern is the reclaim allow-list (first-* namespace prefix).
	// Load it only via LoadRepoRootTaskPattern — never findConfigUpward.
	TaskPattern BranchList `yaml:"taskPattern,omitempty"`

	// PrepareProfile selects a closed integration preparation profile. It is
	// preserved on a repository-root project config, but must never be
	// inherited or merged from a parent, profile, or workspace configuration.
	PrepareProfile string `yaml:"prepareProfile,omitempty" json:"prepareProfile,omitempty"`

	// Readiness is a target-owned integration gate. Like TaskPattern, it is
	// preserved here for project-config round trips but must never be inherited
	// or merged from a parent, profile, workspace, or global configuration.
	Readiness *Readiness `yaml:"readiness,omitempty" json:"readiness,omitempty"`

	// Naming templates the branch names that `gz-git branch name` builds, so a
	// task branch is spelled the same way on every machine and by every agent.
	Naming *branch.Naming `yaml:"naming,omitempty"`
}

BranchConfig holds branch command defaults. Supports both string shorthand and struct format in YAML:

  • branch: develop → BranchConfig{DefaultBranch: ["develop"]}
  • branch: develop,master → BranchConfig{DefaultBranch: ["develop", "master"]}
  • branch: defaultBranch: develop → standard struct format

func (*BranchConfig) UnmarshalYAML

func (b *BranchConfig) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML implements yaml.Unmarshaler to support string shorthand. When YAML contains `branch: develop` (a plain string), it is converted to BranchConfig{DefaultBranch: ["develop"]}. This follows the same pattern as BranchList.UnmarshalYAML.

type BranchList

type BranchList []string

BranchList supports both string and list formats for branch specification. Examples:

  • defaultBranch: develop # single branch
  • defaultBranch: develop,master # comma-separated string
  • defaultBranch: [develop, master] # YAML list

func (BranchList) First

func (b BranchList) First() string

First returns the first branch in the list, or empty string if empty.

func (BranchList) MarshalYAML

func (b BranchList) MarshalYAML() (any, error)

MarshalYAML implements yaml.Marshaler to output as comma-separated string.

func (BranchList) String

func (b BranchList) String() string

String returns the branch list as comma-separated string.

func (*BranchList) UnmarshalYAML

func (b *BranchList) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML implements yaml.Unmarshaler to support both string and list formats.

type ChildConfigFormat

type ChildConfigFormat string

ChildConfigFormat represents the format/origin of a child config file.

const (
	// ChildConfigFormatAutoGenerated indicates the config was auto-generated by gz-git.
	// It can be safely overwritten during sync.
	ChildConfigFormatAutoGenerated ChildConfigFormat = "auto-generated"

	// ChildConfigFormatUserMaintained indicates the config is user-maintained.
	// It should NOT be overwritten without explicit --force flag.
	ChildConfigFormatUserMaintained ChildConfigFormat = "user-maintained"

	// ChildConfigFormatNotFound indicates no config file exists at the path.
	ChildConfigFormatNotFound ChildConfigFormat = "not-found"
)

func DetectChildConfigFormat

func DetectChildConfigFormat(path string) (ChildConfigFormat, error)

DetectChildConfigFormat checks if a config file is auto-generated or user-maintained.

Detection logic:

  • File not exists → ChildConfigFormatNotFound
  • File contains "# AUTO-GENERATED" marker → ChildConfigFormatAutoGenerated
  • Otherwise → ChildConfigFormatUserMaintained

type ChildConfigMode

type ChildConfigMode string

ChildConfigMode controls how child config files are generated during workspace sync.

const (
	// ChildConfigModeRepositories generates a flat array format (default).
	// Example: repositories: [{name: repo1, url: ...}, ...].
	ChildConfigModeRepositories ChildConfigMode = "repositories"

	// ChildConfigModeWorkspaces generates a map structure format.
	// Example: workspaces: {repo1: {path: repo1, type: git}, ...}.
	ChildConfigModeWorkspaces ChildConfigMode = "workspaces"

	// ChildConfigModeNone creates directory only, no config file.
	// Useful when child config is manually maintained or not needed.
	ChildConfigModeNone ChildConfigMode = "none"
)

func (ChildConfigMode) Default

func (m ChildConfigMode) Default() ChildConfigMode

Default returns the default mode if empty.

func (ChildConfigMode) IsValid

func (m ChildConfigMode) IsValid() bool

IsValid returns true if this is a valid child config mode.

type CloneDefaults

type CloneDefaults struct {
	Proto         string `yaml:"proto,omitempty"`         // ssh, https
	SSHPort       int    `yaml:"sshPort,omitempty"`       // Custom SSH port
	SSHKeyPath    string `yaml:"sshKeyPath,omitempty"`    // SSH private key file path
	SSHKeyContent string `yaml:"sshKeyContent,omitempty"` // SSH private key content (use ${ENV_VAR})
}

CloneDefaults holds clone-related default settings.

type Config

type Config struct {

	// Parent specifies an explicit path to a parent config file.
	// When set, the parent config is loaded and merged (child overrides parent).
	// Supports: absolute paths, home-relative (~), relative paths.
	Parent string `yaml:"parent,omitempty"`

	// Profile specifies which profile to use at this level
	Profile string `yaml:"profile,omitempty"`

	// Profiles defines named profiles inline (no external file needed)
	Profiles map[string]*Profile `yaml:"profiles,omitempty"`

	// Defaults groups all default settings
	Defaults *DefaultsConfig `yaml:"defaults,omitempty"`

	Provider         string `yaml:"provider,omitempty"`         // github, gitlab, gitea
	BaseURL          string `yaml:"baseURL,omitempty"`          // API endpoint
	Token            string `yaml:"token,omitempty"`            // API token (use ${ENV_VAR})
	IncludeSubgroups bool   `yaml:"includeSubgroups,omitempty"` // GitLab subgroups
	SubgroupMode     string `yaml:"subgroupMode,omitempty"`     // flat, nested

	// Default workspace settings
	DefaultWorkspaceType WorkspaceType `yaml:"defaultWorkspaceType,omitempty"` // forge/git/config

	// Self-sync configuration (sync config directory itself)
	SelfSync *SelfSyncConfig `yaml:"selfSync,omitempty"`

	// Command-specific overrides
	Sync   *SyncConfig   `yaml:"sync,omitempty"`
	Branch *BranchConfig `yaml:"branch,omitempty"`
	Fetch  *FetchConfig  `yaml:"fetch,omitempty"`
	Pull   *PullConfig   `yaml:"pull,omitempty"`
	Push   *PushConfig   `yaml:"push,omitempty"`

	// Hooks defines global before/after commands for all workspace syncs
	Hooks *Hooks `yaml:"hooks,omitempty"`

	// Workspaces is a map of named workspace configurations
	Workspaces map[string]*Workspace `yaml:"workspaces,omitempty"`

	// Metadata is optional information about this level
	Metadata *Metadata `yaml:"metadata,omitempty"`

	// Discovery controls how workspaces are discovered
	Discovery *DiscoveryConfig `yaml:"discovery,omitempty"`

	// ChildConfigMode sets the default child config mode for all workspaces.
	// Values: "repositories" (default), "workspaces", "none"
	ChildConfigMode ChildConfigMode `yaml:"childConfigMode,omitempty"`

	ParentConfig *Config `yaml:"-"` // Resolved parent config
	ConfigPath   string  `yaml:"-"` // Absolute path to this config file
}

Config represents a hierarchical configuration that can be nested recursively. This is the unified config type used at ALL levels: workstation, workspace, project, submodule, etc.

Example usage:

# ~/.gz-git.yaml (workstation level)
profile: polypia

defaults:
  clone:
    proto: ssh
  sync:
    strategy: reset
    parallel: 10

# Inline profiles (no external file needed!)
profiles:
  polypia:
    provider: gitlab
    baseURL: https://gitlab.polypia.net
    token: ${GITLAB_POLYPIA_TOKEN}
  github-personal:
    provider: github
    token: ${GITHUB_TOKEN}

workspaces:
  devbox:
    path: ~/mydevbox
    source:
      provider: gitlab
      org: devbox
      includeSubgroups: true
      subgroupMode: flat
    sync:
      strategy: pull

func GetParentChain

func GetParentChain(config *Config) []*Config

GetParentChain returns all configs in the parent chain (including current). Useful for debugging and displaying config precedence.

func LoadConfigRecursive

func LoadConfigRecursive(path, configFile string) (*Config, error)

LoadConfigRecursive loads a config file and recursively loads all workspaces. This function works at ANY level (workstation, workspace, project, etc.)

Parameters:

  • path: The directory containing the config file
  • configFile: The config file name (e.g., ".gz-git.yaml", ".gz-git-config.yaml")

Returns:

  • *Config: The loaded config with all workspaces recursively loaded
  • error: Any error encountered during loading

Example:

// Load workstation config
home, _ := os.UserHomeDir()
config, err := LoadConfigRecursive(home, ".gz-git-config.yaml")

// Load workspace config
config, err := LoadConfigRecursive("/home/user/mydevbox", ".gz-git.yaml")

func (*Config) GetCloneProto

func (c *Config) GetCloneProto() string

GetCloneProto returns clone protocol from defaults.clone.proto.

func (*Config) GetCompactOutput

func (c *Config) GetCompactOutput() bool

GetCompactOutput returns compact output setting from defaults.output.compact.

func (*Config) GetExcludePatterns

func (c *Config) GetExcludePatterns() []string

GetExcludePatterns returns exclude patterns from defaults.filter.exclude.

Scope: the forge API repository listing in `workspace sync` only. Setting it does not keep a repository out of `push`, `commit`, or any other bulk command — use defaults.scan.exclude for that.

func (*Config) GetFormat

func (c *Config) GetFormat() string

GetFormat returns output format from defaults.output.format.

func (*Config) GetIncludePatterns

func (c *Config) GetIncludePatterns() []string

GetIncludePatterns returns include patterns from defaults.filter.include.

Scope: the forge API repository listing in `workspace sync` only. It does not filter the local directory scan that bulk commands run — see GetScanExcludePatterns for that.

func (*Config) GetMaxRetries

func (c *Config) GetMaxRetries() int

GetMaxRetries returns max retries (sync.maxRetries overrides defaults.sync.maxRetries).

func (*Config) GetParallel

func (c *Config) GetParallel() int

GetParallel returns parallel worker count from defaults.sync.parallel.

func (*Config) GetSSHKeyContent

func (c *Config) GetSSHKeyContent() string

GetSSHKeyContent returns SSH key content from defaults.clone.sshKeyContent.

func (*Config) GetSSHKeyPath

func (c *Config) GetSSHKeyPath() string

GetSSHKeyPath returns SSH key path from defaults.clone.sshKeyPath.

func (*Config) GetSSHPort

func (c *Config) GetSSHPort() int

GetSSHPort returns SSH port from defaults.clone.sshPort.

func (*Config) GetScanDepth

func (c *Config) GetScanDepth() int

GetScanDepth returns scan depth from defaults.scan.depth.

func (*Config) GetScanExcludePatterns

func (c *Config) GetScanExcludePatterns() []string

GetScanExcludePatterns returns regex patterns from defaults.scan.exclude.

These apply to the local directory scan shared by every bulk command, unlike GetExcludePatterns, which applies only to the forge API listing in `workspace sync`.

func (*Config) GetSyncStrategy

func (c *Config) GetSyncStrategy() string

GetSyncStrategy returns sync strategy (sync.strategy overrides defaults.sync.strategy).

func (*Config) GetTimeout

func (c *Config) GetTimeout() string

GetTimeout returns timeout (sync.timeout overrides defaults.sync.timeout).

type ConfigFileInfo

type ConfigFileInfo struct {
	Path string
	Kind ConfigKind
}

ConfigFileInfo holds detected config file information.

type ConfigKind

type ConfigKind string

ConfigKind represents the type of configuration file.

const (
	// KindRepositories is for simple flat repository lists (repositories array).
	KindRepositories ConfigKind = "repositories"

	// KindWorkspace is for hierarchical workspace configurations (workspaces map).
	KindWorkspace ConfigKind = "workspace"
)

func DetectConfigFileWithKind

func DetectConfigFileWithKind(dir string) (string, ConfigKind)

DetectConfigFileWithKind probes a single directory (no parent walk, unlike DetectConfigFile) for a .gz-git config file, checking supported extensions in their defined order. Use it when the caller specifically wants "is there a config in exactly this directory".

func (ConfigKind) IsValid

func (k ConfigKind) IsValid() bool

IsValid returns true if this is a valid config kind.

type ConfigLoader

type ConfigLoader struct {
	// contains filtered or unexported fields
}

ConfigLoader handles configuration loading with 5-layer precedence.

Precedence (highest to lowest):

  1. Command flags
  2. Project config (.gz-git.yaml)
  3. Active profile
  4. Global config
  5. Built-in defaults

func NewLoader

func NewLoader() (*ConfigLoader, error)

NewLoader creates a new configuration loader.

func (*ConfigLoader) Load

func (l *ConfigLoader) Load() error

Load loads all configuration layers.

func (*ConfigLoader) ResolveConfig

func (l *ConfigLoader) ResolveConfig(flags map[string]any) (*EffectiveConfig, error)

ResolveConfig builds the effective configuration with precedence. Flags parameter contains command-line flag values (highest priority).

func (*ConfigLoader) SetActiveProfileInternal

func (l *ConfigLoader) SetActiveProfileInternal(profile *Profile)

SetActiveProfileInternal sets the active profile without persisting to disk. This is used for temporary profile override (e.g., --profile flag).

type ConfigMeta

type ConfigMeta struct {
	// Version is the schema version (currently 1)
	Version int `yaml:"version,omitempty"`

	// Kind specifies the config type: "repositories" or "workspace"
	// If omitted, inferred from content:
	//   - Has "workspaces" or "profiles" key → workspace
	//   - Otherwise → repositories (default)
	Kind ConfigKind `yaml:"kind,omitempty"`

	// Metadata holds optional descriptive information
	Metadata *Metadata `yaml:"metadata,omitempty"`
}

ConfigMeta holds common metadata for all config file types. This should be at the top of every config file.

Example:

version: 1
kind: repositories
metadata:
  name: "my-devbox"
  team: "platform"

type ConfigSource

type ConfigSource string

ConfigSource represents where a config value came from.

const (
	SourceFlag     ConfigSource = "flag"
	SourceEnv      ConfigSource = "env"
	SourceKeychain ConfigSource = "keychain"
	SourceProject  ConfigSource = "project"
	SourceProfile  ConfigSource = "profile"
	SourceGlobal   ConfigSource = "global"
	SourceDefault  ConfigSource = "default"
)

ConfigSource values identify which layer of the 5-layer precedence provided a value.

func (ConfigSource) String

func (s ConfigSource) String() string

String returns a human-readable source description.

type ControllerConfig

type ControllerConfig struct {
	Path, Digest string
	Workspaces   map[string]*Workspace
}

ControllerConfig is an explicitly selected, non-inheriting devbox policy. It does not load parent configs or recursively discover workspaces.

func LoadControllerConfig

func LoadControllerConfig(path string) (*ControllerConfig, error)

LoadControllerConfig loads one explicitly selected controller file without applying repository config discovery or inheritance.

type DefaultsConfig

type DefaultsConfig struct {
	// Clone settings
	Clone *CloneDefaults `yaml:"clone,omitempty"`

	// Sync settings
	Sync *SyncDefaults `yaml:"sync,omitempty"`

	// Scan settings
	Scan *ScanDefaults `yaml:"scan,omitempty"`

	// Output settings
	Output *OutputDefaults `yaml:"output,omitempty"`

	// Filter settings
	Filter *FilterDefaults `yaml:"filter,omitempty"`
}

DefaultsConfig groups all default settings for clarity. These settings apply globally unless overridden at workspace level.

type DiscoveryConfig

type DiscoveryConfig struct {
	// Mode controls the discovery behavior
	// Values: "explicit" (use children only), "auto" (scan directories),
	//         "hybrid" (use children if defined, otherwise scan)
	// Default: "hybrid"
	Mode DiscoveryMode `yaml:"mode,omitempty"`
}

DiscoveryConfig controls how children are discovered.

type DiscoveryMode

type DiscoveryMode string

DiscoveryMode represents the children discovery mode.

const (
	// ExplicitMode only uses children explicitly defined in the config.
	ExplicitMode DiscoveryMode = "explicit"

	// AutoMode scans directories to find children automatically
	// Ignores explicit children definition.
	AutoMode DiscoveryMode = "auto"

	// HybridMode uses children if defined, otherwise scans directories
	// This is the default mode.
	HybridMode DiscoveryMode = "hybrid"
)

func (DiscoveryMode) Default

func (m DiscoveryMode) Default() DiscoveryMode

Default returns the default discovery mode.

func (DiscoveryMode) IsValid

func (m DiscoveryMode) IsValid() bool

IsValid returns true if this is a valid discovery mode.

type EffectiveConfig

type EffectiveConfig struct {
	// Forge provider settings
	Provider string
	BaseURL  string
	Token    string

	// Clone settings
	CloneProto    string
	SSHPort       int
	SSHKeyPath    string
	SSHKeyContent string

	// Bulk operation settings
	Parallel         int
	IncludeSubgroups bool
	SubgroupMode     string

	// Identity, already resolved against the environment and hostname, so a
	// caller can use it without knowing where it came from.
	Identity identity.Identity

	// Command-specific settings
	Sync   SyncConfig
	Branch BranchConfig
	Fetch  FetchConfig
	Pull   PullConfig
	Push   PushConfig
	Audit  AuditConfig

	// Metadata for debugging
	Sources map[string]string // key -> source (e.g., "provider" -> "profile:work")
}

EffectiveConfig represents the final resolved configuration after applying all precedence layers.

func (*EffectiveConfig) GetBool

func (cfg *EffectiveConfig) GetBool(key string) (value, ok bool)

GetBool retrieves a boolean value by key from effective config.

func (*EffectiveConfig) GetInt

func (cfg *EffectiveConfig) GetInt(key string) (int, bool)

GetInt retrieves an integer value by key from effective config.

func (*EffectiveConfig) GetSource

func (cfg *EffectiveConfig) GetSource(key string) string

GetSource returns the source for a given config key.

func (*EffectiveConfig) GetString

func (cfg *EffectiveConfig) GetString(key string) (string, bool)

GetString retrieves a string value by key from effective config.

type Environment

type Environment struct {
	GitHubToken string `yaml:"githubToken,omitempty"`
	GitLabToken string `yaml:"gitlabToken,omitempty"`
	GiteaToken  string `yaml:"giteaToken,omitempty"`
}

Environment represents a named set of API tokens.

type Facts

type Facts = IntegrationBranchFacts

Facts are the inputs needed to resolve an integration branch.

type FetchConfig

type FetchConfig struct {
	AllRemotes bool `yaml:"allRemotes,omitempty"` // Fetch all remotes
	Prune      bool `yaml:"prune,omitempty"`      // Prune deleted branches
}

FetchConfig holds fetch command defaults.

type FilterDefaults

type FilterDefaults struct {
	Include []string `yaml:"include,omitempty"` // Include repos matching these patterns
	Exclude []string `yaml:"exclude,omitempty"` // Exclude repos matching these patterns
}

FilterDefaults holds filter pattern settings.

type FlexBranch

type FlexBranch string

FlexBranch is a string type that accepts both string and map YAML formats. Use this for per-repo or per-group branch fields where only the branch name(s) matter.

Accepted formats:

branch: develop              → "develop"
branch: develop,master       → "develop,master"
branch:
  defaultBranch: develop     → "develop"
branch:
  defaultBranch: [dev, main] → "dev,main"

func (FlexBranch) MarshalYAML

func (f FlexBranch) MarshalYAML() (any, error)

MarshalYAML implements yaml.Marshaler to output as a plain string.

func (FlexBranch) String

func (f FlexBranch) String() string

String returns the FlexBranch value as a plain string.

func (*FlexBranch) UnmarshalYAML

func (f *FlexBranch) UnmarshalYAML(unmarshal func(any) error) error

UnmarshalYAML implements yaml.Unmarshaler to support both string and map formats.

type ForgeSource

type ForgeSource struct {
	// Provider is the forge type: gitlab, github, gitea
	Provider string `yaml:"provider"`

	// Org is the organization/group to sync from
	Org string `yaml:"org"`

	// BaseURL is the API endpoint (optional, uses default for provider)
	BaseURL string `yaml:"baseURL,omitempty"`

	// Token overrides the profile token (use ${ENV_VAR} for security)
	Token string `yaml:"token,omitempty"`

	// IncludeSubgroups includes subgroups (GitLab only)
	IncludeSubgroups bool `yaml:"includeSubgroups,omitempty"`

	// SubgroupMode controls directory structure: "flat" or "nested"
	SubgroupMode string `yaml:"subgroupMode,omitempty"`
}

ForgeSource defines a forge (GitLab/GitHub/Gitea) to sync repositories from.

Example:

source:
  provider: gitlab
  org: devbox
  baseURL: https://gitlab.polypia.net
  includeSubgroups: true
  subgroupMode: flat

type GlobalConfig

type GlobalConfig struct {
	// ActiveProfile is the default profile to use
	ActiveProfile string `yaml:"activeProfile,omitempty"`

	// Defaults apply to all profiles unless overridden
	Defaults map[string]any `yaml:"defaults,omitempty"`

	// Identity names this machine and, if one is driving, the agent on it.
	// It lives here rather than in a project's .gz-git.yaml because that file
	// is committed, and a shared device name names nothing.
	Identity *identity.Identity `yaml:"identity,omitempty"`

	// Environments define named token sets
	Environments map[string]Environment `yaml:"environments,omitempty"`
}

GlobalConfig represents ~/.config/gz-git/config.yaml

Example global config file:

activeProfile: work
defaults:
  parallel: 5
  cloneProto: ssh
  format: default
environments:
  work:
    gitlabToken: ${WORK_GITLAB_TOKEN}
  personal:
    githubToken: ${PERSONAL_GITHUB_TOKEN}

type Hooks

type Hooks struct {
	Before []string `yaml:"before,omitempty"` // Commands to run before sync operation
	After  []string `yaml:"after,omitempty"`  // Commands to run after sync operation
}

Hooks represents before/after hook commands for sync operations. Hooks are executed without shell interpretation for security (no pipes, redirects, etc.).

type IntegrationBranchFacts

type IntegrationBranchFacts struct {
	Config      []string
	Refs        []string
	Remotes     []string
	DefaultName string
}

IntegrationBranchFacts are the inputs needed to resolve an integration branch.

type IntegrationBranchResolution

type IntegrationBranchResolution struct {
	Participates bool
	Name         string
	Source       string
}

IntegrationBranchResolution is the integration-branch answer for one repository.

func ResolveIntegrationBranch

func ResolveIntegrationBranch(ctx context.Context, exec *gitcmd.Executor, repoPath string, configValues []string) (IntegrationBranchResolution, error)

ResolveIntegrationBranch reads Git facts through exec. Missing refs and remote HEAD are reportable non-participation, not errors.

func ResolveIntegrationBranchFromFacts

func ResolveIntegrationBranchFromFacts(f IntegrationBranchFacts) IntegrationBranchResolution

ResolveIntegrationBranchFromFacts interprets integration-branch participation from already gathered facts. A declared name that does not exist does not fall back to the default branch.

type IntegrationControl

type IntegrationControl struct {
	PrepareProfile string `yaml:"prepareProfile,omitempty"`
}

IntegrationControl is deliberately small: it selects a closed preparation profile, while branch.integrationBranch and branch.taskPattern on the same workspace remain the target and reclaim declarations.

type IntegrationParticipationAction

type IntegrationParticipationAction string

IntegrationParticipationAction describes the repository-local change needed to align workflow.integrationBranch with its repo-root declaration.

const (
	// IntegrationParticipationNoop leaves repository-local configuration unchanged.
	IntegrationParticipationNoop IntegrationParticipationAction = "noop"
	// IntegrationParticipationInstall records a newly managed declaration.
	IntegrationParticipationInstall IntegrationParticipationAction = "install"
	// IntegrationParticipationUpdate changes a previously managed declaration.
	IntegrationParticipationUpdate IntegrationParticipationAction = "update"
	// IntegrationParticipationRemove clears configuration proven to be managed.
	IntegrationParticipationRemove IntegrationParticipationAction = "remove"
	// IntegrationParticipationConflict preserves user-owned or mismatched state.
	IntegrationParticipationConflict IntegrationParticipationAction = "conflict"
)

func PlanIntegrationParticipation

func PlanIntegrationParticipation(state IntegrationParticipationState) IntegrationParticipationAction

PlanIntegrationParticipation applies the ownership state machine without touching Git configuration. Desired is a normalized bare branch name, or empty when the repo-root declaration no longer selects a branch.

func ReconcileIntegrationParticipation

func ReconcileIntegrationParticipation(ctx context.Context, repoPath string) (IntegrationParticipationAction, error)

ReconcileIntegrationParticipation reads only the selected repository's root declaration and local Git metadata. It returns an error before writing when the declaration is invalid, unresolved, or conflicts with user-owned configuration.

type IntegrationParticipationState

type IntegrationParticipationState struct {
	Current string
	Marker  string
	Desired string
}

IntegrationParticipationState is the complete local state used to decide whether a sync may manage workflow.integrationBranch. Marker ownership is deliberately explicit so a workspace sync never overwrites a manual choice.

type KeyringTokenStore

type KeyringTokenStore struct {
	// contains filtered or unexported fields
}

KeyringTokenStore uses the OS keychain via go-keyring. When the backend is unavailable (headless Linux without Secret Service), methods return ErrKeyringUnavailable and Available returns false.

func (*KeyringTokenStore) Available

func (s *KeyringTokenStore) Available() bool

Available reports whether the last keyring operation succeeded.

func (*KeyringTokenStore) Delete

func (s *KeyringTokenStore) Delete(provider string) error

Delete removes a token for the provider from the OS keychain.

func (*KeyringTokenStore) Get

func (s *KeyringTokenStore) Get(provider string) (string, error)

Get retrieves a token for the provider from the OS keychain.

func (*KeyringTokenStore) Set

func (s *KeyringTokenStore) Set(provider, token string) error

Set stores a token for the provider in the OS keychain.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager handles CRUD operations for profiles and configurations.

func NewManager

func NewManager() (*Manager, error)

NewManager creates a new configuration manager.

func (*Manager) CreateProfile

func (m *Manager) CreateProfile(profile *Profile) error

CreateProfile creates a new profile.

func (*Manager) DeleteProfile

func (m *Manager) DeleteProfile(name string) error

DeleteProfile deletes a profile.

func (*Manager) FindNearestConfig

func (m *Manager) FindNearestConfig(configFile string) (string, error)

FindNearestConfig finds the nearest config file by walking up from the current directory.

func (*Manager) GetActiveProfile

func (m *Manager) GetActiveProfile() (string, error)

GetActiveProfile returns the active profile name.

func (*Manager) Initialize

func (m *Manager) Initialize() error

Initialize creates the config directory structure with default profile.

func (*Manager) ListProfiles

func (m *Manager) ListProfiles() ([]string, error)

ListProfiles returns all available profile names.

func (*Manager) LoadConfigRecursiveFromPath

func (m *Manager) LoadConfigRecursiveFromPath(path, configFile string) (*Config, error)

LoadConfigRecursiveFromPath loads a recursive config from the specified path. This is a wrapper around LoadConfigRecursive with manager validation.

func (*Manager) LoadGlobalConfig

func (m *Manager) LoadGlobalConfig() (*GlobalConfig, error)

LoadGlobalConfig loads the global configuration.

func (*Manager) LoadProfile

func (m *Manager) LoadProfile(name string) (*Profile, error)

LoadProfile loads a profile from disk.

func (*Manager) LoadProjectConfig

func (m *Manager) LoadProjectConfig() (*ProjectConfig, error)

LoadProjectConfig loads project-specific configuration.

func (*Manager) ProfileExists

func (m *Manager) ProfileExists(name string) bool

ProfileExists checks if a profile exists.

func (*Manager) SaveConfig

func (m *Manager) SaveConfig(path, configFile string, config *Config) error

SaveConfig saves a recursive config to the specified path.

func (*Manager) SaveGlobalConfig

func (m *Manager) SaveGlobalConfig(config *GlobalConfig) error

SaveGlobalConfig saves the global configuration.

func (*Manager) SaveProfile

func (m *Manager) SaveProfile(profile *Profile) error

SaveProfile saves a profile to disk.

func (*Manager) SaveProjectConfig

func (m *Manager) SaveProjectConfig(config *ProjectConfig) error

SaveProjectConfig saves project configuration to current directory.

func (*Manager) SetActiveProfile

func (m *Manager) SetActiveProfile(name string) error

SetActiveProfile sets the active profile.

type MemoryTokenStore

type MemoryTokenStore struct {
	// contains filtered or unexported fields
}

MemoryTokenStore is an in-memory TokenStore for tests.

func NewMemoryTokenStore

func NewMemoryTokenStore() *MemoryTokenStore

NewMemoryTokenStore creates a test token store.

func (*MemoryTokenStore) Available

func (m *MemoryTokenStore) Available() bool

Available reports the flag SetAvailable last wrote, so a test can simulate a headless machine without a keychain.

func (*MemoryTokenStore) Delete

func (m *MemoryTokenStore) Delete(provider string) error

Delete removes the provider's token, succeeding when there was none.

func (*MemoryTokenStore) Get

func (m *MemoryTokenStore) Get(provider string) (string, error)

Get returns the stored token, or "" with no error when the provider was never set — the same "absent is not a failure" contract KeyringTokenStore has.

func (*MemoryTokenStore) Set

func (m *MemoryTokenStore) Set(provider, token string) error

Set stores a token in memory, or reports ErrKeyringUnavailable while the store is switched off by SetAvailable.

func (*MemoryTokenStore) SetAvailable

func (m *MemoryTokenStore) SetAvailable(v bool)

SetAvailable toggles availability for fallback tests.

type Metadata

type Metadata struct {
	Name       string `yaml:"name,omitempty"`       // workstation, mydevbox, project-name
	Type       string `yaml:"type,omitempty"`       // development, production, personal
	Owner      string `yaml:"owner,omitempty"`      // archmagece, team-name
	Team       string `yaml:"team,omitempty"`       // backend, frontend
	Repository string `yaml:"repository,omitempty"` // https://...
}

Metadata holds optional information about a config level.

type OutputDefaults

type OutputDefaults struct {
	Compact bool   `yaml:"compact,omitempty"` // Omit redundant fields in generated configs
	Format  string `yaml:"format,omitempty"`  // Output format (default, compact, json, llm)
}

OutputDefaults holds output-related default settings.

type Paths

type Paths struct {
	// ConfigDir is the root config directory (~/.config/gz-git)
	ConfigDir string

	// ProfilesDir is the profiles directory (~/.config/gz-git/profiles)
	ProfilesDir string

	// StateDir is the state directory (~/.config/gz-git/state)
	StateDir string

	// GlobalConfigFile is the global config file path
	GlobalConfigFile string

	// ActiveProfileFile tracks the active profile
	ActiveProfileFile string
}

Paths provides access to all config file locations.

func NewPaths

func NewPaths() (*Paths, error)

NewPaths creates a Paths instance with standard locations. It uses XDG_CONFIG_HOME if set, otherwise falls back to ~/.config.

func (*Paths) EnsureDirectories

func (p *Paths) EnsureDirectories() error

EnsureDirectories creates all necessary directories with correct permissions. Directories are created with 0700 (user access only).

func (*Paths) Exists

func (p *Paths) Exists() bool

Exists checks if the config directory exists.

func (*Paths) GetActiveProfile

func (p *Paths) GetActiveProfile() (string, error)

GetActiveProfile reads the active profile name from state file. Returns empty string if not set.

func (*Paths) ListProfiles

func (p *Paths) ListProfiles() ([]string, error)

ListProfiles returns all available profile names.

func (*Paths) ProfileExists

func (p *Paths) ProfileExists(name string) bool

ProfileExists checks if a profile file exists.

func (*Paths) ProfilePath

func (p *Paths) ProfilePath(name string) string

ProfilePath returns the path to a specific profile file, checking for supported extensions.

func (*Paths) SetActiveProfile

func (p *Paths) SetActiveProfile(name string) error

SetActiveProfile writes the active profile name to state file.

type Profile

type Profile struct {
	// Name is the profile identifier (e.g., "work", "personal")
	Name string `yaml:"name"`

	// Forge provider settings
	Provider string `yaml:"provider,omitempty"` // github, gitlab, gitea
	BaseURL  string `yaml:"baseURL,omitempty"`  // API endpoint
	Token    string `yaml:"token,omitempty"`    // API token (use ${ENV_VAR})

	// Clone settings
	CloneProto    string `yaml:"cloneProto,omitempty"`    // ssh, https
	SSHPort       int    `yaml:"sshPort,omitempty"`       // Custom SSH port
	SSHKeyPath    string `yaml:"sshKeyPath,omitempty"`    // SSH private key file path (priority)
	SSHKeyContent string `yaml:"sshKeyContent,omitempty"` // SSH private key content (use ${ENV_VAR})

	// Bulk operation settings
	Parallel         int    `yaml:"parallel,omitempty"`         // Parallel job count
	IncludeSubgroups bool   `yaml:"includeSubgroups,omitempty"` // GitLab subgroups
	SubgroupMode     string `yaml:"subgroupMode,omitempty"`     // flat, nested

	// Identity names the machine and agent recorded on automated commits.
	Identity *identity.Identity `yaml:"identity,omitempty"`

	// Command-specific overrides
	Sync   *SyncConfig   `yaml:"sync,omitempty"`
	Branch *BranchConfig `yaml:"branch,omitempty"`
	Fetch  *FetchConfig  `yaml:"fetch,omitempty"`
	Pull   *PullConfig   `yaml:"pull,omitempty"`
	Push   *PushConfig   `yaml:"push,omitempty"`
	Audit  *AuditConfig  `yaml:"audit,omitempty"`
}

Profile represents a named configuration profile. A profile contains default values for command flags, eliminating the need to repeatedly specify the same options.

Example profile file (~/.config/gz-git/profiles/work.yaml):

name: work
provider: gitlab
baseURL: https://gitlab.company.com
token: ${WORK_GITLAB_TOKEN}
cloneProto: ssh
sshPort: 2224
parallel: 10
sync:
  strategy: reset
  maxRetries: 3

func GetProfileByName

func GetProfileByName(config *Config, name string) *Profile

GetProfileByName returns a profile by name from the config. Lookup order: inline (config.Profiles) → external (~/.config/gz-git/profiles/) Returns nil if profile not found in either location.

func GetProfileFromChain

func GetProfileFromChain(config *Config, name string) *Profile

GetProfileFromChain looks up a profile by traversing the parent config chain. Lookup order:

  1. Current config's inline profiles (config.Profiles)
  2. Parent config's inline profiles (config.ParentConfig.Profiles)
  3. Grandparent config's inline profiles (and so on...)

Returns nil if profile not found in any level. For external profiles (~/.config/gz-git/profiles/), use Manager.GetProfile().

type ProjectConfig

type ProjectConfig struct {
	// Profile specifies which profile to use for this project
	Profile string `yaml:"profile,omitempty"`

	// Command-specific overrides
	Sync   *SyncConfig   `yaml:"sync,omitempty"`
	Branch *BranchConfig `yaml:"branch,omitempty"`
	Fetch  *FetchConfig  `yaml:"fetch,omitempty"`
	Pull   *PullConfig   `yaml:"pull,omitempty"`
	Push   *PushConfig   `yaml:"push,omitempty"`
	Audit  *AuditConfig  `yaml:"audit,omitempty"`

	// Metadata is optional project information
	Metadata *ProjectMetadata `yaml:"metadata,omitempty"`
}

ProjectConfig represents .gz-git.yaml in a project directory. This file is auto-detected by walking up the directory tree.

Example project config file:

profile: work
sync:
  strategy: pull
  parallel: 3
branch:
  defaultBranch: main
  protectedBranches: [main, develop, release/*]
metadata:
  team: backend
  repository: https://gitlab.company.com/backend/myproject

type ProjectMetadata

type ProjectMetadata struct {
	Team       string `yaml:"team,omitempty"`
	Repository string `yaml:"repository,omitempty"`
	Owner      string `yaml:"owner,omitempty"`
}

ProjectMetadata holds optional project information.

type PullConfig

type PullConfig struct {
	Rebase bool `yaml:"rebase,omitempty"` // Use rebase instead of merge
	FFOnly bool `yaml:"ffOnly,omitempty"` // Fast-forward only
}

PullConfig holds pull command defaults.

type PushConfig

type PushConfig struct {
	SetUpstream bool `yaml:"setUpstream,omitempty"` // Auto set upstream

	// Policy restricts which branches push may write and how. Unset means no
	// branch is protected and only the built-in lease-only force rule applies.
	Policy *repository.PushPolicy `yaml:"policy,omitempty"`
}

PushConfig holds push command defaults.

Example:

push:
  setUpstream: true
  policy:
    protected: [main, master]
    forceMode: lease-only
    foreignWork: block

type Readiness

type Readiness struct {
	Version int    `yaml:"version" json:"version"`
	Runner  string `yaml:"runner" json:"runner"`
}

Readiness is the target-owned integration gate contract. It deliberately has no command, argument, environment, or cwd fields: V1 has one fixed runner invocation and accepts no caller-controlled execution surface.

func ParseReadinessDocument

func ParseReadinessDocument(data []byte, isJSON bool) (Readiness, bool, error)

ParseReadinessDocument extracts branch.readiness from a repo-root config. Unknown fields in the readiness node are rejected even though the larger project configuration remains backwards-compatible and extensible.

type Resolution

type Resolution = IntegrationBranchResolution

Resolution is the integration-branch answer for one repository.

func ResolveFromFacts

func ResolveFromFacts(f Facts) Resolution

ResolveFromFacts is the concise form of ResolveIntegrationBranchFromFacts.

type ScanDefaults

type ScanDefaults struct {
	// Depth is the default scan depth for bulk operations. Zero means omitted,
	// so a child value of zero inherits its parent and a top-level zero keeps the
	// CLI default. Positive values opt into a different depth.
	Depth int `yaml:"depth,omitempty"`

	// Exclude holds regex patterns removed from the *local directory scan*
	// that every bulk command runs. It exists because the only way to keep a
	// repository out of `push`/`commit` used to be remembering `--exclude` on
	// every invocation, and forgetting it failed silently: the repository just
	// appeared as another success row.
	//
	// This is deliberately not `defaults.filter.exclude`. That key filters the
	// repository list a forge API returns to `workspace sync` and has never
	// affected the local scan, so reusing it would have made one name mean two
	// scopes. See GetScanExcludePatterns.
	Exclude []string `yaml:"exclude,omitempty"`
}

ScanDefaults holds scan-related default settings.

type SelfSyncConfig

type SelfSyncConfig struct {
	// Enabled controls whether the config directory itself should be synced.
	// Default: false (config directory is not synced)
	Enabled bool `yaml:"enabled,omitempty"`

	// Strategy specifies how to sync the config directory.
	// Values: "fetch" (default, safe), "pull" (with dirty check), "skip"
	// Note: "reset" is not allowed for self-sync to prevent data loss.
	Strategy string `yaml:"strategy,omitempty"`
}

SelfSyncConfig controls sync behavior for the config directory itself. This allows the devbox/orchestrator directory to be synced along with workspaces.

type SyncConfig

type SyncConfig struct {
	Strategy       string `yaml:"strategy,omitempty"`       // pull, reset, rebase, skip, clone
	MaxRetries     int    `yaml:"maxRetries,omitempty"`     // Retry count
	Timeout        string `yaml:"timeout,omitempty"`        // Operation timeout
	Recursive      bool   `yaml:"recursive,omitempty"`      // Auto-sync child workspace repos
	CleanupOrphans bool   `yaml:"cleanupOrphans,omitempty"` // Delete local repos not in forge
}

SyncConfig holds sync command defaults.

type SyncDefaults

type SyncDefaults struct {
	Strategy   string `yaml:"strategy,omitempty"`   // reset, pull, rebase, fetch, skip, clone
	Parallel   int    `yaml:"parallel,omitempty"`   // Parallel workers
	MaxRetries int    `yaml:"maxRetries,omitempty"` // Retry count
	Timeout    string `yaml:"timeout,omitempty"`    // Operation timeout
}

SyncDefaults holds sync-related default settings.

type TaskPatternDecl

type TaskPatternDecl struct {
	Patterns          []string
	IntegrationBranch BranchList
	Source            string
	Facts             []string
}

TaskPatternDecl is the repo-root taskPattern load result.

Missing file → empty Patterns plus a reportable "no declaration" fact. That is not "everything is reclaimable".

func LoadRepoRootTaskPattern

func LoadRepoRootTaskPattern(repoRoot string) (TaskPatternDecl, error)

LoadRepoRootTaskPattern stats only <repoRoot>/.gz-git.{yaml,yml,json}. It does not call findConfigUpward and is not the 5-layer merger.

A non-root .gz-git.yaml that declares taskPattern is ignored and its path is reported. A pattern that equals a literal protected name (main, master, develop, development) rejects the load. Overlap with built-in protect patterns hotfix/* and release/* is allowed.

type TokenStore

type TokenStore interface {
	Set(provider, token string) error
	Get(provider string) (string, error)
	Delete(provider string) error
	Available() bool
}

TokenStore abstracts OS keychain access for forge API tokens.

var DefaultTokenStore TokenStore = &KeyringTokenStore{}

DefaultTokenStore is the process-wide token store (overridable in tests).

type Validator

type Validator struct {
	// ExpandEnvVars enables environment variable expansion
	ExpandEnvVars bool
}

Validator handles configuration validation and environment variable expansion.

func NewValidator

func NewValidator() *Validator

NewValidator creates a new Validator with default settings.

func (*Validator) ExpandEnvVarsInConfig

func (v *Validator) ExpandEnvVarsInConfig(c *Config) error

ExpandEnvVarsInConfig expands environment variables in a recursive config.

func (*Validator) ExpandEnvVarsInGlobalConfig

func (v *Validator) ExpandEnvVarsInGlobalConfig(g *GlobalConfig) error

ExpandEnvVarsInGlobalConfig expands environment variables in global config.

func (*Validator) ExpandEnvVarsInProfile

func (v *Validator) ExpandEnvVarsInProfile(p *Profile) error

ExpandEnvVarsInProfile expands environment variables in a profile. Variables use ${VAR_NAME} syntax.

func (*Validator) ExpandEnvVarsInWorkspace

func (v *Validator) ExpandEnvVarsInWorkspace(ws *Workspace) error

ExpandEnvVarsInWorkspace expands environment variables in a workspace.

func (*Validator) ValidateChildConfigMode

func (v *Validator) ValidateChildConfigMode(mode ChildConfigMode) error

ValidateChildConfigMode validates a child config mode value.

func (*Validator) ValidateConfig

func (v *Validator) ValidateConfig(c *Config) error

ValidateConfig validates a recursive hierarchical config.

func (v *Validator) ValidateConfigLink(path string) error

ValidateConfigLink validates a configLink path.

func (*Validator) ValidateDiscoveryConfig

func (v *Validator) ValidateDiscoveryConfig(d *DiscoveryConfig) error

ValidateDiscoveryConfig validates discovery configuration.

func (*Validator) ValidateForgeSource

func (v *Validator) ValidateForgeSource(s *ForgeSource) error

ValidateForgeSource validates a forge source configuration.

func (*Validator) ValidateGlobalConfig

func (v *Validator) ValidateGlobalConfig(g *GlobalConfig) error

ValidateGlobalConfig validates global configuration.

func (*Validator) ValidateHooks

func (v *Validator) ValidateHooks(hooks *Hooks) error

ValidateHooks validates hook commands for security. Checks that commands don't contain shell special characters.

func (*Validator) ValidateParentPath

func (v *Validator) ValidateParentPath(path string) error

ValidateParentPath validates a parent config path. Checks:

  • Path is not empty (already handled by caller)
  • Path doesn't contain dangerous patterns
  • Path format is valid (allows ~/, ./, absolute, relative)

func (*Validator) ValidateProfile

func (v *Validator) ValidateProfile(p *Profile) error

ValidateProfile validates a profile configuration.

func (*Validator) ValidateProjectConfig

func (v *Validator) ValidateProjectConfig(p *ProjectConfig) error

ValidateProjectConfig validates project configuration.

func (*Validator) ValidateSyncConfig

func (v *Validator) ValidateSyncConfig(s *SyncConfig) error

ValidateSyncConfig validates sync configuration.

func (*Validator) ValidateWorkspace

func (v *Validator) ValidateWorkspace(ws *Workspace, name string) error

ValidateWorkspace validates a workspace entry.

type Workspace

type Workspace struct {
	// Path is the target directory for this workspace
	// Supports: absolute (/foo/bar), relative (./foo), home-relative (~/foo)
	Path string `yaml:"path"`

	// ConfigLink specifies a config file to symlink into the workspace as .gz-git.yaml
	// Supports: absolute paths, home-relative (~/), relative (./), relative to parent config dir
	// The symlink is created at {workspace.Path}/.gz-git.yaml → {configLink}
	ConfigLink string `yaml:"configLink,omitempty"`

	// Hooks defines before/after commands for this workspace sync
	// Before hooks run before clone/update, After hooks run after successful sync
	Hooks *Hooks `yaml:"hooks,omitempty"`

	// Type specifies what kind of workspace this is
	// Values: "forge" (sync from forge), "git" (single repo), "config" (has nested config)
	// Default: "forge" if Source is set, "git" otherwise
	Type WorkspaceType `yaml:"type,omitempty"`

	// Profile overrides the parent profile for this workspace
	Profile string `yaml:"profile,omitempty"`

	// Access controls whether this machine may publish changes to the
	// workspace. Read-only workspaces are still cloned and pulled, but bulk
	// push and handoff never write to their remotes.
	Access WorkspaceAccess `yaml:"access,omitempty"`

	// URL is the git clone URL (required for type=git sync)
	// Supports: HTTPS, SSH, git:// protocols
	URL string `yaml:"url,omitempty"`

	// AdditionalRemotes defines extra git remotes to configure after clone
	// Map of remote name to URL (e.g., {"upstream": "https://github.com/original/repo.git"})
	AdditionalRemotes map[string]string `yaml:"additionalRemotes,omitempty"`

	// Source defines the forge to sync from
	Source *ForgeSource `yaml:"source,omitempty"`

	Sync     *SyncConfig `yaml:"sync,omitempty"`
	Parallel int         `yaml:"parallel,omitempty"`

	CloneProto    string        `yaml:"cloneProto,omitempty"`
	SSHPort       int           `yaml:"sshPort,omitempty"`
	SSHKeyPath    string        `yaml:"sshKeyPath,omitempty"`    // SSH private key file path
	SSHKeyContent string        `yaml:"sshKeyContent,omitempty"` // SSH private key content
	Branch        *BranchConfig `yaml:"branch,omitempty"`
	// Integration is an opt-in controller-owned integration policy. It is read
	// only when a caller explicitly selects this workspace config; it is never
	// inherited by a repository project config.
	Integration *IntegrationControl `yaml:"integration,omitempty"`
	Fetch       *FetchConfig        `yaml:"fetch,omitempty"`
	Pull        *PullConfig         `yaml:"pull,omitempty"`
	Push        *PushConfig         `yaml:"push,omitempty"`

	// Filter patterns (override parent patterns)
	IncludePatterns []string `yaml:"includePatterns,omitempty"` // Include repos matching these patterns
	ExcludePatterns []string `yaml:"excludePatterns,omitempty"` // Exclude repos matching these patterns

	// Workspaces allows nested workspace definitions
	Workspaces map[string]*Workspace `yaml:"workspaces,omitempty"`

	Metadata *Metadata `yaml:"metadata,omitempty"`

	// ChildConfigMode controls how child config files are generated during sync.
	// Values: "repositories" (default), "workspaces", "none"
	ChildConfigMode ChildConfigMode `yaml:"childConfigMode,omitempty"`
}

Workspace represents a named workspace in the hierarchy. Each workspace can sync from a forge source or manage existing git repos.

Example:

devbox:
  path: ./devbox
  configLink: ./gz-git/mydevbox.yaml
  hooks:
    before:
      - mkdir -p logs
    after:
      - make setup
  source:
    provider: gitlab
    org: devbox
    includeSubgroups: true
    subgroupMode: flat
  sync:
    strategy: pull
  workspaces:
    subproject:
      path: ./subproject
      type: git

func GetWorkspaceByName

func GetWorkspaceByName(config *Config, name string) *Workspace

GetWorkspaceByName returns a workspace by name from the config.

type WorkspaceAccess

type WorkspaceAccess string

WorkspaceAccess describes the remote-write contract for a workspace.

const (
	// WorkspaceAccessReadWrite is the default: normal pull and push behavior.
	WorkspaceAccessReadWrite WorkspaceAccess = "read-write"
	// WorkspaceAccessReadOnly keeps the checkout current while forbidding push.
	WorkspaceAccessReadOnly WorkspaceAccess = "read-only"
)

func WorkspacePushAccess

func WorkspacePushAccess(repoPath string) (WorkspaceAccess, string, error)

WorkspacePushAccess resolves the nearest configured workspace declaration for repoPath. It walks through every ancestor config rather than stopping at a repository's own config: a parent workspace owns the access decision for a third-party child checkout.

func WorkspacePushAccessContext

func WorkspacePushAccessContext(ctx context.Context, repoPath string) (WorkspaceAccess, string, error)

WorkspacePushAccessContext is WorkspacePushAccess with caller cancellation for the repository-local Git metadata query.

func (WorkspaceAccess) IsReadOnly

func (a WorkspaceAccess) IsReadOnly() bool

IsReadOnly reports whether remote writes must be suppressed.

func (WorkspaceAccess) IsValid

func (a WorkspaceAccess) IsValid() bool

IsValid reports whether the configured access mode is supported. Empty is the backward-compatible read-write default.

type WorkspaceType

type WorkspaceType string

WorkspaceType represents the type of workspace.

const (
	// WorkspaceTypeForge indicates the workspace syncs from a forge (GitLab/GitHub/Gitea)
	// This is the default when Source is defined.
	WorkspaceTypeForge WorkspaceType = "forge"

	// WorkspaceTypeGit indicates the workspace is a single git repository
	// No forge sync, just manages an existing repo.
	WorkspaceTypeGit WorkspaceType = "git"

	// WorkspaceTypeConfig indicates the workspace has a nested config file
	// Loads .gz-git.yaml from the workspace path.
	WorkspaceTypeConfig WorkspaceType = "config"
)

func (WorkspaceType) IsValid

func (t WorkspaceType) IsValid() bool

IsValid returns true if this is a valid workspace type.

func (WorkspaceType) Resolve

func (t WorkspaceType) Resolve(hasSource bool) WorkspaceType

Resolve returns the effective type based on context. If type is empty, it's inferred from Source presence.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL