diff --git a/README.md b/README.md new file mode 100644 index 0000000..6abac07 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +
+ +# gitconfig + +[![Build Status][ci/badge]][ci/url] +[![Go Reference][godoc/badge]][godoc/url] +[![MIT License][license/badge]][license/url] + +[Documentation][godoc/url] - [Issues][issues] - [Compatibility](docs/COMPATIBILITY.md) + +
+ +`gitconfig` is a Go library to read and edit git configuration the way git +does. + +- **same result as git**: same parsing, same value types (bool, int, path), + tested against the `git` binary. +- **every source**: system, global, local and worktree files, `GIT_CONFIG_*` + variables, `include` and `includeIf`. Each value says which file and line + it came from. +- **lossless edits**: an edit changes only the lines it touches; comments + and layout are kept. +- **safe alongside git**: writes take git's lock, so a concurrent `git config` + and this library never overwrite each other. +- **safe on hostile repositories**: untrusted repositories are refused, as + git refuses them (`safe.directory`). + +Where this library deliberately differs from git, and why, is listed in +[COMPATIBILITY.md](docs/COMPATIBILITY.md). + +## Installation + +``` +go get github.com/git-bug/gitconfig +``` + +## Example + +```go +package main + +import ( + "fmt" + "log" + + "github.com/git-bug/gitconfig" +) + +func main() { + // Find the repository and where git reads its configuration from. + env, err := gitconfig.Discover(".") + if err != nil { + log.Fatal(err) // ErrUntrusted, as git refuses it + } + + // Read the effective configuration: system, global, local, worktree + // and command scopes, with includes, as git would use it. + cfg, err := env.Load() + if err != nil { + log.Fatal(err) + } + if e, ok := cfg.Get("user.name"); ok { + fmt.Printf("user.name = %s (%s)\n", e.Value, e.Origin.File) + } + + // Edit the repository's config. Comments and layout are kept, and the + // file is not touched if the value is already set. + err = env.UpdateScope(gitconfig.ScopeLocal, func(f *gitconfig.File) error { + return f.Set("core.editor", "vim") + }) + if err != nil { + log.Fatal(err) + } +} +``` + +## License + +MIT, see [LICENSE](LICENSE). This is a clean room implementation: no code is +derived from git, see [CLEAN-ROOM.md](docs/CLEAN-ROOM.md). It is meant to interoperate with git. + +[ci/badge]: https://github.com/git-bug/gitconfig/actions/workflows/test.yml/badge.svg +[ci/url]: https://github.com/git-bug/gitconfig/actions/workflows/test.yml +[godoc/badge]: https://pkg.go.dev/badge/github.com/git-bug/gitconfig.svg +[godoc/url]: https://pkg.go.dev/github.com/git-bug/gitconfig +[license/badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license/url]: LICENSE +[issues]: https://github.com/git-bug/gitconfig/issues diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md deleted file mode 100644 index 53ae7eb..0000000 --- a/docs/INTEGRATION.md +++ /dev/null @@ -1,126 +0,0 @@ -# Integration - -## git-bug (primary consumer) - -### Today - -`repository/gogit_config.go` implements git-bug's `Config` interfaces -(`repository/config.go`) on top of go-git: - -- reads and writes through `cfg.Raw` of go-git's `config.Config`, then - `repo.SetConfig(cfg)`, which re-serialises the whole file — comments and - formatting are lost on every write (git-bug#1022), and edits to keys that - go-git also models in its struct can be silently reverted; -- global config is read with go-git's deprecated `config.LoadConfig` (one - file only, no includes) and **cannot be written** (`configPanicWriter`); -- `AnyConfig` merges local over global by hand; system scope and includes are - ignored; -- `resolveRemote` re-implements `url.*.insteadOf`; -- commit identity in `StoreSignedCommit` comes from go-git's - `cfg.Author`/`cfg.Committer` of the local config only, so `user.name` set - globally or through an include is not used. - -Usage outside `repository/`: `ReadString` ×5, `RemoveAll` ×4, `ReadAll` ×3, -`StoreString` ×2, `ReadBool`, `ReadTimestamp`, `StoreTimestamp` ×1 each. - -### Mapping - -`env, err := gitconfig.Discover(workTree)` once per repository (the -working tree, or the git directory of a bare repository; a `.git` file of a -linked worktree is followed); the `Env` is a plain value and can be kept. -`Discover` refuses a repository git would refuse (`ErrUntrusted`): git-bug -gets git's `safe.directory` protection without code of its own. - -| git-bug | gitconfig | -|---|---| -| `LocalConfig()` | `env.UpdateScope(gitconfig.ScopeLocal, fn)` (write), `env.LoadScope(gitconfig.ScopeLocal)` (read) | -| `GlobalConfig()` | `env.UpdateScope(gitconfig.ScopeGlobal, fn)` (write — no more panic writer), `env.LoadScope(gitconfig.ScopeGlobal)` (read: XDG file and `~/.gitconfig`, with includes) | -| `AnyConfig()` | `env.Load()`: system + global + local + worktree + command, with includes | -| `ReadString(key)` | `cfg.GetAll(key)`: 0 → `ErrNoConfigEntry`, >1 → `ErrMultipleConfigEntry` (git-bug's contract is stricter than git's last-wins; keep it in the adapter, or relax it to `cfg.Get` / `cfg.Value`) | -| `ReadBool(key)` | same, then `Entry.Bool()` — fixes `yes`/`on`/`1`/implicit values, which `strconv.ParseBool` gets wrong | -| `ReadTimestamp(key)` | same, then `strconv.Atoi` as today | -| `ReadAll(prefix)` | `cfg.All()` filtered by git-bug's prefix rule | -| `StoreString(key, v)` | `env.UpdateScope(s, func(f *gitconfig.File) error { return f.Set(key, v) })` | -| `StoreBool` / `StoreTimestamp` | `Set` with `strconv.FormatBool` / `strconv.Itoa` | -| `RemoveAll(prefix)` | one `UpdateScope`: `RemoveSection` / `RemoveSubsection` for whole sections or subsections matched, `UnsetAll(key)` for single keys; absent ones are not an error (the methods return what they removed), as git-bug#1455 requires | -| hand-written key case normalisation | `gitconfig.ParseKey` | -| in-memory config (`config_mem.go`) | a `*gitconfig.File` (zero value or `Parse`) edited directly, read through `File.Config()` | -| `resolveRemote` insteadOf logic | `url..insteadOf` helper (C8) once available | -| commit identity | `user.name`/`author.name`/`committer.name` looked up in `env.Load()` | - -Effect for git-bug users: comments and layout preserved, `git bug` only -rewrites the file when a value actually changes (E7), global config is -writable, `includeIf`-based identities work, and a concurrent `git` command -cannot corrupt the file or lose an update. - -Remotes, branches and refspecs can stay on go-git's API for now: they are -read-only uses there, except `CreateRemote` in tests. - -## go-git (secondary target) - -### Fit with go-git's rules - -- Standard library only, no global state, no `init()`, no panics (AGENTS.md). -- Licence: MIT, compatible with Apache-2.0. -- go-git keeps the repository on go-billy (often rooted at the repository, - or in memory) and already has git's lock protocol for references - (`dotgit.lockFile`: exclusive create, timeout, Windows retries, fallback - when rename is unsupported). It uses `Parse`, `File` and `Config`, which do - no I/O (N4), and keeps its own locking and file routing. - `RepositoryFilesystem` must route `config.lock` like `config`, as it - already does for the references' lock files. -- `Env` reads the operating system's filesystem only. It fits go-git's - system and global scopes, which go-git reads from the real disk today. - Includes of a repository config on go-billy need N9 (LATER). -- In-memory storage and reftable have no plain `HEAD` file: - `includeIf "onbranch:"` needs S10 (LATER) there. -- Go version aligned with go-git's `go.mod`. - -### Fit with RFC #2224 and #2002 - -go-git's open RFCs propose making `plumbing/format/config` the model, a -`Getter` / `ConfigSet` read API, an `xconfig.Combined` helper for scopes, and -retiring the typed `config.Config`. This library provides the same concepts -with the lossless file model and git-exact editing those RFCs do not cover: - -| RFC concept | gitconfig | -|---|---| -| `format/config.Config` (one parsed file) | `*gitconfig.File` | -| `Getter` (`Lookup`, `GetAll`) | `*gitconfig.Config` (`Get`, `GetAll`, `All`, `Section`, `Subsection`, `Subsections`) | -| `ConfigSet` | `*gitconfig.Config` returned by `Env.Load` | -| `xconfig.Combined` | `gitconfig.Env` + `Env.Load` | -| typed getters with defaults (#2002) | `Config.Bool/Int(key, def)`, `Config.Path(key, home, def)`, `Entry.Bool/Int/Path` | -| write path (not covered by the RFCs) | `File` edits and `File.Bytes()` under go-git's lock; `Env.UpdateFile` / `Env.UpdateScope` on the real disk | - -If go-git adopts it, align the type names with whatever the RFC settles on -before 1.0. - -### Transitional path inside go-git - -Adoption does not need the full RFC first: - -1. **Writes** (fixes git-bug#1022 for every go-git user): in - `storage/filesystem.ConfigStorage.SetConfig`, instead of writing - `cfg.Marshal()`, take `config.lock` with go-git's lock helper, read - `config` under the lock and `Parse` it, compare the entries of `cfg` with - those of the parsed file, apply the difference as `Set`/`ReplaceAll`/`UnsetAll`/`RemoveSection`/`RemoveSubsection` operations. Comments, layout - and unknown keys survive and the write becomes atomic. Concurrent updates - to keys the caller did not change survive too; a concurrent change to the - same key is still last-writer-wins, as with `git config`. Then write - `File.Bytes()` to the lock file and rename it, only if it changed. -2. **Reads**: build `config.Config` from a `gitconfig.Config` (from - `File.Config()`; following includes from go-billy needs N9) instead of gcfg, - which fixes booleans and continuation lines. -3. **Scopes**: replace the config loader plugin and `ConfigScoped` with - `gitconfig.Env` and `Env.Load` (removes the process-global plugin - registry). This works as is for a repository on the real disk; for - in-memory storage, the local `File.Config()` would have to be combined - with the other scopes, which the API does not offer yet. -4. **API**: expose `gitconfig.Config` per RFC #2224 and deprecate the typed - struct. - -go-git-specific needs to keep in mind: `url.insteadOf`/`pushInsteadOf` -(go-git#865, #844), `http..*` matching (go-git#474), `safe.directory` -and the ownership check (done by `Discover`; go-git must pass the working -tree, and not build the `Env` by hand for a repository it found), `core.sharedRepository` permissions on written files -(go-git#843, #1572) — set by go-git's own write path.