Compare commits

...

6 Commits
v0.3.0 ... main

Author SHA1 Message Date
jolheiser 6ef23a7c5b Refactor for ff, zerolog, and re-integrate SDK (#14)
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/tag/woodpecker Pipeline was successful Details
Reviewed-on: #14
2022-03-20 04:15:24 +00:00
jolheiser 2d644e2711 Fix sakura (#13)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #13
Co-authored-by: jolheiser <john.olheiser@gmail.com>
Co-committed-by: jolheiser <john.olheiser@gmail.com>
2021-05-09 16:22:19 +00:00
jolheiser 0b3fd9c04c Add sakura for CSS (#12)
continuous-integration/drone/push Build is passing Details
Resolves #7

Reviewed-on: #12
Co-authored-by: jolheiser <john.olheiser@gmail.com>
Co-committed-by: jolheiser <john.olheiser@gmail.com>
2021-05-09 05:02:27 +00:00
John Olheiser cf984234fd Convert to gpm-style service (#11)
This is a **massively** breaking change from `v0.2.0`.
For consumers there will be no difference, `go-get` and `git-import` are both still supported.
The change will be for the admin regarding how package management works.

Prior to merging, `v0.2.0` should be moved to another branch in case myself or another party wants to continue with that style of service.

This version follows a similar implementation to [gpm](https://gitea.com/jolheiser/gpm) (and indeed, some code was copied nearly line-by-line)

-----

Vanity runs as a service, same as before. However, rather than automatic cron-style updates using a third-party API, now the service owner uses their local `vanity` binary with a matching `token` to...
* `vanity add` a new package
* `vanity update` an existing package
* `vanity remove` a package

This allows much finer control over which packages are in the service and should required almost no downtime once the service is started other than to update the service itself.

As well, it allows mixing of git providers.

<small>There's also an SDK, which is nice to have.</small>

Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/11
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
2021-04-23 11:21:53 +08:00
John Olheiser c4be5e64b6 Add link to docs for vanity imports (#10)
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/10
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
2021-02-23 22:45:01 +08:00
John Olheiser 2f688b839b Update docker for Go 1.16 (#9)
Co-authored-by: jolheiser <john.olheiser@gmail.com>
Reviewed-on: https://gitea.com/jolheiser/vanity/pulls/9
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-committed-by: John Olheiser <john.olheiser@gmail.com>
2021-02-22 04:28:35 +08:00
51 changed files with 1699 additions and 1760 deletions

6
.gitignore vendored
View File

@ -1,7 +1,5 @@
# GoLand
.idea
.idea/
# Vanity
/vanity
/vanity.exe
.vanity.toml
/vanity*

37
.woodpecker.yml 100644
View File

@ -0,0 +1,37 @@
clone:
git:
image: woodpeckerci/plugin-git:next
pipeline:
compliance:
image: golang:1.17
commands:
- go test -race ./...
- go run github.com/rs/zerolog/cmd/lint ./cmd/vanity-cli
- go run github.com/rs/zerolog/cmd/lint ./cmd/vanity-server
- go vet ./...
when:
event: pull_request
build:
image: golang:1.17
commands:
- GOOS="linux" go build ./cmd/vanity-cli
- GOOS="windows" go build ./cmd/vanity-cli
- GOOS="linux" go build ./cmd/vanity-server
- GOOS="windows" go build ./cmd/vanity-server
release-main:
image: jolheiser/drone-gitea-main:latest
settings:
base: https://git.jojodev.com
token:
from_secret: gitea_token
files:
- "vanity-cli"
- "vanity-cli.exe"
- "vanity-server"
- "vanity-server.exe"
when:
event: push
branch: main

View File

@ -1,27 +0,0 @@
GO ?= go
VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
.PHONY: build
build:
$(GO) build -ldflags '-s -w -X "go.jolheiser.com/vanity/api.Version=$(VERSION)"'
.PHONY: fmt
fmt:
$(GO) fmt ./...
.PHONY: test
test:
$(GO) test --race ./...
.PHONY: vet
vet:
$(GO) vet ./...
.PHONY: docker-build
docker-build:
docker build -f docker/Dockerfile -t jolheiser/vanity .
.PHONY: docker-push
docker-push:
docker push jolheiser/vanity

View File

@ -1,93 +1,26 @@
# Vanity
A simple web service to serve vanity Go imports. Feel free to check it out using [my instance](https://go.jolheiser.com/).
[![Go Reference](https://pkg.go.dev/badge/go.jolheiser.com/vanity.svg)](https://pkg.go.dev/go.jolheiser.com/vanity)
[![Build Status](https://ci.jojodev.com/api/badges/jolheiser/vanity/status.svg)](https://ci.jojodev.com/jolheiser/vanity)
A simple web service to serve [vanity Go imports](https://golang.org/cmd/go/#hdr-Remote_import_paths). Feel free to check it out using [my instance](https://go.jolheiser.com/).
Vanity also supports [git-import](https://gitea.com/jolheiser/git-import).
## Configuration
When choosing a service, the default `base-url` will be the default server of that service:
| Service | Default |
|:-------:|:------------------:|
| Gitea | https://gitea.com |
| GitHub | https://github.com |
| GitLab | https://gitlab.com |
```
NAME:
vanity - Vanity Go Imports
USAGE:
vanity [global options] command [command options] [arguments...]
VERSION:
0.1.0+3-g6d7150e
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--config value Path to a config file [$VANITY_CONFIG]
--port value Port to run the vanity server on (default: 7777) [$VANITY_PORT]
--domain value Vanity domain, e.g. go.domain.tld [$VANITY_DOMAIN]
--service value Service type (Gitea, GitHub, GitLab) (default: "gitea") [$VANITY_SERVICE]
--base-url value Base URL to service [$VANITY_BASE_URL]
--namespace value Owner namespace [$VANITY_NAMESPACE]
--token value Access token [$VANITY_TOKEN]
--include value Repository names to include (regex) [$VANITY_INCLUDE]
--exclude value Repository names to exclude (regex) [$VANITY_EXCLUDE]
--private Include private repositories (default: false) [$VANITY_PRIVATE]
--fork Include forked repositories (default: false) [$VANITY_FORK]
--mirror Include mirrored repositories (default: false) [$VANITY_MIRROR]
--archive Include archived repositories (default: false) [$VANITY_ARCHIVE]
--override value Repository name to override (NAME=OVERRIDE) [$VANITY_OVERRIDE]
--interval value Interval between updating repositories (default: 15m0s) [$VANITY_INTERVAL]
--debug Debug logging (default: false) [$VANITY_DEBUG]
--help, -h show help (default: false)
--version, -v print the version (default: false)
```
## Docker
```sh
docker run \
--env VANITY_DOMAIN=go.domain.tld \
--env VANITY_NAMESPACE=<jolheiser> \
--env VANITY_TOKEN=<token> \
--publish 80:7777 \
--restart always
jolheiser/vanity:latest
```
## Overrides
Certain modules may not align perfectly with their repository name.
Overrides are available via config or by setting an environment variable `VANITY_OVERRIDE_PACKAGE=NAME`
## Config-only Mode
To run Vanity in config-only mode for packages, set `--service` to `off`.
## Manual Mode
To run Vanity without automatic updating, use `--manual`.
When running with manual-mode, the provided button or `/_/update` endpoint can be used once every `--interval`.
## Topic Lists
By setting `--topics`, anyone visiting the index page will see packages grouped by their topics.
Regardless of the setting, you can switch beteween list-view and topic-view with the provided button
or changing the URL between `?format=list` and `?format=topics`.
## API
In order to preserve namespaces for packages, Vanity's API uses the URL `/_/{endpoint}`
Vanity currently supports `/_/status` and `/_/update`, to get some status information and update the package cache respectively.
Check out the [SDK](go-vanity).
## License

View File

@ -1,25 +0,0 @@
package api
import (
"fmt"
"strings"
)
type Package struct {
Name string `toml:"name"`
Description string `toml:"description"`
Branch string `toml:"branch"`
WebURL string `toml:"web_url"`
CloneHTTP string `toml:"clone_http"`
CloneSSH string `toml:"clone_ssh"`
Topics []string `toml:"topics"`
Private bool `toml:"-"`
Fork bool `toml:"-"`
Mirror bool `toml:"-"`
Archive bool `toml:"-"`
}
func (p *Package) Module(domain string) string {
return fmt.Sprintf("%s/%s", domain, strings.ToLower(p.Name))
}

View File

@ -1,3 +0,0 @@
package api
var Version = "develop"

View File

@ -0,0 +1,36 @@
package main
import (
"context"
"net/url"
"go.jolheiser.com/vanity/sdk"
"github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog/log"
)
func add(token, server *string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
pkg, err := pkgPrompt(sdk.Package{})
if err != nil {
return err
}
client := sdk.New(*token, sdk.WithServer(*server))
if err := client.Add(context.Background(), pkg); err != nil {
return err
}
log.Info().Msgf("Added %q", pkg.Name)
return nil
}
}
func validURL(ans interface{}) error {
if err := survey.Required(ans); err != nil {
return err
}
_, err := url.Parse(ans.(string))
return err
}

View File

@ -0,0 +1,127 @@
package main
import (
"context"
"flag"
"fmt"
"net/url"
"strings"
"go.jolheiser.com/vanity/sdk"
"github.com/AlecAivazis/survey/v2"
"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/peterbourgon/ff/v3/fftoml"
)
func New() *ffcli.Command {
fs := flag.NewFlagSet("vanity", flag.ExitOnError)
serverFlag := fs.String("server", sdk.DefaultServer, "vanity server to use")
tokenFlag := fs.String("token", "", "vanity auth token to use")
cmd := &ffcli.Command{
Name: "vanity",
ShortUsage: "vanity [add|update|remove]",
ShortHelp: "Vanity CLI",
LongHelp: "Vanity CLI to work with a remote Vanity server",
FlagSet: fs,
Options: []ff.Option{
ff.WithEnvVarPrefix("VANITY"),
ff.WithAllowMissingConfigFile(true),
ff.WithConfigFileFlag("config"),
ff.WithConfigFileParser(fftoml.New().Parse),
},
Subcommands: []*ffcli.Command{
{
Name: "add",
ShortHelp: "add package",
LongHelp: "add a package to the vanity server",
Exec: add(tokenFlag, serverFlag),
},
{
Name: "remove",
ShortHelp: "remove package",
LongHelp: "remove a package from the vanity server",
Exec: remove(tokenFlag, serverFlag),
},
{
Name: "update",
ShortHelp: "update package",
LongHelp: "update a package on the vanity server",
Exec: update(tokenFlag, serverFlag),
},
},
}
return cmd
}
func listPackages(token, server string) ([]sdk.Package, error) {
client := sdk.New(token, sdk.WithServer(server))
info, err := client.Info(context.Background())
if err != nil {
return nil, err
}
return info.Packages, nil
}
func pkgPrompt(def sdk.Package) (sdk.Package, error) {
if def.Branch == "" {
def.Branch = "main"
}
var pkg sdk.Package
questions := []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{Message: "Name", Default: def.Name},
Validate: survey.Required,
},
{
Name: "description",
Prompt: &survey.Multiline{Message: "Description", Default: def.Description},
Validate: survey.Required,
},
{
Name: "branch",
Prompt: &survey.Input{Message: "Branch", Default: def.Branch},
Validate: survey.Required,
},
{
Name: "weburl",
Prompt: &survey.Input{Message: "Web URL", Default: def.WebURL},
Validate: validURL,
},
}
if err := survey.Ask(questions, &pkg); err != nil {
return pkg, err
}
defHTTP, defSSH := def.CloneHTTP, def.CloneSSH
if def.WebURL != pkg.WebURL {
u, err := url.Parse(pkg.WebURL)
if err != nil {
return pkg, err
}
defHTTP = pkg.WebURL + ".git"
defSSH = fmt.Sprintf("git@%s:%s.git", u.Host, strings.TrimPrefix(u.Path, "/"))
}
questions = []*survey.Question{
{
Name: "clonehttp",
Prompt: &survey.Input{Message: "HTTP(S) CLone URL", Default: defHTTP},
Validate: validURL,
},
{
Name: "clonessh",
Prompt: &survey.Input{Message: "SSH CLone URL", Default: defSSH},
Validate: survey.Required,
},
}
if err := survey.Ask(questions, &pkg); err != nil {
return pkg, err
}
return pkg, nil
}

View File

@ -0,0 +1,18 @@
package main
import (
"context"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
cmd := New()
if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
log.Fatal().Err(err).Msg("")
}
}

View File

@ -0,0 +1,48 @@
package main
import (
"context"
"go.jolheiser.com/vanity/sdk"
"github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog/log"
)
func remove(token, server *string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
pkgs, err := listPackages(*token, *server)
if err != nil {
return err
}
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]sdk.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
pkgQuestion := &survey.Select{
Message: "Select package to remove",
Options: pkgSlice,
}
var pkgName string
if err := survey.AskOne(pkgQuestion, &pkgName); err != nil {
return err
}
pkg := sdk.Package{
Name: pkgName,
}
client := sdk.New(*token, sdk.WithServer(*server))
if err := client.Remove(context.Background(), pkg); err != nil {
return err
}
log.Info().Msgf("Removed %q", pkgName)
return nil
}
}

View File

@ -0,0 +1,49 @@
package main
import (
"context"
"go.jolheiser.com/vanity/sdk"
"github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog/log"
)
func update(token, server *string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
pkgs, err := listPackages(*token, *server)
if err != nil {
return err
}
pkgSlice := make([]string, len(pkgs))
pkgMap := make(map[string]sdk.Package)
for idx, pkg := range pkgs {
pkgSlice[idx] = pkg.Name
pkgMap[pkg.Name] = pkg
}
pkgQuestion := &survey.Select{
Message: "Select package to update",
Options: pkgSlice,
}
var pkgName string
if err := survey.AskOne(pkgQuestion, &pkgName); err != nil {
return err
}
pkg, err := pkgPrompt(pkgMap[pkgName])
if err != nil {
return err
}
client := sdk.New(*token, sdk.WithServer(*server))
if err := client.Update(context.Background(), pkg); err != nil {
return err
}
log.Info().Msgf("Updated %q", pkgName)
return nil
}
}

View File

@ -0,0 +1,18 @@
package main
import (
"context"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
cmd := New()
if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
log.Fatal().Err(err).Msg("")
}
}

View File

@ -0,0 +1,75 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
"go.jolheiser.com/vanity/server/database"
"go.jolheiser.com/vanity/server/router"
"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/peterbourgon/ff/v3/fftoml"
"github.com/rs/zerolog/log"
)
func New() *ffcli.Command {
fs := flag.NewFlagSet("vanity", flag.ExitOnError)
portFlag := fs.Int("port", 3333, "Port to run the vanity server on")
domainFlag := fs.String("domain", "", "The Go module domain (e.g. go.jolheiser.com)")
tokenFlag := fs.String("token", "", "vanity auth token to use")
dbFlag := fs.String("database", dbPath(), "The path to the database")
cmd := &ffcli.Command{
Name: "vanity",
ShortUsage: "vanity",
ShortHelp: "Vanity Server",
LongHelp: "Vanity Server to serve the Go module vanity domain",
FlagSet: fs,
Options: []ff.Option{
ff.WithEnvVarPrefix("VANITY"),
ff.WithAllowMissingConfigFile(true),
ff.WithConfigFileFlag("config"),
ff.WithConfigFileParser(fftoml.New().Parse),
},
Exec: server(portFlag, domainFlag, tokenFlag, dbFlag),
}
return cmd
}
func server(port *int, domain, token, dbPath *string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
if *token == "" || *domain == "" {
return errors.New("vanity server requires --token and --domain")
}
db, err := database.Load(*dbPath)
if err != nil {
log.Fatal().Msgf("could not load database at %s: %v", *dbPath, err)
}
log.Info().Msgf("Running vanity server at http://localhost:%d", *port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), router.New(*token, *domain, db)); err != nil {
return err
}
return nil
}
}
func dbPath() string {
fn := "vanity.db"
home, err := os.UserHomeDir()
if err != nil {
bin, err := os.Executable()
if err != nil {
return fn
}
return filepath.Join(filepath.Dir(bin), fn)
}
return filepath.Join(home, fn)
}

View File

@ -0,0 +1,19 @@
[Unit]
Description=Vanity Go Imports
After=syslog.target
After=network.target
[Service]
RestartSec=2s
Type=simple
User=vanity
Group=vanity
ExecStart=${bin} server
Restart=always
# Required
Environment=VANITY_TOKEN=${VANITY_TOKEN}
Environment=VANITY_DOMAIN=${VANITY_DOMAIN}
[Install]
WantedBy=multi-user.target

View File

@ -1,4 +1,4 @@
FROM golang:1.15-alpine as builder
FROM golang:1.16-alpine as builder
RUN apk --no-cache add build-base git
COPY . /app
WORKDIR /app

View File

@ -5,10 +5,7 @@ services:
image: jolheiser/vanity:latest
environment:
- VANITY_DOMAIN=go.domain.tld
- VANITY_NAMESPACE=<jolheiser>
- VANITY_TOKEN=<token>
#- VANITY_SERVICE=gitea
#- VANITY_BASE_URL=https://gitea.com
restart: always
ports:
- "80:7777"

View File

@ -1,105 +0,0 @@
package flags
import (
"os"
"strings"
"time"
"go.jolheiser.com/vanity/api"
"github.com/pelletier/go-toml"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
type tomlConfig struct {
Port int `toml:"port"`
Domain string `toml:"domain"`
Service string `toml:"service"`
BaseURL string `toml:"base_url"`
Namespace string `toml:"namespace"`
Token string `toml:"token"`
Include []string `toml:"include"`
Exclude []string `toml:"exclude"`
Private bool `toml:"private"`
Fork bool `toml:"fork"`
Mirror bool `toml:"mirror"`
Archive bool `toml:"archive"`
Override []string `toml:"override"`
Interval time.Duration `toml:"interval"`
Debug bool `toml:"debug"`
Packages []*api.Package `toml:"packages"`
}
func setConfig(ctx *cli.Context) {
for _, env := range os.Environ() {
kv := strings.Split(env, "=")
if strings.HasPrefix(kv[0], "VANITY_OVERRIDES_") {
override := strings.ToLower(strings.TrimPrefix(kv[0], "VANITY_OVERRIDES_"))
Override[override] = kv[1]
}
}
var cfg tomlConfig
if configPath != "" {
beaver.Infof("Loading configuration from %s", configPath)
tree, err := toml.LoadFile(configPath)
if err != nil {
beaver.Errorf("Could not load configuration from %s: %v", configPath, err)
return
}
if err = tree.Unmarshal(&cfg); err != nil {
beaver.Errorf("Could not unmarshal configuration from %s: %v", configPath, err)
return
}
}
if !ctx.IsSet("port") && cfg.Port > 0 {
Port = cfg.Port
}
if !ctx.IsSet("domain") && cfg.Domain != "" {
Domain = cfg.Domain
}
if !ctx.IsSet("service") && cfg.Service != "" {
Service = cfg.Service
}
if !ctx.IsSet("base-url") && cfg.BaseURL != "" {
baseURL = cfg.BaseURL
}
if !ctx.IsSet("namespace") && cfg.Namespace != "" {
Namespace = cfg.Namespace
}
if !ctx.IsSet("token") && cfg.Token != "" {
Token = cfg.Token
}
if !ctx.IsSet("include") && len(cfg.Include) > 0 {
_ = include.Set(strings.Join(cfg.Include, ","))
}
if !ctx.IsSet("exclude") && len(cfg.Exclude) > 0 {
_ = exclude.Set(strings.Join(cfg.Exclude, ","))
}
if !ctx.IsSet("override") && len(cfg.Override) > 0 {
_ = override.Set(strings.Join(cfg.Override, ","))
}
if !ctx.IsSet("private") && cfg.Private {
Private = cfg.Private
}
if !ctx.IsSet("fork") && cfg.Fork {
Fork = cfg.Fork
}
if !ctx.IsSet("mirror") && cfg.Mirror {
Mirror = cfg.Mirror
}
if !ctx.IsSet("archive") && cfg.Archive {
Archive = cfg.Archive
}
if !ctx.IsSet("interval") && cfg.Interval.Seconds() > 0 {
Interval = cfg.Interval
}
if !ctx.IsSet("debug") && cfg.Debug {
Debug = cfg.Debug
}
ConfigPackages = cfg.Packages
}

View File

@ -1,236 +0,0 @@
package flags
import (
"errors"
"fmt"
"net/url"
"regexp"
"strings"
"time"
"go.jolheiser.com/vanity/api"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var (
configPath string
baseURL string
include cli.StringSlice
exclude cli.StringSlice
override cli.StringSlice
Port int
Domain string
Service string
BaseURL *url.URL
Namespace string
Token string
Include []*regexp.Regexp
Exclude []*regexp.Regexp
Private bool
Fork bool
Mirror bool
Archive bool
Override = make(map[string]string)
Interval time.Duration
Manual bool
Topics bool
Debug bool
ConfigPackages []*api.Package
)
var Flags = []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Path to a config file",
EnvVars: []string{"VANITY_CONFIG"},
Destination: &configPath,
},
&cli.IntFlag{
Name: "port",
Usage: "Port to run the vanity server on",
Value: 7777,
EnvVars: []string{"VANITY_PORT"},
Destination: &Port,
},
&cli.StringFlag{
Name: "domain",
Usage: "Vanity domain, e.g. go.domain.tld",
EnvVars: []string{"VANITY_DOMAIN"},
Required: true,
Destination: &Domain,
},
&cli.StringFlag{
Name: "service",
Usage: "Service type (Gitea, GitHub, GitLab)",
Value: "gitea",
EnvVars: []string{"VANITY_SERVICE"},
Destination: &Service,
},
&cli.StringFlag{
Name: "base-url",
Usage: "Base URL to service",
EnvVars: []string{"VANITY_BASE_URL"},
Destination: &baseURL,
},
&cli.StringFlag{
Name: "namespace",
Usage: "Owner namespace",
EnvVars: []string{"VANITY_NAMESPACE"},
Destination: &Namespace,
},
&cli.StringFlag{
Name: "token",
Usage: "Access token",
EnvVars: []string{"VANITY_TOKEN"},
Destination: &Token,
},
&cli.StringSliceFlag{
Name: "include",
Usage: "Repository names to include (regex)",
EnvVars: []string{"VANITY_INCLUDE"},
Destination: &include,
},
&cli.StringSliceFlag{
Name: "exclude",
Usage: "Repository names to exclude (regex)",
EnvVars: []string{"VANITY_EXCLUDE"},
Destination: &exclude,
},
&cli.BoolFlag{
Name: "private",
Usage: "Include private repositories",
EnvVars: []string{"VANITY_PRIVATE"},
Destination: &Private,
},
&cli.BoolFlag{
Name: "fork",
Usage: "Include forked repositories",
EnvVars: []string{"VANITY_FORK"},
Destination: &Fork,
},
&cli.BoolFlag{
Name: "mirror",
Usage: "Include mirrored repositories",
EnvVars: []string{"VANITY_MIRROR"},
Destination: &Mirror,
},
&cli.BoolFlag{
Name: "archive",
Usage: "Include archived repositories",
EnvVars: []string{"VANITY_ARCHIVE"},
Destination: &Archive,
},
&cli.StringSliceFlag{
Name: "override",
Usage: "Repository name to override (NAME=OVERRIDE)",
EnvVars: []string{"VANITY_OVERRIDE"},
Destination: &override,
},
&cli.DurationFlag{
Name: "interval",
Usage: "Interval between updating repositories",
Value: time.Minute * 15,
EnvVars: []string{"VANITY_INTERVAL"},
Destination: &Interval,
},
&cli.BoolFlag{
Name: "manual",
Usage: "Disable cron and only update with endpoint",
EnvVars: []string{"VANITY_MANUAL"},
Destination: &Manual,
},
&cli.BoolFlag{
Name: "topics",
Usage: "Group projects by topic by default",
EnvVars: []string{"VANITY_TOPICS"},
Destination: &Topics,
},
&cli.BoolFlag{
Name: "debug",
Usage: "Debug logging",
EnvVars: []string{"VANITY_DEBUG"},
Destination: &Debug,
},
}
func Before(ctx *cli.Context) error {
setConfig(ctx)
var defaultURL string
var configOnly bool
switch strings.ToLower(Service) {
case "gitea":
defaultURL = "https://gitea.com"
case "github":
defaultURL = "https://github.com"
case "gitlab":
defaultURL = "https://gitlab.com"
case "off":
configOnly = true
beaver.Infof("Running in config-only mode")
defaultURL = "https://domain.tld"
default:
return errors.New("unrecognized service type")
}
if baseURL == "" {
baseURL = defaultURL
}
var err error
BaseURL, err = url.Parse(baseURL)
if err != nil {
return err
}
if !configOnly {
errs := make([]string, 0, 2)
if Namespace == "" {
errs = append(errs, "namespace")
}
if Token == "" {
errs = append(errs, "token")
}
if len(errs) > 0 {
return fmt.Errorf("%s is required with a service", strings.Join(errs, ", "))
}
}
Include = make([]*regexp.Regexp, len(include.Value()))
for idx, i := range include.Value() {
Include[idx] = regexp.MustCompile(i)
}
Exclude = make([]*regexp.Regexp, len(exclude.Value()))
for idx, e := range exclude.Value() {
Exclude[idx] = regexp.MustCompile(e)
}
if Manual {
beaver.Info("Running in manual mode")
}
if Debug {
beaver.Console.Level = beaver.DEBUG
}
beaver.Debugf("Port: %d", Port)
beaver.Debugf("Domain: %s", Domain)
beaver.Debugf("Service: %s", Service)
beaver.Debugf("Base URL: %s", baseURL)
beaver.Debugf("Namespace: %s", Namespace)
beaver.Debugf("Include: %s", include.Value())
beaver.Debugf("Exclude: %s", exclude.Value())
beaver.Debugf("Private: %t", Private)
beaver.Debugf("Fork: %t", Fork)
beaver.Debugf("Mirror: %t", Mirror)
beaver.Debugf("Archive: %t", Archive)
beaver.Debugf("Override: %s", override.Value())
beaver.Debugf("Interval: %s", Interval)
beaver.Debugf("Manual: %t", Manual)
return nil
}

19
go.mod
View File

@ -3,17 +3,10 @@ module go.jolheiser.com/vanity
go 1.16
require (
code.gitea.io/sdk/gitea v0.13.2
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/cors v1.1.1 // indirect
github.com/google/go-github/v32 v32.1.0
github.com/pelletier/go-toml v1.8.1
github.com/urfave/cli/v2 v2.2.0
github.com/xanzy/go-gitlab v0.37.0
go.jolheiser.com/beaver v1.0.2
go.jolheiser.com/overlay v0.0.2 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
github.com/AlecAivazis/survey/v2 v2.2.8
github.com/go-chi/chi/v5 v5.0.3
github.com/peterbourgon/ff/v3 v3.1.2
github.com/rs/zerolog v1.26.1
go.etcd.io/bbolt v1.3.5
go.jolheiser.com/overlay v0.0.3
)

449
go.sum
View File

@ -1,413 +1,80 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
code.gitea.io/sdk/gitea v0.13.2 h1:wAnT/J7Z62q3fJXbgnecoaOBh8CM1Qq0/DakWxiv4yA=
code.gitea.io/sdk/gitea v0.13.2/go.mod h1:lee2y8LeV3kQb2iK+hHlMqoadL4bp27QOkOV/hawLKg=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlecAivazis/survey/v2 v2.2.8 h1:TgxCwybKdBckmC+/P9/5h49rw/nAHe/itZL0dgHs+Q0=
github.com/AlecAivazis/survey/v2 v2.2.8/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw=
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.6.4 h1:BbgctKO892xEyOXnGiaAwIoSq1QZ/SS4AhjoAh9DnfY=
github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4=
github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM=
github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/xanzy/go-gitlab v0.37.0 h1:Z/CQkjj5VwbWVYVL7S70kS/TFj5H/pJumV7xbJ0YUQ8=
github.com/xanzy/go-gitlab v0.37.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.jolheiser.com/beaver v1.0.2 h1:KA2D6iO8MQhZi1nZYi/Chak/f1Cxfrs6b1XO623+Khk=
go.jolheiser.com/beaver v1.0.2/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g=
go.jolheiser.com/overlay v0.0.2 h1:cwEHLbWqdH7lEOG87WUwgUGVqfOWBsWe03FiHHmuTWE=
go.jolheiser.com/overlay v0.0.2/go.mod h1:xNbssakJ3HjK4RnjuP38q9yQNS4wxXKsyprYIWWr2bg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.jolheiser.com/overlay v0.0.3 h1:5WoXtnRi1w5KkU9xq+si/wV0GINOMJphfSOvgG2l53I=
go.jolheiser.com/overlay v0.0.3/go.mod h1:xNbssakJ3HjK4RnjuP38q9yQNS4wxXKsyprYIWWr2bg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e h1:1SzTfNOXwIS2oWiMF+6qu0OUDKb0dauo6MoDUQyu+yU=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

47
main.go
View File

@ -1,47 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"go.jolheiser.com/vanity/api"
"go.jolheiser.com/vanity/flags"
"go.jolheiser.com/vanity/router"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
func main() {
app := cli.NewApp()
app.Name = "vanity"
app.Usage = "Vanity Go Imports"
app.Version = api.Version
app.Action = doAction
app.Flags = flags.Flags
app.Before = flags.Before
beaver.Console.Format = beaver.FormatOptions{
TimePrefix: true,
StackPrefix: true,
StackLimit: 15,
LevelPrefix: true,
LevelColor: true,
}
if err := app.Run(os.Args); err != nil {
beaver.Fatal(err)
}
}
func doAction(ctx *cli.Context) error {
mux, err := router.Init()
if err != nil {
return err
}
if err := http.ListenAndServe(fmt.Sprintf(":%s", ctx.String("port")), mux); err != nil {
return err
}
return nil
}

View File

@ -1,71 +0,0 @@
package router
import (
"encoding/json"
"github.com/go-chi/chi"
"github.com/go-chi/cors"
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/api"
"go.jolheiser.com/vanity/flags"
"net/http"
"runtime"
"time"
)
func apiRoutes() *chi.Mux {
r := chi.NewRouter()
r.Use(cors.AllowAll().Handler)
r.Get("/status", doAPIStatus)
r.Get("/update", doAPIUpdate)
return r
}
func doAPIStatus(res http.ResponseWriter, _ *http.Request) {
res.Header().Set("Content-Type", "application/json")
var nextUpdate *string
if !lastUpdate.IsZero() {
nu := lastUpdate.Add(flags.Interval).Format(time.RFC3339)
nextUpdate = &nu
}
resp := map[string]interface{}{
"vanity_version": api.Version,
"go_version": runtime.Version(),
"num_packages": len(cache.Packages),
"next_update": nextUpdate,
}
data, err := json.Marshal(&resp)
if err != nil {
beaver.Errorf("could not marshal API status: %v", err)
data = []byte("{}")
}
if _, err = res.Write(data); err != nil {
beaver.Errorf("could not write response: %v", err)
}
}
func doAPIUpdate(res http.ResponseWriter, _ *http.Request) {
res.Header().Set("Content-Type", "application/json")
resp := map[string]bool{
"updated": false,
}
if canUpdate {
updateCache()
resp["updated"] = true
}
payload, err := json.Marshal(resp)
if err != nil {
beaver.Errorf("could not marshal payload: %v", err)
}
if _, err = res.Write(payload); err != nil {
beaver.Errorf("could not write response: %v", err)
}
}

View File

@ -1,110 +0,0 @@
package router
import (
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/flags"
"go.jolheiser.com/vanity/service"
"strings"
"sync"
"go.jolheiser.com/vanity/api"
)
var cache = &packageCache{
Packages: make(map[string]*api.Package),
}
type packageList map[string]*api.Package
func (p packageList) Names() []string {
names := make([]string, len(p))
idx := 0
for name := range p {
names[idx] = name
idx++
}
return names
}
func (p packageList) Topics() map[string][]*api.Package {
topics := make(map[string][]*api.Package, 0)
for _, pkg := range p {
if len(pkg.Topics) == 0 {
if tt, ok := topics["other"]; ok {
topics["other"] = append(tt, pkg)
} else {
topics["other"] = []*api.Package{pkg}
}
}
for _, t := range pkg.Topics {
if tt, ok := topics[t]; ok {
topics[t] = append(tt, pkg)
} else {
topics[t] = []*api.Package{pkg}
}
}
}
return topics
}
type packageCache struct {
Packages packageList
sync.Mutex
}
func (c *packageCache) Update(packages map[string]*api.Package) {
c.Lock()
c.Packages = packages
c.Unlock()
}
func updateCache() {
packages, err := svc.Packages()
if err != nil {
beaver.Errorf("could not update packages: %v", err)
return
}
// Filter
for name, pkg := range packages {
if err := service.Check(pkg); err != nil {
beaver.Debug(err)
delete(packages, name)
continue
}
goMod, err := svc.GoMod(pkg)
if err != nil {
beaver.Debugf("No go.mod could be found in the root directory of %s", pkg.Name)
delete(packages, name)
continue
}
lines := strings.Split(goMod, "\n")
line := strings.Fields(lines[0])
if !strings.HasPrefix(line[1], flags.Domain) {
beaver.Debugf("%s is a Go project, however its module does not include this domain", pkg.Name)
delete(packages, name)
continue
}
beaver.Debugf("Including %s", pkg.Name)
}
// Overrides
for name, pkg := range packages {
for key, override := range flags.Override {
if strings.EqualFold(name, key) {
beaver.Debugf("Overriding %s -> %s", name, override)
delete(packages, key)
pkg.Name = override
packages[override] = pkg
}
}
}
// Add packages manually added to config
for _, pkg := range flags.ConfigPackages {
packages[pkg.Name] = pkg
}
cache.Update(packages)
canUpdate = false
}

View File

@ -1,30 +0,0 @@
package router
import (
"go.jolheiser.com/beaver"
"go.jolheiser.com/vanity/service"
"time"
"go.jolheiser.com/vanity/flags"
)
var (
svc service.Service
lastUpdate time.Time
canUpdate bool
)
func cronStart() {
canUpdate = true
ticker := time.NewTicker(flags.Interval)
for {
<-ticker.C
if !flags.Manual && canUpdate {
beaver.Debug("Running package update...")
updateCache()
beaver.Debugf("Finished package update: %s", cache.Packages.Names())
lastUpdate = time.Now()
}
canUpdate = true
}
}

View File

@ -1,92 +0,0 @@
package router
import (
"fmt"
"html/template"
"net/http"
"strings"
"time"
"go.jolheiser.com/vanity/flags"
"go.jolheiser.com/vanity/service"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"go.jolheiser.com/beaver"
)
var tmpl *template.Template
func Init() (*chi.Mux, error) {
var err error
tmpl, err = Templates()
if err != nil {
return nil, err
}
r := chi.NewRouter()
r.Use(middleware.RedirectSlashes)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
r.Get("/", doIndex)
r.Get("/*", doVanity)
r.Mount("/_", apiRoutes())
svc = service.New()
beaver.Info("Warming up cache...")
updateCache()
beaver.Infof("Finished warming up cache: %s", cache.Packages.Names())
go cronStart()
beaver.Infof("Running vanity server at http://localhost:%d", flags.Port)
return r, nil
}
func doIndex(res http.ResponseWriter, req *http.Request) {
format := "list"
if flags.Topics {
format = "topics"
}
q := req.URL.Query().Get("format")
if q != "" {
format = q
}
if err := tmpl.Lookup("index.tmpl").Execute(res, map[string]interface{}{
"Packages": cache.Packages,
"Index": true,
"Format": format,
}); err != nil {
beaver.Errorf("could not write response: %v", err)
}
}
func doVanity(res http.ResponseWriter, req *http.Request) {
key := chi.URLParam(req, "*")
pkg, ok := cache.Packages[strings.Split(key, "/")[0]]
if !ok {
http.NotFound(res, req)
return
}
ctx := map[string]interface{}{
"Package": pkg,
"Module": pkg.Module(flags.Domain),
"GoSource": fmt.Sprintf("%s %s %s %s", pkg.Module(flags.Domain), pkg.CloneHTTP, svc.GoDir(pkg), svc.GoFile(pkg)),
"Index": false,
}
q := req.URL.Query()
if q.Get("go-get") != "" || q.Get("git-import") != "" {
if err := tmpl.Lookup("import.tmpl").Execute(res, ctx); err != nil {
beaver.Errorf("could not write response: %v", err)
}
return
}
if err := tmpl.Lookup("vanity.tmpl").Execute(res, ctx); err != nil {
beaver.Errorf("could not write response: %v", err)
}
}

View File

@ -1,44 +0,0 @@
package router
import (
"embed"
"go.jolheiser.com/overlay"
"go.jolheiser.com/vanity/api"
"html/template"
"os"
"path/filepath"
"runtime"
)
//go:embed templates
var templates embed.FS
func Templates() (*template.Template, error) {
bin, err := os.Executable()
if err != nil {
return nil, err
}
customPath := os.Getenv("VANITY_CUSTOM")
if customPath == "" {
customPath = filepath.Join(bin, "custom")
}
ofs, err := overlay.New(customPath, templates)
if err != nil {
return nil, err
}
return template.New("vanity").Funcs(funcMap).ParseFS(ofs, "templates/*")
}
var funcMap = template.FuncMap{
"AppVer": func() string {
return api.Version
},
"GoVer": func() string {
return runtime.Version()
},
"CanUpdate": func() bool {
return canUpdate
},
}

View File

@ -1,25 +0,0 @@
<hr/>
<button id="update-imports" type="button" {{if not CanUpdate}}disabled{{end}}>Update Imports</button>
<br/><br/>
<strong>Vanity Version:</strong>
{{AppVer}}
<br/><br/>
<strong>Go Version:</strong>
{{GoVer}}
<script>
const updateImports = document.querySelector('#update-imports');
updateImports.addEventListener('click', () => {
updateImports.disabled = true;
updateImports.innerHTML = 'Updating...';
fetch('{{if .Index}}{{else}}../{{end}}_/update', {
method: 'GET'
}).then(() => {
location.reload();
}).catch(() => {
updateImports.innerHTML = 'Update Failed';
});
});
</script>
</body>
</html>

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{{if .Package}}
<!-- OGP -->
<meta name="og:title" content="{{.Package.Name}}"/>
<meta name="og:description" content="{{.Package.Description}}"/>
<!-- Go -->
<meta name="go-import" content="{{.Module}} git {{.Package.CloneHTTP}}"/>
<meta name="go-source" content="{{.GoSource}}">
<!-- Git Import -->
<meta name="git-import" content="{{.Package.Name}} {{.Package.CloneHTTP}} {{.Package.CloneSSH}}"/>
{{end}}
<title>Vanity - {{if .Package}}{{.Package.Name}}{{else}}Index{{end}}</title>
</head>
<body>
<h1><a href="{{if .Index}}.{{else}}../{{end}}">Index</a></h1>
<hr/>

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Go -->
<meta name="go-import" content="{{.Module}} git {{.Package.CloneHTTP}}"/>
<meta name="go-source" content="{{.GoSource}}">
<!-- Git Import -->
<meta name="git-import" content="{{.Package.Name}} {{.Package.CloneHTTP}} {{.Package.CloneSSH}}"/>
<title>{{.Module}}</title>
</head>
<body>
<code>go get {{.Module}}</code>
<br/>
<code>git-import {{.Module}}</code>
</body>
</html>

View File

@ -1,29 +0,0 @@
{{template "head.tmpl" .}}
<h3>{{if eq .Format "list"}}Imports{{else}}Topics{{end}}:</h3>
{{if eq .Format "list"}}
<ul>
{{range $path, $package := .Packages}}
<li><a href="{{$package.Name}}">{{$package.Name}}</a></li>
{{end}}
</ul>
{{else}}
{{range $topic, $packages := .Packages.Topics}}
<details>
<summary>{{$topic}}</summary>
<ul>
{{range $package := $packages}}
<li><a href="{{$package.Name}}">{{$package.Name}}</a></li>
{{end}}
</ul>
</details>
{{end}}
{{end}}
<br/>
<form method="get">
{{if eq .Format "list"}}
<button type="submit" name="format" value="topics">See Topics</button>
{{else}}
<button type="submit" name="format" value="list"> See List</button>
{{end}}
</form>
{{template "foot.tmpl" .}}

53
sdk/client.go 100644
View File

@ -0,0 +1,53 @@
package sdk
import (
"context"
"io"
"net/http"
)
const (
DefaultServer = "https://go.jolheiser.com"
TokenHeader = "X-Vanity-Token"
)
type Client struct {
token string
server string
http *http.Client
}
func New(token string, opts ...ClientOption) *Client {
c := &Client{
token: token,
server: DefaultServer,
http: http.DefaultClient,
}
for _, opt := range opts {
opt(c)
}
return c
}
type ClientOption func(*Client)
func WithHTTP(client *http.Client) ClientOption {
return func(c *Client) {
c.http = client
}
}
func WithServer(server string) ClientOption {
return func(c *Client) {
c.server = server
}
}
func (c *Client) newRequest(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
req.Header.Set(TokenHeader, c.token)
return req, nil
}

104
sdk/package.go 100644
View File

@ -0,0 +1,104 @@
package sdk
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
)
type Info struct {
Version string `json:"version"`
NumPackages int `json:"num_packages"`
Packages []Package `json:"packages"`
}
type Package struct {
Name string `json:"name"`
Description string `json:"description"`
Branch string `json:"branch"`
WebURL string `json:"web_url"`
CloneHTTP string `json:"clone_http"`
CloneSSH string `json:"clone_ssh"`
}
func (p Package) Module(domain string) string {
return fmt.Sprintf("%s/%s", strings.TrimSuffix(domain, "/"), strings.ToLower(p.Name))
}
// Info gets Info from a vanity server
func (c *Client) Info(ctx context.Context) (Info, error) {
var info Info
resp, err := c.crud(ctx, Package{}, http.MethodOptions)
if err != nil {
return info, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return info, fmt.Errorf("could not get info: %s", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return info, err
}
return info, nil
}
// Add adds a new Package to a vanity server
func (c *Client) Add(ctx context.Context, pkg Package) error {
resp, err := c.crud(ctx, pkg, http.MethodPost)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("could not add package: %s", resp.Status)
}
return nil
}
// Update updates a Package on a vanity server
func (c *Client) Update(ctx context.Context, pkg Package) error {
resp, err := c.crud(ctx, pkg, http.MethodPatch)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("could not update package: %s", resp.Status)
}
return nil
}
// Remove removes a Package from a vanity server
func (c *Client) Remove(ctx context.Context, pkg Package) error {
resp, err := c.crud(ctx, pkg, http.MethodDelete)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("could not remove package: %s", resp.Status)
}
return nil
}
func (c *Client) crud(ctx context.Context, pkg Package, method string) (*http.Response, error) {
payload, err := json.Marshal(pkg)
if err != nil {
return nil, err
}
req, err := c.newRequest(ctx, method, c.server, bytes.NewReader(payload))
if err != nil {
return nil, err
}
return c.http.Do(req)
}

