├── .dockerignore ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release_build.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── smol-kitten.jpg ├── web_1.png └── web_2.png ├── cmd └── app.go ├── go.mod ├── go.sum ├── pkg ├── config │ ├── config.TEMPLATE.yaml │ └── config.go ├── git │ └── git.go ├── model │ └── model.go ├── route │ ├── route.go │ └── templates │ │ ├── css │ │ ├── pico.min.css │ │ └── style.css │ │ └── html │ │ ├── header.html │ │ ├── layout.html │ │ └── pages │ │ ├── 404.html │ │ ├── 500.html │ │ ├── index.html │ │ └── repo │ │ ├── files.html │ │ ├── log.html │ │ └── refs.html └── ssh │ └── ssh.go ├── smolgit.go └── test └── test.bats /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .github 3 | assets/ 4 | bin/ 5 | test/ 6 | tmp/ 7 | LICENSE 8 | README.md 9 | Makefile -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - [ ] Version (`smolgit --version`) 16 | - [ ] Config (content of `config.yaml` if exist) 17 | - [ ] Is `git` installed? 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | ## Proposed Changes 4 | 5 | - 6 | - 7 | - -------------------------------------------------------------------------------- /.github/workflows/release_build.yml: -------------------------------------------------------------------------------- 1 | name: Release Go project 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" # triggers only if push new tag version, like `0.8.4` or else 7 | 8 | jobs: 9 | build: 10 | name: GoReleaser build 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 # See: https://goreleaser.com/ci/actions/ 18 | 19 | - name: Set up Go 1.22.4 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.22 23 | id: go 24 | 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v6 27 | if: startsWith(github.ref, 'refs/tags/') 28 | with: 29 | version: '~> v2' 30 | args: release --clean 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | build: 6 | name: Build Lint Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Setup BATS 11 | uses: mig4/setup-bats@v1 12 | with: 13 | bats-version: 1.2.1 14 | 15 | - name: Check out code 16 | uses: actions/checkout@v1 17 | 18 | - name: Install deps 19 | run: sudo snap install yq 20 | 21 | - name: Build 22 | run: make build 23 | 24 | - name: Lint 25 | run: make lint 26 | 27 | - name: Config 28 | run: make config 29 | 30 | - name: Test 31 | run: make integration-test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | tmp/ 3 | .DS_Store 4 | config.yaml -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | allow-parallel-runners: true 4 | relative-path-mode: wd 5 | linters: 6 | enable: 7 | - asciicheck 8 | - bodyclose 9 | - dogsled 10 | - dupl 11 | - funlen 12 | - gochecknoinits 13 | - gocognit 14 | - goconst 15 | - gocritic 16 | - gocyclo 17 | - gosec 18 | - lll 19 | - misspell 20 | - nakedret 21 | - noctx 22 | - prealloc 23 | - revive 24 | - rowserrcheck 25 | - staticcheck 26 | - unconvert 27 | - unparam 28 | - whitespace 29 | settings: 30 | funlen: 31 | lines: 168 32 | statements: 50 33 | gocritic: 34 | disabled-checks: 35 | - singleCaseSwitch 36 | lll: 37 | line-length: 193 38 | tab-width: 1 39 | staticcheck: 40 | checks: 41 | - -S1023 42 | - all 43 | exclusions: 44 | generated: lax 45 | presets: 46 | - comments 47 | - common-false-positives 48 | - legacy 49 | - std-error-handling 50 | rules: 51 | - linters: 52 | - bodyclose 53 | - dupl 54 | - funlen 55 | - gochecknoinits 56 | - gocognit 57 | - goconst 58 | - gocyclo 59 | - gosec 60 | - lll 61 | - maligned 62 | - noctx 63 | - revive 64 | - scopelint 65 | - staticcheck 66 | path: _test.go 67 | - linters: 68 | - bodyclose 69 | - dupl 70 | - funlen 71 | - gochecknoinits 72 | - gocognit 73 | - goconst 74 | - gocyclo 75 | - lll 76 | - maligned 77 | - noctx 78 | - revive 79 | - scopelint 80 | - staticcheck 81 | path: _mock.go 82 | paths: 83 | - third_party$ 84 | - builtin$ 85 | - examples$ 86 | output: 87 | formats: 88 | tab: 89 | path: stdout 90 | colors: true 91 | formatters: 92 | enable: 93 | - gofumpt 94 | settings: 95 | gofumpt: 96 | module-path: smolgit 97 | exclusions: 98 | generated: lax 99 | paths: 100 | - third_party$ 101 | - builtin$ 102 | - examples$ 103 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | project_name: smolgit 4 | 5 | # setups builds for linux and darwin on amd64 and arm64 6 | # https://goreleaser.com/customization/build 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | goarch: 14 | - arm 15 | - arm64 16 | - amd64 17 | goarm: 18 | - 6 19 | - 7 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine3.20 AS builder 2 | 3 | RUN mkdir /app && mkdir -p /usr/local/src/smolgit 4 | WORKDIR /usr/local/src/smolgit 5 | 6 | ADD ./go.mod ./go.sum ./ 7 | RUN go mod download 8 | ADD . ./ 9 | 10 | RUN go build -v -o /build/smolgit 11 | 12 | FROM alpine/git:2.45.2 AS runner 13 | 14 | COPY --from=builder /build/smolgit /usr/bin/smolgit 15 | 16 | EXPOSE 3080 17 | EXPOSE 3081 18 | ENTRYPOINT ["/usr/bin/smolgit", "--config", "/etc/smolgit/config.yaml"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Roman Kiselenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice (including the next 13 | paragraph) shall be included in all copies or substantial portions of the 14 | Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 19 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 21 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOCMD=go 2 | GOTEST=$(GOCMD) test 3 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 4 | HASH := $(shell git rev-parse --short HEAD) 5 | GREEN := $(shell tput -Txterm setaf 2) 6 | YELLOW := $(shell tput -Txterm setaf 3) 7 | WHITE := $(shell tput -Txterm setaf 7) 8 | CYAN := $(shell tput -Txterm setaf 6) 9 | RESET := $(shell tput -Txterm sgr0) 10 | 11 | PROJECT_NAME := smolgit 12 | LINTER_BIN ?= golangci-lint 13 | 14 | .PHONY: all test build clean run lint /bin/$(LINTER_BIN) 15 | 16 | all: help 17 | 18 | ## Build: 19 | build: ## Build all the binaries and put the output in bin/ 20 | $(GOCMD) build -ldflags "-X main.version=$(BRANCH)-$(HASH)" -o bin/$(PROJECT_NAME) . 21 | 22 | build-docker: ## Build an image 23 | docker build -t $(PROJECT_NAME) . 24 | 25 | bin/$(LINTER_BIN): 26 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin v2.1.1 27 | 28 | ## Clean: 29 | clean: ## Remove build related file 30 | @-rm -fr ./bin 31 | 32 | ## Lint: 33 | lint: ./bin/$(LINTER_BIN) ## Lint sources with golangci-lint 34 | ./bin/$(LINTER_BIN) run 35 | 36 | ## Run: 37 | run: clean build ## Run the smolgit `make run` 38 | ./bin/$(PROJECT_NAME) $(ARGS) 39 | 40 | run-docker: ## Run smolgit in the container 41 | docker run -it -p 3080:3080 -p 3081:3081 -v $(PWD)/:/etc/smolgit $(PROJECT_NAME) 42 | 43 | config-docker: ## Generate smolgit config 44 | docker run -it $(PROJECT_NAME) config > config.yaml 45 | 46 | config: ## Generate default config 47 | ./bin/$(PROJECT_NAME) config > ./config.yaml 48 | 49 | ## Test: 50 | test: ## Run the tests of the smolgit 51 | $(GOTEST) -v -race ./... 52 | 53 | integration-test: build ## Run the bats tests of the smolgit 54 | @bats ./test/test.bats 55 | 56 | ## Help: 57 | help: ## Show this help. 58 | @echo '' 59 | @echo 'Usage:' 60 | @echo ' ${YELLOW}make${RESET} ${GREEN}${RESET}' 61 | @echo '' 62 | @echo 'Targets:' 63 | @awk 'BEGIN {FS = ":.*?## "} { \ 64 | if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \ 65 | else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \ 66 | }' $(MAKEFILE_LIST) 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | a smol cat by Ron whisky 3 | 4 | **smolgit** offers a minimalist [git](https://git-scm.com/) server, making it perfect for small teams or individual developers. Its minimal simple and just works. It's perfect for those who value simplicity and efficiency in their workflow. Small memory footprint, one binary to go. 5 | 6 | 7 | - [Features](#features) 8 | - [Preview](#preview) 9 | - [Getting Started](#getting-started) 10 | - [Install](#install) 11 | - [Run](#run) 12 | - [Config](#config) 13 | - [Docker](#docker) 14 | - [Prerequisites](#prerequisites) 15 | - [Built with](#built-with) 16 | - [Contribution](#contribution) 17 | 18 | 19 | ### Features 20 | 21 | 1. **git operations** - easily perform `pull`, `push`, `clone` and `fetch` operations. 22 | 1. **repository visualization** - browse files, view logs, explore the commit, branch and tag lists. 23 | 1. **user management** - simple user management, add users with `ssh-keys` to `config.yaml`. 24 | 1. **permissions** - assign persmissions to user. 25 | 1. **ligh-dark** - web theme based on your system settings. 26 | 1. **basic-auth** - web basic auth middleware. 27 | 28 | ### Preview 29 | 30 |

