Help us shape the future of Go! Take the 2026 Go Developer Survey

ermitegit

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

Documentation

Overview

Package ermitegit implements ports.RepoReader over the ermite-git library.

The adapter wraps the gitcli reader. Each method has a route, a constant below: the methods that match gitcli in the shared conformance table and beat it in the benchmarks are served by ermite-git, the others (and GetCommit) by the wrapped reader. A repository that ermite-git cannot read (ErrUnsupported: SHA-256, reftable, alternates, replace refs...) is served by the wrapped reader for every method.

Any other error of the library is returned, never hidden by a fallback. The exceptions are decisions about the data, not failures: a commit whose message is not in UTF-8 (git converts it, the library does not) and an object larger than the library's in-memory bound are read by the wrapped reader.

Freshness: a read that begins after a push has returned sees the push, whichever path made it, because the library reads refs from disk on each call (loose refs always, packed-refs after a stat) and rescans the packs when an object is missing (TestFreshness proves it with no Refresh at all). The adapter still calls Refresh to release replaced packs, pick up a new commit-graph and notice a repository that stopped being supported: at once when ermite itself wrote (the push and the maintenance tell it through Refresh; a merge request merge waits for its maintenance), and as a safety net at most once per Config.RefreshEvery for each repository while it is read, by one reader at a time. A Refresh stats the files of the repository, too much to pay on every read of a method that itself costs 2 µs.

Memory: the cache of open repositories is bounded (LRU) and drops the ones idle longer than Config.IdleTTL; each repository holds at most Config.DeltaCacheSize of delta bases and reads objects up to Config.MaxObjectSize into memory. gitcli's Trash tells the adapter to drop a deleted repository (see Forget).

Index

Constants

View Source
const DefaultDeltaCacheSize = 8 << 20

