A => .gitignore +11 -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
A => Makefile +63 -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
A => README.md +71 -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`.
A => config.example.ini +64 -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.<hash>.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 ("<site-name> 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 (<api-origin>/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
A => contrib/compare-srht.service +22 -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
A => contrib/compare.sr.ht.conf +27 -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;
+ }
+}
A => core/errors.go +30 -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")
+)
A => core/names.go +135 -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
+}
A => core/names_test.go +112 -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)
+ }
+ }
+}
A => core/spec.go +72 -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
+}
A => core/spec_test.go +78 -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)
+ }
+ })
+ }
+}
A => go.mod +51 -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
A => go.sum +233 -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=