Exgit.Config (exgit v0.1.0)

Copy Markdown View Source

Parser and emitter for git-style INI configuration files.

parse/1 accepts arbitrary input — caller- or filesystem-supplied text — and returns a tagged {:ok, _} | {:error, _} result. It never raises on untrusted input.

Threat model

.git/config is treated as caller-controlled input, not remote-controlled. Exgit does not fetch or persist config received over the wire, and it does not act on any config value:

  • no core.sshCommand / core.fsmonitor / core.hookspath — there is no code path that executes a command out of config
  • no http.proxy — Req's proxy comes from env or the explicit transport opts, not from config
  • no insteadOf / pushInsteadOf URL rewriting
  • no include / includeIf expansion — config files are read as-is; [include] path=... entries are parsed but ignored
  • no ~/ or ${VAR} path expansion — paths in config values are treated as opaque strings

This keeps the blast radius of a hostile .git/config to "data the caller reads back out of repo.config and uses themselves." A consumer who reads e.g. Config.get(config, "core", "sshCommand") and hands it to System.cmd/2 is outside exgit's trust boundary.

If submodule support is added, .gitmodules URLs become a remote-controlled surface and will need separate validation — refusing file://, ssh://user@host/…;command, and any URL containing shell-metacharacters. Not currently present.

Summary

Types

section_key()

@type section_key() :: {String.t(), String.t() | nil}

t()

@type t() :: %Exgit.Config{sections: [{section_key(), [{String.t(), String.t()}]}]}

Functions

add(config, section, subsection \\ nil, key, value)

@spec add(t(), String.t(), String.t() | nil, String.t(), String.t()) :: t()

Append another value for key without replacing existing ones.

Unlike set/5 (which replaces), add/5 preserves existing values. This is the equivalent of git config --add section.key value and is required for multi-valued keys like remote.<n>.fetch.

encode(config)

@spec encode(t()) :: iolist()

get(config, section, subsection \\ nil, key)

@spec get(t(), String.t(), String.t() | nil, String.t()) :: String.t() | nil

get_all(config, section, subsection \\ nil, key)

@spec get_all(t(), String.t(), String.t() | nil, String.t()) :: [String.t()]

new()

@spec new() :: t()

parse(text)

@spec parse(String.t()) :: {:ok, t()} | {:error, term()}

read(path)

@spec read(Path.t()) :: {:ok, t()} | {:error, term()}

set(config, section, subsection \\ nil, key, value)

@spec set(t(), String.t(), String.t() | nil, String.t(), String.t()) :: t()

write(config, path)

@spec write(t(), Path.t()) :: :ok | {:error, term()}