├── 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 |
--------------------------------------------------------------------------------