45
sdk/source.go 100644
View File

@ -0,0 +1,45 @@
package sdk
import (
"errors"
"fmt"
"strings"
)
type SourceDirFile struct {
Dir string
File string
}
func GiteaSDF(pkg Package) SourceDirFile {
return SourceDirFile{
Dir: fmt.Sprintf("%s/src/branch/%s{/dir}", pkg.WebURL, pkg.Branch),
File: fmt.Sprintf("%s/src/branch/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch),
}
}
func GitHubSDF(pkg Package) SourceDirFile {
return SourceDirFile{
Dir: fmt.Sprintf("%s/tree/%s{/dir}", pkg.WebURL, pkg.Branch),
File: fmt.Sprintf("%s/blob/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch),
}
}
func GitLabSDF(pkg Package) SourceDirFile {
return SourceDirFile{
Dir: fmt.Sprintf("%s/-/tree/%s{/dir}", pkg.WebURL, pkg.Branch),
File: fmt.Sprintf("%s/-/blob/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch),
}
}
func AnalyzeSDF(pkg Package) (SourceDirFile, error) {
switch {
case strings.Contains(pkg.WebURL, "gitea.com"):
return GiteaSDF(pkg), nil
case strings.Contains(pkg.WebURL, "github.com"):
return GitHubSDF(pkg), nil
case strings.Contains(pkg.WebURL, "gitlab.com"):
return GitLabSDF(pkg), nil
}
return SourceDirFile{}, errors.New("could not detect SDF")
}

194
sdk/vanity_test.go 100644
View File

@ -0,0 +1,194 @@
package sdk
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
var (
server *httptest.Server
token = "TestingLibrary"
version = "VanityTest"
packages = []Package{
{
Name: "test1",
Description: "test1",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test1",
CloneHTTP: "https://gitea.com/jolheiser/test1.git",
CloneSSH: "https://gitea.com/jolheiser/test1",
},
}
)
func TestMain(m *testing.M) {
server = httptest.NewServer(http.HandlerFunc(testServer))
os.Exit(m.Run())
}
func TestClient(t *testing.T) {
ctx := context.Background()
client := New("", WithServer(server.URL))
// Info
checkInfo(t, client, 1)
pkg1 := Package{
Name: "test1",
Description: "test1",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test1",
CloneHTTP: "https://gitea.com/jolheiser/test1.git",
CloneSSH: "https://gitea.com/jolheiser/test1",
}
pkg2 := Package{
Name: "test2",
Description: "test2",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test2",
CloneHTTP: "https://gitea.com/jolheiser/test2.git",
CloneSSH: "https://gitea.com/jolheiser/test2",
}
// Add (without token)
if err := client.Add(ctx, pkg1); err == nil {
t.Log("adding without token should fail")
t.Fail()
}
// Add (with token)
client = New(token, WithServer(server.URL))
checkAdd(t, client, pkg1, pkg2)
// Info (after second package)
checkInfo(t, client, 2)
// Update package
checkUpdate(t, client, pkg1)
// Remove
checkRemove(t, client, pkg1)
// Info (final)
checkInfo(t, client, 1)
}
func checkInfo(t *testing.T, client *Client, numPackages int) {
info, err := client.Info(context.Background())
if err != nil {
t.Logf("info should not return error: %v\n", err)
t.Fail()
}
if info.Version != version || info.NumPackages != numPackages {
t.Log("info did not match expected")
t.Fail()
}
}
func checkAdd(t *testing.T, client *Client, pkg1, pkg2 Package) {
ctx := context.Background()
if err := client.Add(ctx, pkg2); err != nil {
t.Logf("pkg2 should be added: %v\n", err)
t.Fail()
}
// Duplicate package
if err := client.Add(ctx, pkg1); err == nil {
t.Log("pkg1 should already exist")
t.Fail()
}
}
func checkUpdate(t *testing.T, client *Client, pkg Package) {
ctx := context.Background()
// Update invalid package
if err := client.Update(ctx, Package{Name: "test4"}); err == nil {
t.Log("should not be able to update invalid package")
t.Fail()
}
// Update valid package
if err := client.Update(ctx, pkg); err != nil {
t.Logf("should be able to update valid package: %v\n", err)
t.Fail()
}
}
func checkRemove(t *testing.T, client *Client, pkg Package) {
ctx := context.Background()
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package: %v\n", err)
t.Fail()
}
// Remove (idempotent)
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package idempotently: %v\n", err)
t.Fail()
}
}
func testServer(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
switch r.Method {
case http.MethodOptions:
resp := Info{
Version: version,
NumPackages: len(packages),
}
_ = json.NewEncoder(w).Encode(resp)
case http.MethodPost, http.MethodPatch, http.MethodDelete:
if r.Header.Get(TokenHeader) != token {
w.WriteHeader(http.StatusUnauthorized)
return
}
var pkg Package
if err := json.NewDecoder(r.Body).Decode(&pkg); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
switch r.Method {
case http.MethodPost:
for _, p := range packages {
if p.Name == pkg.Name {
w.WriteHeader(http.StatusConflict)
return
}
}
packages = append(packages, pkg)
w.WriteHeader(http.StatusCreated)
case http.MethodPatch:
for idx, p := range packages {
if p.Name == pkg.Name {
packages[idx] = pkg
return
}
}
w.WriteHeader(http.StatusNotFound)
case http.MethodDelete:
for idx, p := range packages {
if p.Name == pkg.Name {
packages = append(packages[:idx], packages[idx+1:]...)
}
}
}
return
}
return
default:
name := strings.TrimPrefix(r.URL.Path, "/")
for _, pkg := range packages {
if pkg.Name == name {
_ = json.NewEncoder(w).Encode(pkg)
return
}
}
}
w.WriteHeader(http.StatusNotImplemented)
}

