From e1835faeeb226d9021a7b37109232106bb37e7be Mon Sep 17 00:00:00 2001 From: bigbes Date: Sat, 18 Jul 2026 20:35:18 +0300 Subject: [PATCH] =?UTF-8?q?compare.sr.ht:=20project=20foundation=20?= =?UTF-8?q?=E2=80=94=20core=20package,=20build=20scaffolding,=20core-go=20?= =?UTF-8?q?fork=20pin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bootstrap the go.bigb.es/sourcehut-compare service: - go.mod (go 1.26.4) pinning core-go to the private fork git.srht.bigb.es/~bigbes/core-go @ c2c2f3848fa9 via a replace directive; go.sum populated by a throwaway smoke build importing core-go config/crypto/client/server + chi + logrus (gates API drift from the verification baseline fdb3662 to upstream 71b2787). - core/: pure domain package — sentinel errors, owner/repo/ref validation (check-ref-format-style, hostile-input hardened), and the compare-spec grammar (ParseCompareSpec, three-dot/two-dot, percent-unescape). Table tests at 97% coverage. - Makefile (all/build/test/css/bundle/run-dev/install), config.example.ini documenting the shared keys read in place, contrib/ nginx block and systemd unit, README skeleton, .gitignore. core-go dep deps and go mod tidy are deferred to later phases per plan. --- .gitignore | 11 ++ Makefile | 63 ++++++++++ README.md | 71 +++++++++++ config.example.ini | 64 ++++++++++ contrib/compare-srht.service | 22 ++++ contrib/compare.sr.ht.conf | 27 ++++ core/errors.go | 30 +++++ core/names.go | 135 ++++++++++++++++++++ core/names_test.go | 112 +++++++++++++++++ core/spec.go | 72 +++++++++++ core/spec_test.go | 78 ++++++++++++ go.mod | 51 ++++++++ go.sum | 233 +++++++++++++++++++++++++++++++++++ 13 files changed, 969 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 config.example.ini create mode 100644 contrib/compare-srht.service create mode 100644 contrib/compare.sr.ht.conf create mode 100644 core/errors.go create mode 100644 core/names.go create mode 100644 core/names_test.go create mode 100644 core/spec.go create mode 100644 core/spec_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..15c619ac1f73238e5e1703fb120e401e8db75c0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Node build-time deps (frontend/ bundle is built and vendored, not committed deps) +node_modules/ + +# Compiled service binary +/comparesrht + +# Transient build artifacts +*.tmp + +# Local instance config +/config.ini diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..4cf2c737e47238627acc85619e68e3320d1e6aeb --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +# compare.sr.ht — build scaffolding (git.sr.ht / sourcehut-dolt style). +# +# Go binary + Go-template SSR live under cmd/ and web/. The CSS is built from +# the shared sourcehut scss partials with sassc; the frontend diff bundle is +# built once with esbuild and vendored into web/static/. Node is a build-time +# dependency only. + +SERVICE=compare.sr.ht +BIN=comparesrht + +PREFIX?=/usr/local +BINDIR?=$(PREFIX)/bin +SHAREDIR?=$(PREFIX)/share +ASSETS?=/usr/share/sourcehut +STATICDIR?=$(ASSETS)/$(SERVICE)/static + +SASSC?=sassc +SASSC_INCLUDE=-I$(ASSETS)/scss + +all: build + +# Compile the service. Intentionally tolerant: cmd/ does not exist until +# phase 3, so this is a no-op until then. +build: + @if [ -d cmd ]; then go build ./cmd/...; else \ + echo "cmd/ not present yet (added in phase 3); nothing to build"; fi + +test: + go test ./... + +# CSS pipeline: sassc -> minify -> content-hashed filename. The running +# service globs web/static/main.min.*.css at startup, so the hash in the name +# is the cache-busting version. Requires the shared scss partials to be +# installed at $(ASSETS)/scss (core.sr.ht `make install`) and scss/main.scss +# to exist (added in phase 2b). +css: web/static/main.min.css + +web/static/main.css: scss/main.scss + mkdir -p $(@D) + $(SASSC) $(SASSC_INCLUDE) $< $@ + +web/static/main.min.css: web/static/main.css + minify -o $@ $< + cp $@ web/static/main.min.$$(sha256sum $@ | cut -c1-8).css + +# Frontend diff/tree bundle: built once, output committed to web/static. +# Requires frontend/ (added in phase 2b). +bundle: + cd frontend && npm ci && \ + npx esbuild src/app.ts --bundle --minify --format=esm \ + --outfile=../web/static/bundle.js + +# Local development run against ./config.ini. Fails until cmd/ exists (phase 3). +run-dev: + go run ./cmd/$(BIN) + +install: build + mkdir -p $(DESTDIR)$(BINDIR) + go build -o $(DESTDIR)$(BINDIR)/$(BIN) ./cmd/$(BIN) + mkdir -p $(DESTDIR)$(STATICDIR) + install -Dm644 -t $(DESTDIR)$(STATICDIR) web/static/* + +.PHONY: all build test css bundle run-dev install diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..28fef7ebb2538a49e7135239b89f97d811b4cd19 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# compare.sr.ht + +A git compare/diff viewer for a self-hosted [SourceHut] instance. It compares +two refs (branches, tags, or commits) of any git.sr.ht repository — `base...head` +or `base..head` — and renders the diff with [`@pierre/diffs`] (Shiki-based +syntax-highlighted diffs) and a [`@pierre/trees`] file tree, plus a +single-commit view. Runs at `https://compare.srht.bigb.es`. + +[SourceHut]: https://sourcehut.org +[`@pierre/diffs`]: https://www.npmjs.com/package/@pierre/diffs +[`@pierre/trees`]: https://www.npmjs.com/package/@pierre/trees + +## Architecture + +The service integrates into SourceHut purely through configuration — no upstream +sources are modified (see the sourcehut-custom-service integration model). It is +**stateless**: it owns no database and no object storage. Repository data is read +directly from the bare repos on disk (`{[git.sr.ht] repos}/~{owner}/{name}`) by +shelling out to `git`, exactly as git.sr.ht itself does. Every request is +authorized against the git.sr.ht GraphQL API using core-go's internal-auth +client, acting as the cookie's logged-in user (or anonymously, which the API +scopes to public/unlisted repos). Identity comes from decrypting the shared +`sr.ht.unified-login.v1` cookie with the instance's network key. Pages are +server-rendered Go templates that reproduce the SourceHut chrome; the diff/tree +UI is one vendored esbuild bundle driven by a JSON blob embedded in the page. + +## Layout + +- `core/` — pure domain: owner/repo/ref validation, the compare-spec grammar, + sentinel errors. No external dependencies. +- `gitx/` — git shell-out (refs, diff, log, commit) with output limits. *(phase 1)* +- `authz/` — cookie→identity and the GraphQL authorizer with a short TTL cache. *(phase 1)* +- `web/` — chi router, handlers, chrome, Go templates, embedded static assets. *(phase 2)* +- `frontend/` + `scss/` — build-time TypeScript diff bundle and the SCSS entry. *(phase 2)* +- `cmd/comparesrht/` — the daemon entry point and startup validation. *(phase 3)* +- `contrib/` — nginx server block and systemd unit. + +The core-go dependency is pinned to a private fork +(`git.srht.bigb.es/~bigbes/core-go`) via a `replace` directive in `go.mod`; the +fork carries one production S3 patch on top of upstream. + +## Build requirements + +- **Go** 1.26+ (module `go.bigb.es/sourcehut-compare`). +- **sassc** and **minify** — CSS pipeline (`make css`), building against the + shared sourcehut scss partials installed at `/usr/share/sourcehut/scss`. +- **Node.js** (build time only) — the frontend diff bundle (`make bundle`, + esbuild). The vendored `web/static/bundle.js` is committed, so Node is not + needed at runtime. + +```sh +make build # compile the service +make test # go test ./... +make css # build the hashed stylesheet +make bundle # build the vendored frontend bundle +``` + +## Configuration + +See `config.example.ini`. On a real instance, append the `[compare.sr.ht]` +section to the shared `/etc/sr.ht/config.ini`; the service reads the instance's +existing shared keys (`[sr.ht]`, `[webhooks]`, `[meta.sr.ht]`, `[git.sr.ht]`) in +place. + +## Deployment + +*Filled in during phase 3.* In outline: install the binary and static assets +(`make install`), add the `[compare.sr.ht]` config section, install +`contrib/compare.sr.ht.conf` (nginx) and `contrib/compare-srht.service` +(systemd), point DNS `compare.srht.bigb.es` at the instance, and ensure this +host is within the git.sr.ht API's `internal-ipnet`. diff --git a/config.example.ini b/config.example.ini new file mode 100644 index 0000000000000000000000000000000000000000..f6e67312a8887dbff635db96f8e7dcd010303086 --- /dev/null +++ b/config.example.ini @@ -0,0 +1,64 @@ +# Example configuration for compare.sr.ht. +# +# compare.sr.ht is a stateless service: it owns no database and no object +# storage. Apart from its own [compare.sr.ht] section it only *reads* keys that +# already exist in the shared sourcehut config.ini of the instance. On a +# deployed instance you normally append the [compare.sr.ht] section below to the +# instance's existing /etc/sr.ht/config.ini rather than shipping a standalone +# file. core-go's LoadConfig() searches: ./config.ini, ../config.ini, +# /etc/sr.ht/config.ini, /etc/sr.ht/*.ini. + +[compare.sr.ht] +# +# Public origin of this service. Used to build self URLs and the logout +# return_to. Cookie auth requires this to be a subdomain of the shared cookie +# domain (*.srht.bigb.es). +origin=https://compare.srht.bigb.es +# +# Directory the built static assets (bundle.js, main.min..css, logo.svg) +# are installed to; globbed at startup to resolve the hashed CSS filename. +static-dir=/usr/share/sourcehut/compare.sr.ht/static +# +# Address the HTTP server binds to. Optional; defaults to 127.0.0.1:5090. +#bind-address=127.0.0.1:5090 + +# -------------------------------------------------------------------------- +# Shared keys read in place (NOT owned by this service). These already exist in +# the instance config; they are documented here so a standalone dev config.ini +# is self-contained. Do not duplicate/override them on a real instance. +# -------------------------------------------------------------------------- + +[sr.ht] +# network-key: Fernet key shared across services. Used to (a) decrypt the +# unified-login cookie (sr.ht.unified-login.v1) to learn the viewer's username +# and (b) mint the Internal auth token for git.sr.ht GraphQL. REQUIRED by +# crypto.InitCrypto. +network-key= +# site-name: shown as the nav brand (" compare"). +site-name=sourcehut +# environment: "production" hides the dev banner; anything else shows it. +environment=production +# internal-ipnet: CIDR the git.sr.ht API trusts for Internal auth. This host +# must fall inside it for anonymous/internal GraphQL calls to be accepted. +internal-ipnet=10.0.0.0/8,127.0.0.0/8 + +[webhooks] +# private-key: base64 Ed25519 seed. compare.sr.ht emits no webhooks, but +# crypto.InitCrypto fatally requires this key to be present. +private-key= + +[meta.sr.ht] +# origin: used to build the login/logout links in the nav +# ({meta}/login?return_to=..., {meta}/logout?return_to=...). +origin=https://meta.srht.bigb.es + +[git.sr.ht] +# origin: public origin of git.sr.ht (link targets, switcher). +origin=https://git.srht.bigb.es +# api-origin: base for the internal GraphQL endpoint (/query). +# GetAPI falls back through api-internal-origin/internal-origin/api-origin/ +# origin; at least one must be set or startup fails. +api-origin=https://git.srht.bigb.es +# repos: on-disk root of bare repositories. compare.sr.ht reads +# {repos}/~{owner}/{name} directly (read-only) after authorization passes. +repos=/var/lib/git diff --git a/contrib/compare-srht.service b/contrib/compare-srht.service new file mode 100644 index 0000000000000000000000000000000000000000..678f8533cb90b6037bbe32a582badfa6b406d4c3 --- /dev/null +++ b/contrib/compare-srht.service @@ -0,0 +1,22 @@ +[Unit] +Description=compare.sr.ht git diff/compare viewer +After=network.target + +[Service] +Type=simple +# Runs as the git user so it has read access to the bare repositories under +# /var/lib/git. compare.sr.ht is stateless — no database or Redis dependency. +User=git +Restart=always +ExecStart=/usr/local/bin/comparesrht + +# The service only ever reads: bare repos on disk and the shared config. It +# writes nothing, so lock the filesystem down. +ProtectSystem=strict +ProtectHome=true +ReadOnlyPaths=/var/lib/git +PrivateTmp=true +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target diff --git a/contrib/compare.sr.ht.conf b/contrib/compare.sr.ht.conf new file mode 100644 index 0000000000000000000000000000000000000000..41ed0f77a21eec1c3e35baeaa61e68f56222ffd1 --- /dev/null +++ b/contrib/compare.sr.ht.conf @@ -0,0 +1,27 @@ +# nginx server block for compare.sr.ht (sr.ht-nginx style). +# +# Drop into the sr.ht-nginx config alongside the other *.sr.ht.conf files and +# adjust server_name to the instance domain (e.g. compare.srht.bigb.es). +# compare.sr.ht serves its own static assets from the embedded FS with +# immutable cache headers, so everything is proxied to the app; there is no +# GraphQL /query endpoint of its own. + +server { + include sourcehut.conf; + include port80.conf; + server_name compare.sr.ht; +} + +server { + include sourcehut.conf; + include port443.conf; + include compare-ssl.conf; + server_name compare.sr.ht; + + location / { + proxy_pass http://127.0.0.1:5090; + include headers.conf; + add_header Content-Security-Policy "default-src 'none'; style-src 'self' 'unsafe-inline'; img-src * data:; script-src 'self'; frame-ancestors 'none'" always; + include web.conf; + } +} diff --git a/core/errors.go b/core/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..9998ace7db0deacace98dc7fff9b6347619585d6 --- /dev/null +++ b/core/errors.go @@ -0,0 +1,30 @@ +// Package core holds the pure domain logic of compare.sr.ht: input +// validation (owner/repo/ref names), the compare-spec grammar, and the +// sentinel errors shared across the service. It has no external dependencies +// and never touches the network or the filesystem, so it is cheap to test. +package core + +import "errors" + +// Sentinel errors. Callers compare with errors.Is; wrapping with %w adds +// human-readable detail without losing the class. +var ( + // ErrNotFound is returned when a repo, ref, or object does not exist — + // or, for authorization, when the viewer is not allowed to know it + // exists. Handlers map this to HTTP 404 (never 403, so private-repo + // existence is not leaked). + ErrNotFound = errors.New("not found") + + // ErrForbidden marks an operation the viewer is authenticated for but + // not permitted to perform. Repo visibility deliberately uses + // ErrNotFound instead; ErrForbidden is reserved for genuine 403 cases. + ErrForbidden = errors.New("forbidden") + + // ErrBadRef is returned for malformed compare specs or refs that fail + // check-ref-format validation. Handlers map this to HTTP 400. + ErrBadRef = errors.New("invalid git ref") + + // ErrTruncated signals that git output exceeded the in-page size cap. + // The handler renders the file list plus a link to the raw .patch. + ErrTruncated = errors.New("output truncated") +) diff --git a/core/names.go b/core/names.go new file mode 100644 index 0000000000000000000000000000000000000000..4a804acd2b2069d09ee085f6a6478d560183ab10 --- /dev/null +++ b/core/names.go @@ -0,0 +1,135 @@ +package core + +import "strings" + +// maxRepoNameLen bounds repository names. Owners are bounded by meta.sr.ht at +// registration time, so ValidOwner enforces only structure, not length. +const maxRepoNameLen = 100 + +// isNameByte reports whether c is allowed in a sourcehut owner or repo name: +// lowercase alphanumerics plus '_', '-', and '.'. Note '/' is deliberately +// excluded, so a name can never span path components. +func isNameByte(c byte) bool { + switch { + case c >= 'a' && c <= 'z': + return true + case c >= '0' && c <= '9': + return true + case c == '_' || c == '-' || c == '.': + return true + default: + return false + } +} + +// validName holds the rules shared by owners and repos: non-empty, drawn from +// the allowed byte set, not starting with '-' (which would look like a git or +// shell option), and containing no ".." (path traversal). '/' is rejected +// implicitly because it is not an allowed byte. +func validName(s string) bool { + if s == "" { + return false + } + if s[0] == '-' { + return false + } + if strings.Contains(s, "..") { + return false + } + for i := 0; i < len(s); i++ { + if !isNameByte(s[i]) { + return false + } + } + return true +} + +// ValidOwner reports whether s is a well-formed sourcehut owner name (the part +// after '~' in a URL). Callers must strip the leading '~' first. +func ValidOwner(s string) bool { + return validName(s) +} + +// ValidRepoName reports whether s is a well-formed repository name (same +// character family as an owner, capped at maxRepoNameLen). +func ValidRepoName(s string) bool { + return len(s) <= maxRepoNameLen && validName(s) +} + +// refForbiddenByte reports whether c is a byte git-check-ref-format forbids +// anywhere in a ref: ASCII control characters, space, DEL, and the special +// set ~ ^ : ? * [ \. High bytes (>= 0x80) are allowed so UTF-8 refs pass. +func refForbiddenByte(c byte) bool { + if c <= 0x20 || c == 0x7f { + return true + } + switch c { + case '~', '^', ':', '?', '*', '[', '\\': + return true + default: + return false + } +} + +// isHexSHA reports whether s is a bare 40-char (SHA-1) or 64-char (SHA-256) +// hexadecimal object id. These are accepted as refs directly. +func isHexSHA(s string) bool { + if len(s) != 40 && len(s) != 64 { + return false + } + for i := 0; i < len(s); i++ { + c := s[i] + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { + return false + } + } + return true +} + +// ValidRef reports whether s is acceptable as a git ref or revision to hand to +// git. The rules follow git-check-ref-format(1) closely enough to keep hostile +// input (option injection, path traversal, revision-syntax tricks) away from +// the git command line, while still accepting ordinary multi-level branch and +// tag names such as "feature/foo" and bare object ids. +// +// git invocations additionally use "--end-of-options"/"--" as defense in +// depth; this function is the first line. +func ValidRef(s string) bool { + if s == "" { + return false + } + // Bare object ids are always fine and skip the component rules. + if isHexSHA(s) { + return true + } + // "@" alone is a git shorthand for HEAD and is not a valid ref name. + if s == "@" { + return false + } + if s[0] == '-' || s[0] == '.' || s[0] == '/' { + return false + } + if strings.HasSuffix(s, "/") || strings.HasSuffix(s, ".") { + return false + } + if strings.Contains(s, "..") || strings.Contains(s, "@{") || strings.Contains(s, "//") { + return false + } + for i := 0; i < len(s); i++ { + if refForbiddenByte(s[i]) { + return false + } + } + // Per-component rules: no component may start with '.' or end with + // ".lock". Empty components are already excluded by the '//', leading + // '/', and trailing '/' checks above. + for _, comp := range strings.Split(s, "/") { + if strings.HasPrefix(comp, ".") { + return false + } + if strings.HasSuffix(comp, ".lock") { + return false + } + } + return true +} diff --git a/core/names_test.go b/core/names_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d596f15966205425596f661ebf812ba0da2c3cc0 --- /dev/null +++ b/core/names_test.go @@ -0,0 +1,112 @@ +package core + +import ( + "strings" + "testing" +) + +func TestValidOwner(t *testing.T) { + tests := []struct { + in string + want bool + }{ + {"bigbes", true}, + {"user_name", true}, + {"user-name", true}, + {"user.name", true}, + {"u123", true}, + {"a", true}, + + {"", false}, + {"-leading", false}, + {"has/slash", false}, + {"has..dots", false}, + {"Upper", false}, + {"has space", false}, + {"tilde~", false}, + {"emoji😀", false}, + } + for _, tc := range tests { + if got := ValidOwner(tc.in); got != tc.want { + t.Errorf("ValidOwner(%q) = %v, want %v", tc.in, got, tc.want) + } + } +} + +func TestValidRepoName(t *testing.T) { + tests := []struct { + in string + want bool + }{ + {"core-go", true}, + {"my.repo", true}, + {"repo_1", true}, + {strings.Repeat("a", 100), true}, + + {"", false}, + {strings.Repeat("a", 101), false}, + {"-dashfirst", false}, + {"a/b", false}, + {"a..b", false}, + {"CamelCase", false}, + } + for _, tc := range tests { + if got := ValidRepoName(tc.in); got != tc.want { + t.Errorf("ValidRepoName(%q) = %v, want %v", tc.in, got, tc.want) + } + } +} + +func TestValidRef(t *testing.T) { + tests := []struct { + in string + want bool + }{ + // Accepted. + {"main", true}, + {"feature/foo", true}, + {"feature/with-slash", true}, + {"release/v1.0", true}, + {"v1.0.0", true}, + {"a/b/c/d", true}, + {"0123456789abcdef0123456789abcdef01234567", true}, // SHA-1 + {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", true}, // SHA-256 + {"DEADBEEFdeadbeef0000111122223333abcdabcd", true}, // mixed-case SHA-1 + {"foo.lockfile", true}, // ".lock" only rejected as a suffix + + // Rejected — structural. + {"", false}, + {"@", false}, + {"-oops", false}, + {".hidden", false}, + {"/leading", false}, + {"trailing/", false}, + {"trailing.", false}, + {"foo.lock", false}, + {"feature/bar.lock", false}, // per-component .lock + {"feature/.hidden", false}, // per-component leading dot + {"a..b", false}, + {"a//b", false}, + {"main@{u}", false}, + + // Rejected — forbidden bytes. + {"has space", false}, + {"tilde~1", false}, + {"caret^", false}, + {"colon:x", false}, + {"star*", false}, + {"quest?", false}, + {"brack[et", false}, + {"back\\slash", false}, + {"ctrl\x01char", false}, + {"del\x7fchar", false}, + + // Not a full-length SHA -> falls through to name rules (valid here). + {"abcdef", true}, + } + for _, tc := range tests { + if got := ValidRef(tc.in); got != tc.want { + t.Errorf("ValidRef(%q) = %v, want %v", tc.in, got, tc.want) + } + } +} diff --git a/core/spec.go b/core/spec.go new file mode 100644 index 0000000000000000000000000000000000000000..3da3a1c14d38050478e1e203cab39111979ada53 --- /dev/null +++ b/core/spec.go @@ -0,0 +1,72 @@ +package core + +import ( + "fmt" + "net/url" + "strings" +) + +// RepoRef identifies a repository by its owner (without the leading '~') and +// name. Both fields should be validated with ValidOwner / ValidRepoName before +// they are used to build a filesystem path. +type RepoRef struct { + Owner string + Name string +} + +// CompareSpec is a parsed "base..head" or "base...head" comparison request. +// ThreeDot selects merge-base (symmetric-difference) semantics, matching git's +// "base...head"; when false the comparison is the plain "base..head" range. +type CompareSpec struct { + Base string + Head string + ThreeDot bool +} + +// ParseCompareSpec parses the compare wildcard from a URL path segment into a +// CompareSpec. The grammar is: +// +// base "..." head -> ThreeDot = true (merge-base / symmetric diff) +// base ".." head -> ThreeDot = false (direct range) +// +// The three-dot form is tried first, because "..." contains "..". Each side is +// then url.PathUnescape'd and validated with ValidRef. Both sides must be +// non-empty and valid or ErrBadRef (wrapped with detail) is returned. +// +// Stripping a trailing ".patch" (the raw-diff escape hatch) is the caller's +// responsibility and is intentionally not handled here. +func ParseCompareSpec(raw string) (CompareSpec, error) { + var base, head string + var threeDot bool + + switch { + case strings.Contains(raw, "..."): + i := strings.Index(raw, "...") + threeDot = true + base, head = raw[:i], raw[i+3:] + case strings.Contains(raw, ".."): + i := strings.Index(raw, "..") + threeDot = false + base, head = raw[:i], raw[i+2:] + default: + return CompareSpec{}, fmt.Errorf("%w: missing '..' or '...' separator in %q", ErrBadRef, raw) + } + + b, err := url.PathUnescape(base) + if err != nil { + return CompareSpec{}, fmt.Errorf("%w: bad percent-encoding in base: %v", ErrBadRef, err) + } + h, err := url.PathUnescape(head) + if err != nil { + return CompareSpec{}, fmt.Errorf("%w: bad percent-encoding in head: %v", ErrBadRef, err) + } + + if !ValidRef(b) { + return CompareSpec{}, fmt.Errorf("%w: invalid base ref %q", ErrBadRef, b) + } + if !ValidRef(h) { + return CompareSpec{}, fmt.Errorf("%w: invalid head ref %q", ErrBadRef, h) + } + + return CompareSpec{Base: b, Head: h, ThreeDot: threeDot}, nil +} diff --git a/core/spec_test.go b/core/spec_test.go new file mode 100644 index 0000000000000000000000000000000000000000..eaa9103848944afed12f6a5482fae523403fe7ad --- /dev/null +++ b/core/spec_test.go @@ -0,0 +1,78 @@ +package core + +import ( + "errors" + "testing" +) + +func TestParseCompareSpec(t *testing.T) { + tests := []struct { + name string + raw string + wantBase string + wantHead string + want3Dot bool + wantErr bool + }{ + // Happy paths. + {name: "three-dot", raw: "main...feature", wantBase: "main", wantHead: "feature", want3Dot: true}, + {name: "two-dot", raw: "main..feature", wantBase: "main", wantHead: "feature", want3Dot: false}, + {name: "slash branch head", raw: "main...feature/foo", wantBase: "main", wantHead: "feature/foo", want3Dot: true}, + {name: "slash branch both", raw: "feature/with-slash...release/v1", wantBase: "feature/with-slash", wantHead: "release/v1", want3Dot: true}, + {name: "two-dot slash", raw: "feature/a..feature/b", wantBase: "feature/a", wantHead: "feature/b", want3Dot: false}, + {name: "sha40 vs branch", raw: "0123456789abcdef0123456789abcdef01234567...main", + wantBase: "0123456789abcdef0123456789abcdef01234567", wantHead: "main", want3Dot: true}, + {name: "sha256 head", raw: "main.." + + "0000000000000000000000000000000000000000000000000000000000000000", + wantBase: "main", wantHead: "0000000000000000000000000000000000000000000000000000000000000000"}, + {name: "percent-escaped slash", raw: "feature%2Ffoo...main", wantBase: "feature/foo", wantHead: "main", want3Dot: true}, + {name: "tag with dot", raw: "v1.0.0...v1.1.0", wantBase: "v1.0.0", wantHead: "v1.1.0", want3Dot: true}, + + // Separator handling. + {name: "no separator", raw: "mainfeature", wantErr: true}, + {name: "empty", raw: "", wantErr: true}, + {name: "empty base", raw: "...main", wantErr: true}, + {name: "empty head", raw: "main...", wantErr: true}, + {name: "both empty two-dot", raw: "..", wantErr: true}, + {name: "four dots splits on three then leading dot", raw: "a....b", wantErr: true}, + + // Hostile input — must be rejected via ValidRef. + {name: "option injection head", raw: "main...--upload-pack=x", wantErr: true}, + {name: "option injection base", raw: "--output=/etc/passwd...main", wantErr: true}, + {name: "revision at-brace", raw: "main...@{u}", wantErr: true}, + {name: "trailing .lock", raw: "main...foo.lock", wantErr: true}, + {name: "leading dot head", raw: "main....hidden", wantErr: true}, + {name: "caret ref", raw: "main...HEAD^", wantErr: true}, + {name: "tilde ref", raw: "main...HEAD~3", wantErr: true}, + {name: "colon ref", raw: "main...refs:x", wantErr: true}, + {name: "space in ref", raw: "main...my branch", wantErr: true}, + {name: "control char", raw: "main...he\x00ad", wantErr: true}, + {name: "escaped dotdot becomes traversal", raw: "main...%2E%2E", wantErr: true}, + {name: "escaped traversal path", raw: "main...%2e%2e%2Fetc", wantErr: true}, + {name: "bad percent-encoding", raw: "main...%zz", wantErr: true}, + {name: "double slash", raw: "main...a//b", wantErr: true}, + {name: "at shorthand", raw: "main...@", wantErr: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ParseCompareSpec(tc.raw) + if tc.wantErr { + if err == nil { + t.Fatalf("ParseCompareSpec(%q) = %+v, want error", tc.raw, got) + } + if !errors.Is(err, ErrBadRef) { + t.Fatalf("ParseCompareSpec(%q) error = %v, want wrapping ErrBadRef", tc.raw, err) + } + return + } + if err != nil { + t.Fatalf("ParseCompareSpec(%q) unexpected error: %v", tc.raw, err) + } + if got.Base != tc.wantBase || got.Head != tc.wantHead || got.ThreeDot != tc.want3Dot { + t.Fatalf("ParseCompareSpec(%q) = {Base:%q Head:%q ThreeDot:%v}, want {Base:%q Head:%q ThreeDot:%v}", + tc.raw, got.Base, got.Head, got.ThreeDot, tc.wantBase, tc.wantHead, tc.want3Dot) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..35572861da11d8496070a43b8f92bce1e4f35e9f --- /dev/null +++ b/go.mod @@ -0,0 +1,51 @@ +module go.bigb.es/sourcehut-compare + +go 1.26.4 + +require ( + git.sr.ht/~sircmpwn/core-go v0.0.0-20260718172441-c2c2f3848fa9 + github.com/go-chi/chi/v5 v5.3.1 + github.com/sirupsen/logrus v1.9.4 +) + +require ( + git.sr.ht/~sircmpwn/dowork v0.0.0-20241216125407-2b00aa42322c // indirect + git.sr.ht/~sircmpwn/getopt v1.0.0 // indirect + git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 // indirect + github.com/99designs/gqlgen v0.17.36 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudflare/circl v1.6.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/emersion/go-message v0.18.2 // indirect + github.com/emersion/go-pgpmail v0.2.2 // indirect + github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect + github.com/emersion/go-smtp v0.21.3 // indirect + github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee // indirect + github.com/go-chi/cors v1.2.2 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/kavu/go_reuseport v1.5.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec // indirect + github.com/vektah/gqlparser/v2 v2.5.8 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) + +replace git.sr.ht/~sircmpwn/core-go => git.srht.bigb.es/~bigbes/core-go v0.0.0-20260718172441-c2c2f3848fa9 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..79a9f6ccd369c75fe364f4723f17b9e2b7e7781d --- /dev/null +++ b/go.sum @@ -0,0 +1,233 @@ +git.sr.ht/~sircmpwn/dowork v0.0.0-20241216125407-2b00aa42322c h1:v2opuaN0C5ZpuCifRNR9ZQ8V9IG+Ja80otK1MFj5RnI= +git.sr.ht/~sircmpwn/dowork v0.0.0-20241216125407-2b00aa42322c/go.mod h1:8neHEO3503w/rNtttnR0JFpQgM/GFhaafVwvkPsFIDw= +git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= +git.sr.ht/~sircmpwn/getopt v1.0.0 h1:/pRHjO6/OCbBF4puqD98n6xtPEgE//oq5U8NXjP7ROc= +git.sr.ht/~sircmpwn/getopt v1.0.0/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 h1:Ahny8Ud1LjVMMAlt8utUFKhhxJtwBAualvsbc/Sk7cE= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA= +git.srht.bigb.es/~bigbes/core-go v0.0.0-20260718172441-c2c2f3848fa9 h1:S/CncMS83NIBz+V5OlZEHlnrl1Yd5I4C7hDQ7lzQsTM= +git.srht.bigb.es/~bigbes/core-go v0.0.0-20260718172441-c2c2f3848fa9/go.mod h1:JmathMemB+hBk1IgrMn6RuBGusd+fTg1s/X6rNVKXXU= +github.com/99designs/gqlgen v0.17.36 h1:u/o/rv2SZ9s5280dyUOOrkpIIkr/7kITMXYD3rkJ9go= +github.com/99designs/gqlgen v0.17.36/go.mod h1:6RdyY8puhCoWAQVr2qzF2OMVfudQzc8ACxzpzluoQm4= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw= +github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= +github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= +github.com/emersion/go-pgpmail v0.2.2 h1:cO2jwsE0gb8aDdCcVH5Dfe1XV3Rhhw2GVWsmQd3CbaI= +github.com/emersion/go-pgpmail v0.2.2/go.mod h1:mRB5P7QKiAuOvcT36tdRZvm7nSt7V+f6jbzzup3HuvU= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= +github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY= +github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee h1:v6Eju/FhxsACGNipFEPBZZAzGr1F/jlRQr1qiBw2nEE= +github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee/go.mod h1:2H9hjfbpSMHwY503FclkV/lZTBh2YlOmLLSda12uL8c= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kavu/go_reuseport v1.5.0 h1:UNuiY2OblcqAtVDE8Gsg1kZz8zbBWg907sP1ceBV+bk= +github.com/kavu/go_reuseport v1.5.0/go.mod h1:CG8Ee7ceMFSMnx/xr25Vm0qXaj2Z4i5PWoUx+JZ5/CU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks= +github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw= +github.com/vektah/gqlparser/v2 v2.5.8 h1:pm6WOnGdzFOCfcQo9L3+xzW51mKrlwTEg4Wr7AH1JW4= +github.com/vektah/gqlparser/v2 v2.5.8/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=