docs(readme): rewrite README with a short overview and docs/ (#6)
- commit
- 39053497968d140289ed1f14fa6a23800dff415d
- author
- brice · · 2026-10-18 17:43 UTC
- committer
- bnema · · 2026-10-18 17:43 UTC
- parent
- 3889d22
- signature
- SSH Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown
2 files · +50 -25
M README.md +30 -25
| @@ -1,10 +1,27 @@ | |||
| 1 | - | # ermite-git | |
| 1 | + | <h1 align="center">ermite-git</h1> | |
| 2 | + | ||
| 3 | + | <p align="center"> | |
| 4 | + | <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License: MIT"></a> | |
| 5 | + | <a href="https://github.com/bnema/ermite-git/commits/main"><img src="https://badgen.net/github/last-commit/bnema/ermite-git/main?icon=github" alt="Last commit"></a> | |
| 6 | + | <a href="https://github.com/bnema/ermite-git/stargazers"><img src="https://badgen.net/github/stars/bnema/ermite-git?icon=github" alt="GitHub stars"></a> | |
| 7 | + | </p> | |
| 8 | + | ||
| 9 | + | <p align="center">A fast, read-only Git repository reader for Go. Standard library only.</p> | |
| 2 | 10 | ||
| 3 | 11 | > Read-only mirror. Development happens at git.bnema.dev; issues and pull requests are closed here. | |
| 4 | 12 | ||
| 5 | - | A fast, read-only Git repository reader for Go: refs, objects, packfiles, trees and history, using commit-graph and changed-path Bloom filters. Standard library only. | |
| 13 | + | --- | |
| 14 | + | ||
| 15 | + | ## Features | |
| 6 | 16 | ||
| 7 | - | Status: early development, no stable API. Phase A reads refs, loose and packed objects, trees, blobs and commits; phase B adds the commit-graph, changed-path Bloom filters and history walks. | |
| 17 | + | - Refs, loose and packed objects, trees, blobs and commits | |
| 18 | + | - History in `git log` order, with `git log -- path` simplification | |
| 19 | + | - Commit-graph and changed-path Bloom filters for fast history | |
| 20 | + | - Safe for concurrent use; no allocation per commit after warm-up | |
| 21 | + | ||
| 22 | + | Status: early development, no stable API. | |
| 23 | + | ||
| 24 | + | ## Usage | |
| 8 | 25 | ||
| 9 | 26 | ```go | |
| 10 | 27 | import git "bnema.dev/ermite-git" | |
| @@ -18,53 +35,41 @@ defer repo.Close() | |||
| 18 | 35 | id, _ := repo.Resolve("main") | |
| 19 | 36 | commitID, _ := repo.PeelToCommit(id) | |
| 20 | 37 | ||
| 21 | - | var c git.Commit // reuse it: no allocation per commit after warm-up | |
| 38 | + | var c git.Commit // reuse it | |
| 22 | 39 | _ = repo.ReadCommit(commitID, &c) | |
| 23 | 40 | _ = repo.ReadTree(c.Tree, func(e git.Entry) bool { | |
| 24 | 41 | fmt.Printf("%s %s\n", e.Mode, e.Name) // e.Name is valid during the callback only | |
| 25 | 42 | return true | |
| 26 | 43 | }) | |
| 27 | - | ``` | |
| 28 | 44 | ||
| 29 | - | History in the order of `git log`, with the same path simplification as `git log -- path`: | |
| 30 | - | ||
| 31 | - | ```go | |
| 32 | - | for c, err := range repo.Walk(ctx, commitID, git.WalkOptions{Path: "pkg/gui", Skip: 100, Limit: 50}) { | |
| 45 | + | for c, err := range repo.Walk(ctx, commitID, git.WalkOptions{Path: "pkg/gui", Limit: 50}) { | |
| 33 | 46 | if err != nil { | |
| 34 | 47 | return err | |
| 35 | 48 | } | |
| 36 | - | fmt.Printf("%s\n", c.ID) // c is reused: valid until the next iteration | |
| 49 | + | fmt.Println(c.ID) // c is reused: valid until the next iteration | |
| 37 | 50 | } | |
| 38 | - | // the last commit of every entry of a directory, in one walk | |
| 51 | + | ||
| 52 | + | // last commit of each entry of a directory, in one walk | |
| 39 | 53 | ids, err := repo.LastCommits(ctx, commitID, "pkg", []string{"gui", "commands"}) | |
| 40 | 54 | ``` | |
| 41 | 55 | ||
| 42 | - | A `Repo` is safe for concurrent use. Call `Refresh` after the repository changed (a push, a repack) to see new refs and packs; reads in flight keep working while packs are replaced. | |
| 56 | + | Call `Refresh` after the repository changes (push, repack) to see new refs and packs. | |
| 43 | 57 | ||
| 44 | - | Repositories the package does not read are rejected by `Open` with `ErrUnsupported`: SHA-256, reftable, other `extensions.*`, shallow, grafts, replace refs, alternates, partial clones, incremental or stale multi-pack-index. | |
| 58 | + | `Open` returns `ErrUnsupported` for SHA-256, reftable, other `extensions.*`, shallow, grafts, replace refs, alternates, partial clones, and incremental or stale multi-pack-index. | |
| 45 | 59 | ||
| 46 | - | ## Limits | |
| 47 | - | ||
| 48 | - | - **A pack truncated in place can crash the process.** Packs and indexes are memory-mapped; if another process shrinks one of them while it is mapped, a read of the missing part ends with SIGBUS, which Go cannot recover from. Git always replaces a pack by renaming a new file over it (repack, gc, fetch), and a mapping keeps the old file alive, so this takes an external actor that edits packs in place, or a failing disk. Not handled. | |
| 49 | - | - **Without mmap, packs are read into memory.** Linux, macOS, FreeBSD, OpenBSD, NetBSD and DragonFly map packs. On Windows and other systems the whole `.pack` and `.idx` are read into memory when a pack is opened, so memory grows with the size of the repository. Not handled. | |
| 50 | - | - **Revision names.** `Resolve` follows `git rev-parse` for plain names: a tag wins over a branch of the same name, and a full 40 digit id is never read as a ref. Use `ResolveRef` for a full ref name and `ResolveCommitPrefix` for an abbreviation that must designate a commit. | |
| 60 | + | Known limits: [docs/limits.md](docs/limits.md). | |
| 51 | 61 | ||
| 52 | 62 | ## Test | |
| 53 | 63 | ||
| 54 | - | The conformance tests compare with the `git` binary, 2.45 or newer (they are skipped without git). | |
| 64 | + | Tests compare with the `git` binary, 2.45 or newer (skipped without it). | |
| 55 | 65 | ||
| 56 | 66 | ```sh | |
| 57 | 67 | go test -race ./... | |
| 58 | - | # also compare every object of a real repository with `git cat-file`: | |
| 59 | - | ERMITE_GIT_BENCH_REPO=/path/to/repo/.git go test -run TestRealRepository -v | |
| 60 | - | # benchmarks (the same variable runs them on that repository): | |
| 68 | + | ERMITE_GIT_BENCH_REPO=/path/to/repo/.git go test -run TestRealRepository -v # every object vs git cat-file | |
| 61 | 69 | go test -run '^$' -bench . -benchmem | |
| 62 | - | # fuzz targets: | |
| 63 | 70 | go test -run '^$' -fuzz FuzzParseCommit -fuzztime 30s | |
| 64 | 71 | ``` | |
| 65 | 72 | ||
| 66 | - | The tests build their fixtures with the `git` binary (2.45 or later) and compare with it. | |
| 67 | - | ||
| 68 | 73 | ## License | |
| 69 | 74 | ||
| 70 | 75 | MIT. See [LICENSE](LICENSE). | |
| @@ -1,10 +1,27 @@ | |||||
| 1 | - | # ermite-git | 1 | + | <h1 align="center">ermite-git</h1> |
| 2 | + | ||||
| 3 | + | <p align="center"> | |||
| 4 | + | <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License: MIT"></a> | |||
| 5 | + | <a href="https://github.com/bnema/ermite-git/commits/main"><img src="https://badgen.net/github/last-commit/bnema/ermite-git/main?icon=github" alt="Last commit"></a> | |||
| 6 | + | <a href="https://github.com/bnema/ermite-git/stargazers"><img src="https://badgen.net/github/stars/bnema/ermite-git?icon=github" alt="GitHub stars"></a> | |||
| 7 | + | </p> | |||
| 8 | + | ||||
| 9 | + | <p align="center">A fast, read-only Git repository reader for Go. Standard library only.</p> | |||
| 2 | 10 | ||||
| 3 | > Read-only mirror. Development happens at git.bnema.dev; issues and pull requests are closed here. | 11 | > Read-only mirror. Development happens at git.bnema.dev; issues and pull requests are closed here. | ||
| 4 | 12 | ||||
| 5 | - | A fast, read-only Git repository reader for Go: refs, objects, packfiles, trees and history, using commit-graph and changed-path Bloom filters. Standard library only. | 13 | + | --- |
| 14 | + | ||||
| 15 | + | ## Features | |||
| 6 | 16 | ||||
| 7 | - | Status: early development, no stable API. Phase A reads refs, loose and packed objects, trees, blobs and commits; phase B adds the commit-graph, changed-path Bloom filters and history walks. | 17 | + | - Refs, loose and packed objects, trees, blobs and commits |
| 18 | + | - History in `git log` order, with `git log -- path` simplification | |||
| 19 | + | - Commit-graph and changed-path Bloom filters for fast history | |||
| 20 | + | - Safe for concurrent use; no allocation per commit after warm-up | |||
| 21 | + | ||||
| 22 | + | Status: early development, no stable API. | |||
| 23 | + | ||||
| 24 | + | ## Usage | |||
| 8 | 25 | ||||
| 9 | ```go | 26 | ```go | ||
| 10 | import git "bnema.dev/ermite-git" | 27 | import git "bnema.dev/ermite-git" | ||
| @@ -18,53 +35,41 @@ defer repo.Close() | |||||
| 18 | id, _ := repo.Resolve("main") | 35 | id, _ := repo.Resolve("main") | ||
| 19 | commitID, _ := repo.PeelToCommit(id) | 36 | commitID, _ := repo.PeelToCommit(id) | ||
| 20 | 37 | ||||
| 21 | - | var c git.Commit // reuse it: no allocation per commit after warm-up | 38 | + | var c git.Commit // reuse it |
| 22 | _ = repo.ReadCommit(commitID, &c) | 39 | _ = repo.ReadCommit(commitID, &c) | ||
| 23 | _ = repo.ReadTree(c.Tree, func(e git.Entry) bool { | 40 | _ = repo.ReadTree(c.Tree, func(e git.Entry) bool { | ||
| 24 | fmt.Printf("%s %s\n", e.Mode, e.Name) // e.Name is valid during the callback only | 41 | fmt.Printf("%s %s\n", e.Mode, e.Name) // e.Name is valid during the callback only | ||
| 25 | return true | 42 | return true | ||
| 26 | }) | 43 | }) | ||
| 27 | - | ``` | |||
| 28 | 44 | ||||
| 29 | - | History in the order of `git log`, with the same path simplification as `git log -- path`: | 45 | + | for c, err := range repo.Walk(ctx, commitID, git.WalkOptions{Path: "pkg/gui", Limit: 50}) { |
| 30 | - | ||||
| 31 | - | ```go | |||
| 32 | - | for c, err := range repo.Walk(ctx, commitID, git.WalkOptions{Path: "pkg/gui", Skip: 100, Limit: 50}) { | |||
| 33 | if err != nil { | 46 | if err != nil { | ||
| 34 | return err | 47 | return err | ||
| 35 | } | 48 | } | ||
| 36 | - | fmt.Printf("%s\n", c.ID) // c is reused: valid until the next iteration | 49 | + | fmt.Println(c.ID) // c is reused: valid until the next iteration |
| 37 | } | 50 | } | ||
| 38 | - | // the last commit of every entry of a directory, in one walk | 51 | + | |
| 52 | + | // last commit of each entry of a directory, in one walk | |||
| 39 | ids, err := repo.LastCommits(ctx, commitID, "pkg", []string{"gui", "commands"}) | 53 | ids, err := repo.LastCommits(ctx, commitID, "pkg", []string{"gui", "commands"}) | ||
| 40 | ``` | 54 | ``` | ||
| 41 | 55 | ||||
| 42 | - | A `Repo` is safe for concurrent use. Call `Refresh` after the repository changed (a push, a repack) to see new refs and packs; reads in flight keep working while packs are replaced. | 56 | + | Call `Refresh` after the repository changes (push, repack) to see new refs and packs. |
| 43 | 57 | ||||
| 44 | - | Repositories the package does not read are rejected by `Open` with `ErrUnsupported`: SHA-256, reftable, other `extensions.*`, shallow, grafts, replace refs, alternates, partial clones, incremental or stale multi-pack-index. | 58 | + | `Open` returns `ErrUnsupported` for SHA-256, reftable, other `extensions.*`, shallow, grafts, replace refs, alternates, partial clones, and incremental or stale multi-pack-index. |
| 45 | 59 | ||||
| 46 | - | ## Limits | 60 | + | Known limits: [docs/limits.md](docs/limits.md). |
| 47 | - | ||||
| 48 | - | - **A pack truncated in place can crash the process.** Packs and indexes are memory-mapped; if another process shrinks one of them while it is mapped, a read of the missing part ends with SIGBUS, which Go cannot recover from. Git always replaces a pack by renaming a new file over it (repack, gc, fetch), and a mapping keeps the old file alive, so this takes an external actor that edits packs in place, or a failing disk. Not handled. | |||
| 49 | - | - **Without mmap, packs are read into memory.** Linux, macOS, FreeBSD, OpenBSD, NetBSD and DragonFly map packs. On Windows and other systems the whole `.pack` and `.idx` are read into memory when a pack is opened, so memory grows with the size of the repository. Not handled. | |||
| 50 | - | - **Revision names.** `Resolve` follows `git rev-parse` for plain names: a tag wins over a branch of the same name, and a full 40 digit id is never read as a ref. Use `ResolveRef` for a full ref name and `ResolveCommitPrefix` for an abbreviation that must designate a commit. | |||
| 51 | 61 | ||||
| 52 | ## Test | 62 | ## Test | ||
| 53 | 63 | ||||
| 54 | - | The conformance tests compare with the `git` binary, 2.45 or newer (they are skipped without git). | 64 | + | Tests compare with the `git` binary, 2.45 or newer (skipped without it). |
| 55 | 65 | ||||
| 56 | ```sh | 66 | ```sh | ||
| 57 | go test -race ./... | 67 | go test -race ./... | ||
| 58 | - | # also compare every object of a real repository with `git cat-file`: | 68 | + | ERMITE_GIT_BENCH_REPO=/path/to/repo/.git go test -run TestRealRepository -v # every object vs git cat-file |
| 59 | - | ERMITE_GIT_BENCH_REPO=/path/to/repo/.git go test -run TestRealRepository -v | |||
| 60 | - | # benchmarks (the same variable runs them on that repository): | |||
| 61 | go test -run '^$' -bench . -benchmem | 69 | go test -run '^$' -bench . -benchmem | ||
| 62 | - | # fuzz targets: | |||
| 63 | go test -run '^$' -fuzz FuzzParseCommit -fuzztime 30s | 70 | go test -run '^$' -fuzz FuzzParseCommit -fuzztime 30s | ||
| 64 | ``` | 71 | ``` | ||
| 65 | 72 | ||||
| 66 | - | The tests build their fixtures with the `git` binary (2.45 or later) and compare with it. | |||
| 67 | - | ||||
| 68 | ## License | 73 | ## License | ||
| 69 | 74 | ||||
| 70 | MIT. See [LICENSE](LICENSE). | 75 | MIT. See [LICENSE](LICENSE). | ||
A docs/limits.md +20 -0
| @@ -0,0 +1,20 @@ | |||
| 1 | + | # Limits | |
| 2 | + | ||
| 3 | + | ## A pack truncated in place can crash the process | |
| 4 | + | ||
| 5 | + | Packs and indexes are memory-mapped. If another process shrinks a mapped file, reading the missing part raises SIGBUS, which Go cannot recover from. | |
| 6 | + | ||
| 7 | + | Git never does this: it replaces packs by renaming a new file over them. It takes an external tool that edits packs in place, or a failing disk. Not handled. | |
| 8 | + | ||
| 9 | + | ## Without mmap, packs are read into memory | |
| 10 | + | ||
| 11 | + | Linux, macOS and the BSDs map packs. On Windows and other systems, the whole `.pack` and `.idx` are read into memory when opened, so memory grows with the repository. Not handled. | |
| 12 | + | ||
| 13 | + | ## Revision names | |
| 14 | + | ||
| 15 | + | `Resolve` follows `git rev-parse` for plain names: | |
| 16 | + | ||
| 17 | + | - a tag wins over a branch of the same name; | |
| 18 | + | - a full 40-digit id is never read as a ref. | |
| 19 | + | ||
| 20 | + | Use `ResolveRef` for a full ref name, and `ResolveCommitPrefix` for an abbreviation that must be a commit. | |
| @@ -0,0 +1,20 @@ | |||||
| 1 | + | # Limits | |||
| 2 | + | ||||
| 3 | + | ## A pack truncated in place can crash the process | |||
| 4 | + | ||||
| 5 | + | Packs and indexes are memory-mapped. If another process shrinks a mapped file, reading the missing part raises SIGBUS, which Go cannot recover from. | |||
| 6 | + | ||||
| 7 | + | Git never does this: it replaces packs by renaming a new file over them. It takes an external tool that edits packs in place, or a failing disk. Not handled. | |||
| 8 | + | ||||
| 9 | + | ## Without mmap, packs are read into memory | |||
| 10 | + | ||||
| 11 | + | Linux, macOS and the BSDs map packs. On Windows and other systems, the whole `.pack` and `.idx` are read into memory when opened, so memory grows with the repository. Not handled. | |||
| 12 | + | ||||
| 13 | + | ## Revision names | |||
| 14 | + | ||||
| 15 | + | `Resolve` follows `git rev-parse` for plain names: | |||
| 16 | + | ||||
| 17 | + | - a tag wins over a branch of the same name; | |||
| 18 | + | - a full 40-digit id is never read as a ref. | |||
| 19 | + | ||||
| 20 | + | Use `ResolveRef` for a full ref name, and `ResolveCommitPrefix` for an abbreviation that must be a commit. | |||