View File

@ -0,0 +1,84 @@
package database
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"go.jolheiser.com/vanity/sdk"
"go.etcd.io/bbolt"
)
var packageBucket = []byte("packages")
type Database struct {
db *bbolt.DB
}
func Load(dbPath string) (*Database, error) {
if err := os.MkdirAll(filepath.Dir(dbPath), os.ModePerm); err != nil {
return nil, err
}
db, err := bbolt.Open(dbPath, os.ModePerm, nil)
if err != nil {
return nil, err
}
return &Database{
db: db,
}, db.Update(func(tx *bbolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(packageBucket)
return err
})
}
func (d *Database) Package(name string) (sdk.Package, error) {
var pkg sdk.Package
data, err := d.PackageJSON(name)
if err != nil {
return pkg, err
}
return pkg, json.NewDecoder(bytes.NewReader(data)).Decode(&pkg)
}
func (d *Database) PackageJSON(name string) (pkg []byte, err error) {
return pkg, d.db.View(func(tx *bbolt.Tx) error {
pkg = tx.Bucket(packageBucket).Get([]byte(name))
if pkg == nil {
return ErrPackageNotFound{
Name: name,
}
}
return nil
})
}
func (d *Database) Packages() (pkgs []sdk.Package, err error) {
return pkgs, d.db.View(func(tx *bbolt.Tx) error {
return tx.Bucket(packageBucket).ForEach(func(key, val []byte) error {
var pkg sdk.Package
if err := json.NewDecoder(bytes.NewReader(val)).Decode(&pkg); err != nil {
return err
}
pkgs = append(pkgs, pkg)
return nil
})
})
}
func (d *Database) PutPackage(pkg sdk.Package) error {
return d.db.Update(func(tx *bbolt.Tx) error {
data, err := json.Marshal(pkg)
if err != nil {
return err
}
return tx.Bucket(packageBucket).Put([]byte(pkg.Name), data)
})
}
func (d *Database) RemovePackage(name string) error {
return d.db.Update(func(tx *bbolt.Tx) error {
return tx.Bucket(packageBucket).Delete([]byte(name))
})
}

