├── misc ├── logo │ ├── attribution │ └── logo.png ├── goreleaser │ └── goreleaser.yml ├── docker │ └── Dockerfile └── golangci │ └── config.toml ├── .github ├── dependabot.yml └── workflows │ ├── deploy.yml │ └── ci.yml ├── internal ├── service │ ├── entry.go │ ├── parser │ │ └── markdown.go │ ├── provider │ │ ├── email.go │ │ ├── github_api.go │ │ ├── email_test.go │ │ ├── file.go │ │ ├── web_test.go │ │ ├── web.go │ │ ├── github.go │ │ ├── github_test.go │ │ └── file_test.go │ ├── worker │ │ └── worker.go │ └── scan │ │ └── scan.go └── client.go ├── .gitignore ├── Makefile ├── cmd ├── markdown-link-check.sample.yml └── main.go ├── go.mod ├── LICENSE ├── README.md └── go.sum /misc/logo/attribution: -------------------------------------------------------------------------------- 1 | Icon made by Pixel perfect from www.flaticon.com -------------------------------------------------------------------------------- /misc/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitro/markdown-link-check/master/misc/logo/logo.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | day: monday 8 | -------------------------------------------------------------------------------- /internal/service/entry.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | // Entry represents the link present at a given file. 4 | type Entry struct { 5 | Path string 6 | Link string 7 | Valid bool 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Config file. 2 | /cmd/markdown-link-check.yml 3 | /markdown-link-check.sample.yml 4 | 5 | # Binary file. 6 | /cmd/markdown-link-check 7 | /cmd/main 8 | /cmd/cmd 9 | /cmd/__debug_bin 10 | 11 | # Temporary files. 12 | /dist 13 | 14 | # Visual Studio Code configuration. 15 | /.vscode -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | DOCKER_IMAGE := gonitro/markdown-link-check:8 3 | 4 | .PHONY: go-build 5 | go-build: 6 | @go build -o cmd/markdown-link-check cmd/main.go 7 | 8 | .PHONY: go-test 9 | go-test: 10 | @go test -race -cover -covermode=atomic -timeout=5m ${ARGS} ./... 11 | 12 | .PHONY: go-lint 13 | go-lint: 14 | @golangci-lint run -c misc/golangci/config.toml ./... 15 | 16 | .PHONY: docker-build 17 | docker-build: 18 | @docker build -t $(DOCKER_IMAGE) -f misc/docker/Dockerfile . 19 | 20 | .PHONY: docker-push 21 | docker-push: 22 | @docker push $(DOCKER_IMAGE) 23 | -------------------------------------------------------------------------------- /cmd/markdown-link-check.sample.yml: -------------------------------------------------------------------------------- 1 | # Both link and file are regex based matchers. More information about the syntax 2 | # at the Go documentation: https://github.com/google/re2/wiki/Syntax 3 | ignore: 4 | link: 5 | - ^ftp:\/\/ 6 | - ^http:\/\/ 7 | - ^https:\/\/ 8 | 9 | file: 10 | - old 11 | - temp/files 12 | 13 | provider: 14 | web: 15 | header: 16 | User-Agent: Chrome 17 | 18 | overwrite: 19 | - endpoint: ^https:\/\/custom-website\.com 20 | header: 21 | Content-Type: application/json 22 | User-Agent: Firefox 23 | 24 | github: 25 | nitro: 26 | owner: nitro 27 | token: token 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '**' 5 | 6 | name: Deploy 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | container: gonitro/markdown-link-check:8 12 | timeout-minutes: 10 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - uses: actions/cache@v2 19 | with: 20 | path: ~/go/pkg/mod 21 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 22 | restore-keys: | 23 | ${{ runner.os }}-go- 24 | 25 | - name: Deploy 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: goreleaser release --config misc/goreleaser/goreleaser.yml --rm-dist 29 | -------------------------------------------------------------------------------- /misc/goreleaser/goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: markdown-link-check 2 | before: 3 | hooks: 4 | - go mod download 5 | - cp cmd/markdown-link-check.sample.yml . 6 | 7 | builds: 8 | - main: ./cmd/main.go 9 | binary: markdown-link-check 10 | goos: 11 | - darwin 12 | - linux 13 | goarch: 14 | - amd64 15 | 16 | archives: 17 | - name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}' 18 | wrap_in_directory: true 19 | format: tar.gz 20 | files: 21 | - markdown-link-check.sample.yml 22 | - README.md 23 | - LICENSE 24 | 25 | checksum: 26 | name_template: 'checksum' 27 | algorithm: sha256 28 | 29 | release: 30 | draft: true 31 | prerelease: true 32 | name_template: '{{.Tag}}' 33 | github: 34 | owner: nitro 35 | name: markdown-link-check 36 | -------------------------------------------------------------------------------- /internal/service/parser/markdown.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/microcosm-cc/bluemonday" 5 | "github.com/russross/blackfriday/v2" 6 | "github.com/shurcooL/sanitized_anchor_name" 7 | ) 8 | 9 | // Markdown expose a parser that transform Markdown into HTML. 10 | type Markdown struct { 11 | policy bluemonday.Policy 12 | } 13 | 14 | // Init the internal state. 15 | func (m *Markdown) Init() { 16 | m.policy = *bluemonday.UGCPolicy() 17 | } 18 | 19 | // Do transform the Markdown into HTML. 20 | func (m Markdown) Do(payload []byte) []byte { 21 | payload = blackfriday.Run(payload, blackfriday.WithExtensions(blackfriday.AutoHeadingIDs)) 22 | return m.policy.SanitizeBytes(payload) 23 | } 24 | 25 | // SanitizedAnchorName process the anchor. 26 | func (m Markdown) SanitizedAnchorName(text string) string { 27 | return sanitized_anchor_name.Create(text) 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module nitro/markdown-link-check 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/PuerkitoBio/goquery v1.7.1 7 | github.com/alecthomas/kong v0.2.17 8 | github.com/go-rod/rod v0.101.5 9 | github.com/google/go-github v17.0.0+incompatible 10 | github.com/google/go-querystring v1.1.0 // indirect 11 | github.com/logrusorgru/aurora v2.0.3+incompatible 12 | github.com/microcosm-cc/bluemonday v1.0.15 13 | github.com/russross/blackfriday/v2 v2.1.0 14 | github.com/shurcooL/sanitized_anchor_name v1.0.0 15 | github.com/spf13/cast v1.4.0 // indirect 16 | github.com/spf13/viper v1.8.1 17 | github.com/stretchr/objx v0.1.1 // indirect 18 | github.com/stretchr/testify v1.7.0 19 | github.com/ysmood/gson v0.7.0 // indirect 20 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect 21 | golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 22 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 23 | google.golang.org/protobuf v1.27.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /misc/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golangci/golangci-lint:v1.41 AS golangci 2 | FROM goreleaser/goreleaser:v0.174.1 AS goreleaser 3 | FROM golang:1.16-buster 4 | 5 | SHELL ["/bin/bash", "-c"] 6 | 7 | RUN apt-get update \ 8 | && apt-get install -y --no-install-recommends chromium=90.* \ 9 | && rm -rf /var/lib/apt/lists/* \ 10 | && wget --quiet https://github.com/hadolint/hadolint/releases/download/v2.6.0/hadolint-Linux-x86_64 -O /usr/local/bin/hadolint \ 11 | && chmod +x /usr/local/bin/hadolint \ 12 | && mkdir /tmp/tparse \ 13 | && wget --quiet https://github.com/mfridman/tparse/releases/download/v0.8.3/tparse_0.8.3_Linux_x86_64.tar.gz -O /tmp/tparse/tparse.tar.gz \ 14 | && tar -xvf /tmp/tparse/tparse.tar.gz -C /tmp/tparse \ 15 | && mv /tmp/tparse/tparse /usr/local/bin/tparse \ 16 | && chmod +x /usr/local/bin/tparse \ 17 | && rm -Rf /tmp/tparse 18 | 19 | COPY --from=golangci /usr/bin/golangci-lint /go/bin/ 20 | COPY --from=goreleaser /usr/local/bin/goreleaser /go/bin/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Nitro Software 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /misc/golangci/config.toml: -------------------------------------------------------------------------------- 1 | [linters] 2 | disable-all = true 3 | enable = [ 4 | 'bodyclose', 5 | 'deadcode', 6 | 'dupl', 7 | 'errcheck', 8 | 'gochecknoglobals', 9 | 'gochecknoinits', 10 | 'goconst', 11 | 'gocritic', 12 | 'gocyclo', 13 | 'gofmt', 14 | 'goimports', 15 | 'golint', 16 | 'gosec', 17 | 'gosimple', 18 | 'govet', 19 | 'lll', 20 | 'ineffassign', 21 | 'maligned', 22 | 'misspell', 23 | 'nakedret', 24 | 'prealloc', 25 | 'scopelint', 26 | 'staticcheck', 27 | 'structcheck', 28 | 'stylecheck', 29 | 'typecheck', 30 | 'unconvert', 31 | 'unparam', 32 | 'unused', 33 | 'varcheck', 34 | 'dogsled', 35 | 'godox', 36 | 'whitespace' 37 | ] 38 | 39 | [linter-settings.goimports] 40 | local-prefixes = 'nitro' 41 | 42 | [linter-settings.errcheck] 43 | check-type-assertions = true 44 | check-blank = true 45 | 46 | [linter-settings.unused] 47 | check-exported = true 48 | 49 | [linter-settings.unparam] 50 | check-exported = true 51 | 52 | [linter-settings.prealloc] 53 | for-loops = true 54 | 55 | [linter-settings.gocritic] 56 | enabled-tags = [ 57 | 'diagnostic', 58 | 'style', 59 | 'performance', 60 | 'experimental', 61 | 'opinionated' 62 | ] -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - '**' 5 | - '!master' 6 | 7 | name: CI 8 | jobs: 9 | quality: 10 | name: Quality 11 | runs-on: ubuntu-latest 12 | container: gonitro/markdown-link-check:8 13 | timeout-minutes: 10 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - uses: actions/cache@v2 20 | with: 21 | path: ~/go/pkg/mod 22 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 23 | restore-keys: | 24 | ${{ runner.os }}-go- 25 | 26 | - name: Run Docker linter 27 | run: hadolint misc/docker/Dockerfile 28 | 29 | - name: Run Go dependency linter 30 | run: | 31 | go mod tidy 32 | git add . 33 | git diff --cached --exit-code 34 | 35 | - name: Run Go Linter 36 | run: make go-lint 37 | 38 | - name: Run Go Test 39 | run: make go-test ARGS='-json' | tparse -all 40 | 41 | build: 42 | name: Build 43 | runs-on: ubuntu-latest 44 | container: gonitro/markdown-link-check:8 45 | timeout-minutes: 10 46 | needs: quality 47 | 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v2 51 | 52 | - uses: actions/cache@v2 53 | with: 54 | path: ~/go/pkg/mod 55 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 56 | restore-keys: | 57 | ${{ runner.os }}-go- 58 | 59 | - name: Run release command test 60 | run: goreleaser release --config misc/goreleaser/goreleaser.yml --rm-dist --snapshot 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Link Check 2 | This application is used to find broken links at markdown files. 3 | 4 | ## Providers 5 | Providers are the core of `markdown-link-check`. They enable the application to perform new kinds of checks like validating a resource that exists in Jira or even at an FTP server. 6 | 7 | ### File 8 | The file provider checks if the links point to valid files or directories. If the link points to a file and it has an anchor it will be validated as well. 9 | 10 | ### GitHub 11 | There is initial support for verification on private GitHub repositories. More information can be found at #7. 12 | 13 | ### Web 14 | The web provider verifies public HTTP endpoints. The link is assumed as valid if the status code is `>=200 and <300`. The redirect status code `301` and `308` will be followed, other redirect codes are treated as an invalid link. 15 | 16 | ## Compiling 17 | ```bash 18 | git clone git@github.com:Nitro/markdown-link-check.git 19 | cd markdown-link-check 20 | make go-build # This generate a binary at './cmd/markdown-link-check' 21 | ``` 22 | 23 | ## How to use it? 24 | ```bash 25 | ➜ ./markdown-link-check --help 26 | 27 | Usage: markdown-link-check --config=STRING 28 | 29 | Arguments: 30 | Path to be processed 31 | 32 | Flags: 33 | --help Show context-sensitive help. 34 | -c, --config=STRING Path to the configuration file. 35 | ``` 36 | 37 | ## CI 38 | ### GitHub Actions 39 | There is a [action](https://github.com/Nitro/markdown-link-check-action) available. 40 | 41 | ### Others 42 | It's possible to build from the source or use a [pre-built release](https://github.com/Nitro/markdown-link-check-action/releases). -------------------------------------------------------------------------------- /internal/service/provider/email.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "regexp" 8 | ) 9 | 10 | type emailChecker interface { 11 | exists(domain string) (bool, error) 12 | } 13 | 14 | // Email handles the verification of email addresses. 15 | type Email struct { 16 | checker emailChecker 17 | regex regexp.Regexp 18 | } 19 | 20 | // Init internal state. 21 | func (e *Email) Init() error { 22 | if err := e.initRegex(); err != nil { 23 | return fmt.Errorf("fail to initialize the regex: %w", err) 24 | } 25 | 26 | if e.checker == nil { 27 | e.checker = emailNetLookupMX{} 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // Authority checks if the email provider is responsible to process the entry. 34 | func (e Email) Authority(uri string) bool { 35 | return e.regex.Match([]byte(uri)) 36 | } 37 | 38 | // Valid check if the address is valid. 39 | func (e Email) Valid(ctx context.Context, _, uri string) (bool, error) { 40 | fragments := e.regex.FindStringSubmatch(uri) 41 | if fragments == nil { 42 | return false, nil 43 | } 44 | 45 | exists, err := e.checker.exists(fragments[1]) 46 | if err != nil { 47 | return false, fmt.Errorf("fail to check the MX DNS entries: %w", err) 48 | } 49 | return exists, nil 50 | } 51 | 52 | func (e *Email) initRegex() error { 53 | rawExpr := "^mailto:[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@(?P[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)" 54 | expr, err := regexp.Compile(rawExpr) 55 | if err != nil { 56 | return fmt.Errorf("fail to compile the expression '%s': %w", rawExpr, err) 57 | } 58 | e.regex = *expr 59 | return nil 60 | } 61 | 62 | type emailNetLookupMX struct{} 63 | 64 | func (emailNetLookupMX) exists(domain string) (bool, error) { 65 | mxs, err := net.LookupMX(domain) 66 | if err != nil { 67 | return false, err 68 | } 69 | return (len(mxs) > 0), nil 70 | } 71 | -------------------------------------------------------------------------------- /internal/service/worker/worker.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "nitro/markdown-link-check/internal/service" 10 | ) 11 | 12 | // Provider represents the providers resonsible to process the entries. 13 | type Provider interface { 14 | Authority(uri string) bool 15 | Valid(ctx context.Context, filePath, uri string) (bool, error) 16 | } // nolint: golint 17 | 18 | type workerError struct { 19 | units []workerErrorUnit 20 | } 21 | 22 | func (w workerError) Error() string { 23 | if len(w.units) == 1 { 24 | return w.units[0].err.Error() 25 | } 26 | 27 | errors := make([]string, 0, len(w.units)) 28 | for _, unit := range w.units { 29 | errors = append(errors, unit.err.Error()) 30 | } 31 | return fmt.Sprintf("multiple errors detected ('%s')", strings.Join(errors, "', '")) 32 | } 33 | 34 | type workerErrorUnit struct { 35 | err error 36 | entry service.Entry 37 | } 38 | 39 | // Worker process the entries to check if they're valid. Everything is basead on providers and they're executed in 40 | // order. 41 | type Worker struct { 42 | Providers []Provider 43 | } 44 | 45 | // Process the entries. 46 | func (w Worker) Process(ctx context.Context, entries []service.Entry) ([]service.Entry, error) { 47 | if len(w.Providers) == 0 { 48 | return nil, errors.New("missing 'providers'") 49 | } 50 | 51 | var ( 52 | errors []workerErrorUnit 53 | result []service.Entry 54 | ) 55 | 56 | for _, entry := range entries { 57 | for _, provider := range w.Providers { 58 | if !provider.Authority(entry.Link) { 59 | continue 60 | } 61 | 62 | valid, err := provider.Valid(ctx, entry.Path, entry.Link) 63 | if err != nil { 64 | errors = append(errors, workerErrorUnit{err: err, entry: entry}) 65 | } else { 66 | entry.Valid = valid 67 | result = append(result, entry) 68 | } 69 | break 70 | } 71 | } 72 | 73 | if len(errors) == 0 { 74 | return result, nil 75 | } 76 | return nil, workerError{units: errors} 77 | } 78 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/alecthomas/kong" 11 | "github.com/spf13/viper" 12 | 13 | "nitro/markdown-link-check/internal" 14 | ) 15 | 16 | type config struct { 17 | Ignore struct { 18 | Link []string `mapstructure:"link"` 19 | File []string `mapstructure:"file"` 20 | } `mapstructure:"ignore"` 21 | Provider struct { 22 | Web struct { 23 | Header map[string][]string `mapstructure:"header"` 24 | Overwrite []struct { 25 | Endpoint string `mapstructure:"endpoint"` 26 | Header map[string][]string `mapstructure:"header"` 27 | } `mapstructure:"overwrite"` 28 | } `mapstructure:"web"` 29 | GitHub map[string]struct { 30 | Owner string `mapstructure:"owner"` 31 | Token string `mapstructure:"token"` 32 | } `mapstructure:"github"` 33 | } `mapstructure:"provider"` 34 | } 35 | 36 | func main() { 37 | var params struct { 38 | Path string `help:"Path to be processed" required:"true" arg:"true" type:"string"` 39 | Config string `help:"Path to the configuration file." required:"true" short:"c" type:"string"` 40 | } 41 | kong.Parse(¶ms, kong.Name("markdown-link-check")) 42 | 43 | client, err := configClient(params.Config) 44 | if err != nil { 45 | handleError("fail to configure the client: %s", err.Error()) 46 | } 47 | client.Path = params.Path 48 | 49 | hasInvalidLinks, err := client.Run(executionContext()) 50 | if err != nil { 51 | handleError("fail at client execution: %s", err.Error()) 52 | } 53 | if hasInvalidLinks { 54 | os.Exit(1) 55 | } 56 | } 57 | 58 | func configClient(path string) (internal.Client, error) { 59 | f, err := os.Open(path) 60 | if err != nil { 61 | return internal.Client{}, fmt.Errorf("fail to open the config file: %w", err) 62 | } 63 | defer f.Close() 64 | 65 | var viper = viper.New() 66 | viper.SetConfigType("yaml") 67 | if err := viper.ReadConfig(f); err != nil { 68 | return internal.Client{}, fmt.Errorf("fail to read the configuration file: %w", err) 69 | } 70 | 71 | var cfg config 72 | if err := viper.Unmarshal(&cfg); err != nil { 73 | return internal.Client{}, fmt.Errorf("fail to unmarshal the configuration: %w", err) 74 | } 75 | 76 | github := make([]internal.ClientProviderGithub, 0, len(cfg.Provider.GitHub)) 77 | for _, gh := range cfg.Provider.GitHub { 78 | github = append(github, internal.ClientProviderGithub{ 79 | Token: gh.Token, 80 | Owner: gh.Owner, 81 | }) 82 | } 83 | 84 | web := internal.ClientProviderWeb{ 85 | Config: cfg.Provider.Web.Header, 86 | ConfigOverwrite: make(map[string]http.Header, len(cfg.Provider.Web.Overwrite)), 87 | } 88 | for _, overwrite := range cfg.Provider.Web.Overwrite { 89 | web.ConfigOverwrite[overwrite.Endpoint] = overwrite.Header 90 | } 91 | 92 | return internal.Client{ 93 | Ignore: internal.ClientIgnore{ 94 | File: cfg.Ignore.File, 95 | Link: cfg.Ignore.Link, 96 | }, 97 | Provider: internal.ClientProvider{ 98 | Github: github, 99 | Web: web, 100 | }, 101 | }, nil 102 | } 103 | 104 | func handleError(mask string, params ...interface{}) { 105 | fmt.Printf(mask+"\n", params...) 106 | os.Exit(1) 107 | } 108 | 109 | func executionContext() context.Context { 110 | ctx, ctxCancel := context.WithCancel(context.Background()) 111 | go func() { 112 | chSignal := make(chan os.Signal, 1) 113 | signal.Notify(chSignal, os.Interrupt) 114 | <-chSignal 115 | ctxCancel() 116 | }() 117 | return ctx 118 | } 119 | -------------------------------------------------------------------------------- /internal/service/provider/github_api.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | 11 | "github.com/google/go-github/github" 12 | "golang.org/x/oauth2" 13 | ) 14 | 15 | type githubAPI struct { 16 | token string 17 | owner string 18 | client *github.Client 19 | httpClient gitHubHTTPClient 20 | } 21 | 22 | func (g *githubAPI) init() error { 23 | if g.token == "" { 24 | return errors.New("missing 'token'") 25 | } 26 | 27 | if g.owner == "" { 28 | return errors.New("missing 'owner'") 29 | } 30 | 31 | if g.httpClient == nil { 32 | return errors.New("missing 'httpClient") 33 | } 34 | 35 | ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: g.token}) 36 | tc := oauth2.NewClient(context.Background(), ts) 37 | g.client = github.NewClient(tc) 38 | return nil 39 | } 40 | 41 | func (g githubAPI) repository(ctx context.Context, repository string) (*github.Response, error) { 42 | _, resp, err := g.client.Repositories.Get(ctx, g.owner, repository) 43 | return resp, err 44 | } 45 | 46 | func (g githubAPI) repositoriesGetCommitSHA1(ctx context.Context, repository, ref string) (*github.Response, error) { 47 | _, resp, err := g.client.Repositories.GetCommitSHA1(ctx, g.owner, repository, ref, "") 48 | return resp, err 49 | } 50 | 51 | func (g githubAPI) issuesGet(ctx context.Context, repo string, number int) (*github.Response, error) { 52 | _, resp, err := g.client.Issues.Get(ctx, g.owner, repo, number) 53 | return resp, err 54 | } 55 | 56 | func (g githubAPI) issuesGetComment(ctx context.Context, repo string, commentID int64) (*github.Response, error) { 57 | _, resp, err := g.client.Issues.GetComment(ctx, g.owner, repo, commentID) 58 | return resp, err 59 | } 60 | 61 | func (g githubAPI) pullRequestsGetRaw(ctx context.Context, repo string, number int) (*github.Response, error) { 62 | _, resp, err := g.client.PullRequests.GetRaw(ctx, g.owner, repo, number, github.RawOptions{Type: github.Patch}) 63 | return resp, err 64 | } 65 | 66 | // RelatedPullRequests is at developer preview and maybe this is the reason it's not available to be used though the 67 | // GitHub client. 68 | // 69 | // For more information: https://developer.github.com/v3/repos/commits/#list-pull-requests-associated-with-commit 70 | func (g githubAPI) relatedPullRequests(ctx context.Context, repository, ref string) ([]int, error) { 71 | endpoint := fmt.Sprintf("https://api.github.com/repos/%s/%s/commits/%s/pulls", g.owner, repository, ref) 72 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) 73 | if err != nil { 74 | return nil, fmt.Errorf("fail to create request to GitHub: %w", err) 75 | } 76 | req.Header.Add("accept", "application/vnd.github.v3+json") 77 | req.Header.Add("accept", "application/vnd.github.groot-preview+json") 78 | req.Header.Add("authorization", fmt.Sprintf("token %s", g.token)) 79 | 80 | httpResponse, err := g.httpClient.Do(req) 81 | if err != nil { 82 | return nil, fmt.Errorf("fail to execute the request to GitHub: %w", err) 83 | } 84 | defer httpResponse.Body.Close() 85 | 86 | if httpResponse.StatusCode != http.StatusOK { 87 | return nil, fmt.Errorf("invalid response code: %d", httpResponse.StatusCode) 88 | } 89 | 90 | payload, err := ioutil.ReadAll(httpResponse.Body) 91 | if err != nil { 92 | return nil, fmt.Errorf("fail to read the response from GitHub: %w", err) 93 | } 94 | 95 | rawResponse := make([]map[string]interface{}, 0) 96 | if err := json.Unmarshal(payload, &rawResponse); err != nil { 97 | return nil, fmt.Errorf("fail to unmarshal the response from GitHub: %w", err) 98 | } 99 | 100 | ids := make([]int, 0, len(rawResponse)) 101 | for _, entry := range rawResponse { 102 | id, ok := entry["number"].(float64) 103 | if !ok { 104 | return nil, errors.New("fail to unmarshal to cast the id into a integer") 105 | } 106 | ids = append(ids, (int)(id)) 107 | } 108 | return ids, nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/service/provider/email_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestEmailInit(t *testing.T) { 12 | t.Parallel() 13 | var client Email 14 | require.NoError(t, client.Init()) 15 | } 16 | 17 | func TestEmailAuthority(t *testing.T) { 18 | t.Parallel() 19 | 20 | var client Email 21 | require.NoError(t, client.Init()) 22 | 23 | tests := []struct { 24 | message string 25 | uri string 26 | hasAuthority bool 27 | }{ 28 | { 29 | message: "have authority #1", 30 | uri: "mailto:milo@gonitro.com", 31 | hasAuthority: true, 32 | }, 33 | { 34 | message: "have authority #2", 35 | uri: "mailto:milo@gonitro.com?subject=something", 36 | hasAuthority: true, 37 | }, 38 | { 39 | message: "have no authority #1", 40 | uri: "http://something.com", 41 | hasAuthority: false, 42 | }, 43 | { 44 | message: "have no authority #2", 45 | uri: "milo@gonitro.com", 46 | hasAuthority: false, 47 | }, 48 | { 49 | message: "have no authority #2", 50 | uri: "milo@gonitro.com?subject=something", 51 | hasAuthority: false, 52 | }, 53 | } 54 | 55 | for i := 0; i < len(tests); i++ { 56 | tt := tests[i] 57 | t.Run("Should "+tt.message, func(t *testing.T) { 58 | t.Parallel() 59 | require.Equal(t, tt.hasAuthority, client.Authority(tt.uri)) 60 | }) 61 | } 62 | } 63 | 64 | func TestEmailValid(t *testing.T) { 65 | t.Parallel() 66 | 67 | tests := []struct { 68 | message string 69 | ctx context.Context 70 | checker emailChecker 71 | uri string 72 | isValid bool 73 | shouldErr bool 74 | }{ 75 | { 76 | message: "attest the email as valid", 77 | ctx: context.Background(), 78 | checker: emailCheckerMock{response: true, shouldErr: false}, 79 | uri: "mailto:milo@gonitro.com", 80 | shouldErr: false, 81 | isValid: true, 82 | }, 83 | { 84 | message: "attest the email with name as valid", 85 | ctx: context.Background(), 86 | checker: emailCheckerMock{response: true, shouldErr: false}, 87 | uri: "mailto:milo@gonitro.com?subject=something", 88 | shouldErr: false, 89 | isValid: true, 90 | }, 91 | { 92 | message: "attest the email as invalid #1", 93 | ctx: context.Background(), 94 | checker: emailCheckerMock{response: true, shouldErr: false}, 95 | uri: "something", 96 | shouldErr: false, 97 | isValid: false, 98 | }, 99 | { 100 | message: "attest the email as invalid #2", 101 | ctx: context.Background(), 102 | checker: emailCheckerMock{response: true, shouldErr: false}, 103 | uri: "http://gonitro.com", 104 | shouldErr: false, 105 | isValid: false, 106 | }, 107 | { 108 | message: "attest the email as invalid #3", 109 | ctx: context.Background(), 110 | checker: emailCheckerMock{response: false, shouldErr: false}, 111 | uri: "unknow@email.com", 112 | shouldErr: false, 113 | isValid: false, 114 | }, 115 | { 116 | message: "attest the email as invalid #4", 117 | ctx: context.Background(), 118 | checker: emailCheckerMock{response: false, shouldErr: true}, 119 | uri: "mailto:valid@email.com", 120 | shouldErr: true, 121 | isValid: false, 122 | }, 123 | } 124 | 125 | for i := 0; i < len(tests); i++ { 126 | tt := tests[i] 127 | t.Run("Should "+tt.message, func(t *testing.T) { 128 | client := Email{checker: tt.checker} 129 | require.NoError(t, client.Init()) 130 | 131 | isValid, err := client.Valid(tt.ctx, "", tt.uri) 132 | require.Equal(t, tt.shouldErr, (err != nil)) 133 | if err != nil { 134 | return 135 | } 136 | require.Equal(t, tt.isValid, isValid) 137 | }) 138 | } 139 | } 140 | 141 | type emailCheckerMock struct { 142 | response bool 143 | shouldErr bool 144 | } 145 | 146 | func (e emailCheckerMock) exists(domain string) (bool, error) { 147 | if e.shouldErr { 148 | return false, errors.New("error during the email check") 149 | } 150 | return e.response, nil 151 | } 152 | -------------------------------------------------------------------------------- /internal/service/scan/scan.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "sort" 12 | 13 | "github.com/PuerkitoBio/goquery" 14 | 15 | "nitro/markdown-link-check/internal/service" 16 | ) 17 | 18 | type scanParser interface { 19 | Do(payload []byte) []byte 20 | } 21 | 22 | // Scan is responsible for reading, parsing and extracting links from the markdown files. 23 | type Scan struct { 24 | IgnoreFile []string 25 | IgnoreLink []string 26 | Parser scanParser 27 | 28 | regexFile []regexp.Regexp 29 | regexLink []regexp.Regexp 30 | } 31 | 32 | // Init the internal state. 33 | func (s *Scan) Init() error { 34 | if s.Parser == nil { 35 | return errors.New("missing 'parser'") 36 | } 37 | 38 | compile := func(expressions []string) ([]regexp.Regexp, error) { 39 | out := make([]regexp.Regexp, 0, len(expressions)) 40 | for _, rawExpr := range expressions { 41 | expr, err := regexp.Compile(rawExpr) 42 | if err != nil { 43 | return nil, fmt.Errorf("fail to compile the expression '%s': %w", rawExpr, err) 44 | } 45 | out = append(out, *expr) 46 | } 47 | return out, nil 48 | } 49 | 50 | var err error 51 | if s.regexFile, err = compile(s.IgnoreFile); err != nil { 52 | return fmt.Errorf("fail to compile ignore file regex: %w", err) 53 | } 54 | 55 | if s.regexLink, err = compile(s.IgnoreLink); err != nil { 56 | return fmt.Errorf("fail to compile ignore link regex: %w", err) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // Process the directory. 63 | func (s Scan) Process(path string) ([]service.Entry, error) { 64 | if err := s.isDir(path); err != nil { 65 | return nil, fmt.Errorf("fail to check if path is directory: %w", err) 66 | } 67 | 68 | files, err := s.listFiles(path) 69 | if err != nil { 70 | return nil, fmt.Errorf("fail to fetch the markdown file: %w", err) 71 | } 72 | sort.Strings(files) 73 | 74 | result := make([]service.Entry, 0, len(files)) 75 | for _, file := range files { 76 | entries, err := s.processFile(file) 77 | if err != nil { 78 | return nil, fmt.Errorf("fail to process the file '%s': %w", file, err) 79 | } 80 | result = append(result, entries...) 81 | } 82 | 83 | return result, nil 84 | } 85 | 86 | func (Scan) isDir(path string) error { 87 | stat, err := os.Stat(path) 88 | if err != nil { 89 | return fmt.Errorf("fail to check the path stat: %w", err) 90 | } 91 | if !stat.IsDir() { 92 | return fmt.Errorf("'%s' expected to be a directory", path) 93 | } 94 | return nil 95 | } 96 | 97 | func (s Scan) listFiles(path string) ([]string, error) { 98 | var paths []string 99 | 100 | walkFn := func(path string, info os.FileInfo, err error) error { 101 | if err != nil { 102 | return err 103 | } 104 | 105 | for _, regex := range s.regexFile { 106 | if regex.Match([]byte(path)) { 107 | return nil 108 | } 109 | } 110 | 111 | if filepath.Ext(path) != ".md" { 112 | return nil 113 | } 114 | 115 | paths = append(paths, path) 116 | return nil 117 | } 118 | if err := filepath.Walk(path, walkFn); err != nil { 119 | return nil, fmt.Errorf("fail to fetch the files paths: %w", err) 120 | } 121 | 122 | return paths, nil 123 | } 124 | 125 | func (s Scan) processFile(path string) ([]service.Entry, error) { 126 | payload, err := ioutil.ReadFile(path) 127 | if err != nil { 128 | return nil, fmt.Errorf("fail to read the file: %w", err) 129 | } 130 | 131 | html := s.Parser.Do(payload) 132 | links, err := s.extractLinks(html) 133 | if err != nil { 134 | return nil, fmt.Errorf("fail to extract links: %w", err) 135 | } 136 | links = s.removeDuplicates(links) 137 | 138 | result := make([]service.Entry, 0, len(links)) 139 | for _, link := range links { 140 | result = append(result, service.Entry{Path: path, Link: link}) 141 | } 142 | 143 | return result, nil 144 | } 145 | 146 | func (s Scan) extractLinks(payload []byte) ([]string, error) { 147 | doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(payload)) 148 | if err != nil { 149 | return nil, fmt.Errorf("fail to parse the HTML: %w", err) 150 | } 151 | 152 | var links []string 153 | doc.Find("a").Each(func(i int, selection *goquery.Selection) { 154 | href, ok := selection.Attr("href") 155 | if !ok { 156 | return 157 | } 158 | 159 | for _, regex := range s.regexLink { 160 | if regex.Match([]byte(href)) { 161 | return 162 | } 163 | } 164 | 165 | links = append(links, href) 166 | }) 167 | return links, nil 168 | } 169 | 170 | func (Scan) removeDuplicates(elements []string) []string { 171 | index := make(map[string]struct{}) 172 | for v := range elements { 173 | index[elements[v]] = struct{}{} 174 | } 175 | 176 | result := make([]string, 0, len(index)) 177 | for key := range index { 178 | result = append(result, key) 179 | } 180 | return result 181 | } 182 | -------------------------------------------------------------------------------- /internal/service/provider/file.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/url" 10 | "os" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | 15 | "github.com/PuerkitoBio/goquery" 16 | ) 17 | 18 | type fileReader interface { 19 | fileExists(string) (os.FileInfo, bool) 20 | readFile(string) ([]byte, error) 21 | } 22 | 23 | type fileParser interface { 24 | Do(payload []byte) []byte 25 | SanitizedAnchorName(text string) string 26 | } 27 | 28 | type fileReaderAPI struct{} 29 | 30 | func (fileReaderAPI) fileExists(item string) (os.FileInfo, bool) { 31 | info, err := os.Stat(item) 32 | return info, !os.IsNotExist(err) 33 | } 34 | 35 | func (fileReaderAPI) readFile(filer string) ([]byte, error) { 36 | return ioutil.ReadFile(filer) 37 | } 38 | 39 | // File provider is responsible for checking if the file exists at the filesystem. 40 | type File struct { 41 | Path string 42 | Parser fileParser 43 | 44 | reader fileReader 45 | schemaRegex regexp.Regexp 46 | } 47 | 48 | // Init internal state. 49 | func (f *File) Init() error { 50 | if f.reader == nil { 51 | f.reader = fileReaderAPI{} 52 | } 53 | 54 | if f.Path == "" { 55 | return errors.New("missing 'path'") 56 | } 57 | 58 | if f.Parser == nil { 59 | return errors.New("missing 'parser'") 60 | } 61 | 62 | if err := f.initRegex(); err != nil { 63 | return fmt.Errorf("fail to initialize the regex expressions: %w", err) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // Authority checks if the file provider is responsible to process the entry. 70 | func (f File) Authority(uri string) bool { 71 | return f.schemaRegex.Match([]byte(uri)) 72 | } 73 | 74 | // Valid check if the link is valid. 75 | func (f File) Valid(ctx context.Context, filePath, uri string) (bool, error) { 76 | if f.isMarkdown(filePath) { 77 | found, err := f.checkMarkdown(filePath, uri) 78 | if err != nil { 79 | return false, fmt.Errorf("fail to check the markdown: %w", err) 80 | } 81 | return found, nil 82 | } 83 | 84 | path := filepath.Join(filepath.Dir(filePath), uri) 85 | _, itemExists := f.reader.fileExists(path) 86 | return itemExists, nil 87 | } 88 | 89 | func (f *File) initRegex() error { 90 | expr := "^.*$" 91 | schema, err := regexp.Compile(expr) 92 | if err != nil { 93 | return fmt.Errorf("fail to compile the expression '%s': %w", expr, err) 94 | } 95 | f.schemaRegex = *schema 96 | return nil 97 | } 98 | 99 | func (File) isMarkdown(path string) bool { 100 | return filepath.Ext(path) == ".md" 101 | } 102 | 103 | // checkMarkdown check if the uri is a Markdown, if positive, it will be responsible to detect if the link is valid. 104 | func (f File) checkMarkdown(path, uri string) (bool, error) { 105 | parsedURI, err := url.Parse(uri) 106 | if err != nil { 107 | return false, fmt.Errorf("fail to parse the uri '%s': %w", uri, err) 108 | } 109 | 110 | // If the link is just a anchor like '#something' it will fit into the first condition. Otherwise it will be something 111 | // like this 'file.md' or 'file.md#something' and it will fall into the second condition. 112 | var expandedPath string 113 | if parsedURI.Path == "" { 114 | expandedPath = path 115 | } else { 116 | expandedPath = filepath.Join(filepath.Dir(path), parsedURI.Path) 117 | } 118 | 119 | // Check if the path exists, if not, it will return to the fallback verification at the caller. 120 | // If the path is a directory we get a valid response. 121 | pathStat, valid := f.reader.fileExists(expandedPath) 122 | if !valid { 123 | return false, nil 124 | } 125 | if pathStat.IsDir() { 126 | return true, nil 127 | } 128 | 129 | if filepath.Ext(expandedPath) != ".md" { 130 | return true, nil 131 | } 132 | 133 | if parsedURI.Fragment == "" { 134 | return true, nil 135 | } 136 | 137 | payload, err := f.reader.readFile(expandedPath) 138 | if err != nil { 139 | return false, fmt.Errorf("fail to read the file '%s': %w", expandedPath, err) 140 | } 141 | payload = f.Parser.Do(payload) 142 | 143 | doc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(payload)) 144 | if err != nil { 145 | return false, fmt.Errorf("fail to parse the HTML: %w", err) 146 | } 147 | 148 | fragment := f.Parser.SanitizedAnchorName(parsedURI.Fragment) 149 | handlers := []func(*goquery.Document, string, string) bool{ 150 | f.checkMarkdownH, 151 | f.checkMarkdownLi, 152 | } 153 | for _, h := range handlers { 154 | if h(doc, fragment, parsedURI.Fragment) { 155 | return true, nil 156 | } 157 | } 158 | 159 | return false, nil 160 | } 161 | 162 | // checkMarkdownH checks if the link is in a 'h' tag. 163 | func (File) checkMarkdownH(doc *goquery.Document, fragment, _ string) bool { 164 | var found bool 165 | for i := 1; (i <= 6) && (!found); i++ { 166 | doc.Find(fmt.Sprintf("h%d", i)).Each(func(i int, selection *goquery.Selection) { 167 | if found { 168 | return 169 | } 170 | 171 | id, ok := selection.Attr("id") 172 | if !ok { 173 | return 174 | } 175 | 176 | // Check if the frament is in the id, this is for normal links. 177 | found = (strings.ToLower(id) == fragment) 178 | if found { 179 | return 180 | } 181 | 182 | // The fragment can point to a link as well, on this case we need to check if there is a link inside the h tag 183 | // with the fragment. 184 | selection.Each(func(_ int, selection *goquery.Selection) { 185 | if found { 186 | return 187 | } 188 | found = (strings.ToLower(selection.Text()) == fragment) 189 | }) 190 | }) 191 | } 192 | return found 193 | } 194 | 195 | // checkMarkdownLi checks if the link is present inside a 'li' tag. 196 | func (f File) checkMarkdownLi(doc *goquery.Document, _, fragment string) bool { 197 | var found bool 198 | doc.Find("li").Each(func(_ int, selection *goquery.Selection) { 199 | if found { 200 | return 201 | } 202 | found = (f.Parser.SanitizedAnchorName(selection.Text()) == fragment) 203 | }) 204 | return found 205 | } 206 | -------------------------------------------------------------------------------- /internal/client.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/logrusorgru/aurora" 13 | 14 | "nitro/markdown-link-check/internal/service" 15 | "nitro/markdown-link-check/internal/service/parser" 16 | "nitro/markdown-link-check/internal/service/provider" 17 | "nitro/markdown-link-check/internal/service/scan" 18 | "nitro/markdown-link-check/internal/service/worker" 19 | ) 20 | 21 | // ClientIgnore holds the ignore list for links and files. 22 | type ClientIgnore struct { 23 | Link []string 24 | File []string 25 | } 26 | 27 | // ClientProviderGithub holds the configuration for the GitHub provider. 28 | type ClientProviderGithub struct { 29 | Token string 30 | Owner string 31 | Repository string 32 | } 33 | 34 | // ClientProviderWeb holds the configuration for the web provider. 35 | type ClientProviderWeb struct { 36 | Config http.Header 37 | ConfigOverwrite map[string]http.Header 38 | } 39 | 40 | // ClientProvider holds the configuration for the providers. 41 | type ClientProvider struct { 42 | Github []ClientProviderGithub 43 | Web ClientProviderWeb 44 | } 45 | 46 | // Client is responsible to bootstrap the application. 47 | type Client struct { 48 | Path string 49 | Ignore ClientIgnore 50 | Provider ClientProvider 51 | 52 | parser parser.Markdown 53 | providers []worker.Provider 54 | } 55 | 56 | // Run starts the application execution. 57 | func (c Client) Run(ctx context.Context) (bool, error) { 58 | if err := c.init(); err != nil { 59 | return false, fmt.Errorf("fail during init: %w", err) 60 | } 61 | 62 | s := scan.Scan{IgnoreFile: c.Ignore.File, IgnoreLink: c.Ignore.Link, Parser: c.parser} 63 | if err := s.Init(); err != nil { 64 | return false, fmt.Errorf("fail to initialize the scan service: %w", err) 65 | } 66 | entries, err := s.Process(c.Path) 67 | if err != nil { 68 | return false, fmt.Errorf("fail to scan the files: %w", err) 69 | } 70 | 71 | w := worker.Worker{Providers: c.providers} 72 | entries, err = w.Process(ctx, entries) 73 | if err != nil { 74 | return false, fmt.Errorf("fail to process the link: %w", err) 75 | } 76 | 77 | return c.output(entries), nil 78 | } 79 | 80 | func (c *Client) init() error { 81 | if c.Path == "" { 82 | return errors.New("missing 'path") 83 | } 84 | 85 | stat, err := os.Stat(c.Path) 86 | if os.IsNotExist(err) { 87 | return fmt.Errorf("path does not exist: %w", err) 88 | } 89 | if !stat.IsDir() { 90 | return errors.New("path is expected to be a directory") 91 | } 92 | 93 | var email provider.Email 94 | if err := email.Init(); err != nil { 95 | return fmt.Errorf("fail to iniitalize the email provider: %w", err) 96 | } 97 | c.providers = append(c.providers, email) 98 | 99 | for _, github := range c.Provider.Github { 100 | client := provider.GitHub{ 101 | Token: github.Token, 102 | Owner: github.Owner, 103 | HTTPClient: http.DefaultClient, 104 | } 105 | if err := client.Init(); err != nil { 106 | return fmt.Errorf("fail to iniitalize the GitHub provider: %w", err) 107 | } 108 | c.providers = append(c.providers, client) 109 | } 110 | 111 | webConfigOverwrites := make(map[string]provider.WebConfig, len(c.Provider.Web.ConfigOverwrite)) 112 | for key, value := range c.Provider.Web.ConfigOverwrite { 113 | webConfigOverwrites[key] = provider.WebConfig{Header: value} 114 | } 115 | w := provider.Web{ 116 | Config: provider.WebConfig{Header: c.Provider.Web.Config}, 117 | ConfigOverwrite: webConfigOverwrites, 118 | } 119 | if err := w.Init(); err != nil { 120 | return fmt.Errorf("fail to initialize the web provider: %w", err) 121 | } 122 | c.providers = append(c.providers, w) 123 | 124 | var p parser.Markdown 125 | p.Init() 126 | c.parser = p 127 | f := provider.File{Path: c.Path, Parser: p} 128 | if err := f.Init(); err != nil { 129 | return fmt.Errorf("fail to initialize the file provider: %w", err) 130 | } 131 | c.providers = append(c.providers, f) 132 | 133 | return nil 134 | } 135 | 136 | func (c Client) output(entries []service.Entry) bool { 137 | var ( 138 | result bool 139 | iter = c.aggregate(entries) 140 | ) 141 | for { 142 | key, entries, ok := iter() 143 | if !ok { 144 | break 145 | } 146 | if !c.hasInvalidLink(entries) { 147 | continue 148 | } 149 | if !result { 150 | result = true 151 | } 152 | 153 | fmt.Print(aurora.Bold(c.relativePath(key))) 154 | for _, entry := range entries { 155 | if entry.Valid { 156 | continue 157 | } 158 | fmt.Printf("\n%s %s", aurora.Bold(aurora.Gray(24, "-")), entry.Link) 159 | } 160 | fmt.Printf("\n\n") 161 | } 162 | return result 163 | } 164 | 165 | func (Client) aggregate(entries []service.Entry) func() (string, []service.Entry, bool) { 166 | var ( 167 | keys = make([]string, 0, len(entries)) 168 | result = make(map[string][]service.Entry) 169 | ) 170 | 171 | for _, entry := range entries { 172 | if _, ok := result[entry.Path]; !ok { 173 | keys = append(keys, entry.Path) 174 | result[entry.Path] = make([]service.Entry, 0) 175 | } 176 | result[entry.Path] = append(result[entry.Path], entry) 177 | } 178 | sort.Strings(keys) 179 | 180 | var index = 0 181 | return func() (string, []service.Entry, bool) { 182 | if index >= len(keys) { 183 | return "", nil, false 184 | } 185 | key := keys[index] 186 | index++ 187 | entries := result[key] 188 | sort.Sort(serviceEntrySort(entries)) 189 | return key, entries, true 190 | } 191 | } 192 | 193 | func (Client) hasInvalidLink(entries []service.Entry) bool { 194 | for _, entry := range entries { 195 | if !entry.Valid { 196 | return true 197 | } 198 | } 199 | return false 200 | } 201 | 202 | func (c Client) relativePath(path string) string { 203 | dirPath := c.Path 204 | if !strings.HasSuffix(dirPath, "/") { 205 | dirPath += "/" 206 | } 207 | return strings.TrimPrefix(path, dirPath) 208 | } 209 | 210 | type serviceEntrySort []service.Entry 211 | 212 | func (s serviceEntrySort) Len() int { 213 | return len(s) 214 | } 215 | 216 | func (s serviceEntrySort) Swap(i, j int) { 217 | s[i], s[j] = s[j], s[i] 218 | } 219 | 220 | func (s serviceEntrySort) Less(i, j int) bool { 221 | return s[i].Link < s[j].Link 222 | } 223 | -------------------------------------------------------------------------------- /internal/service/provider/web_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestWebInit(t *testing.T) { 15 | t.Parallel() 16 | var client Web 17 | require.NoError(t, client.Init()) 18 | } 19 | 20 | func TestWebAuthority(t *testing.T) { 21 | t.Parallel() 22 | 23 | var client Web 24 | require.NoError(t, client.Init()) 25 | 26 | tests := []struct { 27 | message string 28 | uri string 29 | hasAuthority bool 30 | }{ 31 | { 32 | message: "have authority #1", 33 | uri: "https://website.com", 34 | hasAuthority: true, 35 | }, 36 | { 37 | message: "have authority #2", 38 | uri: "http://website.com", 39 | hasAuthority: true, 40 | }, 41 | { 42 | message: "have no authority #1", 43 | uri: "../file.md", 44 | hasAuthority: false, 45 | }, 46 | { 47 | message: "have no authority #2", 48 | uri: "/folder", 49 | hasAuthority: false, 50 | }, 51 | } 52 | 53 | for i := 0; i < len(tests); i++ { 54 | tt := tests[i] 55 | t.Run("Should "+tt.message, func(t *testing.T) { 56 | t.Parallel() 57 | require.Equal(t, tt.hasAuthority, client.Authority(tt.uri)) 58 | }) 59 | } 60 | } 61 | 62 | func TestWebValid(t *testing.T) { 63 | t.Parallel() 64 | 65 | // The fragment is not sent at the HTTP request, so we have some weird endpoints like 66 | // '/valid-fragment-title-from-browser' to have the needed granularity to execute the tests. 67 | // 68 | // More details on this issue: https://github.com/golang/go/issues/3805#issuecomment-66068331 69 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 | require.Equal(t, "true", r.Header.Get("control")) 71 | 72 | const validEndpoint = "/valid" 73 | 74 | if r.URL.Path == validEndpoint { 75 | return 76 | } 77 | 78 | if r.URL.Path == "/301" { 79 | r.URL.Path = validEndpoint 80 | w.Header().Set("location", r.URL.String()) 81 | w.WriteHeader(http.StatusMovedPermanently) 82 | return 83 | } 84 | 85 | if r.URL.Path == "/308" { 86 | r.URL.Path = validEndpoint 87 | w.Header().Set("location", r.URL.String()) 88 | w.WriteHeader(http.StatusPermanentRedirect) 89 | return 90 | } 91 | 92 | if r.URL.Path == "/valid-fragment-title" { 93 | _, err := w.Write([]byte(``)) 94 | require.NoError(t, err) 95 | return 96 | } 97 | 98 | if r.URL.Path == "/valid-fragment-title-from-browser" { 99 | var response string 100 | if r.Header.Get("user-agent") != "Go-http-client/1.1" { 101 | response = `` 102 | } 103 | _, err := w.Write([]byte(response)) 104 | require.NoError(t, err) 105 | return 106 | } 107 | 108 | if r.URL.Path == "/valid-user-agent-chrome" { 109 | if r.Header.Get("user-agent") == "chrome" { 110 | w.WriteHeader(http.StatusOK) 111 | } else { 112 | w.WriteHeader(http.StatusInternalServerError) 113 | } 114 | return 115 | } 116 | 117 | if r.URL.Path == "/valid-user-agent-firefox" { 118 | if r.Header.Get("user-agent") == "firefox" { 119 | w.WriteHeader(http.StatusOK) 120 | } else { 121 | w.WriteHeader(http.StatusInternalServerError) 122 | } 123 | return 124 | } 125 | 126 | if r.URL.Path == "/307" { 127 | w.WriteHeader(http.StatusTemporaryRedirect) 128 | return 129 | } 130 | 131 | if r.URL.Path == "/404" { 132 | w.WriteHeader(http.StatusNotFound) 133 | return 134 | } 135 | 136 | if r.URL.Path == "/invalid-fragment-broken" { 137 | require.Equal(t, "true", r.Header.Get("control-browser")) 138 | _, err := w.Write([]byte(``)) 139 | require.NoError(t, err) 140 | return 141 | } 142 | 143 | require.FailNow(t, "not expected to reach this point") 144 | })) 145 | defer server.Close() 146 | serverEndpoint, err := url.Parse(server.URL) 147 | require.NoError(t, err) 148 | 149 | tests := []struct { 150 | message string 151 | endpoint url.URL 152 | isValid bool 153 | shouldErr bool 154 | }{ 155 | { 156 | message: "attest the URI as valid", 157 | endpoint: url.URL{Path: "/valid"}, 158 | shouldErr: false, 159 | isValid: true, 160 | }, 161 | { 162 | message: "attest the URI as valid after a move permanently redirect (301)", 163 | endpoint: url.URL{Path: "/301"}, 164 | shouldErr: false, 165 | isValid: true, 166 | }, 167 | { 168 | message: "attest the URI as valid after a permanent redirect (308)", 169 | endpoint: url.URL{Path: "/308"}, 170 | shouldErr: false, 171 | isValid: true, 172 | }, 173 | { 174 | message: "attest the URI as valid and also have a valid anchor", 175 | endpoint: url.URL{Path: "/valid-fragment-title", Fragment: "title"}, 176 | shouldErr: false, 177 | isValid: true, 178 | }, 179 | { 180 | message: "attest the URI as valid and also have a valid anchor at the browser", 181 | endpoint: url.URL{Path: "/valid-fragment-title-from-browser", Fragment: "title"}, 182 | shouldErr: false, 183 | isValid: true, 184 | }, 185 | { 186 | message: "attest the URI as valid and have the correct user agent #1", 187 | endpoint: url.URL{Path: "/valid-user-agent-chrome", Opaque: "chrome"}, 188 | shouldErr: false, 189 | isValid: true, 190 | }, 191 | { 192 | message: "attest the URI as valid and have the correct user agent #2", 193 | endpoint: url.URL{Path: "/valid-user-agent-firefox"}, 194 | shouldErr: false, 195 | isValid: true, 196 | }, 197 | { 198 | message: "attest the URI as invalid because of a temporary redirect", 199 | endpoint: url.URL{Path: "/307"}, 200 | shouldErr: false, 201 | isValid: false, 202 | }, 203 | { 204 | message: "attest the URI as invalid because of a not found status", 205 | endpoint: url.URL{Path: "/404"}, 206 | shouldErr: false, 207 | isValid: false, 208 | }, 209 | { 210 | message: "attest the URI as invalid because of a not found anchor", 211 | endpoint: url.URL{Path: "/invalid-fragment-broken", Fragment: "broken"}, 212 | shouldErr: false, 213 | isValid: false, 214 | }, 215 | } 216 | 217 | genEndpoint := func(endpoint url.URL) string { 218 | result := *serverEndpoint 219 | result.Path = endpoint.Path 220 | result.Fragment = endpoint.Fragment 221 | if endpoint.Opaque == "chrome" { 222 | result.Host = strings.ReplaceAll(result.Host, "127.0.0.1", "localhost") 223 | } 224 | return result.String() 225 | } 226 | 227 | for i := 0; i < len(tests); i++ { 228 | tt := tests[i] 229 | t.Run("Should "+tt.message, func(t *testing.T) { 230 | client := Web{ 231 | Config: WebConfig{Header: make(http.Header)}, 232 | ConfigOverwrite: map[string]WebConfig{ 233 | "http://localhost": {Header: make(http.Header)}, 234 | "http://127.0.0.1": {Header: make(http.Header)}, 235 | }, 236 | } 237 | client.Config.Header.Set("control", "true") 238 | client.Config.Header.Set("user-agent", "firefox") 239 | client.ConfigOverwrite["http://localhost"].Header.Set("user-agent", "chrome") 240 | client.ConfigOverwrite["http://127.0.0.1"].Header.Set("control-browser", "true") 241 | require.NoError(t, client.Init()) 242 | defer client.Close() 243 | 244 | isValid, err := client.Valid(context.Background(), "", genEndpoint(tt.endpoint)) 245 | require.Equal(t, tt.shouldErr, (err != nil)) 246 | if err != nil { 247 | return 248 | } 249 | require.Equal(t, tt.isValid, isValid) 250 | }) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /internal/service/provider/web.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "regexp" 12 | "sync" 13 | 14 | "github.com/PuerkitoBio/goquery" 15 | "github.com/go-rod/rod" 16 | "github.com/go-rod/rod/lib/launcher" 17 | "github.com/go-rod/rod/lib/proto" 18 | ) 19 | 20 | // Rod is very sensitive and for now, the best approach is to have a mutex at the package level protecting all the 21 | // operations. 22 | var webBrowserMutex sync.Mutex // nolint: gochecknoglobals 23 | 24 | type webClient interface { 25 | Do(req *http.Request) (*http.Response, error) 26 | } 27 | 28 | type webClientTransport struct { 29 | client webClient 30 | } 31 | 32 | func (w webClientTransport) RoundTrip(req *http.Request) (*http.Response, error) { 33 | return w.client.Do(req) 34 | } 35 | 36 | type webConfigRegex struct { 37 | expression regexp.Regexp 38 | key string 39 | } 40 | 41 | // WebConfig has the information to enhance the request. 42 | type WebConfig struct { 43 | Header http.Header 44 | } 45 | 46 | // Web handle the verification of HTTP endpoints. 47 | type Web struct { 48 | Config WebConfig 49 | ConfigOverwrite map[string]WebConfig 50 | 51 | browser *rod.Browser 52 | client webClient 53 | regex regexp.Regexp 54 | regexConfigOverwrite []webConfigRegex 55 | } 56 | 57 | // Init internal state. 58 | func (w *Web) Init() error { 59 | if err := w.initRegex(); err != nil { 60 | return fmt.Errorf("fail to initialize the regex: %w", err) 61 | } 62 | if err := w.initRegexConfig(); err != nil { 63 | return fmt.Errorf("fail to initialize the regex config: %w", err) 64 | } 65 | w.initHTTP() 66 | if err := w.initBrowser(); err != nil { 67 | return fmt.Errorf("failed to initialize the browser: %w", err) 68 | } 69 | return nil 70 | } 71 | 72 | // Close the provider. 73 | func (w *Web) Close() error { 74 | if err := w.browser.Close(); err != nil { 75 | return fmt.Errorf("failed to close the browser: %w", err) 76 | } 77 | return nil 78 | } 79 | 80 | // Authority checks if the web provider is responsible to process the entry. 81 | func (w Web) Authority(uri string) bool { 82 | return w.regex.Match([]byte(uri)) 83 | } 84 | 85 | // Valid check if the link is valid. 86 | func (w Web) Valid(ctx context.Context, _, uri string) (bool, error) { 87 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) 88 | if err != nil { 89 | return false, fmt.Errorf("fail to create the HTTP request: %w", err) 90 | } 91 | w.configRequest(req) 92 | 93 | resp, err := w.client.Do(req) 94 | if err != nil { 95 | return false, nil 96 | } 97 | defer resp.Body.Close() 98 | 99 | endpoint, err := url.Parse(uri) 100 | if err != nil { 101 | return false, fmt.Errorf("fail to parse uri: %w", err) 102 | } 103 | 104 | isValid := ((resp.StatusCode >= 200) && (resp.StatusCode < 300)) 105 | if !isValid { 106 | return false, nil 107 | } 108 | 109 | validAnchor, err := w.validAnchor(resp.Body, endpoint.Fragment) 110 | if err != nil { 111 | return false, fmt.Errorf("fail to verify the anchor: %w", err) 112 | } 113 | if validAnchor { 114 | return true, nil 115 | } 116 | 117 | validAnchor, err = w.validAnchorBrowser(ctx, uri, endpoint.Fragment) 118 | if err != nil { 119 | return false, fmt.Errorf("fail to verify the anchor with a browser: %w", err) 120 | } 121 | return validAnchor, nil 122 | } 123 | 124 | func (w Web) validAnchor(body io.Reader, anchor string) (bool, error) { 125 | if anchor == "" { 126 | return true, nil 127 | } 128 | anchor = fmt.Sprintf("#%s", anchor) 129 | 130 | doc, err := goquery.NewDocumentFromReader(body) 131 | if err != nil { 132 | return false, fmt.Errorf("failt o parse the response: %w", err) 133 | } 134 | 135 | var found bool 136 | doc.Find("a").Each(func(_ int, selection *goquery.Selection) { 137 | if found { 138 | return 139 | } 140 | 141 | href, ok := selection.Attr("href") 142 | if !ok { 143 | return 144 | } 145 | found = (href == anchor) 146 | }) 147 | 148 | return found, nil 149 | } 150 | 151 | func (w *Web) initRegex() error { 152 | expr := `^(http|https):\/\/` 153 | regex, err := regexp.Compile(expr) 154 | if err != nil { 155 | return fmt.Errorf("fail to compile the expression '%s': %w", expr, err) 156 | } 157 | w.regex = *regex 158 | return nil 159 | } 160 | 161 | func (w *Web) initRegexConfig() error { 162 | w.regexConfigOverwrite = make([]webConfigRegex, 0, len(w.regexConfigOverwrite)) 163 | for key := range w.ConfigOverwrite { 164 | regex, err := regexp.Compile(key) 165 | if err != nil { 166 | return fmt.Errorf("fail to compile the expression '%s': %w", key, err) 167 | } 168 | w.regexConfigOverwrite = append(w.regexConfigOverwrite, webConfigRegex{key: key, expression: *regex}) 169 | } 170 | return nil 171 | } 172 | 173 | func (w *Web) initHTTP() { 174 | w.client = &http.Client{ 175 | Transport: webClientTransport{client: http.DefaultClient}, 176 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 177 | switch req.Response.StatusCode { 178 | case http.StatusPermanentRedirect, http.StatusMovedPermanently: 179 | return nil 180 | default: 181 | return errors.New("redirect not allowed") 182 | } 183 | }, 184 | } 185 | } 186 | 187 | func (w *Web) initBrowser() error { 188 | webBrowserMutex.Lock() 189 | defer webBrowserMutex.Unlock() 190 | 191 | launcherURL, err := launcher.New().Headless(true).Launch() 192 | if err != nil { 193 | return fmt.Errorf("failed to launch the browser: %w", err) 194 | } 195 | 196 | w.browser = rod.New().ControlURL(launcherURL) 197 | if err = w.browser.Connect(); err != nil { 198 | return fmt.Errorf("failed to connect to the browser: %w", err) 199 | } 200 | return nil 201 | } 202 | 203 | func (w Web) validAnchorBrowser(ctx context.Context, endpoint string, anchor string) (_ bool, err error) { 204 | webBrowserMutex.Lock() 205 | defer webBrowserMutex.Unlock() 206 | 207 | pctx, pctxCancel := context.WithCancel(ctx) 208 | defer pctxCancel() 209 | 210 | page, err := w.browser.Page(proto.TargetCreateTarget{}) 211 | if err != nil { 212 | return false, fmt.Errorf("failed to create the browser page: %w", err) 213 | } 214 | defer func() { 215 | if perr := page.Close(); perr != nil { 216 | err = fmt.Errorf("failed to close the browser tab: %w", perr) 217 | } 218 | }() 219 | 220 | if _, err = page.Context(pctx).SetExtraHeaders(w.genHeaders(endpoint)); err != nil { 221 | return false, fmt.Errorf("failed to set the headers at the browser page: %w", err) 222 | } 223 | 224 | if err := page.Navigate(endpoint); err != nil { 225 | return false, fmt.Errorf("failed to navigate to the page: %w", err) 226 | } 227 | 228 | if err := page.WaitLoad(); err != nil { 229 | return false, fmt.Errorf("failed to wait for the page to load: %w", err) 230 | } 231 | 232 | result, err := page.Eval("", "document.documentElement.innerHTML", nil) 233 | if err != nil { 234 | return false, fmt.Errorf("failed to execute the javascript at the page: %w", err) 235 | } 236 | return w.validAnchor(bytes.NewBufferString(result.Value.String()), anchor) 237 | } 238 | 239 | func (w Web) configRequest(r *http.Request) { 240 | setHeader := func(header http.Header) { 241 | for key, values := range header { 242 | for _, value := range values { 243 | r.Header.Set(key, value) 244 | } 245 | } 246 | } 247 | 248 | setHeader(w.Config.Header) 249 | 250 | endpoint := r.URL.String() 251 | for _, cfg := range w.regexConfigOverwrite { 252 | if cfg.expression.Match([]byte(endpoint)) { 253 | setHeader(w.ConfigOverwrite[cfg.key].Header) 254 | return 255 | } 256 | } 257 | } 258 | 259 | func (w Web) genHeaders(endpoint string) []string { 260 | header := make(http.Header) 261 | 262 | setHeader := func(source http.Header) { 263 | for key, values := range source { 264 | for _, value := range values { 265 | header.Set(key, value) 266 | } 267 | } 268 | } 269 | setHeader(w.Config.Header) 270 | 271 | for _, cfg := range w.regexConfigOverwrite { 272 | if cfg.expression.MatchString(endpoint) { 273 | setHeader(w.ConfigOverwrite[cfg.key].Header) 274 | break 275 | } 276 | } 277 | 278 | results := make([]string, 0, len(header)*2) 279 | for key := range header { 280 | results = append(results, key, header.Get(key)) 281 | } 282 | return results 283 | } 284 | -------------------------------------------------------------------------------- /internal/service/provider/github.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/google/go-github/github" 13 | ) 14 | 15 | type gitHubRepository interface { 16 | repository(ctx context.Context, repository string) (*github.Response, error) 17 | repositoriesGetCommitSHA1(ctx context.Context, repository, ref string) (*github.Response, error) 18 | issuesGet(ctx context.Context, repo string, number int) (*github.Response, error) 19 | issuesGetComment(ctx context.Context, repo string, commentID int64) (*github.Response, error) 20 | pullRequestsGetRaw(ctx context.Context, repo string, number int) (*github.Response, error) 21 | relatedPullRequests(ctx context.Context, repository, ref string) ([]int, error) 22 | } 23 | 24 | type gitHubHTTPClient interface { 25 | Do(req *http.Request) (*http.Response, error) 26 | } 27 | 28 | // GitHub provider. 29 | type GitHub struct { 30 | HTTPClient gitHubHTTPClient 31 | Token string 32 | Owner string 33 | 34 | repository gitHubRepository 35 | regexOwner regexp.Regexp 36 | regexRepository regexp.Regexp 37 | regexRaw regexp.Regexp 38 | regexBase regexp.Regexp 39 | regexCommit regexp.Regexp 40 | regexIssue regexp.Regexp 41 | regexPullRequest regexp.Regexp 42 | } 43 | 44 | // Init the internal state. 45 | func (g *GitHub) Init() error { 46 | if g.HTTPClient == nil { 47 | return errors.New("missing 'httpClient") 48 | } 49 | 50 | if g.repository == nil { 51 | api := githubAPI{token: g.Token, owner: g.Owner, httpClient: g.HTTPClient} 52 | if err := api.init(); err != nil { 53 | return fmt.Errorf("fail to initialize the GitHub client: %w", err) 54 | } 55 | g.repository = api 56 | } 57 | 58 | if err := g.initRegex(); err != nil { 59 | return fmt.Errorf("fail to initialize the regex expressions: %w", err) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // Authority checks if the github provider is responsible to process the entry. 66 | func (g GitHub) Authority(uri string) bool { 67 | for _, expr := range []regexp.Regexp{g.regexRaw, g.regexBase} { 68 | if expr.Match([]byte(uri)) { 69 | return true 70 | } 71 | } 72 | return false 73 | } 74 | 75 | // Valid check if the link is valid. 76 | func (g GitHub) Valid(ctx context.Context, _, uri string) (bool, error) { 77 | fns := []func(context.Context, string) (bool, error){ 78 | g.validOwner, 79 | g.validCommit, 80 | g.validIssue, 81 | g.validPullRequest, 82 | g.validRepository, 83 | } 84 | for _, fn := range fns { 85 | valid, err := fn(ctx, uri) 86 | if err != nil { 87 | return false, nil 88 | } 89 | if valid { 90 | return true, nil 91 | } 92 | } 93 | return false, nil 94 | } 95 | 96 | func (g *GitHub) initRegex() error { 97 | compile := func(rawExpr string) (regexp.Regexp, error) { 98 | expr, err := regexp.Compile(rawExpr) 99 | if err != nil { 100 | return regexp.Regexp{}, fmt.Errorf("fail to compile the expression '%s': %w", rawExpr, err) 101 | } 102 | return *expr, nil 103 | } 104 | 105 | var err error 106 | regexRaw := fmt.Sprintf( 107 | `^(?Phttp|https):\/\/raw\.githubusercontent\.com\/((?i)%s)`, 108 | regexp.QuoteMeta(g.Owner), 109 | ) 110 | g.regexRaw, err = compile(regexRaw) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | regexBase := fmt.Sprintf(`^(?Phttp|https):\/\/github\.com\/((?i)%s)`, g.Owner) 116 | g.regexBase, err = compile(regexBase) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | regexOwner := fmt.Sprintf(`^(?Phttp|https):\/\/github\.com\/((?i)%s)$`, g.Owner) 122 | g.regexOwner, err = compile(regexOwner) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | regexCommit := fmt.Sprintf( 128 | `^(?Phttp|https):\/\/github\.com\/((?i)%s)\/(?P.*)\/commit\/(?P.*)$`, g.Owner, 129 | ) 130 | g.regexCommit, err = compile(regexCommit) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | regexRepository := fmt.Sprintf(`^(?Phttp|https):\/\/github\.com\/((?i)%s)\/(?P.*)$`, g.Owner) 136 | g.regexRepository, err = compile(regexRepository) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | var regexIssue strings.Builder 142 | fmt.Fprintf(®exIssue, `^(?Phttp|https):\/\/github\.com\/((?i)%s)\/(?P.*)\/issues\/`, g.Owner) 143 | fmt.Fprint(®exIssue, `(?P[0-9]*)(#issuecomment-(?P[0-9]*))?$`) 144 | g.regexIssue, err = compile(regexIssue.String()) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | var regexPullRequest strings.Builder 150 | fmt.Fprintf( 151 | ®exPullRequest, `^(?Phttp|https):\/\/github\.com\/((?i)%s)\/(?P.*)\/pull\/`, g.Owner, 152 | ) 153 | fmt.Fprint(®exPullRequest, `(?P[0-9]*)(\/commits\/(?P.*))?$`) 154 | g.regexPullRequest, err = compile(regexPullRequest.String()) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | return nil 160 | } 161 | 162 | // validOwner is not doing the complete verification as it's not available at the API. We're trusting that the owner 163 | // exists based on the configuration the client provided. 164 | func (g GitHub) validOwner(ctx context.Context, uri string) (bool, error) { 165 | fragments := g.regexOwner.FindStringSubmatch(uri) 166 | if fragments == nil { 167 | return false, nil 168 | } 169 | return true, nil 170 | } 171 | 172 | func (g GitHub) validRepository(ctx context.Context, uri string) (bool, error) { 173 | fragments := g.regexRepository.FindStringSubmatch(uri) 174 | if fragments == nil { 175 | return false, nil 176 | } 177 | 178 | resp, err := g.repository.repository(ctx, fragments[3]) 179 | if err != nil { 180 | return false, fmt.Errorf("fail to consult the repository: %w", err) 181 | } 182 | resp.Body.Close() 183 | if resp.StatusCode != http.StatusOK { 184 | return false, nil 185 | } 186 | 187 | return true, nil 188 | } 189 | 190 | func (g GitHub) validCommit(ctx context.Context, uri string) (bool, error) { 191 | fragments := g.regexCommit.FindStringSubmatch(uri) 192 | if fragments == nil { 193 | return false, nil 194 | } 195 | 196 | resp, err := g.repository.repositoriesGetCommitSHA1(ctx, fragments[3], fragments[4]) 197 | if err != nil { 198 | return false, fmt.Errorf("fail to consult the commit at GitHub: %w", err) 199 | } 200 | resp.Body.Close() 201 | if resp.StatusCode == http.StatusOK { 202 | return true, nil 203 | } 204 | 205 | return false, nil 206 | } 207 | 208 | func (g GitHub) validIssue(ctx context.Context, uri string) (bool, error) { 209 | fragments := g.regexIssue.FindStringSubmatch(uri) 210 | if fragments == nil { 211 | return false, nil 212 | } 213 | 214 | issue, err := strconv.ParseInt(fragments[4], 10, 64) 215 | if err != nil { 216 | return false, fmt.Errorf("fail to parse the issue value: %w", err) 217 | } 218 | 219 | resp, err := g.repository.issuesGet(ctx, fragments[3], (int)(issue)) 220 | if err != nil { 221 | return false, fmt.Errorf("fail to consult the issue at GitHub: %w", err) 222 | } 223 | resp.Body.Close() 224 | if resp.StatusCode != http.StatusOK { 225 | return false, nil 226 | } 227 | if fragments[6] == "" { 228 | return true, nil 229 | } 230 | 231 | return g.validIssueComment(ctx, fragments) 232 | } 233 | 234 | func (g GitHub) validIssueComment(ctx context.Context, fragments []string) (bool, error) { 235 | comment, err := strconv.ParseInt(fragments[6], 10, 64) 236 | if err != nil { 237 | return false, fmt.Errorf("fail to parse the comment value: %w", err) 238 | } 239 | 240 | resp, err := g.repository.issuesGetComment(ctx, fragments[3], comment) 241 | if err != nil { 242 | return false, fmt.Errorf("fail to consult the issue comment at GitHub: %w", err) 243 | } 244 | defer resp.Body.Close() 245 | return (resp.StatusCode == http.StatusOK), nil 246 | } 247 | 248 | func (g GitHub) validPullRequest(ctx context.Context, uri string) (bool, error) { 249 | fragments := g.regexPullRequest.FindStringSubmatch(uri) 250 | if fragments == nil { 251 | return false, nil 252 | } 253 | 254 | pullRequestID, err := strconv.ParseInt(fragments[4], 10, 64) 255 | if err != nil { 256 | return false, fmt.Errorf("fail to parse the pull request ID value: %w", err) 257 | } 258 | 259 | resp, err := g.repository.pullRequestsGetRaw(ctx, fragments[3], (int)(pullRequestID)) 260 | if err != nil { 261 | return false, fmt.Errorf("fail to consult the pull request at GitHub: %w", err) 262 | } 263 | resp.Body.Close() 264 | 265 | if resp.StatusCode != http.StatusOK { 266 | return false, nil 267 | } 268 | 269 | return g.validPullRequestCommit(ctx, (int)(pullRequestID), fragments) 270 | } 271 | 272 | func (g GitHub) validPullRequestCommit(ctx context.Context, pullRequestID int, fragments []string) (bool, error) { 273 | if fragments[6] == "" { 274 | return true, nil 275 | } 276 | 277 | pullRequestIDS, err := g.repository.relatedPullRequests(ctx, fragments[3], fragments[6]) 278 | if err != nil { 279 | return false, fmt.Errorf("fail to fetch the pull requests associated with the commit: %w", err) 280 | } 281 | 282 | for _, id := range pullRequestIDS { 283 | if id == pullRequestID { 284 | return true, nil 285 | } 286 | } 287 | 288 | return false, nil 289 | } 290 | -------------------------------------------------------------------------------- /internal/service/provider/github_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | "testing" 10 | 11 | "github.com/google/go-github/github" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestGitHubInit(t *testing.T) { 16 | t.Parallel() 17 | 18 | tests := []struct { 19 | message string 20 | client GitHub 21 | shouldErr bool 22 | }{ 23 | { 24 | message: "have an error because of it's missing the HTTP client", 25 | client: GitHub{Token: "token", Owner: "owner"}, 26 | shouldErr: true, 27 | }, 28 | { 29 | message: "have an error because of it's missing the token", 30 | client: GitHub{HTTPClient: http.DefaultClient, Owner: "owner"}, 31 | shouldErr: true, 32 | }, 33 | { 34 | message: "have an error because of it's missing the owner", 35 | client: GitHub{HTTPClient: http.DefaultClient, Token: "token"}, 36 | shouldErr: true, 37 | }, 38 | } 39 | 40 | for i := 0; i < len(tests); i++ { 41 | tt := tests[i] 42 | t.Run("Should "+tt.message, func(t *testing.T) { 43 | t.Parallel() 44 | require.Equal(t, tt.shouldErr, (tt.client.Init() != nil)) 45 | }) 46 | } 47 | } 48 | 49 | func TestGitHubAuthority(t *testing.T) { 50 | t.Parallel() 51 | 52 | client := GitHub{HTTPClient: http.DefaultClient, Token: "token", Owner: "owner"} 53 | require.NoError(t, client.Init()) 54 | 55 | tests := []struct { 56 | message string 57 | uri string 58 | hasAuthority bool 59 | }{ 60 | { 61 | message: "have authority #1", 62 | uri: "https://github.com/owner", 63 | hasAuthority: true, 64 | }, 65 | { 66 | message: "have authority #2", 67 | uri: "https://github.com/Owner", 68 | hasAuthority: true, 69 | }, 70 | { 71 | message: "have authority #3", 72 | uri: "http://github.com/owner", 73 | hasAuthority: true, 74 | }, 75 | { 76 | message: "have authority #4", 77 | uri: "http://github.com/Owner", 78 | hasAuthority: true, 79 | }, 80 | { 81 | message: "have authority #5", 82 | uri: "https://raw.githubusercontent.com/owner", 83 | hasAuthority: true, 84 | }, 85 | { 86 | message: "have authority #6", 87 | uri: "https://raw.githubusercontent.com/Owner", 88 | hasAuthority: true, 89 | }, 90 | { 91 | message: "have authority #7", 92 | uri: "http://raw.githubusercontent.com/owner", 93 | hasAuthority: true, 94 | }, 95 | { 96 | message: "have authority #8", 97 | uri: "http://raw.githubusercontent.com/Owner", 98 | hasAuthority: true, 99 | }, 100 | { 101 | message: "have no authority #1", 102 | uri: "https://google.com", 103 | hasAuthority: false, 104 | }, 105 | { 106 | message: "have no authority #2", 107 | uri: "https://github.com/another-owner", 108 | hasAuthority: false, 109 | }, 110 | } 111 | 112 | for i := 0; i < len(tests); i++ { 113 | tt := tests[i] 114 | t.Run("Should "+tt.message, func(t *testing.T) { 115 | t.Parallel() 116 | require.Equal(t, tt.hasAuthority, client.Authority(tt.uri)) 117 | }) 118 | } 119 | } 120 | 121 | func TestGitHubValid(t *testing.T) { 122 | t.Parallel() 123 | 124 | tests := []struct { 125 | message string 126 | ctx context.Context 127 | repository githubRepositoryMock 128 | uri string 129 | isValid bool 130 | shouldErr bool 131 | }{ 132 | { 133 | message: "attest the URI as a valid owner", 134 | ctx: context.Background(), 135 | uri: "https://github.com/owner", 136 | isValid: true, 137 | shouldErr: false, 138 | repository: githubRepositoryMock{}, 139 | }, 140 | { 141 | message: "attest the URI as a valid issue", 142 | ctx: context.Background(), 143 | uri: "https://github.com/owner/repository/issues/1", 144 | isValid: true, 145 | shouldErr: false, 146 | repository: githubRepositoryMock{ 147 | repo: "repository", 148 | issueID: 1, 149 | }, 150 | }, 151 | { 152 | message: "attest the URI as a invalid issue comment", 153 | ctx: context.Background(), 154 | uri: "https://github.com/owner/repository/issues/1#issuecomment-3", 155 | isValid: true, 156 | shouldErr: false, 157 | repository: githubRepositoryMock{ 158 | repo: "repository", 159 | issueID: 1, 160 | issueCommentID: 3, 161 | }, 162 | }, 163 | { 164 | message: "attest the URI as a valid commit", 165 | ctx: context.Background(), 166 | uri: "https://github.com/owner/repository/commit/c09ea6d", 167 | isValid: true, 168 | shouldErr: false, 169 | repository: githubRepositoryMock{ 170 | repo: "repository", 171 | ref: "c09ea6d", 172 | }, 173 | }, 174 | { 175 | message: "attest the URI as a valid pull request", 176 | ctx: context.Background(), 177 | uri: "https://github.com/owner/repository/pull/3", 178 | isValid: true, 179 | shouldErr: false, 180 | repository: githubRepositoryMock{ 181 | repo: "repository", 182 | pullRequestID: 3, 183 | }, 184 | }, 185 | { 186 | message: "attest the URI as a valid commit associated with the pull request", 187 | ctx: context.Background(), 188 | uri: "https://github.com/owner/repository/pull/3/commits/c09ea6d", 189 | isValid: true, 190 | shouldErr: false, 191 | repository: githubRepositoryMock{ 192 | repo: "repository", 193 | pullRequestID: 3, 194 | ref: "c09ea6d", 195 | }, 196 | }, 197 | { 198 | message: "attest the URI as a valid repository", 199 | ctx: context.Background(), 200 | uri: "https://github.com/owner/repository", 201 | isValid: true, 202 | shouldErr: false, 203 | repository: githubRepositoryMock{ 204 | repo: "repository", 205 | }, 206 | }, 207 | { 208 | message: "attest the URI as a invalid because of a invalid owner", 209 | ctx: context.Background(), 210 | uri: "https://github.com/another-owner/repository/issues/1", 211 | isValid: false, 212 | shouldErr: false, 213 | repository: githubRepositoryMock{}, 214 | }, 215 | { 216 | message: "attest the URI as a invalid because of a invalid URI", 217 | ctx: context.Background(), 218 | uri: "https://github.com/owner/action", 219 | isValid: false, 220 | shouldErr: false, 221 | repository: githubRepositoryMock{}, 222 | }, 223 | } 224 | 225 | for i := 0; i < len(tests); i++ { 226 | tt := tests[i] 227 | t.Run("Should "+tt.message, func(t *testing.T) { 228 | t.Parallel() 229 | 230 | client := GitHub{HTTPClient: http.DefaultClient, Token: "token", Owner: "owner"} 231 | client.repository = tt.repository 232 | require.NoError(t, client.Init()) 233 | 234 | isValid, err := client.Valid(tt.ctx, "", tt.uri) 235 | require.Equal(t, tt.shouldErr, (err != nil)) 236 | if err != nil { 237 | return 238 | } 239 | require.Equal(t, tt.isValid, isValid) 240 | }) 241 | } 242 | } 243 | 244 | type githubRepositoryMock struct { 245 | issueID int 246 | issueCommentID int64 247 | repo string 248 | ref string 249 | pullRequestID int 250 | } 251 | 252 | func (g githubRepositoryMock) repository(ctx context.Context, repository string) (*github.Response, error) { 253 | if g.repo != repository { 254 | return nil, errors.New("fail") 255 | } 256 | return &github.Response{Response: &http.Response{ 257 | StatusCode: http.StatusOK, 258 | Body: ioutil.NopCloser(bytes.NewReader([]byte{})), 259 | }}, nil 260 | } 261 | 262 | func (g githubRepositoryMock) repositoriesGetCommitSHA1( 263 | ctx context.Context, repository, ref string, 264 | ) (*github.Response, error) { 265 | if (g.repo != repository) || (g.ref != ref) { 266 | return nil, errors.New("fail") 267 | } 268 | return &github.Response{Response: &http.Response{ 269 | StatusCode: http.StatusOK, 270 | Body: ioutil.NopCloser(bytes.NewReader([]byte{})), 271 | }}, nil 272 | } 273 | 274 | func (g githubRepositoryMock) issuesGet(ctx context.Context, repo string, number int) (*github.Response, error) { 275 | if (g.repo != repo) || (g.issueID != number) { 276 | return nil, errors.New("fail") 277 | } 278 | return &github.Response{Response: &http.Response{ 279 | StatusCode: http.StatusOK, 280 | Body: ioutil.NopCloser(bytes.NewReader([]byte{})), 281 | }}, nil 282 | } 283 | 284 | func (g githubRepositoryMock) issuesGetComment(ctx context.Context, repo string, id int64) (*github.Response, error) { 285 | if (g.repo != repo) || (g.issueCommentID != id) { 286 | return nil, errors.New("fail") 287 | } 288 | return &github.Response{Response: &http.Response{ 289 | StatusCode: http.StatusOK, 290 | Body: ioutil.NopCloser(bytes.NewReader([]byte{})), 291 | }}, nil 292 | } 293 | 294 | func (g githubRepositoryMock) pullRequestsGetRaw( 295 | ctx context.Context, repo string, number int, 296 | ) (*github.Response, error) { 297 | if (g.repo != repo) || (g.pullRequestID != number) { 298 | return nil, errors.New("fail") 299 | } 300 | return &github.Response{Response: &http.Response{ 301 | StatusCode: http.StatusOK, 302 | Body: ioutil.NopCloser(bytes.NewReader([]byte{})), 303 | }}, nil 304 | } 305 | 306 | func (g githubRepositoryMock) relatedPullRequests(ctx context.Context, repository, ref string) ([]int, error) { 307 | if (g.repo != repository) || (g.ref != ref) { 308 | return nil, errors.New("fail") 309 | } 310 | return []int{g.pullRequestID}, nil 311 | } 312 | -------------------------------------------------------------------------------- /internal/service/provider/file_test.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/mock" 12 | "github.com/stretchr/testify/require" 13 | 14 | "nitro/markdown-link-check/internal/service/parser" 15 | ) 16 | 17 | func TestFileInit(t *testing.T) { 18 | t.Parallel() 19 | 20 | tests := []struct { 21 | message string 22 | client File 23 | shouldErr bool 24 | }{ 25 | { 26 | message: "have an error because of it's missing the path", 27 | client: File{Parser: &parser.Markdown{}}, 28 | shouldErr: true, 29 | }, 30 | { 31 | message: "have an error because of it's missing the parser", 32 | client: File{Path: "something"}, 33 | shouldErr: true, 34 | }, 35 | { 36 | message: "succeed", 37 | client: File{Path: "something", Parser: &parser.Markdown{}}, 38 | shouldErr: false, 39 | }, 40 | } 41 | 42 | for i := 0; i < len(tests); i++ { 43 | tt := tests[i] 44 | t.Run("Should "+tt.message, func(t *testing.T) { 45 | t.Parallel() 46 | require.Equal(t, tt.shouldErr, (tt.client.Init() != nil)) 47 | }) 48 | } 49 | } 50 | 51 | func TestFileAuthority(t *testing.T) { 52 | t.Parallel() 53 | 54 | client := File{Path: "something", Parser: &parser.Markdown{}} 55 | require.NoError(t, client.Init()) 56 | 57 | tests := []struct { 58 | message string 59 | uri string 60 | hasAuthority bool 61 | }{ 62 | { 63 | message: "have authority #1", 64 | uri: "/etc/hosts", 65 | hasAuthority: true, 66 | }, 67 | { 68 | message: "have authority #2", 69 | uri: "actually this provider matches anything", 70 | hasAuthority: true, 71 | }, 72 | } 73 | 74 | for i := 0; i < len(tests); i++ { 75 | tt := tests[i] 76 | t.Run("Should "+tt.message, func(t *testing.T) { 77 | t.Parallel() 78 | require.Equal(t, tt.hasAuthority, client.Authority(tt.uri)) 79 | }) 80 | } 81 | } 82 | 83 | func TestFileValid(t *testing.T) { 84 | t.Parallel() 85 | 86 | tests := []struct { 87 | message string 88 | ctx context.Context 89 | path string 90 | uri string 91 | isValid bool 92 | shouldErr bool 93 | reader func() *fileReaderMock 94 | }{ 95 | { 96 | message: "attest that the file doesn't exist", 97 | ctx: context.Background(), 98 | path: "file", 99 | uri: "link", 100 | isValid: false, 101 | shouldErr: false, 102 | reader: func() *fileReaderMock { 103 | var reader fileReaderMock 104 | reader.On("fileExists", "link").Return(fileInfoMock{}, false) 105 | return &reader 106 | }, 107 | }, 108 | { 109 | message: "attest that the file exists", 110 | ctx: context.Background(), 111 | path: "file", 112 | uri: "link", 113 | isValid: true, 114 | shouldErr: false, 115 | reader: func() *fileReaderMock { 116 | var reader fileReaderMock 117 | reader.On("fileExists", "link").Return(fileInfoMock{}, true) 118 | return &reader 119 | }, 120 | }, 121 | { 122 | message: "attest that the markdown file exists and the uri is valid #1", 123 | ctx: context.Background(), 124 | path: "file.md", 125 | uri: "another-file", 126 | isValid: true, 127 | shouldErr: false, 128 | reader: func() *fileReaderMock { 129 | var reader fileReaderMock 130 | reader.On("fileExists", "another-file").Return(fileInfoMock{}, true) 131 | return &reader 132 | }, 133 | }, 134 | { 135 | message: "attest that the markdown file exists and the uri is valid #2", 136 | ctx: context.Background(), 137 | path: "file.md", 138 | uri: "another-file.md", 139 | isValid: true, 140 | shouldErr: false, 141 | reader: func() *fileReaderMock { 142 | var reader fileReaderMock 143 | reader.On("fileExists", "another-file.md").Return(fileInfoMock{}, true) 144 | return &reader 145 | }, 146 | }, 147 | { 148 | message: "attest that the markdown file exists and the anchor is correct", 149 | ctx: context.Background(), 150 | path: "file.md", 151 | uri: "#anchor", 152 | isValid: true, 153 | shouldErr: false, 154 | reader: func() *fileReaderMock { 155 | var reader fileReaderMock 156 | reader.On("fileExists", "file.md").Return(fileInfoMock{}, true) 157 | reader.On("readFile", "file.md").Return([]byte("#anchor"), nil) 158 | return &reader 159 | }, 160 | }, 161 | { 162 | message: "attest that the markdown file exists and the uri is valid", 163 | ctx: context.Background(), 164 | path: "file.md", 165 | uri: "another-file", 166 | isValid: true, 167 | shouldErr: false, 168 | reader: func() *fileReaderMock { 169 | var reader fileReaderMock 170 | reader.On("fileExists", "another-file").Return(fileInfoMock{}, true) 171 | return &reader 172 | }, 173 | }, 174 | { 175 | message: "attest that the markdown file exists and the uri is directory", 176 | ctx: context.Background(), 177 | path: "file.md", 178 | uri: "directory", 179 | isValid: true, 180 | shouldErr: false, 181 | reader: func() *fileReaderMock { 182 | var reader fileReaderMock 183 | reader.On("fileExists", "directory").Return(fileInfoMock{isDirValue: true}, true) 184 | return &reader 185 | }, 186 | }, 187 | { 188 | message: "attest that the markdown file has the anchor #1", 189 | ctx: context.Background(), 190 | path: "file.md", 191 | uri: "#anchor", 192 | isValid: true, 193 | shouldErr: false, 194 | reader: func() *fileReaderMock { 195 | var reader fileReaderMock 196 | reader.On("fileExists", "file.md").Return(fileInfoMock{}, true) 197 | reader.On("readFile", "file.md").Return([]byte("# anchor"), nil) 198 | return &reader 199 | }, 200 | }, 201 | { 202 | message: "attest that the markdown file has the anchor #2", 203 | ctx: context.Background(), 204 | path: "file.md", 205 | uri: "#anchor", 206 | isValid: true, 207 | shouldErr: false, 208 | reader: func() *fileReaderMock { 209 | var reader fileReaderMock 210 | reader.On("fileExists", "file.md").Return(fileInfoMock{}, true) 211 | reader.On("readFile", "file.md").Return([]byte("# Anchor"), nil) 212 | return &reader 213 | }, 214 | }, 215 | { 216 | message: "attest that the markdown file has the anchor #3", 217 | ctx: context.Background(), 218 | path: "file.md", 219 | uri: "another.md#anchor", 220 | isValid: true, 221 | shouldErr: false, 222 | reader: func() *fileReaderMock { 223 | var reader fileReaderMock 224 | reader.On("fileExists", "another.md").Return(fileInfoMock{}, true) 225 | reader.On("readFile", "another.md").Return([]byte("# Anchor"), nil) 226 | return &reader 227 | }, 228 | }, 229 | { 230 | message: "attest that the markdown file has the anchor #4", 231 | ctx: context.Background(), 232 | path: "file.md", 233 | uri: "another.md#Anchor", 234 | isValid: true, 235 | shouldErr: false, 236 | reader: func() *fileReaderMock { 237 | var reader fileReaderMock 238 | reader.On("fileExists", "another.md").Return(fileInfoMock{}, true) 239 | reader.On("readFile", "another.md").Return([]byte("# anchor"), nil) 240 | return &reader 241 | }, 242 | }, 243 | { 244 | message: "attest that the markdown file has the anchor #5", 245 | ctx: context.Background(), 246 | path: "file.md", 247 | uri: "another.md#Anchor", 248 | isValid: true, 249 | shouldErr: false, 250 | reader: func() *fileReaderMock { 251 | payload := bytes.NewBufferString("") 252 | fmt.Fprintln(payload, "# anchor") 253 | fmt.Fprintln(payload, "# another-one") 254 | 255 | var reader fileReaderMock 256 | reader.On("fileExists", "another.md").Return(fileInfoMock{}, true) 257 | reader.On("readFile", "another.md").Return(payload.Bytes(), nil) 258 | return &reader 259 | }, 260 | }, 261 | { 262 | message: "attest that the markdown file has the anchor #6", 263 | ctx: context.Background(), 264 | path: "file.md", 265 | uri: "another.md#anchor", 266 | isValid: true, 267 | shouldErr: false, 268 | reader: func() *fileReaderMock { 269 | var reader fileReaderMock 270 | reader.On("fileExists", "another.md").Return(fileInfoMock{}, true) 271 | reader.On("readFile", "another.md").Return([]byte("# [anchor](http://endpoint)"), nil) 272 | return &reader 273 | }, 274 | }, 275 | { 276 | message: "attest that the markdown file has the anchor #7", 277 | ctx: context.Background(), 278 | path: "file.md", 279 | uri: "another.md#anchor", 280 | isValid: true, 281 | shouldErr: false, 282 | reader: func() *fileReaderMock { 283 | payload := bytes.NewBufferString("") 284 | fmt.Fprintln(payload, "1. # something") 285 | fmt.Fprintln(payload, "2. ### anchor") 286 | fmt.Fprintln(payload, "3. ## something else") 287 | 288 | var reader fileReaderMock 289 | reader.On("fileExists", "another.md").Return(fileInfoMock{}, true) 290 | reader.On("readFile", "another.md").Return(payload.Bytes(), nil) 291 | return &reader 292 | }, 293 | }, 294 | { 295 | message: "attest that that an error happens during the file read", 296 | ctx: context.Background(), 297 | path: "file.md", 298 | uri: "another-file.md#anchor", 299 | isValid: false, 300 | shouldErr: true, 301 | reader: func() *fileReaderMock { 302 | var reader fileReaderMock 303 | reader.On("fileExists", "another-file.md").Return(fileInfoMock{}, true) 304 | reader.On("readFile", "another-file.md").Return([]byte{}, errors.New("failed to read the file")) 305 | return &reader 306 | }, 307 | }, 308 | { 309 | message: "attest that the markdown file path is invalid", 310 | ctx: context.Background(), 311 | path: "file.md", 312 | uri: "http://192.168.0.%31", 313 | isValid: false, 314 | shouldErr: true, 315 | reader: func() *fileReaderMock { return &fileReaderMock{} }, 316 | }, 317 | { 318 | message: "attest that the file doesn't exists", 319 | ctx: context.Background(), 320 | path: "file.md", 321 | uri: "link.md", 322 | isValid: false, 323 | shouldErr: false, 324 | reader: func() *fileReaderMock { 325 | var reader fileReaderMock 326 | reader.On("fileExists", "link.md").Return(fileInfoMock{}, false) 327 | return &reader 328 | }, 329 | }, 330 | { 331 | message: "attest that the file exists but the anchor doesn't #1", 332 | ctx: context.Background(), 333 | path: "file.md", 334 | uri: "#anchor", 335 | isValid: false, 336 | shouldErr: false, 337 | reader: func() *fileReaderMock { 338 | var reader fileReaderMock 339 | reader.On("fileExists", "file.md").Return(fileInfoMock{}, true) 340 | reader.On("readFile", "file.md").Return([]byte("#another"), nil) 341 | return &reader 342 | }, 343 | }, 344 | { 345 | message: "attest that the file exists but the anchor doesn't #2", 346 | ctx: context.Background(), 347 | path: "file.md", 348 | uri: "#anchor", 349 | isValid: false, 350 | shouldErr: false, 351 | reader: func() *fileReaderMock { 352 | var reader fileReaderMock 353 | reader.On("fileExists", "file.md").Return(fileInfoMock{}, true) 354 | reader.On("readFile", "file.md").Return([]byte(`
value
`), nil) 355 | return &reader 356 | }, 357 | }, 358 | } 359 | 360 | for i := 0; i < len(tests); i++ { 361 | tt := tests[i] 362 | t.Run("Should "+tt.message, func(t *testing.T) { 363 | t.Parallel() 364 | 365 | reader := tt.reader() 366 | defer reader.AssertExpectations(t) 367 | 368 | var parser parser.Markdown 369 | parser.Init() 370 | 371 | client := File{Path: "something", Parser: parser, reader: reader} 372 | require.NoError(t, client.Init()) 373 | 374 | isValid, err := client.Valid(tt.ctx, tt.path, tt.uri) 375 | require.Equal(t, tt.shouldErr, (err != nil)) 376 | if err != nil { 377 | return 378 | } 379 | require.Equal(t, tt.isValid, isValid) 380 | }) 381 | } 382 | } 383 | 384 | type fileReaderMock struct { 385 | mock.Mock 386 | } 387 | 388 | func (f *fileReaderMock) fileExists(path string) (os.FileInfo, bool) { 389 | args := f.Called(path) 390 | return args.Get(0).(os.FileInfo), args.Bool(1) 391 | } 392 | 393 | func (f *fileReaderMock) readFile(path string) ([]byte, error) { 394 | args := f.Called(path) 395 | return args.Get(0).([]byte), args.Error(1) 396 | } 397 | 398 | type fileInfoMock struct { 399 | os.FileInfo 400 | isDirValue bool 401 | } 402 | 403 | func (fi fileInfoMock) IsDir() bool { 404 | return fi.isDirValue 405 | } 406 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= 19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= 20 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= 21 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 22 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 23 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 24 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 25 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 26 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 27 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 28 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 29 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 40 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 41 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 42 | github.com/PuerkitoBio/goquery v1.7.1 h1:oE+T06D+1T7LNrn91B4aERsRIeCLJ/oPSa6xB9FPnz4= 43 | github.com/PuerkitoBio/goquery v1.7.1/go.mod h1:XY0pP4kfraEmmV1O7Uf6XyjoslwsneBbgeDjLYuN8xY= 44 | github.com/alecthomas/kong v0.2.17 h1:URDISCI96MIgcIlQyoCAlhOmrSw6pZScBNkctg8r0W0= 45 | github.com/alecthomas/kong v0.2.17/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ= 46 | github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE= 47 | github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= 48 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 49 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 50 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 51 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 52 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 53 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 54 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 55 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 56 | github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= 57 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 58 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 59 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 60 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 61 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 62 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 63 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 64 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 65 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 66 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 67 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 68 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 69 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 70 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 71 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 72 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 73 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 74 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 75 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 76 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 77 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 78 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 79 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 80 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 81 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 82 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 83 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 84 | github.com/go-rod/rod v0.101.5 h1:Dc3IDAQ0k8BUuKsF+xEg23SimHEs5uoTEiEH1zBf7W0= 85 | github.com/go-rod/rod v0.101.5/go.mod h1:+iB8bs4SPa2DKxDUo1jy316LoQ5uEE6k58UfQdQTMhs= 86 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 87 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 88 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 89 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 90 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 91 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 92 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 93 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 94 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 95 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 96 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 97 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 98 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 99 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 100 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 101 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 102 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 103 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 104 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 105 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 106 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 107 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 108 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 109 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 110 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 111 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 112 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 113 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 114 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 115 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 116 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 117 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 118 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 119 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 120 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 121 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 122 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 123 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 124 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 125 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 131 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 132 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 133 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 134 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 135 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 136 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 137 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 138 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 139 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 140 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 141 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 142 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 143 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 144 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 145 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 146 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 147 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 148 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 149 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 150 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 151 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 152 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 153 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 154 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 155 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 156 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 157 | github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= 158 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= 159 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 160 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 161 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 162 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 163 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 164 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 165 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 166 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 167 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 168 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 169 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 170 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 171 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 172 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 173 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 174 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 175 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 176 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 177 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 178 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 179 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 180 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 181 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 182 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 183 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 184 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 185 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 186 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 187 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 188 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 189 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 190 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 191 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 192 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 193 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 194 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 195 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 196 | github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= 197 | github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 198 | github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= 199 | github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 200 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 201 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 202 | github.com/microcosm-cc/bluemonday v1.0.15 h1:J4uN+qPng9rvkBZBoBb8YGR+ijuklIMpSOZZLjYpbeY= 203 | github.com/microcosm-cc/bluemonday v1.0.15/go.mod h1:ZLvAzeakRwrGnzQEvstVzVt3ZpqOF2+sdFr0Om+ce30= 204 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 205 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 206 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 207 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 208 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 209 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 210 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 211 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 212 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 213 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 214 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 215 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 216 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 217 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 218 | github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= 219 | github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 220 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 221 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 222 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 223 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 224 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 225 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 226 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 227 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 228 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 229 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 230 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 231 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 232 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 233 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 234 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 235 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 236 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 237 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 238 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 239 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 240 | github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= 241 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 242 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 243 | github.com/spf13/cast v1.4.0 h1:WhlbjwB9EGCc8W5Rxdkus+wmH2ASRwwTJk6tgHKwdqQ= 244 | github.com/spf13/cast v1.4.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 245 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 246 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 247 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 248 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 249 | github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= 250 | github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= 251 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 252 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 253 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 254 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 255 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 256 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 257 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 258 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 259 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 260 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 261 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 262 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 263 | github.com/ysmood/goob v0.3.0 h1:XZ51cZJ4W3WCoCiUktixzMIQF86W7G5VFL4QQ/Q2uS0= 264 | github.com/ysmood/goob v0.3.0/go.mod h1:S3lq113Y91y1UBf1wj1pFOxeahvfKkCk6mTWTWbDdWs= 265 | github.com/ysmood/got v0.14.1 h1:lTtBNVF2nxLs/jcV7leNUWVYO9jgjOUpClXbu3ihIPA= 266 | github.com/ysmood/got v0.14.1/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= 267 | github.com/ysmood/gotrace v0.2.2 h1:006KHGRThSRf8lwh4EyhNmuuq/l+Ygs+JqojkhEG1/E= 268 | github.com/ysmood/gotrace v0.2.2/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= 269 | github.com/ysmood/gson v0.6.4/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= 270 | github.com/ysmood/gson v0.7.0 h1:oQhY2FQtfy3+bgaNeqopd7NGAB6Me+UpG0n7oO4VDko= 271 | github.com/ysmood/gson v0.7.0/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= 272 | github.com/ysmood/leakless v0.7.0 h1:XCGdaPExyoreoQd+H5qgxM3ReNbSPFsEXpSKwbXbwQw= 273 | github.com/ysmood/leakless v0.7.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= 274 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 275 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 276 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 277 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 278 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 279 | go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= 280 | go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= 281 | go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= 282 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 283 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 284 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 285 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 286 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 287 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 288 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 289 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 290 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 291 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 292 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 293 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 294 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 295 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 296 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 297 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 298 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 299 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 300 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 301 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 302 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 303 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 304 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 305 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 306 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 307 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 308 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 309 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 310 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 311 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 312 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 313 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 314 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 315 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 316 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 317 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 318 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 319 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 320 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 321 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 322 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 323 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 324 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 325 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 326 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 327 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 328 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 329 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 330 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 331 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 332 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 333 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 334 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 335 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 336 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 337 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 338 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 339 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 340 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 341 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 342 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 343 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 344 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 345 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 346 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 347 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 348 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 349 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 350 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 351 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 352 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 353 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 354 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 355 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 356 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 357 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 358 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 359 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 360 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 361 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 362 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 363 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 364 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 365 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 366 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 367 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 368 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 369 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= 370 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 371 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 372 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= 373 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 374 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 375 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 376 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 377 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 378 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 379 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 380 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 381 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 382 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 383 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 384 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 385 | golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 386 | golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8= 387 | golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 388 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 389 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 390 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 391 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 392 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 393 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 394 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 395 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 396 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 397 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 398 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 399 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 400 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 401 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 402 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 403 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 410 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 411 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 412 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 413 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 418 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 427 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 428 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 429 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 430 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 435 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 436 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 437 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 438 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 441 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 442 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 443 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 444 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 445 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 446 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 447 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 448 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 449 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 450 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 451 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 452 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 453 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 454 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 455 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 456 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 457 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 458 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 459 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 460 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 461 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 462 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 463 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 464 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 465 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 466 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 467 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 468 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 469 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 470 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 471 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 472 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 473 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 474 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 475 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 476 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 477 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 478 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 479 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 480 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 481 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 482 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 483 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 484 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 485 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 486 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 487 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 488 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 489 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 490 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 491 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 492 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 493 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 494 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 495 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 496 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 497 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 498 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 499 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 500 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 501 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 502 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 503 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 504 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 505 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 506 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 507 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 508 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 509 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 510 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 511 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 512 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 513 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 514 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 515 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 516 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 517 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 518 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 519 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 520 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 521 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 522 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 523 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 524 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 525 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 526 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 527 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 528 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 529 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 530 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 531 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= 532 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= 533 | google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= 534 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 535 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 536 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 537 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 538 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 539 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 540 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 541 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 542 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 543 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 544 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 545 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 546 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 547 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 548 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 549 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 550 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 551 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 552 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 553 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 554 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 555 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 556 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 557 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 558 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 559 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 560 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 561 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 562 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 563 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 564 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 565 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 566 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 567 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 568 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 569 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 570 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 571 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 572 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 573 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 574 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 575 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 576 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 577 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 578 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 579 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 580 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 581 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 582 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 583 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 584 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 585 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 586 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 587 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 588 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 589 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 590 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 591 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 592 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 593 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 594 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 595 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 596 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 597 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 598 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 599 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 600 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 601 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 602 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 603 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 604 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 605 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 606 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 607 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 608 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 609 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 610 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 611 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 612 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 613 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 614 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 615 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 616 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 617 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 618 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 619 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 620 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 621 | gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= 622 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 623 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 624 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 625 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 626 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 627 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 628 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 629 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 630 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 631 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 632 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 633 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 634 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 635 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 636 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 637 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 638 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 639 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 640 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 641 | --------------------------------------------------------------------------------