DefaultDeltaCacheSize is the budget of the cache of delta bases of each open repository when Config.DeltaCacheSize is zero (the library's own is 32 MiB).

View Source
const DefaultIdleTTL = 10 * time.Minute

DefaultIdleTTL is how long a repository may go unused before it is closed, when Config.IdleTTL is zero.

View Source
const DefaultMaxObjectSize = 16 << 20

DefaultMaxObjectSize is the largest object a repository holds in memory when Config.MaxObjectSize is zero (the library's own is 128 MiB). A bigger one is read by the wrapped reader.

View Source
const DefaultMaxRepos = 64

DefaultMaxRepos is the number of repositories kept open when Config.MaxRepos is zero.

View Source
const DefaultRefreshEvery = 5 * time.Second

DefaultRefreshEvery is how long a repository goes without a Refresh when Config.RefreshEvery is zero. See Config.RefreshEvery for what depends on it. A write that goes through ermite refreshes the repository at once (see Adapter.Refresh), so this is only the safety net for the other writers.

Variables

View Source
var ErrClosed = errors.New("ermitegit: reader is closed")

ErrClosed is returned by reads after Close.

Functions

This section is empty.

Types

type Adapter

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

Adapter is a ports.RepoReader that routes each method to ermite-git or to the wrapped reader.

func New

func New(cfg Config) *Adapter

New builds an adapter.

func (*Adapter) Close

func (a *Adapter) Close()

Close closes the open repositories. Reads after Close fail with ErrClosed; blob streams already returned stay valid until they are closed. The wrapped reader is not closed.

func (*Adapter) Forget

func (a *Adapter) Forget(dir string)

Forget drops the open repository of dir, which was moved away or deleted.

func (*Adapter) GetCommit

func (a *Adapter) GetCommit(ctx context.Context, repo domain.RepoName, rev domain.Revision, limits domain.DiffLimits) (domain.CommitDetail, error)

GetCommit is served by the wrapped reader: the diff stays on git.

func (*Adapter) GetTree

func (a *Adapter) GetTree(ctx context.Context, repo domain.RepoName, rev domain.Revision, path domain.TreePath, includeLastCommit bool) (domain.Tree, error)

GetTree lists a directory.

func (*Adapter) ListCommits

func (a *Adapter) ListCommits(ctx context.Context, repo domain.RepoName, q ports.CommitQuery) (ports.CommitPage, error)

ListCommits returns a page of history.

func (*Adapter) ListRefs

func (a *Adapter) ListRefs(ctx context.Context, repo domain.RepoName) ([]domain.Ref, error)

ListRefs lists HEAD, the branches and the tags.

func (*Adapter) OpenBlob

OpenBlob describes a file and returns its content as a stream.

func (*Adapter) OpenRepos

func (a *Adapter) OpenRepos() int

OpenRepos returns the number of repositories in the cache, for tests.

func (*Adapter) Refresh

func (a *Adapter) Refresh(ctx context.Context, repo domain.RepoName)

Refresh tells the adapter that ermite wrote to the repository (a push, the maintenance): the library refreshes it now, if it is open, instead of at the next read after RefreshEvery. A repository that is not open has nothing to refresh. A failure is logged, not returned: reads do not depend on it.

func (*Adapter) ResolveRefPath

func (a *Adapter) ResolveRefPath(ctx context.Context, repo domain.RepoName, path string) (domain.RefPath, error)

ResolveRefPath splits a path into a revision and the rest.

func (*Adapter) ResolveRevision

func (a *Adapter) ResolveRevision(ctx context.Context, repo domain.RepoName, rev domain.Revision) (domain.ResolvedRevision, error)

ResolveRevision turns a revision into a commit.

type Backend

type Backend uint8

Backend says which reader serves a method.

const (
	// CLI is the git binary, through the gitcli reader.
	CLI Backend = iota
	// ErmiteGit is the ermite-git library.
	ErmiteGit
)

Backends.

func (Backend) String

func (b Backend) String() string

String returns "cli" or "ermite-git".

type Config

type Config struct {
	// Fallback serves the methods that are not routed to ermite-git and the
	// repositories ermite-git cannot read. Required.
	Fallback ports.RepoReader
	// RepoDir returns the directory of a repository. It does not check that it
	// exists. Required.
	RepoDir func(domain.RepoName) string
	// RefreshEvery is the longest a repository goes without a Refresh of the
	// library while it is read. Zero means DefaultRefreshEvery; a negative
	// value refreshes before every read. It does not affect what a read
	// sees: see the package comment.
	RefreshEvery time.Duration
	// MaxRepos bounds the repositories kept open; zero means DefaultMaxRepos.
	MaxRepos int
	// IdleTTL is how long a repository may go unused before it is closed.
	// Zero means DefaultIdleTTL; a negative value keeps repositories open
	// until evicted.
	IdleTTL time.Duration
	// DeltaCacheSize is the budget in bytes of the cache of delta bases of
	// each open repository. Zero means DefaultDeltaCacheSize; a negative value
	// disables the cache.
	DeltaCacheSize int64
	// MaxObjectSize is the size in bytes above which an object that must be
	// held in memory is read by the wrapped reader instead. Zero means
	// DefaultMaxObjectSize; a negative value means the library default
	// (128 MiB).
	MaxObjectSize int64
	// Routes overrides DefaultRoutes when not nil.
	Routes *Routes
}

Config configures an Adapter.

type Routes

type Routes struct {
	ListRefs Backend
	// ResolveRevision is a revision that is a name or a full id.
	ResolveRevision Backend
	// ResolveRevisionAbbrev is a revision that is an abbreviated id.
	ResolveRevisionAbbrev Backend
	ResolveRefPath        Backend
	// GetTree is a directory listing without last commits.
	GetTree Backend
	// GetTreeLastCommits is a listing with the last commit of each entry.
	GetTreeLastCommits Backend
	OpenBlob           Backend
	// ListCommits is a page of history of a revision.
	ListCommits Backend
	// ListCommitsPath is a page of the history of a file or a directory.
	ListCommitsPath Backend
}

Routes says which reader serves each method. GetCommit is not here: it is always served by the wrapped reader.

func AllRoutes

func AllRoutes(b Backend) Routes

AllRoutes returns routes that send every method to b. The tests and the benchmarks use it to exercise a backend whatever the production routes are.

func DefaultRoutes

func DefaultRoutes() Routes

DefaultRoutes returns the routes of production.

The rule: a method is routed to ermite-git only when it passes the shared conformance table (TestConformance, TestCompareWithCLI) and its benchmark beats the CLI adapter in time, in bytes AND in allocations per call, warm AND cold, on the generated fixture AND on lazygit (BenchmarkRepoReader, ERMITE_GIT_BENCH_REPO). Where a method has several shapes, every shape must win: the constants GetTree, GetTreeLastCommits, ResolveRevisionAbbrev and ListCommitsPath cover directories of any size and files of any history. A method that does not win stays on the CLI. GetCommit is not routable: the diff stays on git.

Warm is a repository that is open; cold closes it before every call, the price of the first read after the idle TTL, an eviction or a restart (a cold ermite-git call opens the packs and the commit-graph: about 65 µs and 130 allocations before the call itself). The CLI keeps nothing open, so its cold run is its warm run. Only the allocations of the Go process are counted, which favours the CLI (its work happens in git processes). The parallel runs (b.RunParallel) were also made: they agree with the warm runs and widen the time gap.

The numbers are the median of six runs of `go test -bench RepoReader -benchmem -benchtime 500ms` on an AMD Ryzen 9 7900X3D, git 2.56, ermite-git 5228465, as "time bytes allocations per call", CLI against ermite-git (K and M are KiB and MiB). The fixture has 220 commits, 8 directories of 40 files and a 10 MiB blob; lazygit has 7,550 commits. "loses" names what the library does not win: t time, b bytes, a allocations.

shape                   repo     mode  cli                  ermite-git           verdict
ListRefs                fixture  warm  905µs 116.3K 125     24µs 19.0K 73        win
ListRefs                fixture  cold  904µs 116.3K 125     88µs 35.2K 203       loses a
ListRefs                lazygit  warm  901µs 105.2K 88      15µs 2.2K 27         win
ListRefs                lazygit  cold  908µs 105.4K 88      88µs 14.2K 163       loses a
ResolveRevision/branch  fixture  warm  22µs 0.9K 19         4µs 0.4K 8           win
ResolveRevision/branch  fixture  cold  22µs 0.9K 19         68µs 16.6K 138       loses tba
ResolveRevision/branch  lazygit  warm  22µs 1.0K 19         4µs 0.5K 8           win
ResolveRevision/branch  lazygit  cold  23µs 1.0K 19         72µs 12.4K 144       loses tba
ResolveRevision/fullid  fixture  warm  9µs 1.3K 22          2µs 0.4K 6           win
ResolveRevision/fullid  fixture  cold  9µs 1.3K 22          65µs 16.6K 136       loses tba
ResolveRevision/fullid  lazygit  warm  9µs 1.3K 22          2µs 0.4K 6           win
ResolveRevision/fullid  lazygit  cold  9µs 1.3K 22          70µs 12.3K 142       loses tba
ResolveRevision/abbrev  fixture  warm  63µs 1.5K 31         9µs 0.7K 11          win
ResolveRevision/abbrev  fixture  cold  64µs 1.5K 31         71µs 16.8K 141       loses tba
ResolveRevision/abbrev  lazygit  warm  63µs 1.5K 31         9µs 0.6K 11          win
ResolveRevision/abbrev  lazygit  cold  63µs 1.5K 31         76µs 12.6K 147       loses tba
ResolveRefPath          fixture  warm  104µs 2.4K 59        15µs 0.8K 22         win
ResolveRefPath          fixture  cold  105µs 2.4K 59        78µs 17.0K 152       loses ba
ResolveRefPath          lazygit  warm  150µs 4.1K 79        22µs 1.4K 29         win
ResolveRefPath          lazygit  cold  148µs 4.1K 79        89µs 13.4K 165       loses ba
GetTree/root            fixture  warm  717µs 108.4K 114     13µs 5.8K 24         win
GetTree/root            fixture  cold  725µs 108.4K 114     78µs 22.4K 154       loses a
GetTree/root            lazygit  warm  827µs 121.4K 167     29µs 18.2K 56        win
GetTree/root            lazygit  cold  831µs 121.5K 167     376µs 1.6M 200       loses ba
GetTree/dir             fixture  warm  742µs 109.5K 141     16µs 9.2K 26         win
GetTree/dir             fixture  cold  745µs 109.5K 141     109µs 75.8K 158      loses a
GetTree/dir             lazygit  warm  1.4ms 267.9K 1058    193µs 318.4K 73      loses b
GetTree/dir             lazygit  cold  1.4ms 268.0K 1058    4.2ms 2.0M 2490      loses tba
GetTree/root+last       fixture  warm  4.0ms 1.2M 1023      70µs 108.3K 107      win
GetTree/root+last       fixture  cold  4.0ms 1.2M 1024      165µs 127.8K 249     win
GetTree/root+last       lazygit  warm  20.2ms 3.8M 3072     6.1ms 367.0K 510     win
GetTree/root+last       lazygit  cold  20.3ms 3.8M 3073     8.5ms 3.4M 4204      loses a
GetTree/dir+last        fixture  warm  5.3ms 1.8M 1454      190µs 166.1K 142     win
GetTree/dir+last        fixture  cold  5.4ms 1.8M 1455      316µs 247.6K 323     win
GetTree/dir+last        lazygit  warm  195.2ms 20.5M 17116  7.9ms 1.5M 855       win
GetTree/dir+last        lazygit  cold  195.9ms 20.5M 17107  14.8ms 5.1M 6989     win
OpenBlob/small          fixture  warm  46µs 6.1K 25         17µs 5.8K 14         win
OpenBlob/small          fixture  cold  47µs 6.1K 25         87µs 27.7K 147       loses tba
OpenBlob/small          lazygit  warm  51µs 5.4K 25         29µs 7.0K 42         loses ba
OpenBlob/small          lazygit  cold  50µs 5.4K 25         131µs 25.1K 184      loses tba
OpenBlob/largest        fixture  warm  21.4ms 69.7K 64      52.5ms 9.0K 13       loses t
OpenBlob/largest        fixture  cold  21.6ms 70.0K 64      51.4ms 26.2K 143     loses ta
OpenBlob/largest        lazygit  warm  3.3ms 69.9K 64       5.7ms 16.3K 123      loses ta
OpenBlob/largest        lazygit  cold  3.3ms 69.9K 64       5.8ms 31.6K 283      loses ta
ListCommits/page1       fixture  warm  1.0ms 122.5K 404     61µs 19.1K 108       win
ListCommits/page1       fixture  cold  1.0ms 122.5K 404     134µs 36.9K 244      win
ListCommits/page1       lazygit  warm  1.3ms 157.2K 415     361µs 120.1K 224     win
ListCommits/page1       lazygit  cold  1.3ms 157.2K 415     503µs 136.2K 377     win
ListCommits/deep        fixture  warm  1.0ms 122.1K 404     71µs 19.3K 108       win
ListCommits/deep        fixture  cold  1.0ms 122.1K 404     136µs 36.1K 238      win
ListCommits/deep        lazygit  warm  2.0ms 123.2K 404     424µs 23.9K 112      win
ListCommits/deep        lazygit  cold  2.0ms 123.2K 404     544µs 36.3K 248      win
ListCommits/path        fixture  warm  1.6ms 108.8K 138     443µs 25.4K 42       win
ListCommits/path        fixture  cold  1.6ms 108.9K 138     594µs 93.6K 360      loses a
ListCommits/path        lazygit  warm  12.9ms 108.7K 118    6.0ms 29.2K 58       win
ListCommits/path        lazygit  cold  12.8ms 107.7K 118    9.5ms 2.4M 6498      loses ba
ListCommits/dirpath     fixture  warm  1.3ms 123.0K 403     199µs 33.8K 113      win
ListCommits/dirpath     fixture  cold  1.3ms 123.0K 403     277µs 55.4K 268      win
ListCommits/dirpath     lazygit  warm  6.7ms 127.8K 403     2.0ms 50.2K 181      win
ListCommits/dirpath     lazygit  cold  6.7ms 127.8K 403     3.3ms 887.1K 2019    loses ba

What decides the routes, with ermite-git 5228465 (zlib reader with a reused adler32, header-size cache, Refresh and LookupRefID without allocations):

  • The warm runs all win but GetTree of a large directory on lazygit (time is won since the zlib change, bytes are not) and OpenBlob, whose allocations come from the inflate of a small blob and whose time is lost for a 10 MiB blob (inflate speed: 52 ms against 21 ms).
  • The cold runs lose in allocations for every method that does little work after the open (ListRefs, GetTree, ResolveRefPath: 150 to 200 against 60 to 140), and in time too for the ones that cost a few microseconds warm (ResolveRevision: 65 µs against 9 to 63 µs).
  • Only the history of a revision (ListCommits page 1 and deep) wins every line, so it is the only method routed. GetTree dir+last also wins every line, but GetTreeLastCommits covers GetTree root+last too, which loses cold allocations on lazygit (4204 against 3073): it is the nearest miss, and the first to flip once opening a repository allocates less.
  • OpenBlob by size: below 1 MiB the small blob loses cold and, on lazygit, warm bytes and allocations; above, it loses time. No size makes it win, so there is no size-based route.

Once opening a repository allocates less, re-run the benchmark and flip the constants it wins: ListRefs, ResolveRefPath and ResolveRevision win warm by 4 to 60 times in time.

Jump to

Keyboard shortcuts

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