View File

@ -0,0 +1,16 @@
package database
import "fmt"
type ErrPackageNotFound struct {
Name string
}
func (e ErrPackageNotFound) Error() string {
return fmt.Sprintf("package not found: %s", e.Name)
}
func IsErrPackageNotFound(err error) bool {
_, ok := err.(ErrPackageNotFound)
return ok
}

View File

@ -0,0 +1,208 @@
package router
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"go.jolheiser.com/vanity/sdk"
"go.jolheiser.com/vanity/server/database"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
)
func New(token, domain string, db *database.Database) *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
r.Mount("/_/", http.StripPrefix("/_/", static()))
r.Get("/", indexGET(domain, db))
r.Options("/", infoPackages(db))
r.Post("/", addUpdatePackage(db, token))
r.Patch("/", addUpdatePackage(db, token))
r.Delete("/", removePackage(db, token))
r.Get("/*", vanityGET(domain, db))
return r
}
func indexGET(domain string, db *database.Database) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
packages, err := db.Packages()
if err != nil {
log.Error().Msgf("could not load packages: %v", err)
http.Error(res, "could not load packages", http.StatusInternalServerError)
return
}
tpl, err := tmpl(domain, "index.tmpl")
if err != nil {
log.Warn().Msgf("could not load index template: %v", err)
}
if err := tpl.Execute(res, map[string]interface{}{
"Packages": packages,
"Index": true,
}); err != nil {
log.Error().Msgf("could not write response: %v", err)
}
}
}
func vanityGET(domain string, db *database.Database) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
key := chi.URLParam(req, "*")
key = strings.Split(key, "/")[0]
pkg, err := db.Package(key)
if err != nil {
if database.IsErrPackageNotFound(err) {
http.NotFound(res, req)
return
}
http.Error(res, "could not load package", http.StatusInternalServerError)
return
}
sdf, err := sdk.AnalyzeSDF(pkg)
if err != nil {
log.Warn().Msgf("could not get SDF for %s: %v", key, err)
}
ctx := map[string]interface{}{
"Package": pkg,
"Module": pkg.Module(domain),
"GoSource": fmt.Sprintf("%s %s %s %s", pkg.Module(domain), pkg.CloneHTTP, sdf.Dir, sdf.File),
"Index": false,
}
q := req.URL.Query()
if q.Get("go-get") != "" || q.Get("git-import") != "" {
tpl, err := tmpl(domain, "import.tmpl")
if err != nil {
log.Warn().Msgf("could not load import template: %v", err)
}
if err := tpl.Execute(res, ctx); err != nil {
log.Error().Msgf("could not write response: %v", err)
}
return
}
tpl, err := tmpl(domain, "vanity.tmpl")
if err != nil {
log.Warn().Msgf("could not load vanity template: %v", err)
}
if err := tpl.Execute(res, ctx); err != nil {
log.Error().Msgf("could not write response: %v", err)
}
}
}
func infoPackages(db *database.Database) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
packages, err := db.Packages()
if err != nil {
http.Error(res, "could not load package", http.StatusInternalServerError)
return
}
info := sdk.Info{
Version: Version,
NumPackages: len(packages),
Packages: packages,
}
if err := json.NewEncoder(res).Encode(info); err != nil {
http.Error(res, "could not marshal info", http.StatusInternalServerError)
}
}
}
func addUpdatePackage(db *database.Database, token string) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
if req.Header.Get(sdk.TokenHeader) != token {
res.WriteHeader(http.StatusUnauthorized)
return
}
data, err := io.ReadAll(req.Body)
if err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
var pkg sdk.Package
if err := json.Unmarshal(data, &pkg); err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
exists, err := db.PackageJSON(pkg.Name)
if err != nil && !database.IsErrPackageNotFound(err) {
res.WriteHeader(http.StatusInternalServerError)
return
}
switch req.Method {
case http.MethodPost:
if exists != nil {
res.WriteHeader(http.StatusConflict)
return
}
case http.MethodPatch:
if exists == nil {
res.WriteHeader(http.StatusNotFound)
return
}
}
if err := db.PutPackage(pkg); err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
}
switch req.Method {
case http.MethodPost:
res.WriteHeader(http.StatusCreated)
case http.MethodPatch:
res.WriteHeader(http.StatusOK)
}
}
}
func removePackage(db *database.Database, token string) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
if req.Header.Get(sdk.TokenHeader) != token {
res.WriteHeader(http.StatusUnauthorized)
return
}
data, err := io.ReadAll(req.Body)
if err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
var pkg sdk.Package
if err := json.Unmarshal(data, &pkg); err != nil {
res.WriteHeader(http.StatusBadRequest)
return
}
if err := db.RemovePackage(pkg.Name); err != nil {
res.WriteHeader(http.StatusInternalServerError)
return
}
res.WriteHeader(http.StatusOK)
}
}