31 | screenshot 32 |

33 |

34 | screenshot 35 |

36 | 37 | 38 | ### Getting Started 39 | 40 | #### Install 41 | 42 | 1. Download binary from [ release page ](https://github.com/roman-kiselenko/smolgit/releases). 43 | 1. Generate default `config.yaml` file with command `./smolgit config > config.yaml`. 44 | - Use [`yq`](https://github.com/mikefarah/yq) for inline changes `./smolgit config | yq '.server.disabled = true' > config.yaml` 45 | 1. Run `./smolgit` 46 | 47 | ```shell 48 | $> ./smolgit 49 | 10:08AM INF set loglevel level=DEBUG 50 | 10:08AM INF version version=main-a4f6438 51 | 10:08AM INF initialize web server addr=:3080 52 | 10:08AM INF initialize ssh server addr=:3081 53 | 10:08AM INF start server brand=smolgit address=:3080 54 | 10:08AM INF starting SSH server addr=:3081 55 | ``` 56 | 57 | #### Config 58 | 59 | Generate default `config.yaml` file with command `./bin/smolgit config > config.yaml`. 60 | 61 | ```yaml 62 | log: 63 | # Color log output 64 | color: true 65 | # Log as json 66 | json: false 67 | # Log level (INFO, DEBUG, TRACE, WARN) 68 | level: DEBUG 69 | server: 70 | # Disable web server 71 | disabled: false 72 | # Enable basic http auth 73 | auth: 74 | enabled: false 75 | # Credentials for basic auth 76 | accounts: 77 | - login: user2 78 | password: bar 79 | - login: user1 80 | password: foo 81 | # Web server address 82 | addr: ":3080" 83 | # Navbar brand string 84 | brand: "smolgit" 85 | ssh: 86 | # SSH server address 87 | addr: ":3081" 88 | git: 89 | # Folder to save git repositories 90 | path: /tmp/smolgit 91 | # Base for clone string formating 92 | # (e.g. ssh://git@my-git-server.lan/myuser/project.git) 93 | base: "git@my-git-server.lan" 94 | users: 95 | # User name used for folder in git.path 96 | - name: "bob" 97 | # Permissions, wildcard or regex 98 | # User to check access for other repositories 99 | # '*' - access for all repositories 100 | # 'admin' - access for admin's repositories 101 | # '(admin|billy)' - access for admin's and billy's repositories 102 | permissions: "*" 103 | keys: 104 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq9rD9b8tYyuSLsTECHCn... developer@mail.com 105 | ``` 106 | 107 | cli options: 108 | 109 | ```shell 110 | $> ./smolgit --help 111 | Usage of ./smolgit: 112 | -config string 113 | path to config (default "./config.yaml") 114 | ``` 115 | 116 | #### Docker 117 | 118 | In order to run `smolgit` in docker there is the [`Dockerfile`](/Dockerfile). 119 | 120 | 1. Build image `make build-docker` 121 | 1. Generate `config.yaml` file `make config-docker`, it'll create `config.yaml` in the current directory and mount it for docker. 122 | 1. Run `smolgit` in docker: 123 | 124 | ```shell 125 | $> make run-docker 126 | docker run -it -p 3080:3080 -p 3081:3081 -v /path-to-smolgit-project/smolgit/:/etc/smolgit smolgit 127 | 3:53PM INF set loglevel level=DEBUG 128 | 3:53PM INF version version=dev 129 | 3:53PM INF initialize web server addr=:3080 130 | 3:53PM INF initialize ssh server addr=:3081 131 | 3:53PM INF start server brand=smolgit address=:3080 132 | 3:53PM INF starting SSH server addr=:3081 133 | ``` 134 | 135 | ### Prerequisites 136 | 137 | - git 138 | 139 | ### Built with 140 | 141 | :heart: 142 | 143 | - [golang](https://go.dev/) 144 | - [gin](https://github.com/gin-gonic/gin) 145 | - [go-git](https://github.com/go-git/go-git) 146 | - [pico](https://picocss.com/docs) 147 | - [gossh](https://github.com/gliderlabs/ssh) 148 | 149 | ### Local development 150 | 151 | - [golang](https://go.dev/) 152 | - [yq](https://mikefarah.gitbook.io/yq) 153 | - [bats](https://bats-core.readthedocs.io/en/stable/) 154 | 155 | ### Contribution 156 | 157 | Contributions are more than welcome! Thank you! 158 | -------------------------------------------------------------------------------- /assets/smol-kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roman-kiselenko/smolgit/a17cc2b8a302bf6c98fe6a4816b245ec01eaf430/assets/smol-kitten.jpg -------------------------------------------------------------------------------- /assets/web_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roman-kiselenko/smolgit/a17cc2b8a302bf6c98fe6a4816b245ec01eaf430/assets/web_1.png -------------------------------------------------------------------------------- /assets/web_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roman-kiselenko/smolgit/a17cc2b8a302bf6c98fe6a4816b245ec01eaf430/assets/web_2.png -------------------------------------------------------------------------------- /cmd/app.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "time" 8 | 9 | "smolgit/pkg/config" 10 | "smolgit/pkg/route" 11 | "smolgit/pkg/ssh" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/knadh/koanf/parsers/yaml" 15 | "github.com/knadh/koanf/providers/file" 16 | "github.com/knadh/koanf/v2" 17 | "github.com/lmittmann/tint" 18 | ) 19 | 20 | var ( 21 | k = koanf.New(".") 22 | logOutput = os.Stdout 23 | logger *slog.Logger 24 | cfg config.Config 25 | ) 26 | 27 | type App struct { 28 | Config *config.Config 29 | signchnl chan (os.Signal) 30 | exitSig chan (os.Signal) 31 | } 32 | 33 | func New(version string, configPath *string, exitchnl, signchnl chan (os.Signal)) (*App, error) { 34 | app := &App{exitSig: exitchnl, signchnl: signchnl} 35 | f := file.Provider(*configPath) 36 | cfg.Version = version 37 | k = koanf.New(".") 38 | if err := k.Load(f, yaml.Parser()); err != nil { 39 | return app, err 40 | } 41 | if err := k.UnmarshalWithConf("", &cfg, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true}); err != nil { 42 | return app, err 43 | } 44 | app.Config = &cfg 45 | logger = initLogger() 46 | return app, nil 47 | } 48 | 49 | func (a *App) Run() error { 50 | logger.Info("version", "version", a.Config.Version) 51 | 52 | if !a.Config.ServerDisabled { 53 | if err := a.initWebServer(); err != nil { 54 | return fmt.Errorf("cant run web server %w", err) 55 | } 56 | } 57 | 58 | logger.Info("initialize ssh server", "addr", a.Config.SSHAddr) 59 | sshServer, err := ssh.New(a.Config) 60 | if err != nil { 61 | return fmt.Errorf("cant run ssh server %w", err) 62 | } 63 | 64 | go func() { 65 | logger.Info("starting SSH server", "addr", a.Config.SSHAddr) 66 | if err := sshServer.ListenAndServe(); err != nil { 67 | logger.Error("cant run ssh server", "error", err) 68 | } 69 | }() 70 | 71 | go func() { 72 | code := <-a.signchnl 73 | logger.Info("os signal received", "signal", code) 74 | if err := sshServer.Close(); err != nil { 75 | logger.Error("cant stop ssh server", "error", err) 76 | } 77 | a.exitSig <- code 78 | }() 79 | 80 | return nil 81 | } 82 | 83 | func initLogger() *slog.Logger { 84 | level := new(slog.LevelVar) 85 | handler := &slog.HandlerOptions{ 86 | Level: level, 87 | } 88 | if cfg.LogJSON { 89 | logger = slog.New(slog.NewJSONHandler(logOutput, handler)) 90 | } else { 91 | if cfg.LogColor { 92 | logger = slog.New(tint.NewHandler(logOutput, &tint.Options{ 93 | Level: level, 94 | TimeFormat: time.Kitchen, 95 | })) 96 | } else { 97 | logger = slog.New(slog.NewTextHandler(logOutput, handler)) 98 | } 99 | } 100 | 101 | slog.SetDefault(logger) 102 | if err := level.UnmarshalText([]byte(cfg.LogLevel)); err != nil { 103 | level.Set(slog.LevelDebug) 104 | } 105 | logger.Info("set loglevel", "level", level) 106 | 107 | return logger 108 | } 109 | 110 | func (a *App) initWebServer() error { 111 | logger.Info("initialize web server", "addr", a.Config.ServerAddr) 112 | gin.SetMode(gin.ReleaseMode) 113 | router := gin.New() 114 | router.Use(gin.Recovery()) 115 | if cfg.ServerAuthEnabled { 116 | logger.Info("web auth", "enabled", a.Config.ServerAuthEnabled) 117 | accounts := gin.Accounts{} 118 | for _, a := range cfg.ServerAuthAccounts { 119 | accounts[a["login"]] = a["password"] 120 | } 121 | router.Use(gin.BasicAuth(accounts)) 122 | } 123 | r, err := route.New(router, a.Config) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | router.GET("/", r.Index) 129 | router.GET("/css/pico.min.css", r.ExternalStyle) 130 | router.GET("/css/style.css", r.Style) 131 | router.GET("/repo/log/:user/:path", r.Log) 132 | router.GET("/repo/files/:user/:path", r.Files) 133 | router.GET("/repo/refs/:user/:path", r.Refs) 134 | 135 | addr := a.Config.ServerAddr 136 | go func() { 137 | logger.Info("start server", "brand", a.Config.ServerBrand, "address", addr) 138 | if err := router.Run(addr); err != nil { 139 | logger.Error("cant run ssh server", "error", err) 140 | } 141 | }() 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module smolgit 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/gin-gonic/gin v1.10.0 9 | github.com/gliderlabs/ssh v0.3.7 10 | github.com/go-git/go-billy/v5 v5.5.0 11 | github.com/go-git/go-git/v5 v5.12.0 12 | github.com/itchyny/timefmt-go v0.1.6 13 | github.com/knadh/koanf/parsers/yaml v0.1.0 14 | github.com/knadh/koanf/providers/file v1.0.0 15 | github.com/knadh/koanf/v2 v2.1.1 16 | github.com/lmittmann/tint v1.0.4 17 | golang.org/x/crypto v0.37.0 18 | ) 19 | 20 | require ( 21 | dario.cat/mergo v1.0.0 // indirect 22 | github.com/Microsoft/go-winio v0.6.1 // indirect 23 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 24 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 25 | github.com/bytedance/sonic v1.11.6 // indirect 26 | github.com/bytedance/sonic/loader v0.1.1 // indirect 27 | github.com/cloudflare/circl v1.3.7 // indirect 28 | github.com/cloudwego/base64x v0.1.4 // indirect 29 | github.com/cloudwego/iasm v0.2.0 // indirect 30 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 31 | github.com/emirpasic/gods v1.18.1 // indirect 32 | github.com/fsnotify/fsnotify v1.7.0 // indirect 33 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 34 | github.com/gin-contrib/sse v0.1.0 // indirect 35 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 36 | github.com/go-playground/locales v0.14.1 // indirect 37 | github.com/go-playground/universal-translator v0.18.1 // indirect 38 | github.com/go-playground/validator/v10 v10.20.0 // indirect 39 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 40 | github.com/goccy/go-json v0.10.2 // indirect 41 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 42 | github.com/google/go-cmp v0.7.0 // indirect 43 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 44 | github.com/json-iterator/go v1.1.12 // indirect 45 | github.com/kevinburke/ssh_config v1.2.0 // indirect 46 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 47 | github.com/knadh/koanf/maps v0.1.1 // indirect 48 | github.com/leodido/go-urn v1.4.0 // indirect 49 | github.com/mattn/go-isatty v0.0.20 // indirect 50 | github.com/mitchellh/copystructure v1.2.0 // indirect 51 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 53 | github.com/modern-go/reflect2 v1.0.2 // indirect 54 | github.com/onsi/gomega v1.36.3 // indirect 55 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 56 | github.com/pjbgf/sha1cd v0.3.0 // indirect 57 | github.com/rogpeppe/go-internal v1.14.1 // indirect 58 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 59 | github.com/skeema/knownhosts v1.2.2 // indirect 60 | github.com/stretchr/testify v1.10.0 // indirect 61 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 62 | github.com/ugorji/go/codec v1.2.12 // indirect 63 | github.com/xanzy/ssh-agent v0.3.3 // indirect 64 | golang.org/x/arch v0.8.0 // indirect 65 | golang.org/x/mod v0.24.0 // indirect 66 | golang.org/x/net v0.39.0 // indirect 67 | golang.org/x/sync v0.13.0 // indirect 68 | golang.org/x/sys v0.32.0 // indirect 69 | golang.org/x/text v0.24.0 // indirect 70 | golang.org/x/tools v0.32.0 // indirect 71 | google.golang.org/protobuf v1.36.6 // indirect 72 | gopkg.in/warnings.v0 v0.1.2 // indirect 73 | gopkg.in/yaml.v3 v3.0.1 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 4 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 5 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 6 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 7 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 8 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 12 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 13 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 14 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 15 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 16 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 17 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 18 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 19 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 20 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 21 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 22 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 23 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 24 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 25 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 26 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 28 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 30 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 31 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 32 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 33 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 34 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 35 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 36 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 37 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 38 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 39 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 40 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 41 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 42 | github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 43 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 44 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 45 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 46 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 47 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 48 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 49 | github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= 50 | github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= 51 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 52 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 53 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 54 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 55 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 56 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 57 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 58 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 59 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 60 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 61 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 62 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 63 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 64 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 65 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 66 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 67 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 68 | github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= 69 | github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= 70 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 71 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 72 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 73 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 74 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 75 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 76 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 77 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 78 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 79 | github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= 80 | github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 81 | github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= 82 | github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= 83 | github.com/knadh/koanf/providers/file v1.0.0 h1:DtPvSQBeF+N0QLPMz0yf2bx0nFSxUcncpqQvzCxfCyk= 84 | github.com/knadh/koanf/providers/file v1.0.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= 85 | github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= 86 | github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= 87 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 88 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 89 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 90 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 91 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 92 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 93 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 94 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 95 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 96 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 97 | github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= 98 | github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 99 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 100 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 101 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 102 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 103 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 104 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 105 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 106 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 107 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 108 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 109 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 110 | github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= 111 | github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= 112 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 113 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 114 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 115 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 116 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 117 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 118 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 119 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 120 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 121 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 122 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 123 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 124 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 125 | github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= 126 | github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 127 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 128 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 129 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 130 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 131 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 132 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 133 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 134 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 135 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 136 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 137 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 138 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 139 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 140 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 141 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 142 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 143 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 144 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 145 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 146 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 147 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 148 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 149 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 150 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 151 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 152 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 153 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 154 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 155 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 156 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 157 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 158 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 159 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 160 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 161 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 162 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 163 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 164 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 165 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 166 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 167 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 168 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 169 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 173 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 174 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 180 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 181 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 182 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 183 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 184 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 185 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 186 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 188 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 189 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 190 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 191 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 192 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 193 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 194 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 195 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 196 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 197 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 198 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 199 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 200 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 201 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 202 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 203 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 204 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 205 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 206 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 207 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 208 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 209 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 210 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 211 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 212 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 213 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 214 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 215 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 216 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 217 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 218 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 219 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 220 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 221 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 222 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 223 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 224 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 225 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 226 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 227 | -------------------------------------------------------------------------------- /pkg/config/config.TEMPLATE.yaml: -------------------------------------------------------------------------------- 1 | log: 2 | # Color log output 3 | color: true 4 | # Log as json 5 | json: false 6 | # Log level (INFO, DEBUG, TRACE, WARN) 7 | level: DEBUG 8 | server: 9 | # Disable web server 10 | disabled: false 11 | # Enable basic http auth 12 | auth: 13 | enabled: false 14 | # Credentials for basic auth 15 | accounts: 16 | - login: user2 17 | password: bar 18 | - login: user1 19 | password: foo 20 | # Web server address 21 | addr: ":3080" 22 | # Navbar brand string 23 | brand: "smolgit" 24 | ssh: 25 | # SSH server address 26 | addr: ":3081" 27 | git: 28 | # Folder to save git repositories 29 | path: /tmp/smolgit 30 | # Base for clone string formating 31 | # (e.g. ssh://git@my-git-server.lan/myuser/project.git) 32 | base: "git@my-git-server.lan" 33 | users: 34 | # User name used for folder in git.path 35 | - name: "bob" 36 | # Permissions, wildcard or regex 37 | # User to check access for other repositories 38 | # '*' - access for all repositories 39 | # 'admin' - access for admin's repositories 40 | # '(admin|billy)' - access for admin's and billy's repositories 41 | permissions: "*" 42 | keys: 43 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCq9rD9b8tYyuSLsTECHCn... developer@mail.com -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "errors" 7 | "html/template" 8 | "strings" 9 | 10 | "smolgit/pkg/model" 11 | ) 12 | 13 | type Config struct { 14 | LogColor bool `koanf:"log.color"` 15 | LogJSON bool `koanf:"log.json"` 16 | LogLevel string `koanf:"log.level"` 17 | ServerDisabled bool `koanf:"server.disabled"` 18 | ServerAddr string `koanf:"server.addr"` 19 | ServerAuthEnabled bool `koanf:"server.auth.enabled"` 20 | ServerAuthAccounts []map[string]string `koanf:"server.auth.accounts"` 21 | ServerBrand string `koanf:"server.brand"` 22 | SSHAddr string `koanf:"ssh.addr"` 23 | GitPath string `koanf:"git.path"` 24 | GitBase string `koanf:"git.base"` 25 | GitUsers []map[string]interface{} `koanf:"git.users"` 26 | Version string 27 | } 28 | 29 | func (c *Config) Validate() error { 30 | // TODO 31 | return nil 32 | } 33 | 34 | func (c *Config) FindUserByKey(key string) (model.User, error) { 35 | for _, u := range c.GitUsers { 36 | for _, k := range u["keys"].([]interface{}) { 37 | p, _ := u["permissions"].(string) 38 | if strings.HasPrefix(k.(string), key) { 39 | return model.User{ 40 | Name: u["name"].(string), 41 | Permissions: p, 42 | }, nil 43 | } 44 | } 45 | } 46 | return model.User{}, errors.New("user not found") 47 | } 48 | 49 | func (c *Config) FindUserByName(name string) (model.User, error) { 50 | for _, u := range c.GitUsers { 51 | if u["name"].(string) == name { 52 | p, _ := u["permissions"].(string) 53 | return model.User{ 54 | Name: u["name"].(string), 55 | Permissions: p, 56 | }, nil 57 | } 58 | } 59 | return model.User{}, errors.New("user not found") 60 | } 61 | 62 | //go:embed config.TEMPLATE.yaml 63 | var configTemplate string 64 | 65 | func GenerateConfig() ([]byte, error) { 66 | return executeTemplate(configTemplate) 67 | } 68 | 69 | func executeTemplate(tmpl string) ([]byte, error) { 70 | x, err := template.New("").Parse(tmpl) 71 | if err != nil { 72 | return nil, err 73 | } 74 | var b bytes.Buffer 75 | if err := x.Execute(&b, map[string]string{}); err != nil { 76 | return nil, err 77 | } 78 | return b.Bytes(), nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | 14 | "smolgit/pkg/model" 15 | 16 | "github.com/gliderlabs/ssh" 17 | billy "github.com/go-git/go-billy/v5" 18 | "github.com/go-git/go-billy/v5/osfs" 19 | "github.com/go-git/go-git/v5" 20 | "github.com/go-git/go-git/v5/plumbing" 21 | "github.com/go-git/go-git/v5/plumbing/cache" 22 | "github.com/go-git/go-git/v5/storage/filesystem" 23 | ) 24 | 25 | type Repo struct { 26 | *git.Repository 27 | } 28 | 29 | func RunCommand( 30 | cwd string, 31 | session ssh.Session, 32 | args []string, 33 | environ []string, 34 | ) int { 35 | cmd := exec.Command(args[0], args[1:]...) //nolint:gosec 36 | cmd.Dir = cwd 37 | 38 | cmd.Env = append(cmd.Env, environ...) 39 | cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH")) 40 | 41 | stdin, err := cmd.StdinPipe() 42 | if err != nil { 43 | slog.Error("failed to get stdin pipe", "err", err) 44 | return 1 45 | } 46 | 47 | stdout, err := cmd.StdoutPipe() 48 | if err != nil { 49 | slog.Error("failed to get stdout pipe", "err", err) 50 | return 1 51 | } 52 | 53 | stderr, err := cmd.StderrPipe() 54 | if err != nil { 55 | slog.Error("failed to get stderr pipe", "err", err) 56 | return 1 57 | } 58 | 59 | wg := &sync.WaitGroup{} 60 | wg.Add(2) 61 | 62 | if err = cmd.Start(); err != nil { 63 | slog.Error("failed to start command", "err", err) 64 | return 1 65 | } 66 | 67 | go func() { 68 | defer stdin.Close() 69 | 70 | if _, stdinErr := io.Copy(stdin, session); stdinErr != nil { 71 | slog.Error("failed to write session to stdin", "err", err) 72 | } 73 | }() 74 | 75 | go func() { 76 | defer wg.Done() 77 | 78 | if _, stdoutErr := io.Copy(session, stdout); stdoutErr != nil { 79 | slog.Error("failed to write stdout to session", "err", err) 80 | } 81 | }() 82 | 83 | go func() { 84 | defer wg.Done() 85 | 86 | if _, stderrErr := io.Copy(session.Stderr(), stderr); stderrErr != nil { 87 | slog.Error("failed to write stderr to session", "err", err) 88 | } 89 | }() 90 | 91 | wg.Wait() 92 | 93 | err = cmd.Wait() 94 | if err != nil { 95 | slog.Error("failed to wait for command exit", "err", err) 96 | return 1 97 | } 98 | 99 | if err == nil { 100 | return 0 101 | } 102 | 103 | var exitErr *exec.ExitError 104 | if !errors.As(err, &exitErr) { 105 | return 1 106 | } 107 | 108 | return exitErr.ProcessState.ExitCode() //nolint 109 | } 110 | 111 | func EnsureRepo(baseFS billy.Filesystem, base, path string) (*Repo, error) { 112 | info, err := baseFS.Stat(path) 113 | if err != nil && errors.Is(err, os.ErrNotExist) { 114 | if err := baseFS.MkdirAll(path, 0o700); err != nil { 115 | return nil, fmt.Errorf("cant create directory: %s err: %w", path, err) 116 | } 117 | slog.Debug("directory created", "path", path) 118 | } 119 | fs, err := baseFS.Chroot(path) 120 | if err != nil { 121 | return nil, fmt.Errorf("cant chroot to path: %s err: %w", path, err) 122 | } 123 | repoFS := filesystem.NewStorage(fs, cache.NewObjectLRUDefault()) 124 | if info == nil { 125 | slog.Debug("init repo", "path", path) 126 | repo, err := git.Init(repoFS, nil) 127 | if err != nil { 128 | return nil, fmt.Errorf("cant init git: %w", err) 129 | } 130 | return &Repo{repo}, nil 131 | } 132 | slog.Debug("open repo", "path", path) 133 | repo, err := git.Open(repoFS, osfs.New(base)) 134 | if err != nil { 135 | return nil, fmt.Errorf("cant open git: %w", err) 136 | } 137 | 138 | return &Repo{repo}, nil 139 | } 140 | 141 | func ListRepos(base string) ([]model.Repository, error) { 142 | repos := []model.Repository{} 143 | info, err := os.Stat(base) 144 | if err != nil && errors.Is(err, os.ErrNotExist) { 145 | return repos, fmt.Errorf("%s is not exist err: %s", base, err) 146 | } 147 | if !info.IsDir() { 148 | return repos, fmt.Errorf("%s is not a directory", base) 149 | } 150 | entries, err := filepath.Glob(base + "/*/*.git") 151 | if err != nil { 152 | return repos, fmt.Errorf("cant read path: %s err: %s", base, err) 153 | } 154 | for _, e := range entries { 155 | paths := strings.Split(strings.TrimPrefix(e, base), "/") 156 | repos = append(repos, model.Repository{ 157 | User: &model.User{Name: paths[1]}, 158 | Path: paths[2], 159 | }) 160 | } 161 | return repos, nil 162 | } 163 | 164 | func OpenRepo(baseFS billy.Filesystem, base, path string) (*Repo, error) { 165 | _, err := baseFS.Stat(path) 166 | if err != nil { 167 | slog.Debug("cant find path", "path", path, "err", err) 168 | return nil, fmt.Errorf("cant find path %w %s", err, path) 169 | } 170 | fs, err := baseFS.Chroot(path) 171 | if err != nil { 172 | return nil, fmt.Errorf("cant chroot to path: %s err: %w", path, err) 173 | } 174 | repoFS := filesystem.NewStorage(fs, cache.NewObjectLRUDefault()) 175 | slog.Debug("open repo", "path", path) 176 | repo, err := git.Open(repoFS, osfs.New(base)) 177 | if err != nil { 178 | return nil, fmt.Errorf("cant open git: %w", err) 179 | } 180 | 181 | return &Repo{repo}, nil 182 | } 183 | 184 | func (r *Repo) GetTags(clean bool) ([]string, error) { 185 | ti, err := r.Tags() 186 | if err != nil { 187 | return []string{}, err 188 | } 189 | tags := []string{} 190 | if err := ti.ForEach(func(tag *plumbing.Reference) error { 191 | tags = append(tags, tag.Name().String()) 192 | return nil 193 | }); err != nil { 194 | return []string{}, err 195 | } 196 | defer ti.Close() 197 | if clean { 198 | cleaned := []string{} 199 | for _, t := range tags { 200 | cleaned = append(cleaned, strings.TrimPrefix(t, "refs/tags/")) 201 | } 202 | return cleaned, nil 203 | } 204 | return tags, nil 205 | } 206 | 207 | func (r *Repo) GetBranches(clean bool) ([]string, error) { 208 | bi, err := r.Branches() 209 | if err != nil { 210 | return []string{}, err 211 | } 212 | refs := []string{} 213 | if err := bi.ForEach(func(ref *plumbing.Reference) error { 214 | refs = append(refs, ref.Name().String()) 215 | return nil 216 | }); err != nil { 217 | return []string{}, err 218 | } 219 | defer bi.Close() 220 | if clean { 221 | cleaned := []string{} 222 | for _, t := range refs { 223 | cleaned = append(cleaned, strings.TrimPrefix(t, "refs/heads/")) 224 | } 225 | return cleaned, nil 226 | } 227 | return refs, nil 228 | } 229 | -------------------------------------------------------------------------------- /pkg/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type User struct { 4 | Name string 5 | Permissions string 6 | Repos []*Repository 7 | Keys []string 8 | } 9 | 10 | type Repository struct { 11 | User *User 12 | Path string 13 | Refs []string 14 | Tags []string 15 | } 16 | 17 | func (r Repository) GetFullPath() string { 18 | return r.User.Name + "/" + r.Path 19 | } 20 | -------------------------------------------------------------------------------- /pkg/route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "html/template" 7 | "log/slog" 8 | "net/http" 9 | "sort" 10 | "strings" 11 | "time" 12 | 13 | "smolgit/pkg/config" 14 | "smolgit/pkg/git" 15 | "smolgit/pkg/model" 16 | 17 | "github.com/go-git/go-billy/v5" 18 | "github.com/go-git/go-billy/v5/osfs" 19 | gogit "github.com/go-git/go-git/v5" 20 | "github.com/go-git/go-git/v5/plumbing/object" 21 | strftime "github.com/itchyny/timefmt-go" 22 | 23 | "github.com/gin-gonic/gin" 24 | ) 25 | 26 | type Route struct { 27 | fs billy.Filesystem 28 | cfg *config.Config 29 | } 30 | 31 | //go:embed templates/html 32 | var htmlTemplates embed.FS 33 | 34 | func New(ginEngine *gin.Engine, cfg *config.Config) (Route, error) { 35 | r := Route{ 36 | fs: osfs.New(cfg.GitPath), 37 | cfg: cfg, 38 | } 39 | temp := template.New("").Funcs(template.FuncMap{ 40 | "formatAsDate": formatAsDate, 41 | "formatAsGit": r.formatAsGit, 42 | "getBrand": r.getBrand, 43 | "formatPath": formatPath, 44 | }) 45 | tmpl, err := temp.ParseFS( 46 | htmlTemplates, 47 | "templates/html/*.html", 48 | "templates/html/**/*.html", 49 | "templates/html/**/**/*.html", 50 | ) 51 | if err != nil { 52 | return r, err 53 | } 54 | ginEngine.SetHTMLTemplate(tmpl) 55 | ginEngine.NoRoute(func(c *gin.Context) { 56 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": "Not Found"}) 57 | }) 58 | return r, nil 59 | } 60 | 61 | func (r *Route) formatAsGit(path string) string { 62 | return fmt.Sprintf("ssh://%s/%s", r.cfg.GitBase, path) 63 | } 64 | 65 | func (r *Route) getBrand() string { 66 | return r.cfg.ServerBrand 67 | } 68 | 69 | func formatPath(path string) string { 70 | chunks := strings.Split(path, "/") 71 | return strings.TrimSuffix(chunks[2], ".git") 72 | } 73 | 74 | func formatAsDate(t time.Time) string { 75 | return strftime.Format(t, "%Y/%m/%d %H:%M:%S") 76 | } 77 | 78 | func (r *Route) Index(c *gin.Context) { 79 | slog.Debug("hit route", "route", "/") 80 | repos, err := git.ListRepos(r.cfg.GitPath) 81 | if err != nil { 82 | slog.Error("cant find repository", "err", err) 83 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": "cant find any repository"}) 84 | return 85 | } 86 | for _, repo := range repos { 87 | gitRepo, _ := git.OpenRepo(r.fs, r.cfg.GitPath, repo.GetFullPath()) 88 | tags, _ := gitRepo.GetTags(true) 89 | repo.Tags = tags 90 | refs, _ := gitRepo.GetBranches(true) 91 | repo.Refs = refs 92 | } 93 | 94 | c.HTML(http.StatusOK, "index.html", gin.H{ 95 | "title": "Repo", 96 | "repos": repos, 97 | }) 98 | } 99 | 100 | func (r *Route) Refs(c *gin.Context) { 101 | user, repoPath := c.Param("user"), c.Param("path") 102 | slog.Debug("hit route", "route", "/repo/refs/:user/:path", "user", user, "path", repoPath) 103 | fullPath := "/" + user + "/" + repoPath 104 | // TODO check if repo exist 105 | gitRepo, err := git.OpenRepo(r.fs, r.cfg.GitPath, fullPath) 106 | if err != nil { 107 | slog.Error("cant find repository", "err", err) 108 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 109 | return 110 | } 111 | tags, err := gitRepo.GetTags(true) 112 | if err != nil { 113 | slog.Error("cant get tags", "err", err) 114 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 115 | return 116 | } 117 | refs, err := gitRepo.GetBranches(true) 118 | if err != nil { 119 | slog.Error("cant get branches", "err", err) 120 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 121 | return 122 | } 123 | c.HTML(http.StatusOK, "refs.html", gin.H{ 124 | "title": "Refs", 125 | "repo": model.Repository{Path: repoPath, User: &model.User{Name: user}}, 126 | "refs": sort.StringSlice(refs), 127 | "tags": sort.StringSlice(tags), 128 | }) 129 | } 130 | 131 | func (r *Route) Files(c *gin.Context) { 132 | user, repoPath := c.Param("user"), c.Param("path") 133 | slog.Debug("hit route", "route", "/repo/files/:user/:path", "user", user, "path", repoPath) 134 | fullPath := "/" + user + "/" + repoPath 135 | // TODO check if repo exist 136 | gitRepo, err := git.OpenRepo(r.fs, r.cfg.GitPath, fullPath) 137 | if err != nil { 138 | slog.Error("cant find repository", "err", err) 139 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 140 | return 141 | } 142 | // TODO consider Until option 143 | ci, err := gitRepo.Log(&gogit.LogOptions{}) 144 | if err != nil { 145 | slog.Error("git log", "err", err) 146 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 147 | return 148 | } 149 | files := []string{} 150 | count := 0 151 | if err := ci.ForEach(func(cmt *object.Commit) error { 152 | if count == 1 { 153 | return nil 154 | } 155 | fi, err := cmt.Files() 156 | if err != nil { 157 | return err 158 | } 159 | count++ 160 | if err := fi.ForEach(func(file *object.File) error { 161 | files = append(files, file.Name) 162 | return nil 163 | }); err != nil { 164 | return err 165 | } 166 | return nil 167 | }); err != nil { 168 | slog.Error("repo commits", "err", err) 169 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 170 | return 171 | } 172 | defer ci.Close() 173 | 174 | c.HTML(http.StatusOK, "files.html", gin.H{ 175 | "title": "Files", 176 | "repo": model.Repository{Path: repoPath, User: &model.User{Name: user}}, 177 | "files": sort.StringSlice(files), 178 | }) 179 | } 180 | 181 | func (r *Route) Log(c *gin.Context) { 182 | user, repoPath := c.Param("user"), c.Param("path") 183 | slog.Debug("hit route", "route", "/repo/log/:user/:path", "user", user, "path", repoPath) 184 | fullPath := "/" + user + "/" + repoPath 185 | // TODO check if repo exist 186 | gitRepo, err := git.OpenRepo(r.fs, r.cfg.GitPath, fullPath) 187 | if err != nil { 188 | slog.Error("cant find repository", "err", err) 189 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 190 | return 191 | } 192 | // TODO consider Until option 193 | ct, err := gitRepo.Log(&gogit.LogOptions{}) 194 | if err != nil { 195 | slog.Error("git log", "err", err) 196 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 197 | return 198 | } 199 | type commit struct { 200 | Hash string 201 | Message string 202 | Author string 203 | Date string 204 | } 205 | commits := []commit{} 206 | if err := ct.ForEach(func(cmt *object.Commit) error { 207 | commits = append(commits, commit{ 208 | Hash: cmt.Hash.String()[0:8], 209 | Message: cmt.Message, 210 | Author: cmt.Author.Name, 211 | Date: formatAsDate(cmt.Author.When), 212 | }) 213 | return nil 214 | }); err != nil { 215 | slog.Error("repo commits", "err", err) 216 | c.HTML(http.StatusNotFound, "404.html", gin.H{"title": repoPath + " not found"}) 217 | return 218 | } 219 | defer ct.Close() 220 | c.HTML(http.StatusOK, "log.html", gin.H{ 221 | "title": "Repo", 222 | "repo": model.Repository{Path: repoPath, User: &model.User{Name: user}}, 223 | "commits": commits, 224 | }) 225 | } 226 | 227 | //go:embed templates/css 228 | var styleFs embed.FS 229 | 230 | func (r *Route) ExternalStyle(c *gin.Context) { 231 | c.Header("Content-Type", "text/css") 232 | data, err := styleFs.ReadFile("templates/css/pico.min.css") 233 | if err != nil { 234 | panic(err) 235 | } 236 | _, _ = c.Writer.Write(data) 237 | } 238 | 239 | func (r *Route) Style(c *gin.Context) { 240 | c.Header("Content-Type", "text/css") 241 | data, err := styleFs.ReadFile("templates/css/style.css") 242 | if err != nil { 243 | panic(err) 244 | } 245 | _, _ = c.Writer.Write(data) 246 | } 247 | -------------------------------------------------------------------------------- /pkg/route/templates/css/pico.min.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";/*! 2 | * Pico CSS ✨ v2.0.6 (https://picocss.com) 3 | * Copyright 2019-2024 - Licensed under MIT 4 | */:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:root{--pico-font-size:106.25%}}@media (min-width:768px){:root{--pico-font-size:112.5%}}@media (min-width:1024px){:root{--pico-font-size:118.75%}}@media (min-width:1280px){:root{--pico-font-size:125%}}@media (min-width:1536px){:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}a.contrast,a.secondary{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}details.dropdown summary:not([role=button]){--pico-outline-width:0.0625rem}nav details.dropdown summary:focus-visible{--pico-outline-width:0.125rem}[role=search]{--pico-border-radius:5rem}[role=group]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus),[role=search]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[role=group]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus),[role=search]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]:not(.outline)::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0) invert(1)}:root:not([data-theme=dark]),[data-theme=light]{--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(2, 154, 232, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:#e7eaf0;--pico-primary:#0172ad;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 114, 173, 0.5);--pico-primary-hover:#015887;--pico-primary-hover-background:#02659a;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(2, 154, 232, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:#fde7c0;--pico-mark-color:#0f1114;--pico-ins-color:#1d6a54;--pico-del-color:#883935;--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:#f3f5f7;--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:#fbfcfc;--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:#b86a6b;--pico-form-element-invalid-active-border-color:#c84f48;--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#4c9b8a;--pico-form-element-valid-active-border-color:#279977;--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:#fbfcfc;--pico-dropdown-background-color:#fff;--pico-dropdown-border-color:#eff1f4;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#eff1f4;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 155, 138)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200, 79, 72)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");color-scheme:light}:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:root:not([data-theme]){--pico-background-color:#13171f;--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 9, 12, 0.06),0 0 0 0.0625rem rgba(7, 9, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:#ce7e7b;--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:#1a1f28;--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:#1c212c;--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:#1a1f28;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:#964a50;--pico-form-element-invalid-active-border-color:#b7403b;--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:#16896a;--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:#1a1f28;--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(8, 9, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");color-scheme:dark}:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}:root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}:root:not([data-theme]) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}}[data-theme=dark]{--pico-background-color:#13171f;--pico-color:#c2c7d0;--pico-text-selection-color:rgba(1, 170, 255, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#01aaff;--pico-primary-background:#0172ad;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(1, 170, 255, 0.5);--pico-primary-hover:#79c0ff;--pico-primary-hover-background:#017fc0;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(1, 170, 255, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 9, 12, 0.06),0 0 0 0.0625rem rgba(7, 9, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:#ce7e7b;--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:#1a1f28;--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:#1c212c;--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:#1a1f28;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:#964a50;--pico-form-element-invalid-active-border-color:#b7403b;--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:#16896a;--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:#1a1f28;--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(8, 9, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");color-scheme:dark}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[data-theme=dark] details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}[data-theme=dark] [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{padding-block:var(--pico-block-spacing-vertical)}section{margin-bottom:var(--pico-block-spacing-vertical)}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--pico-spacing);padding-left:var(--pico-spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:1024px){.container{max-width:950px}}@media (min-width:1280px){.container{max-width:1200px}}@media (min-width:1536px){.container{max-width:1450px}}.grid{grid-column-gap:var(--pico-grid-column-gap);grid-row-gap:var(--pico-grid-row-gap);display:grid;grid-template-columns:1fr}@media (min-width:768px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}.overflow-auto{overflow:auto}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}:where(a:not([role=button])).secondary,[role=link].secondary{--pico-color:var(--pico-secondary);--pico-underline:var(--pico-secondary-underline)}:where(a:not([role=button])).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-underline:var(--pico-secondary-hover-underline)}:where(a:not([role=button])).contrast,[role=link].contrast{--pico-color:var(--pico-contrast);--pico-underline:var(--pico-contrast-underline)}:where(a:not([role=button])).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-underline:var(--pico-contrast-hover-underline)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}:is(button,[type=submit],[type=button],[role=button]).secondary,[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}:is(button,[type=submit],[type=button],[role=button]).secondary:focus,:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:is(button,[type=submit],[type=button],[role=button]).contrast{--pico-background-color:var(--pico-contrast-background);--pico-border-color:var(--pico-contrast-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-contrast-hover-background);--pico-border-color:var(--pico-contrast-hover-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:focus,:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}:is(button,[type=submit],[type=button],[role=button]).outline,[type=reset].outline{--pico-background-color:transparent;--pico-color:var(--pico-primary);--pico-border-color:var(--pico-primary)}:is(button,[type=submit],[type=button],[role=button]).outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:transparent;--pico-color:var(--pico-primary-hover);--pico-border-color:var(--pico-primary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary,[type=reset].outline{--pico-color:var(--pico-secondary);--pico-border-color:var(--pico-secondary)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-border-color:var(--pico-secondary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast{--pico-color:var(--pico-contrast);--pico-border-color:var(--pico-contrast)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-border-color:var(--pico-contrast-hover)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset,.grid)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}details.dropdown{position:relative;border-bottom:none}details.dropdown summary::after,details.dropdown>a::after,details.dropdown>button::after{display:block;width:1rem;height:calc(1rem * var(--pico-line-height,1.5));margin-inline-start:.25rem;float:right;transform:rotate(0) translateX(.2rem);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}nav details.dropdown{margin-bottom:0}details.dropdown summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-form-element-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-form-element-background-color);color:var(--pico-form-element-placeholder-color);line-height:inherit;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}details.dropdown summary:not([role]):active,details.dropdown summary:not([role]):focus{border-color:var(--pico-form-element-active-border-color);background-color:var(--pico-form-element-active-background-color)}details.dropdown summary:not([role]):focus{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}details.dropdown summary:not([role]):focus-visible{outline:0}details.dropdown summary:not([role])[aria-invalid=false]{--pico-form-element-border-color:var(--pico-form-element-valid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-valid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-valid-focus-color)}details.dropdown summary:not([role])[aria-invalid=true]{--pico-form-element-border-color:var(--pico-form-element-invalid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-invalid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-invalid-focus-color)}nav details.dropdown{display:inline;margin:calc(var(--pico-nav-element-spacing-vertical) * -1) 0}nav details.dropdown summary::after{transform:rotate(0) translateX(0)}nav details.dropdown summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2);padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav details.dropdown summary:not([role]):focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}details.dropdown summary+ul{display:flex;z-index:99;position:absolute;left:0;flex-direction:column;width:100%;min-width:-moz-fit-content;min-width:fit-content;margin:0;margin-top:var(--pico-outline-width);padding:0;border:var(--pico-border-width) solid var(--pico-dropdown-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-dropdown-background-color);box-shadow:var(--pico-dropdown-box-shadow);color:var(--pico-dropdown-color);white-space:nowrap;opacity:0;transition:opacity var(--pico-transition),transform 0s ease-in-out 1s}details.dropdown summary+ul[dir=rtl]{right:0;left:auto}details.dropdown summary+ul li{width:100%;margin-bottom:0;padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);list-style:none}details.dropdown summary+ul li:first-of-type{margin-top:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown summary+ul li:last-of-type{margin-bottom:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown summary+ul li a{display:block;margin:calc(var(--pico-form-element-spacing-vertical) * -.5) calc(var(--pico-form-element-spacing-horizontal) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);overflow:hidden;border-radius:0;color:var(--pico-dropdown-color);text-decoration:none;text-overflow:ellipsis}details.dropdown summary+ul li a:active,details.dropdown summary+ul li a:focus,details.dropdown summary+ul li a:focus-visible,details.dropdown summary+ul li a:hover,details.dropdown summary+ul li a[aria-current]:not([aria-current=false]){background-color:var(--pico-dropdown-hover-background-color)}details.dropdown summary+ul li label{width:100%}details.dropdown summary+ul li:has(label):hover{background-color:var(--pico-dropdown-hover-background-color)}details.dropdown[open] summary{margin-bottom:0}details.dropdown[open] summary+ul{transform:scaleY(1);opacity:1;transition:opacity var(--pico-transition),transform 0s ease-in-out 0s}details.dropdown[open] summary::before{display:block;z-index:1;position:fixed;width:100vw;height:100vh;inset:0;background:0 0;content:"";cursor:default}label>details.dropdown{margin-top:calc(var(--pico-spacing) * .25)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog article{max-width:510px}}@media (min-width:768px){dialog article{max-width:700px}}dialog article>header>*{margin-bottom:0}dialog article>header .close,dialog article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog article>footer{text-align:right}dialog article>footer [role=button],dialog article>footer button{margin-bottom:0}dialog article>footer [role=button]:not(:first-of-type),dialog article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog article .close,dialog article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog article .close:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),dialog article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--pico-scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto;touch-action:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} -------------------------------------------------------------------------------- /pkg/route/templates/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --pico-font-size: 80%; 3 | --pico-border-radius: 1rem; 4 | --pico-typography-spacing-vertical: 1.5rem; 5 | --pico-form-element-spacing-vertical: 0.5rem; 6 | --pico-form-element-spacing-horizontal: 1.25rem; 7 | } 8 | button { 9 | --pico-font-weight: 400; 10 | } 11 | 12 | .repository-header { 13 | grid-template-columns: 2fr 1fr 1fr 1fr; 14 | } 15 | 16 | /* :root { 17 | --pico-font-family-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 18 | --pico-font-family-sans-serif: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif, var(--pico-font-family-emoji); 19 | --pico-font-family-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, var(--pico-font-family-emoji); 20 | --pico-font-family: var(--pico-font-family-sans-serif); 21 | --pico-line-height: 1.5; 22 | --pico-font-weight: 400; 23 | --pico-font-size: 100%; 24 | --pico-text-underline-offset: 0.1rem; 25 | --pico-border-radius: 0.25rem; 26 | --pico-border-width: 0.0625rem; 27 | --pico-outline-width: 0.125rem; 28 | --pico-transition: 0.2s ease-in-out; 29 | --pico-spacing: 1rem; 30 | --pico-typography-spacing-vertical: 1rem; 31 | --pico-block-spacing-vertical: var(--pico-spacing); 32 | --pico-block-spacing-horizontal: var(--pico-spacing); 33 | --pico-grid-column-gap: var(--pico-spacing); 34 | --pico-grid-row-gap: var(--pico-spacing); 35 | --pico-form-element-spacing-vertical: 0.75rem; 36 | --pico-form-element-spacing-horizontal: 1rem; 37 | --pico-group-box-shadow: 0 0 0 rgba(0, 0, 0, 0); 38 | --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); 39 | --pico-group-box-shadow-focus-with-input: 0 0 0 0.0625rem var(--pico-form-element-border-color); 40 | --pico-modal-overlay-backdrop-filter: blur(0.375rem); 41 | --pico-nav-element-spacing-vertical: 1rem; 42 | --pico-nav-element-spacing-horizontal: 0.5rem; 43 | --pico-nav-link-spacing-vertical: 0.5rem; 44 | --pico-nav-link-spacing-horizontal: 0.5rem; 45 | --pico-nav-breadcrumb-divider: ">"; 46 | --pico-icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); 47 | --pico-icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); 48 | --pico-icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); 49 | --pico-icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); 50 | --pico-icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); 51 | --pico-icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); 52 | --pico-icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); 53 | --pico-icon-loading: url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E"); 54 | } 55 | @media (min-width: 576px) { 56 | :root { 57 | --pico-font-size: 106.25%; 58 | } 59 | } 60 | @media (min-width: 768px) { 61 | :root { 62 | --pico-font-size: 112.5%; 63 | } 64 | } 65 | @media (min-width: 1024px) { 66 | :root { 67 | --pico-font-size: 118.75%; 68 | } 69 | } 70 | @media (min-width: 1280px) { 71 | :root { 72 | --pico-font-size: 125%; 73 | } 74 | } 75 | @media (min-width: 1536px) { 76 | :root { 77 | --pico-font-size: 131.25%; 78 | } 79 | } 80 | 81 | a { 82 | --pico-text-decoration: underline; 83 | } 84 | a.secondary, a.contrast { 85 | --pico-text-decoration: underline; 86 | } 87 | 88 | small { 89 | --pico-font-size: 0.875em; 90 | } 91 | 92 | h1, 93 | h2, 94 | h3, 95 | h4, 96 | h5, 97 | h6 { 98 | --pico-font-weight: 700; 99 | } 100 | 101 | h1 { 102 | --pico-font-size: 2rem; 103 | --pico-line-height: 1.125; 104 | --pico-typography-spacing-top: 3rem; 105 | } 106 | 107 | h2 { 108 | --pico-font-size: 1.75rem; 109 | --pico-line-height: 1.15; 110 | --pico-typography-spacing-top: 2.625rem; 111 | } 112 | 113 | h3 { 114 | --pico-font-size: 1.5rem; 115 | --pico-line-height: 1.175; 116 | --pico-typography-spacing-top: 2.25rem; 117 | } 118 | 119 | h4 { 120 | --pico-font-size: 1.25rem; 121 | --pico-line-height: 1.2; 122 | --pico-typography-spacing-top: 1.874rem; 123 | } 124 | 125 | h5 { 126 | --pico-font-size: 1.125rem; 127 | --pico-line-height: 1.225; 128 | --pico-typography-spacing-top: 1.6875rem; 129 | } 130 | 131 | h6 { 132 | --pico-font-size: 1rem; 133 | --pico-line-height: 1.25; 134 | --pico-typography-spacing-top: 1.5rem; 135 | } 136 | 137 | thead th, 138 | thead td, 139 | tfoot th, 140 | tfoot td { 141 | --pico-font-weight: 600; 142 | --pico-border-width: 0.1875rem; 143 | } 144 | 145 | pre, 146 | code, 147 | kbd, 148 | samp { 149 | --pico-font-family: var(--pico-font-family-monospace); 150 | } 151 | 152 | kbd { 153 | --pico-font-weight: bolder; 154 | } 155 | 156 | input:not([type=submit], 157 | [type=button], 158 | [type=reset], 159 | [type=checkbox], 160 | [type=radio], 161 | [type=file]), 162 | :where(select, textarea) { 163 | --pico-outline-width: 0.0625rem; 164 | } 165 | 166 | [type=search] { 167 | --pico-border-radius: 5rem; 168 | } 169 | 170 | [type=checkbox], 171 | [type=radio] { 172 | --pico-border-width: 0.125rem; 173 | } 174 | 175 | [type=checkbox][role=switch] { 176 | --pico-border-width: 0.1875rem; 177 | } 178 | 179 | details.dropdown summary:not([role=button]) { 180 | --pico-outline-width: 0.0625rem; 181 | } 182 | 183 | nav details.dropdown summary:focus-visible { 184 | --pico-outline-width: 0.125rem; 185 | } 186 | 187 | [role=search] { 188 | --pico-border-radius: 5rem; 189 | } 190 | 191 | [role=search]:has(button.secondary:focus, 192 | [type=submit].secondary:focus, 193 | [type=button].secondary:focus, 194 | [role=button].secondary:focus), 195 | [role=group]:has(button.secondary:focus, 196 | [type=submit].secondary:focus, 197 | [type=button].secondary:focus, 198 | [role=button].secondary:focus) { 199 | --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); 200 | } 201 | [role=search]:has(button.contrast:focus, 202 | [type=submit].contrast:focus, 203 | [type=button].contrast:focus, 204 | [role=button].contrast:focus), 205 | [role=group]:has(button.contrast:focus, 206 | [type=submit].contrast:focus, 207 | [type=button].contrast:focus, 208 | [role=button].contrast:focus) { 209 | --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-contrast-focus); 210 | } 211 | [role=search] button, 212 | [role=search] [type=submit], 213 | [role=search] [type=button], 214 | [role=search] [role=button], 215 | [role=group] button, 216 | [role=group] [type=submit], 217 | [role=group] [type=button], 218 | [role=group] [role=button] { 219 | --pico-form-element-spacing-horizontal: 2rem; 220 | } 221 | 222 | details summary[role=button]:not(.outline)::after { 223 | filter: brightness(0) invert(1); 224 | } 225 | 226 | [aria-busy=true]:not(input, select, textarea):is(button, [type=submit], [type=button], [type=reset], [role=button]):not(.outline)::before { 227 | filter: brightness(0) invert(1); 228 | } */ -------------------------------------------------------------------------------- /pkg/route/templates/html/header.html: -------------------------------------------------------------------------------- 1 | {{ define "header"}} 2 |
3 |
{{ .GetFullPath | formatAsGit }}
4 |
Log
5 |
Files
6 |
Refs
7 |
8 | {{ end }} -------------------------------------------------------------------------------- /pkg/route/templates/html/layout.html: -------------------------------------------------------------------------------- 1 | {{ define "layout" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ . }} 10 | 11 | 12 | 13 | 14 |
15 | 23 |
24 | 25 | 26 | {{ end }} -------------------------------------------------------------------------------- /pkg/route/templates/html/pages/404.html: -------------------------------------------------------------------------------- 1 | {{ $title := .title }} 2 | {{ template "layout" $title }} 3 | 4 |
5 | {{ .title }} 6 |
-------------------------------------------------------------------------------- /pkg/route/templates/html/pages/500.html: -------------------------------------------------------------------------------- 1 | {{ $title := .title }} 2 | {{ template "layout" $title }} 3 | 4 |
5 | {{ .error }} 6 |
-------------------------------------------------------------------------------- /pkg/route/templates/html/pages/index.html: -------------------------------------------------------------------------------- 1 | {{ $title := .title }} 2 | {{ template "layout" $title }} 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ range .repos }} 17 | 18 | 19 | 20 | 25 | {{ if not .Tags }} 26 | 27 | {{ else }} 28 | 33 | {{ end }} 34 | 35 | {{ end }} 36 | 37 |
NameOwnerBranchesTags
{{ .GetFullPath }}{{ .User.Name }} 21 | {{ range .Refs }} 22 | {{ . }}
23 | {{ end }} 24 |
empty 29 | {{ range .Tags }} 30 | {{ . }}
31 | {{ end }} 32 |
38 |
39 | -------------------------------------------------------------------------------- /pkg/route/templates/html/pages/repo/files.html: -------------------------------------------------------------------------------- 1 | {{ $title := .title }} 2 | {{ template "layout" $title }} 3 | 4 |
5 | {{ template "header" .repo }} 6 |
7 |

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ range .files }} 19 | 20 | 21 | 22 | {{ end }} 23 | 24 |
Name
{{ . }}
25 |
-------------------------------------------------------------------------------- /pkg/route/templates/html/pages/repo/log.html: -------------------------------------------------------------------------------- 1 | {{ $title := .title }} 2 | {{ template "layout" $title }} 3 | 4 |
5 | {{ template "header" .repo }} 6 |
7 |

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{ range .commits }} 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{ end }} 29 | 30 |
DateMessageHashAuthor
{{ .Date }}{{ .Message }}{{ .Hash }}{{ .Author }}
31 | 32 |
-------------------------------------------------------------------------------- /pkg/route/templates/html/pages/repo/refs.html: -------------------------------------------------------------------------------- 1 | {{ $title := .title }} 2 | {{ template "layout" $title }} 3 | 4 |
5 | {{ template "header" .repo }} 6 |
7 |

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ range .refs }} 19 | 20 | 21 | 22 | {{ end }} 23 | 24 |
Branch
{{ . }}
25 |
26 |
27 | {{ if not .tags }} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
Tags not exist
39 | {{ else }} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {{ range .tags }} 50 | 51 | 52 | 53 | {{ end }} 54 | 55 |
Tag
{{ . }}
56 | {{ end }} 57 |
-------------------------------------------------------------------------------- /pkg/ssh/ssh.go: -------------------------------------------------------------------------------- 1 | package ssh 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log/slog" 7 | "regexp" 8 | "strings" 9 | 10 | "smolgit/pkg/config" 11 | "smolgit/pkg/git" 12 | 13 | "github.com/go-git/go-billy/v5/osfs" 14 | 15 | "github.com/gliderlabs/ssh" 16 | "github.com/go-git/go-billy/v5" 17 | gssh "golang.org/x/crypto/ssh" 18 | ) 19 | 20 | type Server struct { 21 | fs billy.Filesystem 22 | ssh *ssh.Server 23 | cfg *config.Config 24 | } 25 | 26 | func New(cfg *config.Config) (*Server, error) { 27 | srv := &Server{ 28 | cfg: cfg, 29 | fs: osfs.New(cfg.GitPath), 30 | } 31 | 32 | srv.ssh = &ssh.Server{ 33 | Handler: srv.handler, 34 | PublicKeyHandler: srv.pkHandler, 35 | } 36 | 37 | return srv, nil 38 | } 39 | 40 | func (srv *Server) handler(s ssh.Session) { 41 | cmd := s.Command() 42 | slog.Debug("new connection", "cmd", cmd) 43 | 44 | if len(cmd) == 0 { 45 | cmd = []string{"whoami"} 46 | } 47 | 48 | var exit int 49 | 50 | switch cmd[0] { 51 | case "git-receive-pack": 52 | slog.Debug("receive cmd git-receive-pack", "cmd", cmd) 53 | exit = srv.cmdRepo(s, cmd) 54 | case "git-upload-pack": 55 | slog.Debug("receive cmd git-upload-pack", "cmd", cmd) 56 | exit = srv.cmdRepo(s, cmd) 57 | default: 58 | slog.Debug("command not found\r\n", "cmd", cmd[0]) 59 | exit = 1 60 | } 61 | 62 | slog.Info("return_code", "exit_code", exit) 63 | _ = s.Exit(exit) 64 | } 65 | 66 | func (srv *Server) pkHandler(ctx ssh.Context, incomingKey ssh.PublicKey) bool { 67 | slog.Info("handle key", "remote_user", ctx.User(), "remote_addr", ctx.RemoteAddr().String()) 68 | 69 | if ctx.User() != "git" { 70 | slog.Error("wrong remote_user", "user", ctx.User()) 71 | return false 72 | } 73 | 74 | user, err := srv.cfg.FindUserByKey(string(bytes.TrimSpace(gssh.MarshalAuthorizedKey(incomingKey)))) 75 | if err != nil { 76 | slog.Error("user not found", "err", err) 77 | return false 78 | } 79 | ctx.SetValue("user_name", user.Name) 80 | slog.Debug("found user", "name", user.Name) 81 | return true 82 | } 83 | 84 | func (srv *Server) ListenAndServe() error { 85 | srv.ssh.Addr = srv.cfg.SSHAddr 86 | return srv.ssh.ListenAndServe() 87 | } 88 | 89 | func (srv *Server) Close() error { 90 | return srv.ssh.Close() 91 | } 92 | 93 | func (srv *Server) cmdRepo(s ssh.Session, cmd []string) int { 94 | if len(cmd) != 2 { 95 | _, _ = io.WriteString(s.Stderr(), "Missing repo name argument\r\n") 96 | return 1 97 | } 98 | 99 | repoName := cmd[1] 100 | 101 | userName, ok := s.Context().Value("user_name").(string) 102 | if !ok || userName == "" { 103 | slog.Error("cant find user with", "user", userName) 104 | _, _ = io.WriteString(s.Stderr(), "Permission denied\r\n") 105 | return 1 106 | } 107 | user, err := srv.cfg.FindUserByName(userName) 108 | if err != nil { 109 | slog.Error("cant find user with", "user", userName) 110 | _, _ = io.WriteString(s.Stderr(), "Permission denied\r\n") 111 | return 1 112 | } 113 | 114 | // TODO better permissions check 115 | if user.Permissions != "*" { 116 | r, _ := regexp.Compile(user.Permissions) 117 | repoNamespace := strings.Split(repoName, "/") 118 | ns := strings.Join(repoNamespace[1:len(repoNamespace)-1], "") 119 | if !r.MatchString(ns) { 120 | slog.Error("wrong repo prefix", "repoName", repoName, "ns", ns, "userName", userName, "permission", user.Permissions) 121 | _, _ = io.WriteString(s.Stderr(), "Permission denied\r\n") 122 | return 1 123 | } 124 | } 125 | 126 | if _, err := git.EnsureRepo(srv.fs, srv.cfg.GitBase, repoName); err != nil { 127 | slog.Error("cant find or create repository", "err", err) 128 | _, _ = io.WriteString(s.Stderr(), "Repo doesnt exist\r\n") 129 | return 1 130 | } 131 | // TODO sanitize input 132 | // Get path from user 133 | slog.Debug("run command", "cmd", cmd, "root", srv.fs.Root(), "path", repoName[1:]) 134 | returnCode := git.RunCommand(srv.fs.Root(), s, []string{cmd[0], repoName[1:]}, []string{}) 135 | return returnCode 136 | } 137 | 138 | func Parsepk(data []byte) (ssh.PublicKey, error) { 139 | publicKey, _, _, _, err := ssh.ParseAuthorizedKey(data) //nolint:dogsled 140 | return publicKey, err 141 | } 142 | -------------------------------------------------------------------------------- /smolgit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "smolgit/cmd" 12 | "smolgit/pkg/config" 13 | ) 14 | 15 | var ( 16 | version = "dev" 17 | configPath = flag.String("config", "./config.yaml", "path to config") 18 | ) 19 | 20 | func main() { 21 | flag.Parse() 22 | 23 | command := os.Args[1:] 24 | if len(command) > 0 && command[0] == "config" { 25 | cfg, err := config.GenerateConfig() 26 | if err != nil { 27 | log.Fatalf("failed to generate config: %s", err) 28 | } 29 | fmt.Print(string(cfg)) 30 | os.Exit(0) 31 | } 32 | sigchnl := make(chan os.Signal, 1) 33 | signal.Notify(sigchnl, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) 34 | exitchnl := make(chan os.Signal) 35 | app, err := cmd.New(version, configPath, exitchnl, sigchnl) 36 | if err != nil { 37 | log.Fatalf("failed to init app: %s", err) 38 | } 39 | if err := app.Run(); err != nil { 40 | log.Fatalf("failed to start app: %s", err) 41 | } 42 | <-exitchnl 43 | } 44 | -------------------------------------------------------------------------------- /test/test.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | teardown() { 4 | cat output.log 5 | cat cfg.yaml 6 | rm -rf cfg.yaml 7 | rm -rf output.log 8 | } 9 | 10 | @test "smolgit can show help" { 11 | run ./bin/smolgit --help 12 | [ "${lines[0]}" = "Usage of ./bin/smolgit:" ] 13 | } 14 | 15 | @test "smolgit can generate config" { 16 | run ./bin/smolgit config 17 | [ "${lines[0]}" = "log:" ] 18 | ./bin/smolgit config > cfg.yaml 19 | test -f cfg.yaml 20 | } 21 | 22 | @test "smolgit up and running" { 23 | ./bin/smolgit config | yq '.log.color = false | ... comments=""' > cfg.yaml 24 | ./bin/smolgit --config=./cfg.yaml > output.log & 25 | server_pid=$! 26 | sleep 1 27 | kill $server_pid 28 | cat output.log | grep '"initialize web server" addr=:3080' 29 | cat output.log | grep '"initialize ssh server" addr=:3081' 30 | cat output.log | grep '"start server" brand=smolgit address=:3080' 31 | cat output.log | grep '"starting SSH server" addr=:3081' 32 | cat output.log | grep 'msg="os signal received" signal=terminated' 33 | } 34 | 35 | @test "web server respond 404 no repositories found" { 36 | ./bin/smolgit config | yq '.log.color = false | ... comments=""' > cfg.yaml 37 | ./bin/smolgit --config=./cfg.yaml > output.log & 38 | server_pid=$! 39 | sleep 1 40 | response_code=$(curl -so /dev/null -w '%{response_code}' localhost:3080/) 41 | echo "$response_code" 42 | [ "$response_code" -ne "200" ] 43 | kill $server_pid 44 | cat output.log | grep 'msg="hit route" route=/' 45 | } --------------------------------------------------------------------------------