On this page
Package repo manages the .git/safegit/ data directory including initialization, configuration loading, validation, and path helpers for all state files.
#internal/repo
#internal/repo
Package repo manages the .git/safegit/ data directory including initialization, configuration loading, validation, and path helpers for all state files.
#Config
type Config structConfig holds safegit configuration persisted in config.json.
A key this struct no longer declares (log.maxSizeMB, removed with oplog rotation) still LOADS from an existing config.json: encoding/json ignores unknown members. Writing one does not: GetConfigValue and SetConfigValue answer "unknown config key" for anything outside ValidConfigKeys.
#CommitConfig
type CommitConfig structCommitConfig holds commit-related settings.
#LockConfig
type LockConfig structLockConfig holds ref-lock acquisition settings.
#HooksConfig
type HooksConfig structHooksConfig holds hook-related settings.
#PrePrePushConfig
type PrePrePushConfig structPrePrePushConfig holds pre-pre-push hook timeout settings.
#PushConfig
type PushConfig structPushConfig holds push retry settings.
#UninstallTarget
type UninstallTarget structUninstallTarget is one path a repository-wide uninstall removes.
#DefaultConfig
func DefaultConfig() ConfigDefaultConfig returns the default safegit configuration.
#SafegitDir
func SafegitDir(gitDir string) stringSafegitDir returns the path to .git/safegit/ given a .git directory path.
#SharedGitDir
func SharedGitDir(ctx context.Context, gitDir string) stringSharedGitDir returns the COMMON git directory: the one every worktree of a repository shares. For a normal repository it is gitDir itself; for a linked worktree, whose git dir is
It is the anchor for everything that is repository-level policy rather than checkout state -- the ref locks, and safegit's live hook store -- so that two worktrees can never disagree about it. Git's own hook directory is common too, which is why the pre-migration hook location is resolved from here.
The parameter accepts either the git directory (.git) or the safegit directory (.git/safegit); callers use both forms.
The answer never depends on the process working directory. CommonGitDirOf runs git in the git directory it is handed, so an answer that comes back relative is relative to THAT directory and is anchored there -- filepath.Abs, which would resolve it against this process's own directory, is exactly the wrong anchor and is not used.
#SharedSafegitDir
func SharedSafegitDir(ctx context.Context, gitDir string) stringSharedSafegitDir returns the safegit directory under the common .git dir. For normal repos this is identical to SafegitDir(gitDir). For worktrees it returns
It takes the same parameter forms as SharedGitDir, whose resolution it is.
#IsInitialized
func IsInitialized(gitDir string) boolIsInitialized reports whether this repository has a usable safegit data directory, which is decided by config.json rather than by the directory alone. A directory that exists without config.json is half-initialized -- an interrupted Init, or any stray subdirectory created under it -- and reporting that as initialized would make EnsureInitialized a no-op and leave every command failing on the missing config.json. Reporting it as uninitialized lets Init complete it (Init is idempotent over the directories it creates).
#Init
func Init(ctx context.Context, gitDir string) errorInit creates the .git/safegit/ directory structure and writes default config.json. Idempotent: returns nil if already initialized.
The context is the dispatch's own: the worktree check below asks git where the common git directory is, and that call belongs on the same context as every other git call the invocation makes.
#EnsureInitialized
func EnsureInitialized(ctx context.Context, gitDir string) errorEnsureInitialized auto-initializes .git/safegit/ if it doesn't exist yet.
#UninstallPlan
func UninstallPlan(ctx context.Context, gitDir string) ([]UninstallTarget, error)UninstallPlan enumerates every path a repository-wide uninstall removes, without removing any of them. It returns an error when there is nothing to remove, which is the "safegit is not initialized" refusal.
Enumerating and removing are deliberately separate: the plan is what the command prints before it asks for consent, and the removal itself goes through the caller's effects handle so that a dry run records it instead of performing it. There is no companion function that both plans and removes -- one existed, only its own tests called it, and it could not be previewed.
What the plan covers: safegit's state directory entirely -- which since the hook store moved there takes the installed hooks with it -- plus the repository-level state under the common git dir in worktree setups (the shared locks and the live hook store, which is the one pushes actually run), plus the two safegit-owned names that may still be sitting in git's own hook directory from before the move. Leaving any of those behind would keep an uninstalled tool's checks running on every push with no state directory left to explain where they came from.
The hook store the CHECKOUT provides (.safegit/hooks in the work tree) is deliberately absent from the plan: it is part of the repository's content, shared with everyone who cloned it, and removing it would be an uncommitted deletion of somebody else's file.
Uninstalling is a REPOSITORY operation, not a per-checkout one. A repository with linked worktrees holds safegit state in one directory per worktree git dir, plus the shared store under the common git dir where the locks and the hooks every push runs live. An uninstall that took only the invoking worktree's directory left the rest of it in place while reporting the tool uninstalled -- config, oplog and all.
The set of state directories is read off disk rather than from git worktree list. A worktree's git dir is always
#LoadConfig
func LoadConfig(gitDir string) (*Config, error)LoadConfig reads and parses config.json from the safegit directory.
#LoadConfigFrom
func LoadConfigFrom(path string) (*Config, error)LoadConfigFrom reads and parses config from an arbitrary path.
#MarshalConfig
func MarshalConfig(cfg *Config) ([]byte, error)MarshalConfig renders config.json's exact bytes. Rendering is split from writing so callers mint the write as an effect instead of performing it here, which is what lets --dry-run record a config change without making one.
This package therefore writes config.json in exactly one place -- Init, whose write goes through writeFileAtomic. config set renders here and hands the bytes to the effects handle. A save helper that plain-writes the file would be a third, non-atomic writer of the path every command reads.
#ConfigPath
func ConfigPath(gitDir string) stringConfigPath is where config.json lives for the given git dir.
#GetConfigValue
func GetConfigValue(cfg *Config, key string) (interface{}, error)GetConfigValue returns the value for a dot-separated config key.
#SetConfigValue
func SetConfigValue(cfg *Config, key, value string) errorSetConfigValue sets a dot-separated config key to the given string value.
The key is resolved BEFORE the value is parsed, so an unknown key is always reported as an unknown key whatever its value looks like: config set log.maxSizeMB abc names the retired key, not the shape of "abc".
#ValidConfigKeys
func ValidConfigKeys() []stringValidConfigKeys returns the list of supported config keys.
#Config.Validate
func (c *Config) Validate() errorValidate checks that all config values are within acceptable ranges.