View File

@ -0,0 +1,175 @@
package router
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"go.jolheiser.com/vanity/sdk"
"go.jolheiser.com/vanity/server/database"
"github.com/rs/zerolog/log"
)
var (
server *httptest.Server
token = "TestingRouter"
)
// NOTE: The router test is more or less a copy/paste from go-vanity
// However, this ensures that testing is the same with the "real" router and DB
func TestMain(m *testing.M) {
tmp, err := os.MkdirTemp(os.TempDir(), "vanity")
if err != nil {
panic(err)
}
dbPath := filepath.Join(tmp, "vanity.db")
db, err := database.Load(dbPath)
if err != nil {
log.Fatal().Msgf("could not load database at %s: %v", dbPath, err)
}
server = httptest.NewServer(New(token, "", db))
code := m.Run()
// Cleanup
if err := os.RemoveAll(tmp); err != nil {
panic(err)
}
os.Exit(code)
}
func TestRouter(t *testing.T) {
ctx := context.Background()
client := sdk.New("", sdk.WithServer(server.URL))
// Info
checkInfo(t, client, 0)
pkg1 := sdk.Package{
Name: "test1",
Description: "test1",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test1",
CloneHTTP: "https://gitea.com/jolheiser/test1.git",
CloneSSH: "https://gitea.com/jolheiser/test1",
}
pkg2 := sdk.Package{
Name: "test2",
Description: "test2",
Branch: "main",
WebURL: "https://gitea.com/jolheiser/test2",
CloneHTTP: "https://gitea.com/jolheiser/test2.git",
CloneSSH: "https://gitea.com/jolheiser/test2",
}
// Add (without token)
if err := client.Add(ctx, pkg1); err == nil {
t.Log("adding without token should fail")
t.Fail()
}
// Add (with token)
client = sdk.New(token, sdk.WithServer(server.URL))
checkAdd(t, client, pkg1, pkg2)
// Info (after second package)
checkInfo(t, client, 2)
// Check invalid package (404)
checkResp(t, "test3", http.StatusNotFound)
// Check valid package (200)
checkResp(t, "test1", http.StatusOK)
// Check valid sub-package (200)
checkResp(t, "test1/foo/bar", http.StatusOK)
// Update package
checkUpdate(t, client, pkg1)
// Remove
checkRemove(t, client, pkg1)
// Info (final)
checkInfo(t, client, 1)
}
func checkInfo(t *testing.T, client *sdk.Client, numPackages int) {
info, err := client.Info(context.Background())
if err != nil {
t.Logf("info should not return error: %v\n", err)
t.Fail()
}
if info.Version != Version || info.NumPackages != numPackages {
t.Log("info did not match expected")
t.Fail()
}
}
func checkAdd(t *testing.T, client *sdk.Client, pkg1, pkg2 sdk.Package) {
ctx := context.Background()
if err := client.Add(ctx, pkg1); err != nil {
t.Logf("pkg1 should be added: %v\n", err)
t.Fail()
}
if err := client.Add(ctx, pkg2); err != nil {
t.Logf("pkg2 should be added: %v\n", err)
t.Fail()
}
// Duplicate package
if err := client.Add(ctx, pkg1); err == nil {
t.Log("pkg1 should already exist")
t.Fail()
}
}
func checkUpdate(t *testing.T, client *sdk.Client, pkg sdk.Package) {
ctx := context.Background()
// Update invalid package
if err := client.Update(ctx, sdk.Package{Name: "test4"}); err == nil {
t.Log("should not be able to update invalid package")
t.Fail()
}
// Update valid package
if err := client.Update(ctx, pkg); err != nil {
t.Logf("should be able to update valid package: %v\n", err)
t.Fail()
}
}
func checkRemove(t *testing.T, client *sdk.Client, pkg sdk.Package) {
ctx := context.Background()
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package: %v\n", err)
t.Fail()
}
// Remove (idempotent)
if err := client.Remove(ctx, pkg); err != nil {
t.Logf("should be able to remove package idempotently: %v\n", err)
t.Fail()
}
}
func checkResp(t *testing.T, path string, status int) {
resp, err := http.Get(fmt.Sprintf("%s/%s", server.URL, path))
if err != nil {
t.Logf("could not GET %s: %v", path, err)
t.Fail()
return
}
if resp.StatusCode != status {
t.Logf("incorrect response from %s, expected %d but got %d", path, status, resp.StatusCode)
t.Fail()
}
}

