feat(cmd): choose the git reader with ERMITE_GIT_READER
cli|ermite-git, default ermite-git, validated in config. forge.go wraps gitcli in the ermitegit adapter unless cli is asked for, and hooks Trash to the adapter so that a deleted repository is dropped. The e2e harness takes the reader from the environment: the suite runs with both. Documented in the README.
- commit
- c38360a31f4db661ebf6abcb58e6f253aa599052
- author
- brice · · 2026-10-10 23:07 UTC
- parent
- b54f0d3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +119 -1
M README.md +1 -0
| @@ -21,6 +21,7 @@ ermite serve | |||
| 21 | 21 | | `ERMITE_ADDR` | `:7341` | listen address | | |
| 22 | 22 | | `ERMITE_TRUSTED_PROXIES` | empty | comma-separated CIDRs | | |
| 23 | 23 | | `ERMITE_GIT` | `git` | git 2.45 or newer | | |
| 24 | + | | `ERMITE_GIT_READER` | `ermite-git` | `ermite-git` reads refs, trees, blobs and history with the [ermite-git](https://git.bnema.dev/bnema/ermite-git) library where it is faster than the `git` binary and falls back to `git` elsewhere and for repositories it does not read; `cli` uses the `git` binary for every read (the escape hatch if a read looks wrong: set it and restart) | | |
| 24 | 25 | | `ERMITE_SOURCE_URL` | `$ERMITE_PUBLIC_URL/ermite` | link to the source (AGPL) | | |
| 25 | 26 | | `ERMITE_LOG_LEVEL` | `info` | `trace`, `debug`, `info`, `warn`, `error` | | |
| 26 | 27 | | `ERMITE_LOG_FORMAT` | `json` | `json` or `console` | | |
| @@ -21,6 +21,7 @@ ermite serve | |||||
| 21 | | `ERMITE_ADDR` | `:7341` | listen address | | 21 | | `ERMITE_ADDR` | `:7341` | listen address | | ||
| 22 | | `ERMITE_TRUSTED_PROXIES` | empty | comma-separated CIDRs | | 22 | | `ERMITE_TRUSTED_PROXIES` | empty | comma-separated CIDRs | | ||
| 23 | | `ERMITE_GIT` | `git` | git 2.45 or newer | | 23 | | `ERMITE_GIT` | `git` | git 2.45 or newer | | ||
| 24 | + | | `ERMITE_GIT_READER` | `ermite-git` | `ermite-git` reads refs, trees, blobs and history with the [ermite-git](https://git.bnema.dev/bnema/ermite-git) library where it is faster than the `git` binary and falls back to `git` elsewhere and for repositories it does not read; `cli` uses the `git` binary for every read (the escape hatch if a read looks wrong: set it and restart) | | |||
| 24 | | `ERMITE_SOURCE_URL` | `$ERMITE_PUBLIC_URL/ermite` | link to the source (AGPL) | | 25 | | `ERMITE_SOURCE_URL` | `$ERMITE_PUBLIC_URL/ermite` | link to the source (AGPL) | | ||
| 25 | | `ERMITE_LOG_LEVEL` | `info` | `trace`, `debug`, `info`, `warn`, `error` | | 26 | | `ERMITE_LOG_LEVEL` | `info` | `trace`, `debug`, `info`, `warn`, `error` | | ||
| 26 | | `ERMITE_LOG_FORMAT` | `json` | `json` or `console` | | 27 | | `ERMITE_LOG_FORMAT` | `json` | `json` or `console` | | ||
M cmd/ermite/e2e_helpers_test.go +4 -0
| @@ -69,6 +69,10 @@ func newHarness(t *testing.T, o harnessOpts) *harness { | |||
| 69 | 69 | if o.trustedProxies != "" { | |
| 70 | 70 | env[config.EnvTrustedProxies] = o.trustedProxies | |
| 71 | 71 | } | |
| 72 | + | // The suite runs with either reader: ERMITE_GIT_READER=cli go test ./cmd/ermite | |
| 73 | + | if r := os.Getenv(config.EnvGitReader); r != "" { | |
| 74 | + | env[config.EnvGitReader] = r | |
| 75 | + | } | |
| 72 | 76 | cfg, err := config.Load(func(k string) string { return env[k] }) | |
| 73 | 77 | require.NoError(t, err) | |
| 74 | 78 | ||
| @@ -69,6 +69,10 @@ func newHarness(t *testing.T, o harnessOpts) *harness { | |||||
| 69 | if o.trustedProxies != "" { | 69 | if o.trustedProxies != "" { | ||
| 70 | env[config.EnvTrustedProxies] = o.trustedProxies | 70 | env[config.EnvTrustedProxies] = o.trustedProxies | ||
| 71 | } | 71 | } | ||
| 72 | + | // The suite runs with either reader: ERMITE_GIT_READER=cli go test ./cmd/ermite | |||
| 73 | + | if r := os.Getenv(config.EnvGitReader); r != "" { | |||
| 74 | + | env[config.EnvGitReader] = r | |||
| 75 | + | } | |||
| 72 | cfg, err := config.Load(func(k string) string { return env[k] }) | 76 | cfg, err := config.Load(func(k string) string { return env[k] }) | ||
| 73 | require.NoError(t, err) | 77 | require.NoError(t, err) | ||
| 74 | 78 | ||||
M cmd/ermite/forge.go +17 -1
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | 15 | ||
| 16 | 16 | "github.com/bnema/zerowrap" | |
| 17 | 17 | ||
| 18 | + | "bnema.dev/ermite/internal/adapters/ermitegit" | |
| 18 | 19 | "bnema.dev/ermite/internal/adapters/eventbus" | |
| 19 | 20 | "bnema.dev/ermite/internal/adapters/gitcli" | |
| 20 | 21 | "bnema.dev/ermite/internal/adapters/githttp" | |
| @@ -57,6 +58,8 @@ type forge struct { | |||
| 57 | 58 | hooks *githttp.HookServer | |
| 58 | 59 | maint *app.Maintainer | |
| 59 | 60 | git *gitcli.Git | |
| 61 | + | // reader is the ermite-git reader when ERMITE_GIT_READER selects it. | |
| 62 | + | reader *ermitegit.Adapter | |
| 60 | 63 | ||
| 61 | 64 | // The use cases and the API are exposed for tests and for the phases | |
| 62 | 65 | // that add the web. | |
| @@ -166,7 +169,17 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||
| 166 | 169 | return nil, err | |
| 167 | 170 | } | |
| 168 | 171 | ||
| 169 | - | f.Browse = app.NewBrowse(store, git) | |
| 172 | + | var reader ports.RepoReader = git | |
| 173 | + | if cfg.GitReader == config.GitReaderErmiteGit { | |
| 174 | + | f.reader = ermitegit.New(ermitegit.Config{ | |
| 175 | + | Fallback: git, | |
| 176 | + | RepoDir: func(name domain.RepoName) string { return gitcli.RepoDir(cfg.DataDir, name) }, | |
| 177 | + | }) | |
| 178 | + | git.OnTrash(f.reader.Forget) | |
| 179 | + | reader = f.reader | |
| 180 | + | } | |
| 181 | + | log.Info().Str("reader", cfg.GitReader).Msg("git reader selected") | |
| 182 | + | f.Browse = app.NewBrowse(store, reader) | |
| 170 | 183 | f.EventLog = app.NewEvents(ctx, f.Repos, f.Bus, clock, app.EventsConfig{ | |
| 171 | 184 | Size: opts.eventBuffer, Auth: f.Auth, RevalidateEvery: opts.revalidateEvery, | |
| 172 | 185 | }) | |
| @@ -283,6 +296,9 @@ func (f *forge) Close(ctx context.Context) { | |||
| 283 | 296 | if f.EventLog != nil { | |
| 284 | 297 | f.EventLog.Close(ctx) | |
| 285 | 298 | } | |
| 299 | + | if f.reader != nil { | |
| 300 | + | f.reader.Close() | |
| 301 | + | } | |
| 286 | 302 | if f.git != nil { | |
| 287 | 303 | if err := f.git.Close(ctx); err != nil { | |
| 288 | 304 | log.Error().Err(err).Msg("close git reader") | |
| @@ -15,6 +15,7 @@ import ( | |||||
| 15 | 15 | ||||
| 16 | "github.com/bnema/zerowrap" | 16 | "github.com/bnema/zerowrap" | ||
| 17 | 17 | ||||
| 18 | + | "bnema.dev/ermite/internal/adapters/ermitegit" | |||
| 18 | "bnema.dev/ermite/internal/adapters/eventbus" | 19 | "bnema.dev/ermite/internal/adapters/eventbus" | ||
| 19 | "bnema.dev/ermite/internal/adapters/gitcli" | 20 | "bnema.dev/ermite/internal/adapters/gitcli" | ||
| 20 | "bnema.dev/ermite/internal/adapters/githttp" | 21 | "bnema.dev/ermite/internal/adapters/githttp" | ||
| @@ -57,6 +58,8 @@ type forge struct { | |||||
| 57 | hooks *githttp.HookServer | 58 | hooks *githttp.HookServer | ||
| 58 | maint *app.Maintainer | 59 | maint *app.Maintainer | ||
| 59 | git *gitcli.Git | 60 | git *gitcli.Git | ||
| 61 | + | // reader is the ermite-git reader when ERMITE_GIT_READER selects it. | |||
| 62 | + | reader *ermitegit.Adapter | |||
| 60 | 63 | ||||
| 61 | // The use cases and the API are exposed for tests and for the phases | 64 | // The use cases and the API are exposed for tests and for the phases | ||
| 62 | // that add the web. | 65 | // that add the web. | ||
| @@ -166,7 +169,17 @@ func newForge(ctx context.Context, cfg config.Config, opts options) (_ *forge, e | |||||
| 166 | return nil, err | 169 | return nil, err | ||
| 167 | } | 170 | } | ||
| 168 | 171 | ||||
| 169 | - | f.Browse = app.NewBrowse(store, git) | 172 | + | var reader ports.RepoReader = git |
| 173 | + | if cfg.GitReader == config.GitReaderErmiteGit { | |||
| 174 | + | f.reader = ermitegit.New(ermitegit.Config{ | |||
| 175 | + | Fallback: git, | |||
| 176 | + | RepoDir: func(name domain.RepoName) string { return gitcli.RepoDir(cfg.DataDir, name) }, | |||
| 177 | + | }) | |||
| 178 | + | git.OnTrash(f.reader.Forget) | |||
| 179 | + | reader = f.reader | |||
| 180 | + | } | |||
| 181 | + | log.Info().Str("reader", cfg.GitReader).Msg("git reader selected") | |||
| 182 | + | f.Browse = app.NewBrowse(store, reader) | |||
| 170 | f.EventLog = app.NewEvents(ctx, f.Repos, f.Bus, clock, app.EventsConfig{ | 183 | f.EventLog = app.NewEvents(ctx, f.Repos, f.Bus, clock, app.EventsConfig{ | ||
| 171 | Size: opts.eventBuffer, Auth: f.Auth, RevalidateEvery: opts.revalidateEvery, | 184 | Size: opts.eventBuffer, Auth: f.Auth, RevalidateEvery: opts.revalidateEvery, | ||
| 172 | }) | 185 | }) | ||
| @@ -283,6 +296,9 @@ func (f *forge) Close(ctx context.Context) { | |||||
| 283 | if f.EventLog != nil { | 296 | if f.EventLog != nil { | ||
| 284 | f.EventLog.Close(ctx) | 297 | f.EventLog.Close(ctx) | ||
| 285 | } | 298 | } | ||
| 299 | + | if f.reader != nil { | |||
| 300 | + | f.reader.Close() | |||
| 301 | + | } | |||
| 286 | if f.git != nil { | 302 | if f.git != nil { | ||
| 287 | if err := f.git.Close(ctx); err != nil { | 303 | if err := f.git.Close(ctx); err != nil { | ||
| 288 | log.Error().Err(err).Msg("close git reader") | 304 | log.Error().Err(err).Msg("close git reader") | ||
A cmd/ermite/gitreader_test.go +59 -0
| @@ -0,0 +1,59 @@ | |||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |
| 2 | + | ||
| 3 | + | package main | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "os" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | ||
| 10 | + | "github.com/stretchr/testify/assert" | |
| 11 | + | "github.com/stretchr/testify/require" | |
| 12 | + | ||
| 13 | + | "bnema.dev/ermite/internal/app" | |
| 14 | + | "bnema.dev/ermite/internal/config" | |
| 15 | + | "bnema.dev/ermite/internal/domain" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // The suite runs with the reader of ERMITE_GIT_READER (ermite-git by default). | |
| 19 | + | // This test says which one that is, so that a run cannot claim a reader it did | |
| 20 | + | // not exercise, and checks the read-after-push guarantee through the real | |
| 21 | + | // HTTP push path and the browse use case. | |
| 22 | + | func TestGitReaderOfTheForgeAndReadAfterPush(t *testing.T) { | |
| 23 | + | h := newHarness(t, harnessOpts{}) | |
| 24 | + | want := os.Getenv(config.EnvGitReader) | |
| 25 | + | if want == "" { | |
| 26 | + | want = config.GitReaderErmiteGit | |
| 27 | + | } | |
| 28 | + | assert.Equal(t, want, h.forge.cfg.GitReader) | |
| 29 | + | assert.Equal(t, want == config.GitReaderErmiteGit, h.forge.reader != nil, "the ermite-git adapter exists only for ermite-git") | |
| 30 | + | ||
| 31 | + | _, err := h.forge.Repos.Create(h.ctx, domain.Root(), app.CreateParams{ | |
| 32 | + | Name: h.name("fresh"), Visibility: domain.VisibilityPublic, | |
| 33 | + | }) | |
| 34 | + | require.NoError(t, err) | |
| 35 | + | wt := h.t.TempDir() | |
| 36 | + | h.mustGit(wt, "init", "-q", "--initial-branch=main") | |
| 37 | + | remote := h.urlAuth(adminToken, "/fresh.git") | |
| 38 | + | var last string | |
| 39 | + | for _, content := range []string{"one\n", "two\n", "three\n"} { | |
| 40 | + | h.writeFile(wt, "f.txt", []byte(content)) // commits | |
| 41 | + | h.mustGit(wt, "push", "-q", remote, "main") | |
| 42 | + | last = strings.TrimSpace(h.mustGit(wt, "rev-parse", "HEAD")) | |
| 43 | + | ||
| 44 | + | // The very next read, no wait: the browse sees the push. | |
| 45 | + | res, err := h.forge.Browse.ResolveRevision(h.ctx, domain.Root(), h.name("fresh"), domain.Revision{}) | |
| 46 | + | require.NoError(t, err) | |
| 47 | + | assert.Equal(t, last, res.CommitID.String()) | |
| 48 | + | tree, err := h.forge.Browse.GetTree(h.ctx, domain.Root(), h.name("fresh"), domain.Revision{}, domain.TreePath{}, true) | |
| 49 | + | require.NoError(t, err) | |
| 50 | + | require.Len(t, tree.Entries, 1) | |
| 51 | + | assert.Equal(t, last, tree.Entries[0].LastCommit.ID.String()) | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // Deleting the repository drops what the reader holds; a repository of the | |
| 55 | + | // same name is a new one. | |
| 56 | + | require.NoError(t, h.forge.Repos.Delete(h.ctx, domain.Root(), h.name("fresh"))) | |
| 57 | + | _, err = h.forge.Browse.ResolveRevision(h.ctx, domain.Root(), h.name("fresh"), domain.Revision{}) | |
| 58 | + | require.Error(t, err) | |
| 59 | + | } | |
| @@ -0,0 +1,59 @@ | |||||
| 1 | + | // SPDX-License-Identifier: AGPL-3.0-or-later | |||
| 2 | + | ||||
| 3 | + | package main | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "os" | |||
| 7 | + | "strings" | |||
| 8 | + | "testing" | |||
| 9 | + | ||||
| 10 | + | "github.com/stretchr/testify/assert" | |||
| 11 | + | "github.com/stretchr/testify/require" | |||
| 12 | + | ||||
| 13 | + | "bnema.dev/ermite/internal/app" | |||
| 14 | + | "bnema.dev/ermite/internal/config" | |||
| 15 | + | "bnema.dev/ermite/internal/domain" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // The suite runs with the reader of ERMITE_GIT_READER (ermite-git by default). | |||
| 19 | + | // This test says which one that is, so that a run cannot claim a reader it did | |||
| 20 | + | // not exercise, and checks the read-after-push guarantee through the real | |||
| 21 | + | // HTTP push path and the browse use case. | |||
| 22 | + | func TestGitReaderOfTheForgeAndReadAfterPush(t *testing.T) { | |||
| 23 | + | h := newHarness(t, harnessOpts{}) | |||
| 24 | + | want := os.Getenv(config.EnvGitReader) | |||
| 25 | + | if want == "" { | |||
| 26 | + | want = config.GitReaderErmiteGit | |||
| 27 | + | } | |||
| 28 | + | assert.Equal(t, want, h.forge.cfg.GitReader) | |||
| 29 | + | assert.Equal(t, want == config.GitReaderErmiteGit, h.forge.reader != nil, "the ermite-git adapter exists only for ermite-git") | |||
| 30 | + | ||||
| 31 | + | _, err := h.forge.Repos.Create(h.ctx, domain.Root(), app.CreateParams{ | |||
| 32 | + | Name: h.name("fresh"), Visibility: domain.VisibilityPublic, | |||
| 33 | + | }) | |||
| 34 | + | require.NoError(t, err) | |||
| 35 | + | wt := h.t.TempDir() | |||
| 36 | + | h.mustGit(wt, "init", "-q", "--initial-branch=main") | |||
| 37 | + | remote := h.urlAuth(adminToken, "/fresh.git") | |||
| 38 | + | var last string | |||
| 39 | + | for _, content := range []string{"one\n", "two\n", "three\n"} { | |||
| 40 | + | h.writeFile(wt, "f.txt", []byte(content)) // commits | |||
| 41 | + | h.mustGit(wt, "push", "-q", remote, "main") | |||
| 42 | + | last = strings.TrimSpace(h.mustGit(wt, "rev-parse", "HEAD")) | |||
| 43 | + | ||||
| 44 | + | // The very next read, no wait: the browse sees the push. | |||
| 45 | + | res, err := h.forge.Browse.ResolveRevision(h.ctx, domain.Root(), h.name("fresh"), domain.Revision{}) | |||
| 46 | + | require.NoError(t, err) | |||
| 47 | + | assert.Equal(t, last, res.CommitID.String()) | |||
| 48 | + | tree, err := h.forge.Browse.GetTree(h.ctx, domain.Root(), h.name("fresh"), domain.Revision{}, domain.TreePath{}, true) | |||
| 49 | + | require.NoError(t, err) | |||
| 50 | + | require.Len(t, tree.Entries, 1) | |||
| 51 | + | assert.Equal(t, last, tree.Entries[0].LastCommit.ID.String()) | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // Deleting the repository drops what the reader holds; a repository of the | |||
| 55 | + | // same name is a new one. | |||
| 56 | + | require.NoError(t, h.forge.Repos.Delete(h.ctx, domain.Root(), h.name("fresh"))) | |||
| 57 | + | _, err = h.forge.Browse.ResolveRevision(h.ctx, domain.Root(), h.name("fresh"), domain.Revision{}) | |||
| 58 | + | require.Error(t, err) | |||
| 59 | + | } | |||
M internal/config/config.go +22 -0
| @@ -25,6 +25,7 @@ const ( | |||
| 25 | 25 | EnvAdminToken = "ERMITE_ADMIN_TOKEN" | |
| 26 | 26 | EnvTrustedProxies = "ERMITE_TRUSTED_PROXIES" | |
| 27 | 27 | EnvGit = "ERMITE_GIT" | |
| 28 | + | EnvGitReader = "ERMITE_GIT_READER" | |
| 28 | 29 | EnvSourceURL = "ERMITE_SOURCE_URL" | |
| 29 | 30 | EnvLogLevel = "ERMITE_LOG_LEVEL" | |
| 30 | 31 | EnvLogFormat = "ERMITE_LOG_FORMAT" | |
| @@ -38,12 +39,22 @@ const ( | |||
| 38 | 39 | const ( | |
| 39 | 40 | DefaultAddr = ":7341" | |
| 40 | 41 | DefaultGit = "git" | |
| 42 | + | DefaultGitReader = GitReaderErmiteGit | |
| 41 | 43 | DefaultLogLevel = "info" | |
| 42 | 44 | DefaultLogFormat = "json" | |
| 43 | 45 | // DefaultCommitterName is the committer name of the commits ermite creates. | |
| 44 | 46 | DefaultCommitterName = "ermite" | |
| 45 | 47 | ) | |
| 46 | 48 | ||
| 49 | + | // Values of ERMITE_GIT_READER. | |
| 50 | + | const ( | |
| 51 | + | // GitReaderCLI reads repositories with the git binary only. | |
| 52 | + | GitReaderCLI = "cli" | |
| 53 | + | // GitReaderErmiteGit reads them with the ermite-git library where it is | |
| 54 | + | // proven faster, and with the git binary elsewhere. | |
| 55 | + | GitReaderErmiteGit = "ermite-git" | |
| 56 | + | ) | |
| 57 | + | ||
| 47 | 58 | // MinAdminTokenLen is the shortest accepted admin token, in characters. | |
| 48 | 59 | const MinAdminTokenLen = 32 | |
| 49 | 60 | ||
| @@ -62,6 +73,8 @@ type Config struct { | |||
| 62 | 73 | TrustedProxies []netip.Prefix | |
| 63 | 74 | // Git is the git executable, a name or a path. | |
| 64 | 75 | Git string | |
| 76 | + | // GitReader is GitReaderCLI or GitReaderErmiteGit. | |
| 77 | + | GitReader string | |
| 65 | 78 | // SourceURL is the link to the source of the running version. | |
| 66 | 79 | SourceURL string | |
| 67 | 80 | // LogLevel is one of trace, debug, info, warn, error. | |
| @@ -148,6 +161,15 @@ func Load(get func(string) string) (Config, error) { | |||
| 148 | 161 | c.Git = DefaultGit | |
| 149 | 162 | } | |
| 150 | 163 | ||
| 164 | + | c.GitReader = strings.ToLower(strings.TrimSpace(get(EnvGitReader))) | |
| 165 | + | switch c.GitReader { | |
| 166 | + | case "": | |
| 167 | + | c.GitReader = DefaultGitReader | |
| 168 | + | case GitReaderCLI, GitReaderErmiteGit: | |
| 169 | + | default: | |
| 170 | + | bad(EnvGitReader, "must be %s or %s (got %q)", GitReaderCLI, GitReaderErmiteGit, get(EnvGitReader)) | |
| 171 | + | } | |
| 172 | + | ||
| 151 | 173 | switch raw := get(EnvSourceURL); { | |
| 152 | 174 | case raw != "": | |
| 153 | 175 | if u, err := parseURL(raw, true); err != nil { | |
| @@ -25,6 +25,7 @@ const ( | |||||
| 25 | EnvAdminToken = "ERMITE_ADMIN_TOKEN" | 25 | EnvAdminToken = "ERMITE_ADMIN_TOKEN" | ||
| 26 | EnvTrustedProxies = "ERMITE_TRUSTED_PROXIES" | 26 | EnvTrustedProxies = "ERMITE_TRUSTED_PROXIES" | ||
| 27 | EnvGit = "ERMITE_GIT" | 27 | EnvGit = "ERMITE_GIT" | ||
| 28 | + | EnvGitReader = "ERMITE_GIT_READER" | |||
| 28 | EnvSourceURL = "ERMITE_SOURCE_URL" | 29 | EnvSourceURL = "ERMITE_SOURCE_URL" | ||
| 29 | EnvLogLevel = "ERMITE_LOG_LEVEL" | 30 | EnvLogLevel = "ERMITE_LOG_LEVEL" | ||
| 30 | EnvLogFormat = "ERMITE_LOG_FORMAT" | 31 | EnvLogFormat = "ERMITE_LOG_FORMAT" | ||
| @@ -38,12 +39,22 @@ const ( | |||||
| 38 | const ( | 39 | const ( | ||
| 39 | DefaultAddr = ":7341" | 40 | DefaultAddr = ":7341" | ||
| 40 | DefaultGit = "git" | 41 | DefaultGit = "git" | ||
| 42 | + | DefaultGitReader = GitReaderErmiteGit | |||
| 41 | DefaultLogLevel = "info" | 43 | DefaultLogLevel = "info" | ||
| 42 | DefaultLogFormat = "json" | 44 | DefaultLogFormat = "json" | ||
| 43 | // DefaultCommitterName is the committer name of the commits ermite creates. | 45 | // DefaultCommitterName is the committer name of the commits ermite creates. | ||
| 44 | DefaultCommitterName = "ermite" | 46 | DefaultCommitterName = "ermite" | ||
| 45 | ) | 47 | ) | ||
| 46 | 48 | ||||
| 49 | + | // Values of ERMITE_GIT_READER. | |||
| 50 | + | const ( | |||
| 51 | + | // GitReaderCLI reads repositories with the git binary only. | |||
| 52 | + | GitReaderCLI = "cli" | |||
| 53 | + | // GitReaderErmiteGit reads them with the ermite-git library where it is | |||
| 54 | + | // proven faster, and with the git binary elsewhere. | |||
| 55 | + | GitReaderErmiteGit = "ermite-git" | |||
| 56 | + | ) | |||
| 57 | + | ||||
| 47 | // MinAdminTokenLen is the shortest accepted admin token, in characters. | 58 | // MinAdminTokenLen is the shortest accepted admin token, in characters. | ||
| 48 | const MinAdminTokenLen = 32 | 59 | const MinAdminTokenLen = 32 | ||
| 49 | 60 | ||||
| @@ -62,6 +73,8 @@ type Config struct { | |||||
| 62 | TrustedProxies []netip.Prefix | 73 | TrustedProxies []netip.Prefix | ||
| 63 | // Git is the git executable, a name or a path. | 74 | // Git is the git executable, a name or a path. | ||
| 64 | Git string | 75 | Git string | ||
| 76 | + | // GitReader is GitReaderCLI or GitReaderErmiteGit. | |||
| 77 | + | GitReader string | |||
| 65 | // SourceURL is the link to the source of the running version. | 78 | // SourceURL is the link to the source of the running version. | ||
| 66 | SourceURL string | 79 | SourceURL string | ||
| 67 | // LogLevel is one of trace, debug, info, warn, error. | 80 | // LogLevel is one of trace, debug, info, warn, error. | ||
| @@ -148,6 +161,15 @@ func Load(get func(string) string) (Config, error) { | |||||
| 148 | c.Git = DefaultGit | 161 | c.Git = DefaultGit | ||
| 149 | } | 162 | } | ||
| 150 | 163 | ||||
| 164 | + | c.GitReader = strings.ToLower(strings.TrimSpace(get(EnvGitReader))) | |||
| 165 | + | switch c.GitReader { | |||
| 166 | + | case "": | |||
| 167 | + | c.GitReader = DefaultGitReader | |||
| 168 | + | case GitReaderCLI, GitReaderErmiteGit: | |||
| 169 | + | default: | |||
| 170 | + | bad(EnvGitReader, "must be %s or %s (got %q)", GitReaderCLI, GitReaderErmiteGit, get(EnvGitReader)) | |||
| 171 | + | } | |||
| 172 | + | ||||
| 151 | switch raw := get(EnvSourceURL); { | 173 | switch raw := get(EnvSourceURL); { | ||
| 152 | case raw != "": | 174 | case raw != "": | ||
| 153 | if u, err := parseURL(raw, true); err != nil { | 175 | if u, err := parseURL(raw, true); err != nil { | ||
M internal/config/config_test.go +16 -0
| @@ -44,6 +44,7 @@ func TestLoadDefaults(t *testing.T) { | |||
| 44 | 44 | assert.Equal(t, "/var/lib/ermite", c.DataDir) | |
| 45 | 45 | assert.Equal(t, "https://git.example.com", c.PublicURL) | |
| 46 | 46 | assert.Equal(t, "git", c.Git) | |
| 47 | + | assert.Equal(t, "ermite-git", c.GitReader, "ermite-git reads by default") | |
| 47 | 48 | assert.Equal(t, "https://git.example.com/ermite", c.SourceURL) | |
| 48 | 49 | assert.Equal(t, "info", c.LogLevel) | |
| 49 | 50 | assert.Equal(t, "json", c.LogFormat) | |
| @@ -58,6 +59,7 @@ func TestLoadAllValues(t *testing.T) { | |||
| 58 | 59 | config.EnvPublicURL: "http://localhost:7341/", | |
| 59 | 60 | config.EnvTrustedProxies: "10.1.2.3/8, fd00::1/8", | |
| 60 | 61 | config.EnvGit: "/usr/local/bin/git", | |
| 62 | + | config.EnvGitReader: " CLI ", | |
| 61 | 63 | config.EnvSourceURL: "https://forge.example.org/src/", | |
| 62 | 64 | config.EnvLogLevel: "DEBUG", | |
| 63 | 65 | config.EnvLogFormat: "Console", | |
| @@ -68,6 +70,7 @@ func TestLoadAllValues(t *testing.T) { | |||
| 68 | 70 | assert.Equal(t, "http://localhost:7341", c.PublicURL) | |
| 69 | 71 | assert.Equal(t, []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("fd00::/8")}, c.TrustedProxies) | |
| 70 | 72 | assert.Equal(t, "/usr/local/bin/git", c.Git) | |
| 73 | + | assert.Equal(t, "cli", c.GitReader) | |
| 71 | 74 | assert.Equal(t, "https://forge.example.org/src", c.SourceURL) | |
| 72 | 75 | assert.Equal(t, "debug", c.LogLevel) | |
| 73 | 76 | assert.Equal(t, "console", c.LogFormat) | |
| @@ -105,6 +108,8 @@ func TestLoadProblems(t *testing.T) { | |||
| 105 | 108 | {"port too big", map[string]string{config.EnvAddr: ":70000"}, config.EnvAddr, "port"}, | |
| 106 | 109 | {"bad cidr", map[string]string{config.EnvTrustedProxies: "10.0.0.1"}, config.EnvTrustedProxies, "CIDR"}, | |
| 107 | 110 | {"empty cidr entry", map[string]string{config.EnvTrustedProxies: "10.0.0.0/8,,"}, config.EnvTrustedProxies, "empty"}, | |
| 111 | + | {"bad git reader", map[string]string{config.EnvGitReader: "libgit2"}, config.EnvGitReader, "cli or ermite-git"}, | |
| 112 | + | {"git reader typo", map[string]string{config.EnvGitReader: "ermite_git"}, config.EnvGitReader, `"ermite_git"`}, | |
| 108 | 113 | {"bad source url", map[string]string{config.EnvSourceURL: "nope"}, config.EnvSourceURL, "scheme"}, | |
| 109 | 114 | {"bad level", map[string]string{config.EnvLogLevel: "loud"}, config.EnvLogLevel, "trace, debug"}, | |
| 110 | 115 | {"bad format", map[string]string{config.EnvLogFormat: "xml"}, config.EnvLogFormat, "json or console"}, | |
| @@ -192,3 +197,14 @@ func TestLoadRejectsBadCommitter(t *testing.T) { | |||
| 192 | 197 | }) | |
| 193 | 198 | } | |
| 194 | 199 | } | |
| 200 | + | ||
| 201 | + | func TestLoadGitReader(t *testing.T) { | |
| 202 | + | for in, want := range map[string]string{ | |
| 203 | + | "": config.GitReaderErmiteGit, "ermite-git": config.GitReaderErmiteGit, "ERMITE-GIT": config.GitReaderErmiteGit, | |
| 204 | + | "cli": config.GitReaderCLI, "Cli": config.GitReaderCLI, | |
| 205 | + | } { | |
| 206 | + | c, err := config.Load(lookup(valid(map[string]string{config.EnvGitReader: in}))) | |
| 207 | + | require.NoError(t, err, in) | |
| 208 | + | assert.Equal(t, want, c.GitReader, in) | |
| 209 | + | } | |
| 210 | + | } | |
| @@ -44,6 +44,7 @@ func TestLoadDefaults(t *testing.T) { | |||||
| 44 | assert.Equal(t, "/var/lib/ermite", c.DataDir) | 44 | assert.Equal(t, "/var/lib/ermite", c.DataDir) | ||
| 45 | assert.Equal(t, "https://git.example.com", c.PublicURL) | 45 | assert.Equal(t, "https://git.example.com", c.PublicURL) | ||
| 46 | assert.Equal(t, "git", c.Git) | 46 | assert.Equal(t, "git", c.Git) | ||
| 47 | + | assert.Equal(t, "ermite-git", c.GitReader, "ermite-git reads by default") | |||
| 47 | assert.Equal(t, "https://git.example.com/ermite", c.SourceURL) | 48 | assert.Equal(t, "https://git.example.com/ermite", c.SourceURL) | ||
| 48 | assert.Equal(t, "info", c.LogLevel) | 49 | assert.Equal(t, "info", c.LogLevel) | ||
| 49 | assert.Equal(t, "json", c.LogFormat) | 50 | assert.Equal(t, "json", c.LogFormat) | ||
| @@ -58,6 +59,7 @@ func TestLoadAllValues(t *testing.T) { | |||||
| 58 | config.EnvPublicURL: "http://localhost:7341/", | 59 | config.EnvPublicURL: "http://localhost:7341/", | ||
| 59 | config.EnvTrustedProxies: "10.1.2.3/8, fd00::1/8", | 60 | config.EnvTrustedProxies: "10.1.2.3/8, fd00::1/8", | ||
| 60 | config.EnvGit: "/usr/local/bin/git", | 61 | config.EnvGit: "/usr/local/bin/git", | ||
| 62 | + | config.EnvGitReader: " CLI ", | |||
| 61 | config.EnvSourceURL: "https://forge.example.org/src/", | 63 | config.EnvSourceURL: "https://forge.example.org/src/", | ||
| 62 | config.EnvLogLevel: "DEBUG", | 64 | config.EnvLogLevel: "DEBUG", | ||
| 63 | config.EnvLogFormat: "Console", | 65 | config.EnvLogFormat: "Console", | ||
| @@ -68,6 +70,7 @@ func TestLoadAllValues(t *testing.T) { | |||||
| 68 | assert.Equal(t, "http://localhost:7341", c.PublicURL) | 70 | assert.Equal(t, "http://localhost:7341", c.PublicURL) | ||
| 69 | assert.Equal(t, []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("fd00::/8")}, c.TrustedProxies) | 71 | assert.Equal(t, []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("fd00::/8")}, c.TrustedProxies) | ||
| 70 | assert.Equal(t, "/usr/local/bin/git", c.Git) | 72 | assert.Equal(t, "/usr/local/bin/git", c.Git) | ||
| 73 | + | assert.Equal(t, "cli", c.GitReader) | |||
| 71 | assert.Equal(t, "https://forge.example.org/src", c.SourceURL) | 74 | assert.Equal(t, "https://forge.example.org/src", c.SourceURL) | ||
| 72 | assert.Equal(t, "debug", c.LogLevel) | 75 | assert.Equal(t, "debug", c.LogLevel) | ||
| 73 | assert.Equal(t, "console", c.LogFormat) | 76 | assert.Equal(t, "console", c.LogFormat) | ||
| @@ -105,6 +108,8 @@ func TestLoadProblems(t *testing.T) { | |||||
| 105 | {"port too big", map[string]string{config.EnvAddr: ":70000"}, config.EnvAddr, "port"}, | 108 | {"port too big", map[string]string{config.EnvAddr: ":70000"}, config.EnvAddr, "port"}, | ||
| 106 | {"bad cidr", map[string]string{config.EnvTrustedProxies: "10.0.0.1"}, config.EnvTrustedProxies, "CIDR"}, | 109 | {"bad cidr", map[string]string{config.EnvTrustedProxies: "10.0.0.1"}, config.EnvTrustedProxies, "CIDR"}, | ||
| 107 | {"empty cidr entry", map[string]string{config.EnvTrustedProxies: "10.0.0.0/8,,"}, config.EnvTrustedProxies, "empty"}, | 110 | {"empty cidr entry", map[string]string{config.EnvTrustedProxies: "10.0.0.0/8,,"}, config.EnvTrustedProxies, "empty"}, | ||
| 111 | + | {"bad git reader", map[string]string{config.EnvGitReader: "libgit2"}, config.EnvGitReader, "cli or ermite-git"}, | |||
| 112 | + | {"git reader typo", map[string]string{config.EnvGitReader: "ermite_git"}, config.EnvGitReader, `"ermite_git"`}, | |||
| 108 | {"bad source url", map[string]string{config.EnvSourceURL: "nope"}, config.EnvSourceURL, "scheme"}, | 113 | {"bad source url", map[string]string{config.EnvSourceURL: "nope"}, config.EnvSourceURL, "scheme"}, | ||
| 109 | {"bad level", map[string]string{config.EnvLogLevel: "loud"}, config.EnvLogLevel, "trace, debug"}, | 114 | {"bad level", map[string]string{config.EnvLogLevel: "loud"}, config.EnvLogLevel, "trace, debug"}, | ||
| 110 | {"bad format", map[string]string{config.EnvLogFormat: "xml"}, config.EnvLogFormat, "json or console"}, | 115 | {"bad format", map[string]string{config.EnvLogFormat: "xml"}, config.EnvLogFormat, "json or console"}, | ||
| @@ -192,3 +197,14 @@ func TestLoadRejectsBadCommitter(t *testing.T) { | |||||
| 192 | }) | 197 | }) | ||
| 193 | } | 198 | } | ||
| 194 | } | 199 | } | ||
| 200 | + | ||||
| 201 | + | func TestLoadGitReader(t *testing.T) { | |||
| 202 | + | for in, want := range map[string]string{ | |||
| 203 | + | "": config.GitReaderErmiteGit, "ermite-git": config.GitReaderErmiteGit, "ERMITE-GIT": config.GitReaderErmiteGit, | |||
| 204 | + | "cli": config.GitReaderCLI, "Cli": config.GitReaderCLI, | |||
| 205 | + | } { | |||
| 206 | + | c, err := config.Load(lookup(valid(map[string]string{config.EnvGitReader: in}))) | |||
| 207 | + | require.NoError(t, err, in) | |||
| 208 | + | assert.Equal(t, want, c.GitReader, in) | |||
| 209 | + | } | |||
| 210 | + | } | |||