View File

@ -0,0 +1,59 @@
package router
import (
"embed"
"html/template"
"io/fs"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"go.jolheiser.com/overlay"
)
var (
//go:embed templates
templateFS embed.FS
ofs = overlay.MustNew(customRoot(), templateFS)
Version string
)
func customRoot() string {
bin, err := os.Executable()
if err != nil {
bin = ""
}
customPath := os.Getenv("VANITY_CUSTOM")
if customPath == "" {
customPath = filepath.Join(bin, "custom")
}
return customPath
}
func tmpl(domain, name string) (*template.Template, error) {
return template.New(name).Funcs(funcMap(domain)).ParseFS(ofs, "templates/base.tmpl", "templates/"+name)
}
func static() http.Handler {
sub, err := fs.Sub(ofs, "templates/static")
if err != nil {
return nil
}
return http.FileServer(http.FS(sub))
}
func funcMap(domain string) template.FuncMap {
return template.FuncMap{
"AppVer": func() string {
return Version
},
"GoVer": func() string {
return runtime.Version()
},
"Domain": func() string {
return strings.TrimSuffix(domain, "/")
},
}
}

View File

@ -0,0 +1,34 @@
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{{if .Package}}
<!-- OGP -->
<meta name="og:title" content="{{.Package.Name}}"/>
<meta name="og:description" content="{{.Package.Description}}"/>
<!-- Go -->
<meta name="go-import" content="{{.Module}} git {{.Package.CloneHTTP}}"/>
<meta name="go-source" content="{{.GoSource}}">
<!-- Git Import -->
<meta name="git-import" content="{{.Package.Name}} {{.Package.CloneHTTP}} {{.Package.CloneSSH}}"/>
{{end}}
<title>Vanity - {{if .Package}}{{.Package.Name}}{{else}}Index{{end}}</title>
<link rel="stylesheet" href="/_/sakura.css">
</head>
<body>
<h1><a href="{{if .Index}}.{{else}}../{{end}}">Index</a></h1>
<hr/>
{{block "content" .}}{{end}}
<hr/>
<strong>Vanity Version:</strong>
{{AppVer}}
<br/><br/>
<strong>Go Version:</strong>
{{GoVer}}
</body>
</html>
{{end}}

View File

@ -0,0 +1,6 @@
{{template "base" .}}
{{define "content"}}
<code>go get {{.Module}}</code>
<br/>
<code>git-import {{.Module}}</code>
{{end}}

View File

@ -0,0 +1,9 @@
{{template "base" .}}
{{define "content"}}
<h3>Imports:</h3>
<ul>
{{range $path, $package := .Packages}}
<li><a href="{{$package.Name}}">{{$package.Name}}</a></li>
{{end}}
</ul>
{{end}}

View File

@ -0,0 +1,202 @@
/* Sakura.css v1.3.1
* ================
* Minimal css theme.
* Project: https://github.com/oxalorg/sakura/
*/
/* Body */
:root {
--color-blossom: #1d7484;
--color-fade: #982c61;
--color-bg: #f9f9f9;
--color-bg-alt: #f1f1f1;
--color-text: #4a4a4a;
}
@media (prefers-color-scheme: dark) {
:root {
--color-blossom: #ffffff;
--color-fade: #c9c9c9;
--color-bg: #222222;
--color-bg-alt: #4a4a4a;
--color-text: #c9c9c9;
}
}
html {
font-size: 62.5%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
body {
font-size: 1.8rem;
line-height: 1.618;
max-width: 38em;
margin: auto;
color: var(--color-text);
background-color: var(--color-bg);
padding: 13px; }
@media (max-width: 684px) {
body {
font-size: 1.53rem; } }
@media (max-width: 382px) {
body {
font-size: 1.35rem; } }
h1, h2, h3, h4, h5, h6 {
line-height: 1.1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-weight: 700;
margin-top: 3rem;
margin-bottom: 1.5rem;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word; }
h1 {
font-size: 2.35em; }
h2 {
font-size: 2.00em; }
h3 {
font-size: 1.75em; }
h4 {
font-size: 1.5em; }
h5 {
font-size: 1.25em; }
h6 {
font-size: 1em; }
p {
margin-top: 0px;
margin-bottom: 2.5rem; }
small, sub, sup {
font-size: 75%; }
hr {
border-color: var(--color-blossom); }
a {
text-decoration: none;
color: var(--color-blossom); }
a:hover {
color: var(--color-fade);
border-bottom: 2px solid var(--color-text); }
a:visited {
color: var(--color-blossom); }
ul {
padding-left: 1.4em;
margin-top: 0px;
margin-bottom: 2.5rem; }
li {
margin-bottom: 0.4em; }
blockquote {
margin-left: 0px;
margin-right: 0px;
padding-left: 1em;
padding-top: 0.8em;
padding-bottom: 0.8em;
padding-right: 0.8em;
border-left: 5px solid var(--color-blossom);
margin-bottom: 2.5rem;
background-color: var(--color-bg-alt); }
blockquote p {
margin-bottom: 0; }
img, video {
height: auto;
max-width: 100%;
margin-top: 0px;
margin-bottom: 2.5rem; }
/* Pre and Code */
pre {
background-color: var(--color-bg-alt);
display: block;
padding: 1em;
overflow-x: auto;
margin-top: 0px;
margin-bottom: 2.5rem; }
code {
font-size: 0.9em;
padding: 0 0.5em;
background-color: var(--color-bg-alt);
white-space: pre-wrap; }
pre > code {
padding: 0;
background-color: transparent;
white-space: pre; }
/* Tables */
table {
text-align: justify;
width: 100%;
border-collapse: collapse; }
td, th {
padding: 0.5em;
border-bottom: 1px solid var(--color-bg-alt); }
/* Buttons, forms and input */
input, textarea {
border: 1px solid var(--color-text); }
input:focus, textarea:focus {
border: 1px solid var(--color-blossom); }
textarea {
width: 100%; }
.button, button, input[type="submit"], input[type="reset"], input[type="button"] {
display: inline-block;
padding: 5px 10px;
text-align: center;
text-decoration: none;
white-space: nowrap;
background-color: var(--color-blossom);
color: var(--color-bg);
border-radius: 1px;
border: 1px solid var(--color-blossom);
cursor: pointer;
box-sizing: border-box; }
.button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] {
cursor: default;
opacity: .5; }
.button:focus:enabled, .button:hover:enabled, button:focus:enabled, button:hover:enabled, input[type="submit"]:focus:enabled, input[type="submit"]:hover:enabled, input[type="reset"]:focus:enabled, input[type="reset"]:hover:enabled, input[type="button"]:focus:enabled, input[type="button"]:hover:enabled {
background-color: var(--color-fade);
border-color: var(--color-fade);
color: var(--color-bg);
outline: 0; }
textarea, select, input {
color: var(--color-text);
padding: 6px 10px;
/* The 6px vertically centers text on FF, ignored by Webkit */
margin-bottom: 10px;
background-color: var(--color-bg-alt);
border: 1px solid var(--color-bg-alt );
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
textarea:focus, select:focus, input:focus {
border: 1px solid var(--color-blossom);
outline: 0; }
input[type="checkbox"]:focus {
outline: 1px dotted var(--color-blossom); }
label, legend, fieldset {
display: block;
margin-bottom: .5rem;
font-weight: 600; }

View File

@ -1,4 +1,5 @@
{{template "head.tmpl" .}}
{{template "base" .}}
{{define "content"}}
<p>
<strong>Name:</strong>
{{.Package.Name}}
@ -17,4 +18,4 @@
<strong>Documentation:</strong>
<a href="https://pkg.go.dev/{{.Module}}">https://pkg.go.dev/{{.Module}}</a>
</p>
{{template "foot.tmpl" .}}
{{end}}

View File

@ -1,95 +0,0 @@
package service
import (
"fmt"
"go.jolheiser.com/vanity/api"
"go.jolheiser.com/vanity/flags"
"code.gitea.io/sdk/gitea"
"go.jolheiser.com/beaver"
)
var _ Service = &Gitea{}
func NewGitea() *Gitea {
client, err := gitea.NewClient(flags.BaseURL.String(), gitea.SetToken(flags.Token))
if err != nil {
beaver.Errorf("could not create Gitea client: %v", err)
}
return &Gitea{
client: client,
}
}
type Gitea struct {
client *gitea.Client
}
func (g Gitea) Packages() (map[string]*api.Package, error) {
packages := make(map[string]*api.Package)
topicOpts := gitea.ListRepoTopicsOptions{
ListOptions: gitea.ListOptions{
Page: 1,
PageSize: 50,
},
}
page := 0
for {
opts := gitea.ListReposOptions{
ListOptions: gitea.ListOptions{
Page: page,
PageSize: 50,
},
}
repos, _, err := g.client.ListUserRepos(flags.Namespace, opts)
if err != nil {
return nil, err
}
for _, repo := range repos {
pkg := &api.Package{
Name: repo.Name,
Description: repo.Description,
Branch: repo.DefaultBranch,
WebURL: repo.HTMLURL,
CloneHTTP: repo.CloneURL,
CloneSSH: repo.SSHURL,
Private: repo.Private,
Fork: repo.Fork,
Mirror: repo.Mirror,
Archive: repo.Archived,
}
// Get tags
topics, _, err := g.client.ListRepoTopics(flags.Namespace, repo.Name, topicOpts)
if err != nil {
beaver.Warnf("could not get topics for %s: %v", repo.Name, err)
}
pkg.Topics = topics
packages[repo.Name] = pkg
}
page++
if len(repos) == 0 {
break
}
}
return packages, nil
}
func (g Gitea) GoDir(pkg *api.Package) string {
return fmt.Sprintf("%s/src/branch/%s{/dir}", pkg.WebURL, pkg.Branch)
}
func (g Gitea) GoFile(pkg *api.Package) string {
return fmt.Sprintf("%s/src/branch/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch)
}
func (g Gitea) GoMod(pkg *api.Package) (string, error) {
content, _, err := g.client.GetFile(flags.Namespace, pkg.Name, pkg.Branch, "go.mod")
return string(content), err
}

View File

@ -1,90 +0,0 @@
package service
import (
"context"
"fmt"
"go.jolheiser.com/vanity/api"
"go.jolheiser.com/vanity/flags"
"github.com/google/go-github/v32/github"
"golang.org/x/oauth2"
)
var _ Service = &GitHub{}
func NewGitHub() *GitHub {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: flags.Token},
)
client := oauth2.NewClient(context.Background(), ts)
ghClient := github.NewClient(client)
ghClient.BaseURL = flags.BaseURL
return &GitHub{
client: ghClient,
}
}
type GitHub struct {
client *github.Client
}
func (g GitHub) Packages() (map[string]*api.Package, error) {
packages := make(map[string]*api.Package)
page := 0
for {
opts := github.RepositoryListOptions{
ListOptions: github.ListOptions{
Page: page,
PerPage: 50,
},
}
repos, _, err := g.client.Repositories.List(context.Background(), flags.Namespace, &opts)
if err != nil {
return nil, err
}
for _, repo := range repos {
packages[repo.GetName()] = &api.Package{
Name: repo.GetName(),
Description: repo.GetDescription(),
Branch: repo.GetDefaultBranch(),
WebURL: repo.GetHTMLURL(),
CloneHTTP: repo.GetCloneURL(),
CloneSSH: repo.GetSSHURL(),
Private: repo.GetPrivate(),
Fork: repo.GetFork(),
Mirror: false,
Archive: repo.GetArchived(),
Topics: repo.Topics,
}
}
page++
if len(repos) == 0 {
break
}
}
return packages, nil
}
func (g GitHub) GoDir(pkg *api.Package) string {
return fmt.Sprintf("%s/tree/%s{/dir}", pkg.WebURL, pkg.Branch)
}
func (g GitHub) GoFile(pkg *api.Package) string {
return fmt.Sprintf("%s/blob/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch)
}
func (g GitHub) GoMod(pkg *api.Package) (string, error) {
content, _, _, err := g.client.Repositories.GetContents(context.Background(), flags.Namespace, pkg.Name, "go.mod",
&github.RepositoryContentGetOptions{
Ref: pkg.Branch,
})
if err != nil {
return "", err
}
return content.GetContent()
}

View File

@ -1,85 +0,0 @@
package service
import (
"fmt"
"html"
"go.jolheiser.com/vanity/api"
"go.jolheiser.com/vanity/flags"
"github.com/xanzy/go-gitlab"
"go.jolheiser.com/beaver"
)
var _ Service = &GitLab{}
func NewGitLab() *GitLab {
client, err := gitlab.NewClient(flags.Token, gitlab.WithBaseURL(flags.BaseURL.String()))
if err != nil {
beaver.Errorf("could not create GitLab client: %v", err)
}
return &GitLab{
client: client,
}
}
type GitLab struct {
client *gitlab.Client
}
func (g GitLab) Packages() (map[string]*api.Package, error) {
packages := make(map[string]*api.Package)
page := 0
for {
opts := gitlab.ListProjectsOptions{
ListOptions: gitlab.ListOptions{
Page: page,
PerPage: 50,
},
}
repos, _, err := g.client.Projects.ListUserProjects(flags.Namespace, &opts)
if err != nil {
return nil, err
}
for _, repo := range repos {
packages[repo.Name] = &api.Package{
Name: repo.Name,
Description: repo.Description,
Branch: repo.DefaultBranch,
WebURL: repo.WebURL,
CloneHTTP: repo.HTTPURLToRepo,
CloneSSH: repo.SSHURLToRepo,
Private: repo.Visibility != gitlab.PublicVisibility,
Fork: repo.ForkedFromProject != nil,
Mirror: repo.Mirror,
Archive: repo.Archived,
Topics: repo.TagList,
}
}
page++
if len(repos) == 0 {
break
}
}
return packages, nil
}
func (g GitLab) GoDir(pkg *api.Package) string {
return fmt.Sprintf("%s/-/tree/%s{/dir}", pkg.WebURL, pkg.Branch)
}
func (g GitLab) GoFile(pkg *api.Package) string {
return fmt.Sprintf("%s/-/blob/%s{/dir}/{file}#L{line}", pkg.WebURL, pkg.Branch)
}
func (g GitLab) GoMod(pkg *api.Package) (string, error) {
id := fmt.Sprintf("%s/%s", flags.Namespace, pkg.Name)
content, _, err := g.client.RepositoryFiles.GetRawFile(html.EscapeString(id), "go.mod", &gitlab.GetRawFileOptions{
Ref: &pkg.Branch,
})
return string(content), err
}

View File

@ -1,23 +0,0 @@
package service
import "go.jolheiser.com/vanity/api"
var _ Service = Off{}
type Off struct{}
func (o Off) Packages() (map[string]*api.Package, error) {
return make(map[string]*api.Package), nil
}
func (o Off) GoDir(*api.Package) string {
return ""
}
func (o Off) GoFile(*api.Package) string {
return ""
}
func (o Off) GoMod(*api.Package) (string, error) {
return "", nil
}

View File

@ -1,75 +0,0 @@
package service
import (
"fmt"
"strings"
"go.jolheiser.com/vanity/api"
"go.jolheiser.com/vanity/flags"
)
type Service interface {
Packages() (map[string]*api.Package, error)
GoDir(*api.Package) string
GoFile(*api.Package) string
GoMod(*api.Package) (string, error)
}
func New() Service {
switch strings.ToLower(flags.Service) {
case "gitea":
return NewGitea()
case "github":
return NewGitHub()
case "gitlab":
return NewGitLab()
default:
return Off{}
}
}
func Check(pkg *api.Package) error {
// Private
if pkg.Private && !flags.Private {
return fmt.Errorf("%s is private and --private wasn't used", pkg.Name)
}
// Forked
if pkg.Fork && !flags.Fork {
return fmt.Errorf("%s is a fork and --fork wasn't used", pkg.Name)
}
// Mirrored
if pkg.Mirror && !flags.Mirror {
return fmt.Errorf("%s is a mirror and --mirror wasn't used", pkg.Name)
}
// Archived
if pkg.Archive && !flags.Archive {
return fmt.Errorf("%s is archived and --archive wasn't used", pkg.Name)
}
// Exclusions
for _, exclude := range flags.Exclude {
if exclude.MatchString(pkg.Name) {
return fmt.Errorf("%s was excluded by the rule %s", pkg.Name, exclude.String())
}
}
// Inclusions
if len(flags.Include) > 0 {
var included bool
for _, include := range flags.Include {
if include.MatchString(pkg.Name) {
included = true
break
}
}
if !included {
return fmt.Errorf("%s wasn't included by any existing inclusion rule", pkg.Name)
}
}
return nil
}

8
tools.go 100644
View File

@ -0,0 +1,8 @@
//go:build tools
// +build tools
package main
import (
_ "github.com/rs/zerolog/cmd/lint"
)

View File

@ -1,21 +0,0 @@
[Unit]
Description=Vanity Go Imports
After=syslog.target
After=network.target
[Service]
RestartSec=2s
Type=simple
User=vanity
Group=vanity
ExecStart=/usr/local/bin/vanity
Restart=always
# Required
Environment=VANITY_BASE_URL=
Environment=VANITY_NAMESPACE=
Environment=VANITY_TOKEN=
Environment=VANITY_DOMAIN=
[Install]
WantedBy=multi-user.target