├── .github ├── dependabot.yml └── workflows │ ├── go-build.yml │ ├── go-test.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── cmd ├── issues-comments.go ├── issues.go ├── leaderboard.go ├── prs.go ├── review.go ├── root.go └── server.go ├── code-of-conduct.md ├── contributing.md ├── deploy └── kubernetes │ ├── deployment.yaml │ └── service.yaml ├── go.mod ├── go.sum ├── pkg ├── client │ └── client.go ├── ghcache │ └── ghcache.go ├── leaderboard │ ├── issues.go │ ├── leaderboard.go │ ├── leaderboard_templage.go │ ├── prs.go │ └── reviews.go ├── print │ └── print.go ├── repo │ ├── files.go │ ├── issue.go │ ├── issue_comments.go │ ├── pr.go │ ├── repo.go │ ├── repos.go │ └── review_comments.go ├── server │ ├── job │ │ ├── job.go │ │ └── updater.go │ ├── server.go │ └── site │ │ ├── site.go │ │ └── template │ │ └── home.html └── summary │ └── summary.go ├── pullsheet.go └── skaffold.yaml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/go-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: [ 'main', 'release-*'] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | strategy: 11 | matrix: 12 | go-version: ['1.23.x'] 13 | platform: [ubuntu-latest] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - name: Set up Go ${{ matrix.go-version }} 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | id: go 21 | 22 | - name: Check out code 23 | uses: actions/checkout@v4 24 | 25 | - name: Build 26 | run: | 27 | go build . 28 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | tags: [ 'v*' ] 6 | branches: [ 'main' ] 7 | pull_request: 8 | branches: [ 'main', 'release-*'] 9 | 10 | jobs: 11 | test: 12 | name: Unit Tests 13 | strategy: 14 | matrix: 15 | go-version: ['1.22.x'] 16 | platform: [ubuntu-latest] 17 | runs-on: ${{ matrix.platform }} 18 | steps: 19 | - name: Set up Go ${{ matrix.go-version }} 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | id: go 24 | 25 | - name: Check out code 26 | uses: actions/checkout@v4 27 | 28 | - name: Check for .codecov.yaml 29 | id: codecov-enabled 30 | uses: andstor/file-existence-action@v3 31 | with: 32 | files: .codecov.yaml 33 | 34 | - if: steps.codecov-enabled.outputs.files_exists == 'true' 35 | name: Produce Go Coverage 36 | run: echo 'COVER_OPTS=-coverprofile=coverage.txt -covermode=atomic' >> $GITHUB_ENV 37 | 38 | - name: Test 39 | run: go test -race $COVER_OPTS ./... 40 | 41 | - if: steps.codecov-enabled.outputs.files_exists == 'true' 42 | name: Codecov 43 | uses: codecov/codecov-action@v5 44 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | tags: [ 'v*' ] 6 | branches: [ 'main', 'release-*'] 7 | pull_request: 8 | branches: [ 'main', 'release-*'] 9 | 10 | jobs: 11 | golangci: 12 | name: lint 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/setup-go@v3 16 | - uses: actions/checkout@v4 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v6 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # goland, IntelliJ 5 | .idea 6 | 7 | # binaries 8 | pullsheet -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 6 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - asciicheck 8 | - bodyclose 9 | - deadcode 10 | - depguard 11 | - dogsled 12 | - errcheck 13 | - goconst 14 | - gocritic 15 | - gocyclo 16 | - godox 17 | - gofmt 18 | - gofumpt 19 | - goheader 20 | - goimports 21 | - golint 22 | - gomodguard 23 | - goprintffuncname 24 | - gosimple 25 | - govet 26 | - ineffassign 27 | - interfacer 28 | - misspell 29 | - nakedret 30 | - rowserrcheck 31 | - sqlclosecheck 32 | - staticcheck 33 | - structcheck 34 | - stylecheck 35 | - typecheck 36 | - unconvert 37 | - unparam 38 | - unused 39 | - varcheck 40 | - whitespace 41 | 42 | linters-settings: 43 | godox: 44 | keywords: 45 | - BUG 46 | - FIXME 47 | - HACK 48 | errcheck: 49 | check-type-assertions: true 50 | check-blank: true 51 | gocritic: 52 | enabled-checks: 53 | # Diagnostic 54 | - appendAssign 55 | - argOrder 56 | - badCond 57 | - caseOrder 58 | - codegenComment 59 | - commentedOutCode 60 | - deprecatedComment 61 | - dupArg 62 | - dupBranchBody 63 | - dupCase 64 | - dupSubExpr 65 | - exitAfterDefer 66 | - flagDeref 67 | - flagName 68 | - nilValReturn 69 | - offBy1 70 | - sloppyReassign 71 | - weakCond 72 | - octalLiteral 73 | 74 | # Performance 75 | - appendCombine 76 | - equalFold 77 | - hugeParam 78 | - indexAlloc 79 | - rangeExprCopy 80 | - rangeValCopy 81 | 82 | # Style 83 | - assignOp 84 | - boolExprSimplify 85 | - captLocal 86 | - commentFormatting 87 | - commentedOutImport 88 | - defaultCaseOrder 89 | - docStub 90 | - elseif 91 | - emptyFallthrough 92 | - emptyStringTest 93 | - hexLiteral 94 | - methodExprCall 95 | - regexpMust 96 | - singleCaseSwitch 97 | - sloppyLen 98 | - stringXbytes 99 | - switchTrue 100 | - typeAssertChain 101 | - typeSwitchVar 102 | - underef 103 | - unlabelStmt 104 | - unlambda 105 | - unslice 106 | - valSwap 107 | - wrapperFunc 108 | - yodaStyleExpr 109 | # - ifElseChain 110 | 111 | # Opinionated 112 | - builtinShadow 113 | - importShadow 114 | - initClause 115 | - nestingReduce 116 | - ptrToRefParam 117 | - typeUnparen 118 | - unnamedResult 119 | - unnecessaryBlock 120 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build pullsheet 2 | FROM golang AS builder 3 | WORKDIR /src 4 | ENV GO111MODULE=on 5 | RUN mkdir -p /src/cmd /src/pkg 6 | COPY go.* /src/ 7 | COPY pullsheet.go /src/ 8 | COPY cmd /src/cmd/ 9 | COPY pkg /src/pkg/ 10 | RUN go mod download 11 | RUN go build 12 | 13 | # Setup in /app 14 | FROM gcr.io/distroless/base AS pullsheet 15 | WORKDIR /app 16 | COPY --from=builder /src/pullsheet /app/ 17 | 18 | CMD ["/app/pullsheet", "server", "--log-level=info"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pullsheet 2 | 3 | pullsheet generates a CSV (comma separated values) & HTML output about GitHub activity across a series of repositories. 4 | 5 | It currently supports CSV exports for: 6 | 7 | * Merged Pull Requests: `pullsheet prs [FLAGS]` 8 | * Pull Request Reviews: `pullsheet reviews [FLAGS]` 9 | * Opening/Closing Issues: `pullsheet issues [FLAGS]` 10 | * Issue Comments: `pullsheet issue-comments [FLAGS]` 11 | 12 | As well as a new HTML leaderboard mode: `pullsheet leaderboard [FLAGS]` 13 | 14 | This tool was created as a brain-tickler for what PR's to discuss when asking for that big promotion. 15 | 16 | ## Usage 17 | 18 | `go run pullsheet [subcommand] --repos --since 2006-01-02 --token-path [--users=]` 19 | 20 | You will need a GitHub authentication token from https://github.com/settings/tokens 21 | 22 | ## Example: Merged PRs for 1 person across repos 23 | 24 | `go run pullsheet.go prs --repos kubernetes/minikube,GoogleContainerTools/skaffold --since 2019-10-01 --token-path /path/to/github/token/file --users someone > someone.csv` 25 | 26 | ## Example: Merged PR Reviews for all users in a repo 27 | 28 | `go run pullsheet.go reviews --repos kubernetes/minikube --kind=reviews --since 2020-12-24 --token-path /path/to/github/token/file > reviews.csv` 29 | 30 | ## Example: Merged PRs for a user in all repos within an org 31 | 32 | `go run pullsheet.go prs --org google --since 2020-12-24 --token-path /path/to/github/token/file > reviews.csv` 33 | 34 | 35 | ## CSV fields 36 | 37 | ### Merged Pull Requests 38 | 39 | ``` 40 | URL string 41 | Date string 42 | User string 43 | Project string 44 | Type string 45 | Title string 46 | Delta int 47 | Added int 48 | Deleted int 49 | FilesTotal int 50 | Files string // newline delimited 51 | Description string 52 | ``` 53 | 54 | ### Merged Pull Request Reviews 55 | 56 | ``` 57 | URL string 58 | Date string 59 | Reviewer string 60 | PRAuthor string 61 | Project string 62 | Title string 63 | PRComments int 64 | ReviewComments int 65 | Words int 66 | ``` 67 | 68 | ### Closed/Opened Issues 69 | 70 | ``` 71 | URL string 72 | Date string 73 | Author string 74 | Closer string 75 | Project string 76 | Type string 77 | Title string 78 | ``` 79 | 80 | ### Issue Comments 81 | 82 | ``` 83 | URL string 84 | Date string 85 | Project string 86 | Commenter string 87 | IssueAuthor string 88 | IssueState string 89 | Comments int 90 | Words int 91 | Title string 92 | ``` 93 | -------------------------------------------------------------------------------- /cmd/issues-comments.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/google/pullsheet/pkg/print" 21 | "github.com/google/pullsheet/pkg/summary" 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/google/pullsheet/pkg/client" 25 | ) 26 | 27 | // issuesCommentsCmd represents the subcommand for `pullsheet issue-comments` 28 | var issuesCommentsCmd = &cobra.Command{ 29 | Use: "issue-comments", 30 | Short: "Generate data around issues", 31 | SilenceUsage: true, 32 | SilenceErrors: true, 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | return runIssueComments(rootOpts) 35 | }, 36 | } 37 | 38 | func init() { 39 | rootCmd.AddCommand(issuesCommentsCmd) 40 | } 41 | 42 | func runIssueComments(rootOpts *rootOptions) error { 43 | ctx := context.Background() 44 | c, err := client.New(ctx, client.Config{GitHubTokenPath: rootOpts.tokenPath}) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | data, err := summary.Comments(ctx, c, rootOpts.repos, rootOpts.users, rootOpts.sinceParsed, rootOpts.untilParsed) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | err = print.Print(data, rootOpts.out) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /cmd/issues.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/google/pullsheet/pkg/print" 21 | "github.com/google/pullsheet/pkg/summary" 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/google/pullsheet/pkg/client" 25 | ) 26 | 27 | // issuesCmd represents the subcommand for `pullsheet issues` 28 | var issuesCmd = &cobra.Command{ 29 | Use: "issues", 30 | Short: "Generate data around issues", 31 | SilenceUsage: true, 32 | SilenceErrors: true, 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | return runIssues(rootOpts) 35 | }, 36 | } 37 | 38 | func init() { 39 | rootCmd.AddCommand(issuesCmd) 40 | } 41 | 42 | func runIssues(rootOpts *rootOptions) error { 43 | ctx := context.Background() 44 | c, err := client.New(ctx, client.Config{GitHubTokenPath: rootOpts.tokenPath}) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | data, err := summary.Issues(ctx, c, rootOpts.repos, rootOpts.users, rootOpts.sinceParsed, rootOpts.untilParsed) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | err = print.Print(data, rootOpts.out) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /cmd/leaderboard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "os" 22 | "strings" 23 | "time" 24 | 25 | "github.com/google/pullsheet/pkg/repo" 26 | "github.com/google/pullsheet/pkg/summary" 27 | "github.com/karrick/tparse" 28 | "k8s.io/klog/v2" 29 | 30 | "github.com/spf13/cobra" 31 | 32 | "github.com/google/pullsheet/pkg/client" 33 | "github.com/google/pullsheet/pkg/leaderboard" 34 | ) 35 | 36 | var ( 37 | // leaderBoardCmd represents the subcommand for `pullsheet leaderboard` 38 | leaderBoardCmd = &cobra.Command{ 39 | Use: "leaderboard", 40 | Short: "Generate leaderboard data", 41 | SilenceUsage: true, 42 | SilenceErrors: true, 43 | RunE: func(cmd *cobra.Command, args []string) error { 44 | return runLeaderBoard(rootOpts) 45 | }, 46 | } 47 | 48 | disableCaching bool 49 | hideCommand bool 50 | jsonFiles []string 51 | jsonOutput string 52 | sinceDisplay string 53 | untilDisplay string 54 | sinceParsedDisplay time.Time 55 | untilParsedDisplay time.Time 56 | ) 57 | 58 | type data struct { 59 | PRs []*repo.PRSummary 60 | Reviews []*repo.ReviewSummary 61 | Issues []*repo.IssueSummary 62 | Comments []*repo.CommentSummary 63 | } 64 | 65 | func init() { 66 | leaderBoardCmd.Flags().BoolVar( 67 | &disableCaching, 68 | "no-caching", 69 | false, 70 | "Disable caching on resulting HTML files") 71 | 72 | leaderBoardCmd.Flags().BoolVar( 73 | &hideCommand, 74 | "hide-command", 75 | false, 76 | "Hide the command-line args in the HTML") 77 | 78 | leaderBoardCmd.Flags().StringSliceVar( 79 | &jsonFiles, 80 | "json-files", 81 | []string{}, 82 | "List of JSON files to append to the results", 83 | ) 84 | 85 | leaderBoardCmd.Flags().StringVar( 86 | &jsonOutput, 87 | "json-output", 88 | "", 89 | "Filepath to write the resulting JSON to, will omit if none specified", 90 | ) 91 | 92 | leaderBoardCmd.Flags().StringVar( 93 | &sinceDisplay, 94 | "since-display", 95 | "", 96 | "This overrides the since date displayed on the leaderboard, primary used if appending past JSON files", 97 | ) 98 | 99 | leaderBoardCmd.Flags().StringVar( 100 | &untilDisplay, 101 | "until-display", 102 | "", 103 | "This overrides the until date displayed on the leaderboard, primary used if appending past JSON files", 104 | ) 105 | 106 | rootCmd.AddCommand(leaderBoardCmd) 107 | } 108 | 109 | func stringToTime(s string, root time.Time) (time.Time, error) { 110 | if s == "" { 111 | return root, nil 112 | } 113 | parsed, err := tparse.ParseNow(dateForm, s) 114 | if err != nil { 115 | klog.Infof("%q not a duration: %v", s, err) 116 | return time.Time{}, err 117 | } 118 | return parsed, nil 119 | } 120 | 121 | func runLeaderBoard(rootOpts *rootOptions) error { 122 | var err error 123 | 124 | sinceParsedDisplay, err = stringToTime(sinceDisplay, rootOpts.sinceParsed) 125 | if err != nil { 126 | return err 127 | } 128 | untilParsedDisplay, err = stringToTime(untilDisplay, rootOpts.untilParsed) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | d, err := dataFromGitHub() 134 | if err != nil { 135 | return err 136 | } 137 | d, err = appendJSONFiles(d) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | if err := writeToJSON(d); err != nil { 143 | return err 144 | } 145 | 146 | title := rootOpts.title 147 | if title == "" { 148 | title = strings.Join(rootOpts.repos, ", ") 149 | } 150 | 151 | out, err := leaderboard.Render(leaderboard.Options{ 152 | Title: title, 153 | Since: sinceParsedDisplay, 154 | Until: untilParsedDisplay, 155 | DisableCaching: disableCaching, 156 | HideCommand: hideCommand, 157 | }, rootOpts.users, d.PRs, d.Reviews, d.Issues, d.Comments) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | klog.Infof("%d bytes of issue-comments output", len(out)) 163 | fmt.Print(out) 164 | 165 | return nil 166 | } 167 | 168 | func dataFromGitHub() (*data, error) { 169 | ctx := context.Background() 170 | c, err := client.New(ctx, client.Config{GitHubTokenPath: rootOpts.tokenPath}) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | prs, err := summary.Pulls(ctx, c, rootOpts.repos, rootOpts.users, rootOpts.branches, rootOpts.sinceParsed, rootOpts.untilParsed) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | reviews, err := summary.Reviews(ctx, c, rootOpts.repos, rootOpts.users, rootOpts.sinceParsed, rootOpts.untilParsed) 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | issues, err := summary.Issues(ctx, c, rootOpts.repos, rootOpts.users, rootOpts.sinceParsed, rootOpts.untilParsed) 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | comments, err := summary.Comments(ctx, c, rootOpts.repos, rootOpts.users, rootOpts.sinceParsed, rootOpts.untilParsed) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | return &data{prs, reviews, issues, comments}, nil 196 | } 197 | 198 | func appendJSONFiles(d *data) (*data, error) { 199 | for _, file := range jsonFiles { 200 | b, err := os.ReadFile(file) 201 | if err != nil { 202 | return nil, err 203 | } 204 | var u data 205 | if err := json.Unmarshal(b, &u); err != nil { 206 | return nil, err 207 | } 208 | d.PRs = append(d.PRs, u.PRs...) 209 | d.Reviews = append(d.Reviews, u.Reviews...) 210 | d.Issues = append(d.Issues, u.Issues...) 211 | d.Comments = append(d.Comments, u.Comments...) 212 | } 213 | return d, nil 214 | } 215 | 216 | func writeToJSON(d *data) error { 217 | if jsonOutput == "" { 218 | return nil 219 | } 220 | b, err := json.Marshal(d) 221 | if err != nil { 222 | return err 223 | } 224 | return os.WriteFile(jsonOutput, b, 0o644) 225 | } 226 | -------------------------------------------------------------------------------- /cmd/prs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/google/pullsheet/pkg/print" 21 | "github.com/google/pullsheet/pkg/repo" 22 | "github.com/google/pullsheet/pkg/summary" 23 | "github.com/spf13/cobra" 24 | 25 | "github.com/google/pullsheet/pkg/client" 26 | ) 27 | 28 | // prsCmd represents the subcommand for `pullsheet prs` 29 | var prsCmd = &cobra.Command{ 30 | Use: "prs", 31 | Short: "Generate data around pull requests", 32 | SilenceUsage: true, 33 | SilenceErrors: true, 34 | RunE: func(cmd *cobra.Command, args []string) error { 35 | return runPRs(rootOpts) 36 | }, 37 | } 38 | 39 | func init() { 40 | rootCmd.AddCommand(prsCmd) 41 | } 42 | 43 | func runPRs(rootOpts *rootOptions) error { 44 | ctx := context.Background() 45 | c, err := client.New(ctx, client.Config{GitHubTokenPath: rootOpts.tokenPath}) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | var repos []string 51 | 52 | if rootOpts.org != "" { 53 | repos, _ = repo.ListRepoNames(ctx, c, rootOpts.org) 54 | } else { 55 | repos = rootOpts.repos 56 | } 57 | 58 | data, err := summary.Pulls(ctx, c, repos, rootOpts.users, rootOpts.branches, rootOpts.sinceParsed, rootOpts.untilParsed) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | err = print.Print(data, rootOpts.out) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /cmd/review.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/google/pullsheet/pkg/summary" 21 | "github.com/spf13/cobra" 22 | 23 | "github.com/google/pullsheet/pkg/client" 24 | "github.com/google/pullsheet/pkg/print" 25 | ) 26 | 27 | // reviewsCmd represents the subcommand for `pullsheet reviews` 28 | var reviewsCmd = &cobra.Command{ 29 | Use: "reviews", 30 | Short: "Generate data around reviews", 31 | SilenceUsage: true, 32 | SilenceErrors: true, 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | return runReviews(rootOpts) 35 | }, 36 | } 37 | 38 | func init() { 39 | rootCmd.AddCommand(reviewsCmd) 40 | } 41 | 42 | func runReviews(rootOpts *rootOptions) error { 43 | ctx := context.Background() 44 | c, err := client.New(ctx, client.Config{GitHubTokenPath: rootOpts.tokenPath}) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | data, err := summary.Reviews(ctx, c, rootOpts.repos, rootOpts.users, rootOpts.sinceParsed, rootOpts.untilParsed) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | err = print.Print(data, rootOpts.out) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/karrick/tparse" 23 | "github.com/pkg/errors" 24 | "github.com/spf13/cobra" 25 | "github.com/spf13/viper" 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | const dateForm = "2006-01-02" 30 | 31 | // rootCmd represents the base command when called without any subcommands 32 | var rootCmd = &cobra.Command{ 33 | Use: "pullsheet", 34 | Long: `pullsheet - Generate spreadsheets based on GitHub contributions 35 | 36 | pullsheet generates a CSV (comma separated values) & HTML output about GitHub activity across a series of repositories.`, 37 | PersistentPreRunE: initCommand, 38 | } 39 | 40 | type rootOptions struct { 41 | org string 42 | repos []string 43 | users []string 44 | since string 45 | until string 46 | sinceParsed time.Time 47 | untilParsed time.Time 48 | title string 49 | tokenPath string 50 | branches []string 51 | out string 52 | includeBots bool // if true will include bots in the metrics 53 | } 54 | 55 | var rootOpts = &rootOptions{} 56 | 57 | // Execute adds all child commands to the root command and sets flags appropriately. 58 | // This is called by main.main(). It only needs to happen once to the rootCmd. 59 | func Execute() { 60 | if err := rootCmd.Execute(); err != nil { 61 | klog.Fatal(err) 62 | } 63 | } 64 | 65 | func init() { 66 | klog.InitFlags(nil) 67 | 68 | rootCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) 69 | 70 | rootCmd.PersistentFlags().StringVar( 71 | &rootOpts.org, 72 | "org", 73 | "", 74 | "github org name", 75 | ) 76 | 77 | rootCmd.PersistentFlags().StringSliceVar( 78 | &rootOpts.repos, 79 | "repos", 80 | []string{}, 81 | "comma-delimited list of repositories. ex: kubernetes/minikube, google/pullsheet", 82 | ) 83 | 84 | rootCmd.PersistentFlags().StringSliceVar( 85 | &rootOpts.branches, 86 | "branches", 87 | []string{}, 88 | "comma-delimited list of branches ex: master,main,head", 89 | ) 90 | rootCmd.PersistentFlags().StringSliceVar( 91 | &rootOpts.users, 92 | "users", 93 | []string{}, 94 | "comma-delimiited list of users", 95 | ) 96 | 97 | rootCmd.PersistentFlags().StringVar( 98 | &rootOpts.since, 99 | "since", 100 | "now-90d", 101 | "when to query from (date or duration)", 102 | ) 103 | 104 | rootCmd.PersistentFlags().StringVar( 105 | &rootOpts.until, 106 | "until", 107 | "now", 108 | "when to query till (date or duration)", 109 | ) 110 | 111 | rootCmd.PersistentFlags().BoolVarP( 112 | &rootOpts.includeBots, 113 | "include-bots", 114 | "", 115 | false, 116 | "include bots in the stats", 117 | ) 118 | 119 | rootCmd.PersistentFlags().StringVar( 120 | &rootOpts.title, 121 | "title", 122 | "", 123 | "Title to use for output pages", 124 | ) 125 | 126 | rootCmd.PersistentFlags().StringVar( 127 | &rootOpts.tokenPath, 128 | "token-path", 129 | "", 130 | "GitHub token path", 131 | ) 132 | 133 | rootCmd.PersistentFlags().StringVar( 134 | &rootOpts.out, 135 | "out", 136 | "CSV", 137 | "Output type - CSV/JSON. Default is CSV", 138 | ) 139 | 140 | // Set up viper flag handling 141 | if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { 142 | panic(err) 143 | } 144 | } 145 | 146 | // initRootOpts sets up root options, using env variables to set options if 147 | // they haven't been set by flags 148 | func initRootOpts() error { 149 | // Set up viper environment variable handling 150 | viper.SetEnvPrefix("pullsheet") 151 | envKeys := []string{ 152 | "repos", "branches", "users", "since", "until", "title", "token-path", "out", 153 | } 154 | for _, key := range envKeys { 155 | if err := viper.BindEnv(key); err != nil { 156 | return err 157 | } 158 | } 159 | 160 | if rootOpts.out != "JSON" && rootOpts.out != "CSV" { 161 | return fmt.Errorf("invalid out parameter %s. Must be JSON or CSV", rootOpts.out) 162 | } 163 | 164 | // Set options. viper will prioritize flags over env variables 165 | rootOpts.repos = viper.GetStringSlice("repos") 166 | rootOpts.branches = viper.GetStringSlice("branches") 167 | rootOpts.users = viper.GetStringSlice("users") 168 | rootOpts.since = viper.GetString("since") 169 | rootOpts.until = viper.GetString("until") 170 | rootOpts.title = viper.GetString("title") 171 | rootOpts.tokenPath = viper.GetString("token-path") 172 | rootOpts.out = viper.GetString("out") 173 | rootOpts.includeBots = viper.GetBool("include-bots") 174 | return nil 175 | } 176 | 177 | func initCommand(*cobra.Command, []string) error { 178 | if err := initRootOpts(); err != nil { 179 | return err 180 | } 181 | 182 | var err error 183 | 184 | t, err := tparse.ParseNow(dateForm, rootOpts.since) 185 | if err == nil { 186 | rootOpts.sinceParsed = t 187 | } else { 188 | klog.Infof("%q not a duration: %v", rootOpts.since, err) 189 | rootOpts.sinceParsed, err = time.Parse(dateForm, rootOpts.since) 190 | if err != nil { 191 | return errors.Wrap(err, "since time parse") 192 | } 193 | } 194 | 195 | rootOpts.untilParsed = time.Now() 196 | if rootOpts.since != "" { 197 | t, err := tparse.ParseNow(dateForm, rootOpts.until) 198 | if err == nil { 199 | rootOpts.untilParsed = t 200 | } else { 201 | klog.Infof("%q not a duration: %v", rootOpts.until, err) 202 | rootOpts.untilParsed, err = time.Parse(dateForm, rootOpts.until) 203 | if err != nil { 204 | return errors.Wrap(err, "until time parse") 205 | } 206 | } 207 | } 208 | 209 | return nil 210 | } 211 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "net/http" 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | 25 | "github.com/google/pullsheet/pkg/client" 26 | "github.com/google/pullsheet/pkg/server" 27 | "github.com/google/pullsheet/pkg/server/job" 28 | ) 29 | 30 | // serverCmd represents the subcommand for `pullsheet server` 31 | var serverCmd = &cobra.Command{ 32 | Use: "server", 33 | Short: "Serve leaderboard data with web UI", 34 | SilenceUsage: true, 35 | SilenceErrors: true, 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | return runServer(rootOpts) 38 | }, 39 | } 40 | 41 | var port int 42 | 43 | func init() { 44 | serverCmd.Flags().IntVar( 45 | &port, 46 | "port", 47 | 8080, 48 | "Port for server to listen on") 49 | serverCmd.Flags().BoolVar( 50 | &disableCaching, 51 | "no-caching", 52 | false, 53 | "Disable caching on resulting HTML files") 54 | 55 | rootCmd.AddCommand(serverCmd) 56 | } 57 | 58 | func runServer(rootOpts *rootOptions) error { 59 | ctx := context.Background() 60 | c, err := client.New(ctx, client.Config{GitHubTokenPath: rootOpts.tokenPath}) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | // setup initial job 66 | j := job.New( 67 | &job.Opts{ 68 | Repos: rootOpts.repos, 69 | Users: rootOpts.users, 70 | Branches: rootOpts.branches, 71 | Since: rootOpts.sinceParsed, 72 | Until: rootOpts.untilParsed, 73 | Title: rootOpts.title, 74 | DisableCaching: disableCaching, 75 | }) 76 | 77 | s := server.New(ctx, c, j) 78 | http.HandleFunc("/", s.Root()) 79 | http.HandleFunc("/home", s.Home()) 80 | http.HandleFunc("/job/", s.Job()) 81 | http.HandleFunc("/new-job", s.NewJob()) 82 | http.HandleFunc("/healthz", s.Healthz()) 83 | http.HandleFunc("/threadz", s.Threadz()) 84 | 85 | listenAddr := fmt.Sprintf(":%s", os.Getenv("PORT")) 86 | if listenAddr == ":" { 87 | listenAddr = fmt.Sprintf(":%d", port) 88 | } 89 | return http.ListenAndServe(listenAddr, nil) 90 | } 91 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the 73 | Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out to the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /deploy/kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pullsheet 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: pullsheet 9 | template: 10 | metadata: 11 | labels: 12 | app: pullsheet 13 | spec: 14 | containers: 15 | - name: pullsheet 16 | image: pullsheet 17 | imagePullPolicy: Always 18 | ports: 19 | - containerPort: 8080 20 | env: 21 | - name: GITHUB_TOKEN 22 | valueFrom: 23 | secretKeyRef: 24 | name: pullsheet-github-token 25 | key: token 26 | - name: PULLSHEET_REPOS 27 | value: "google/pullsheet" 28 | - name: PULLSHEET_BRANCHES 29 | value: "main" 30 | - name: PULLSHEET_USERS 31 | value: "tstromberg marlongamez" 32 | - name: PULLSHEET_SINCE 33 | value: "2021-03-01" 34 | - name: PULLSHEET_UNTIL 35 | value: "2021-03-24" 36 | - name: PULLSHEET_TITLE 37 | value: "Pullsheet Stats" 38 | -------------------------------------------------------------------------------- /deploy/kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: pullsheet 5 | spec: 6 | type: NodePort 7 | ports: 8 | - port: 8080 9 | protocol: TCP 10 | selector: 11 | app: pullsheet 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/pullsheet 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/blevesearch/segment v0.9.1 9 | github.com/gocarina/gocsv v0.0.0-20201208093247-67c824bc04d4 10 | github.com/google/go-github/v33 v33.0.0 11 | github.com/google/triage-party v1.6.0 12 | github.com/karrick/tparse v2.4.2+incompatible 13 | github.com/pkg/errors v0.9.1 14 | github.com/spf13/cobra v1.9.1 15 | github.com/spf13/viper v1.7.1 16 | golang.org/x/oauth2 v0.29.0 17 | k8s.io/klog/v2 v2.0.0 18 | ) 19 | 20 | require ( 21 | cloud.google.com/go/compute/metadata v0.3.0 // indirect 22 | github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20200501161113-5e9e23d7cb91 // indirect 23 | github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318 // indirect 24 | github.com/fsnotify/fsnotify v1.4.7 // indirect 25 | github.com/go-logr/logr v0.1.0 // indirect 26 | github.com/go-sql-driver/mysql v1.5.0 // indirect 27 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 28 | github.com/golang/protobuf v1.5.3 // indirect 29 | github.com/google/btree v1.0.0 // indirect 30 | github.com/google/go-querystring v1.0.0 // indirect 31 | github.com/google/uuid v1.3.0 // indirect 32 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 33 | github.com/googleapis/gax-go/v2 v2.7.1 // indirect 34 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 35 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 36 | github.com/hashicorp/hcl v1.0.0 // indirect 37 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 38 | github.com/jmoiron/sqlx v1.2.0 // indirect 39 | github.com/lib/pq v1.3.0 // indirect 40 | github.com/magiconair/properties v1.8.1 // indirect 41 | github.com/mitchellh/mapstructure v1.1.2 // indirect 42 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 43 | github.com/pelletier/go-toml v1.2.0 // indirect 44 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 45 | github.com/spf13/afero v1.1.2 // indirect 46 | github.com/spf13/cast v1.3.0 // indirect 47 | github.com/spf13/jwalterweatherman v1.0.0 // indirect 48 | github.com/spf13/pflag v1.0.6 // indirect 49 | github.com/subosito/gotenv v1.2.0 // indirect 50 | github.com/xanzy/go-gitlab v0.36.0 // indirect 51 | go.opencensus.io v0.24.0 // indirect 52 | golang.org/x/crypto v0.35.0 // indirect 53 | golang.org/x/net v0.36.0 // indirect 54 | golang.org/x/sys v0.30.0 // indirect 55 | golang.org/x/text v0.22.0 // indirect 56 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect 57 | google.golang.org/api v0.114.0 // indirect 58 | google.golang.org/appengine v1.6.7 // indirect 59 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 60 | google.golang.org/grpc v1.56.3 // indirect 61 | google.golang.org/protobuf v1.33.0 // indirect 62 | gopkg.in/ini.v1 v1.51.0 // indirect 63 | gopkg.in/yaml.v2 v2.4.0 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= 2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 5 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 6 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= 14 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 15 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 16 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 17 | cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= 18 | cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 19 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 20 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 21 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 22 | cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= 23 | cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 28 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 29 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 30 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 31 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 32 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 33 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 34 | github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20200501161113-5e9e23d7cb91 h1:KxsIcqivuZu1VnrQRTSWdKgu/5CeryWzjakR81XSIBs= 35 | github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20200501161113-5e9e23d7cb91/go.mod h1:JaTTAYKXdMsyO5t+knEPNeaonOxMb/+0wYbO0pbiGuo= 36 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 37 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 38 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 39 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 40 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 41 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 42 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 43 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 44 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 45 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 46 | github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= 47 | github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= 48 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 49 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 50 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 51 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 52 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 53 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 54 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 55 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 56 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 57 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 58 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 59 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 60 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 61 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 62 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 63 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 64 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 65 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 66 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 67 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 68 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 69 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 70 | github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318 h1:iguwbR+9xsizl84VMHU47I4OOWYSex1HZRotEoqziWQ= 71 | github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318/go.mod h1:O/QFFckzvu1KpS1AOuQGgi6ErznEF8nZZVNDDMXlDP4= 72 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 73 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 74 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 75 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 76 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 77 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 78 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 79 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 80 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 81 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 82 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 83 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 84 | github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg= 85 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 86 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 87 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 88 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 89 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 90 | github.com/gocarina/gocsv v0.0.0-20201208093247-67c824bc04d4 h1:Q7s2AN3DhFJKOnzO0uTKLhJTfXTEcXcvw5ylf2BHJw4= 91 | github.com/gocarina/gocsv v0.0.0-20201208093247-67c824bc04d4/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= 92 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 93 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 94 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 95 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 96 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 97 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 98 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 99 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 100 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 101 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 102 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 103 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 104 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 105 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 106 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 107 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 108 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 109 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 110 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 111 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 112 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 113 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 114 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 115 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 116 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 117 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 118 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 119 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 120 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 121 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 122 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 123 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 124 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 125 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 126 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.0/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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 131 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 132 | github.com/google/go-github/v33 v33.0.0 h1:qAf9yP0qc54ufQxzwv+u9H0tiVOnPJxo0lI/JXqw3ZM= 133 | github.com/google/go-github/v33 v33.0.0/go.mod h1:GMdDnVZY/2TsWgp/lkYnpSAh6TrzhANBBwm6k6TTEXg= 134 | github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= 135 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 136 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 137 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 138 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 139 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 140 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 141 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 142 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 143 | github.com/google/triage-party v1.6.0 h1:AAhTHQ6QEPG7vGgFoepA+NS4Fo+ryPM+YOL8dua4QZU= 144 | github.com/google/triage-party v1.6.0/go.mod h1:HhRuy1CrG8JswMuPytEjvbrwx4UJgvmA0anGbrRP8DI= 145 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 146 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 147 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 148 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= 149 | github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= 150 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 151 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 152 | github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A= 153 | github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= 154 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 155 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 156 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 157 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 158 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 159 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 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-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 165 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 166 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 167 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 168 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 169 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 170 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 171 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 172 | github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= 173 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 174 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 175 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 176 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 177 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 178 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 179 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 180 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 181 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 182 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 183 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 184 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 185 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 186 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 187 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 188 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 189 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 190 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 191 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 192 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= 193 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 194 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 195 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 196 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 197 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 198 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 199 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 200 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 201 | github.com/karrick/tparse v2.4.2+incompatible h1:+cW306qKAzrASC5XieHkgN7/vPaGKIuK62Q7nI7DIRc= 202 | github.com/karrick/tparse v2.4.2+incompatible/go.mod h1:ASPA+vrIcN1uEW6BZg8vfWbzm69ODPSYZPU6qJyfdK0= 203 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 204 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 205 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 206 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 207 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 208 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 209 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 210 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 211 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 212 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 213 | github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= 214 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 215 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 216 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 217 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 218 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 219 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 220 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 221 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 222 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 223 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= 224 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 225 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 226 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 227 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 228 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 229 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 230 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 231 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 232 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 233 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 234 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 235 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 236 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 237 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 238 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 239 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 240 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 241 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 242 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 243 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 244 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 245 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 246 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 247 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 248 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 249 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 250 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 251 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 252 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 253 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 254 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 255 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 256 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 257 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 258 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 259 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 260 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 261 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 262 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 263 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 264 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 265 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 266 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 267 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 268 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 269 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 270 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 271 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 272 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 273 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 274 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 275 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 276 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 277 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 278 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 279 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 280 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 281 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 282 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 283 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 284 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 285 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 286 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 287 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 288 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 289 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 290 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 291 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 292 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 293 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 294 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 295 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 296 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 297 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 298 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 299 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 300 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 301 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 302 | github.com/xanzy/go-gitlab v0.36.0 h1:YSYC7Kh31bPtfJwMCa+cxoSymw2EJxvgXNi1B3IvwE8= 303 | github.com/xanzy/go-gitlab v0.36.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= 304 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 305 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 306 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 307 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 308 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 309 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 310 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 311 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 312 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 313 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 314 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 315 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 316 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 317 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 318 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 319 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 320 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 321 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 322 | golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 323 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 324 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 325 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 326 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 327 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 328 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 329 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 330 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 331 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 332 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 333 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 334 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 335 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 336 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 337 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 338 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 339 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 340 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 341 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 342 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 343 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 344 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 345 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 346 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 347 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 348 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 349 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 350 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 351 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 352 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 353 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 354 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 355 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 356 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 357 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 358 | golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 359 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 360 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 361 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 362 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 363 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 364 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 365 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 366 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 367 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 368 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 369 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 370 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 371 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 372 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 373 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 374 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 375 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 376 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 377 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 378 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 379 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 380 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 381 | golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 382 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 383 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 384 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 385 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 386 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 387 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 396 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 397 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 398 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 399 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 400 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 401 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 402 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 410 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 411 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 412 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 413 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 418 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 422 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 423 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 424 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 425 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 426 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 427 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 428 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 429 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 430 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 431 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 432 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 433 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= 434 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 435 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 436 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 437 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 438 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 439 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 440 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 441 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 442 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 443 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 444 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 445 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 446 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 447 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 448 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 449 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 450 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 451 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 452 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 453 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 454 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 455 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 456 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 457 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 458 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 459 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 460 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 461 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 462 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 463 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 464 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 465 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 466 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 467 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 468 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 469 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 470 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 471 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 472 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 473 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 474 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 475 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 476 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 477 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 478 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 479 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 480 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 481 | google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 482 | google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= 483 | google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= 484 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 485 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 486 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 487 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 488 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 489 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 490 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 491 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 492 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 493 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 494 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 495 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 496 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 497 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 498 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 499 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 500 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 501 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 502 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 503 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 504 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 505 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 506 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 507 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 508 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 509 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 510 | google.golang.org/genproto v0.0.0-20200420144010-e5e8543f8aeb/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 511 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 512 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= 513 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= 514 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 515 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 516 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 517 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 518 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 519 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 520 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 521 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 522 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 523 | google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 524 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 525 | google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= 526 | google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= 527 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 528 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 529 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 530 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 531 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 532 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 533 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 534 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 535 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 536 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 537 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 538 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 539 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 540 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 541 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 542 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 543 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 544 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 545 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 546 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 547 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 548 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 549 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 550 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 551 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 552 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 553 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 554 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 555 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 556 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 557 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 558 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 559 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 560 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 561 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 562 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 563 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 564 | k8s.io/klog/v2 v2.0.0 h1:Foj74zO6RbjjP4hBEKjnYtjjAhGg4jNynUdYF6fJrok= 565 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 566 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 567 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 568 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 569 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "strings" 22 | 23 | "github.com/google/go-github/v33/github" 24 | "golang.org/x/oauth2" 25 | 26 | "github.com/google/triage-party/pkg/persist" 27 | ) 28 | 29 | // Client is a client for interacting with GitHub and a cache. 30 | type Client struct { 31 | Cache persist.Cacher 32 | GitHubClient *github.Client 33 | } 34 | 35 | // Config is the configuration for a Client. 36 | type Config struct { 37 | GitHubTokenPath string 38 | GitHubToken string 39 | PersistBackend string // Backend to persist data. 40 | PersistPath string // Path to persist data. 41 | } 42 | 43 | // New creates a new github Client. 44 | func New(ctx context.Context, c Config) (*Client, error) { 45 | if c.PersistBackend == "" { 46 | c.PersistBackend = os.Getenv("PERSIST_BACKEND") 47 | } 48 | 49 | if c.PersistPath == "" { 50 | c.PersistPath = os.Getenv("PERSIST_PATH") 51 | } 52 | 53 | if c.GitHubToken == "" { 54 | c.GitHubToken = strings.TrimSpace(os.Getenv("GITHUB_TOKEN")) 55 | } 56 | 57 | if c.GitHubToken == "" { 58 | bs, err := os.ReadFile(c.GitHubTokenPath) 59 | if err != nil { 60 | return nil, err 61 | } 62 | c.GitHubToken = strings.TrimSpace(string(bs)) 63 | } 64 | 65 | tc := oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.GitHubToken})) 66 | gc := github.NewClient(tc) 67 | 68 | p, err := persist.FromEnv("pullsheet", c.PersistBackend, c.PersistPath) 69 | if err != nil { 70 | return nil, fmt.Errorf("persist fromenv: %v", err) 71 | } 72 | 73 | if err := p.Initialize(); err != nil { 74 | return nil, fmt.Errorf("persist init: %v", err) 75 | } 76 | 77 | return &Client{ 78 | Cache: p, 79 | GitHubClient: gc, 80 | }, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/ghcache/ghcache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ghcache 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/google/go-github/v33/github" 23 | "github.com/google/triage-party/pkg/persist" 24 | "k8s.io/klog/v2" 25 | ) 26 | 27 | // PullRequestsGet gets a pull request data from the cache or GitHub. 28 | func PullRequestsGet(ctx context.Context, p persist.Cacher, c *github.Client, t time.Time, org string, project string, num int) (*github.PullRequest, error) { 29 | key := fmt.Sprintf("pr-%s-%s-%d", org, project, num) 30 | val := p.Get(key, t) 31 | 32 | if val != nil { 33 | return val.GHPullRequest, nil 34 | } 35 | 36 | if val == nil { 37 | klog.Infof("cache miss for %v", key) 38 | pr, _, err := c.PullRequests.Get(ctx, org, project, num) 39 | if err != nil { 40 | return nil, fmt.Errorf("get: %v", err) 41 | } 42 | return pr, p.Set(key, &persist.Blob{GHPullRequest: pr}) 43 | } 44 | 45 | klog.Infof("cache hit: %v", key) 46 | return val.GHPullRequest, nil 47 | } 48 | 49 | // PullRequestsListFiles gets a list of files in a pull request from the cache or GitHub. 50 | func PullRequestsListFiles(ctx context.Context, p persist.Cacher, c *github.Client, t time.Time, org string, project string, num int) ([]*github.CommitFile, error) { 51 | key := fmt.Sprintf("pr-listfiles-%s-%s-%d", org, project, num) 52 | val := p.Get(key, t) 53 | 54 | if val != nil { 55 | return val.GHCommitFiles, nil 56 | } 57 | 58 | klog.Infof("cache miss for %v", key) 59 | 60 | opts := &github.ListOptions{PerPage: 100} 61 | fs := []*github.CommitFile{} 62 | 63 | for { 64 | fsp, resp, err := c.PullRequests.ListFiles(ctx, org, project, num, opts) 65 | if err != nil { 66 | return nil, fmt.Errorf("get: %v", err) 67 | } 68 | fs = append(fs, fsp...) 69 | 70 | if resp.NextPage == 0 { 71 | break 72 | } 73 | 74 | opts.Page = resp.NextPage 75 | } 76 | 77 | return fs, p.Set(key, &persist.Blob{GHCommitFiles: fs}) 78 | } 79 | 80 | // PullRequestsListComments gets a list of comments in a pull request from the cache or GitHub for a given org, project, and number. 81 | func PullRequestsListComments(ctx context.Context, p persist.Cacher, c *github.Client, t time.Time, org string, project string, num int) ([]*github.PullRequestComment, error) { 82 | key := fmt.Sprintf("pr-comments-%s-%s-%d", org, project, num) 83 | val := p.Get(key, t) 84 | 85 | if val != nil { 86 | return val.GHPullRequestComments, nil 87 | } 88 | 89 | klog.Infof("cache miss for %v", key) 90 | 91 | cs := []*github.PullRequestComment{} 92 | opts := &github.PullRequestListCommentsOptions{ 93 | ListOptions: github.ListOptions{PerPage: 100}, 94 | } 95 | 96 | for { 97 | csp, resp, err := c.PullRequests.ListComments(ctx, org, project, num, opts) 98 | if err != nil { 99 | return nil, fmt.Errorf("get: %v", err) 100 | } 101 | 102 | cs = append(cs, csp...) 103 | 104 | if resp.NextPage == 0 { 105 | break 106 | } 107 | opts.ListOptions.Page = resp.NextPage 108 | } 109 | 110 | return cs, p.Set(key, &persist.Blob{GHPullRequestComments: cs}) 111 | } 112 | 113 | // IssuesGet gets an issue from the cache or GitHub for a given org, project, and number. 114 | func IssuesGet(ctx context.Context, p persist.Cacher, c *github.Client, t time.Time, org string, project string, num int) (*github.Issue, error) { 115 | key := fmt.Sprintf("issue-%s-%s-%d", org, project, num) 116 | val := p.Get(key, t) 117 | 118 | if val != nil { 119 | return val.GHIssue, nil 120 | } 121 | 122 | klog.Infof("cache miss for %v", key) 123 | 124 | i, _, err := c.Issues.Get(ctx, org, project, num) 125 | if err != nil { 126 | return nil, fmt.Errorf("get: %v", err) 127 | } 128 | 129 | return i, p.Set(key, &persist.Blob{GHIssue: i}) 130 | } 131 | 132 | // IssuesListComments gets a list of comments in an issue from the cache or GitHub for a given org, project, and number. 133 | func IssuesListComments(ctx context.Context, p persist.Cacher, c *github.Client, t time.Time, org string, project string, num int) ([]*github.IssueComment, error) { 134 | key := fmt.Sprintf("issue-comments-%s-%s-%d", org, project, num) 135 | val := p.Get(key, t) 136 | 137 | if val != nil { 138 | return val.GHIssueComments, nil 139 | } 140 | 141 | opts := &github.IssueListCommentsOptions{ 142 | ListOptions: github.ListOptions{PerPage: 100}, 143 | } 144 | 145 | cs := []*github.IssueComment{} 146 | for { 147 | csp, resp, err := c.Issues.ListComments(ctx, org, project, num, opts) 148 | if err != nil { 149 | return nil, fmt.Errorf("get: %v", err) 150 | } 151 | 152 | cs = append(cs, csp...) 153 | 154 | if resp.NextPage == 0 { 155 | break 156 | } 157 | 158 | opts.ListOptions.Page = resp.NextPage 159 | } 160 | 161 | return cs, p.Set(key, &persist.Blob{GHIssueComments: cs}) 162 | } 163 | -------------------------------------------------------------------------------- /pkg/leaderboard/issues.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package leaderboard 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/google/pullsheet/pkg/repo" 21 | ) 22 | 23 | func issueCloserChart(is []*repo.IssueSummary, users []string) chart { 24 | matchUser := map[string]bool{} 25 | for _, u := range users { 26 | matchUser[strings.ToLower(u)] = true 27 | } 28 | 29 | uMap := map[string]int{} 30 | for _, i := range is { 31 | if i.Author != i.Closer { 32 | if len(matchUser) > 0 && !matchUser[strings.ToLower(i.Closer)] { 33 | continue 34 | } 35 | if !strings.HasSuffix(i.Closer, "bot") { 36 | uMap[i.Closer]++ 37 | } 38 | } 39 | } 40 | 41 | return chart{ 42 | ID: "issueCloser", 43 | Title: "Top Closers", 44 | Metric: "# of issues closed (excludes authored)", 45 | Items: topItems(mapToItems(uMap)), 46 | } 47 | } 48 | 49 | func commentWordsChart(cs []*repo.CommentSummary, _ []string) chart { 50 | uMap := map[string]int{} 51 | for _, c := range cs { 52 | if c.IssueAuthor != c.Commenter { 53 | uMap[c.Commenter] += c.Words 54 | } 55 | } 56 | 57 | return chart{ 58 | ID: "commentWords", 59 | Title: "Most Helpful", 60 | Metric: "# of words (excludes authored)", 61 | Items: topItems(mapToItems(uMap)), 62 | } 63 | } 64 | 65 | func commentsChart(cs []*repo.CommentSummary, _ []string) chart { 66 | uMap := map[string]int{} 67 | for _, c := range cs { 68 | uMap[c.Commenter] += c.Comments 69 | } 70 | 71 | return chart{ 72 | ID: "comments", 73 | Title: "Most Active", 74 | Metric: "# of comments", 75 | Items: topItems(mapToItems(uMap)), 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pkg/leaderboard/leaderboard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package leaderboard 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os" 21 | "path/filepath" 22 | "sort" 23 | "strings" 24 | "text/template" 25 | "time" 26 | 27 | "github.com/google/pullsheet/pkg/repo" 28 | ) 29 | 30 | const dateForm = "2006-01-02" 31 | 32 | // TopX is how many items to include in graphs 33 | var TopX = 15 34 | 35 | // Options to use for rendering the leaderboard 36 | type Options struct { 37 | Title string 38 | Since time.Time 39 | Until time.Time 40 | DisableCaching bool 41 | HideCommand bool 42 | } 43 | 44 | type category struct { 45 | Title string 46 | Charts []chart 47 | } 48 | 49 | type chart struct { 50 | ID string 51 | Title string 52 | Object string 53 | Metric string 54 | Items []item 55 | } 56 | 57 | type item struct { 58 | Name string 59 | Count int 60 | } 61 | 62 | // Render returns an HTML formatted leaderboard page 63 | func Render(options Options, users []string, prs []*repo.PRSummary, reviews []*repo.ReviewSummary, issues []*repo.IssueSummary, comments []*repo.CommentSummary) (string, error) { 64 | funcMap := template.FuncMap{} 65 | tmpl, err := template.New("LeaderBoard").Funcs(funcMap).Parse(leaderboardTmpl) 66 | if err != nil { 67 | return "", fmt.Errorf("parsefiles: %v", err) 68 | } 69 | 70 | data := struct { 71 | Title string 72 | From string 73 | Until string 74 | DisableCaching bool 75 | Command string 76 | HideCommand bool 77 | Categories []category 78 | }{ 79 | Title: options.Title, 80 | From: options.Since.Format(dateForm), 81 | Until: options.Until.Format(dateForm), 82 | DisableCaching: options.DisableCaching, 83 | Command: filepath.Base(os.Args[0]) + " " + strings.Join(os.Args[1:], " "), 84 | HideCommand: options.HideCommand, 85 | Categories: []category{ 86 | { 87 | Title: "Reviewers", 88 | Charts: []chart{ 89 | reviewsChart(reviews, users), 90 | reviewWordsChart(reviews, users), 91 | reviewCommentsChart(reviews, users), 92 | }, 93 | }, 94 | { 95 | Title: "Pull Requests", 96 | Charts: []chart{ 97 | mergeChart(prs, users), 98 | deltaChart(prs, users), 99 | sizeChart(prs, users), 100 | }, 101 | }, 102 | { 103 | Title: "Issues", 104 | Charts: []chart{ 105 | commentsChart(comments, users), 106 | commentWordsChart(comments, users), 107 | issueCloserChart(issues, users), 108 | }, 109 | }, 110 | }, 111 | } 112 | 113 | var tpl bytes.Buffer 114 | if err = tmpl.Execute(&tpl, data); err != nil { 115 | return "", fmt.Errorf("execute: %w", err) 116 | } 117 | 118 | out := tpl.String() 119 | return out, nil 120 | } 121 | 122 | func topItems(items []item) []item { 123 | sort.Slice(items, func(i, j int) bool { return items[i].Count > items[j].Count }) 124 | 125 | if len(items) > TopX { 126 | items = items[:TopX] 127 | } 128 | return items 129 | } 130 | 131 | func mapToItems(m map[string]int) []item { 132 | items := []item{} 133 | for u, count := range m { 134 | items = append(items, item{Name: u, Count: count}) 135 | } 136 | return items 137 | } 138 | -------------------------------------------------------------------------------- /pkg/leaderboard/leaderboard_templage.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package leaderboard 16 | 17 | const leaderboardTmpl = ` 18 | 19 | {{ .Title }} - Leaderboard 20 | {{ if .DisableCaching }} 21 | 22 | 23 | 24 | {{ end }} 25 | 26 | 27 | 28 | 31 | 102 | 103 | 104 |

{{ .Title }}

105 |
{{.From}} — {{.Until}}
106 | {{ if not .HideCommand }} 107 |

Command-line

108 |
{{.Command}}
109 | {{ end }} 110 | {{ range .Categories }} 111 |

{{ .Title }}

112 | 113 | {{ range .Charts }} 114 |
115 |

{{ .Title }}

116 |

{{ .Metric }}

117 |
118 | 145 |
146 | {{ end }} 147 | {{ end}} 148 | 149 | 150 | ` 151 | -------------------------------------------------------------------------------- /pkg/leaderboard/prs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package leaderboard 16 | 17 | import ( 18 | "github.com/google/pullsheet/pkg/repo" 19 | ) 20 | 21 | func mergeChart(prs []*repo.PRSummary, _ []string) chart { 22 | uMap := map[string]int{} 23 | for _, pr := range prs { 24 | uMap[pr.User]++ 25 | } 26 | 27 | return chart{ 28 | ID: "prCounts", 29 | Title: "Most Active", 30 | Metric: "# of Pull Requests Merged", 31 | Items: topItems(mapToItems(uMap)), 32 | } 33 | } 34 | 35 | func deltaChart(prs []*repo.PRSummary, _ []string) chart { 36 | uMap := map[string]int{} 37 | for _, pr := range prs { 38 | uMap[pr.User] += pr.Delta 39 | } 40 | 41 | return chart{ 42 | ID: "prDeltas", 43 | Title: "Big Movers", 44 | Metric: "Lines of code (delta)", 45 | Items: topItems(mapToItems(uMap)), 46 | } 47 | } 48 | 49 | func sizeChart(prs []*repo.PRSummary, _ []string) chart { 50 | sz := map[string][]int{} 51 | for _, pr := range prs { 52 | sz[pr.User] = append(sz[pr.User], pr.Delta-pr.Deleted) 53 | } 54 | 55 | uMap := map[string]int{} 56 | for u, deltas := range sz { 57 | sum := 0 58 | for _, delta := range deltas { 59 | sum += delta 60 | } 61 | 62 | uMap[u] = sum / len(deltas) 63 | } 64 | 65 | return chart{ 66 | ID: "prSize", 67 | Title: "Most difficult to review", 68 | Metric: "Average PR size (added+changed)", 69 | Items: topItems(mapToItems(uMap)), 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pkg/leaderboard/reviews.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package leaderboard 16 | 17 | import ( 18 | "github.com/google/pullsheet/pkg/repo" 19 | ) 20 | 21 | func reviewsChart(reviews []*repo.ReviewSummary, _ []string) chart { 22 | uMap := map[string]int{} 23 | for _, r := range reviews { 24 | uMap[r.Reviewer]++ 25 | } 26 | 27 | return chart{ 28 | ID: "reviewCounts", 29 | Title: "Most Influential", 30 | Metric: "# of Merged PRs reviewed", 31 | Items: topItems(mapToItems(uMap)), 32 | } 33 | } 34 | 35 | func reviewCommentsChart(reviews []*repo.ReviewSummary, _ []string) chart { 36 | uMap := map[string]int{} 37 | for _, r := range reviews { 38 | uMap[r.Reviewer] += r.ReviewComments 39 | } 40 | 41 | return chart{ 42 | ID: "reviewComments", 43 | Title: "Most Demanding", 44 | Metric: "# of Review Comments in merged PRs", 45 | Items: topItems(mapToItems(uMap)), 46 | } 47 | } 48 | 49 | func reviewWordsChart(reviews []*repo.ReviewSummary, _ []string) chart { 50 | uMap := map[string]int{} 51 | for _, r := range reviews { 52 | uMap[r.Reviewer] += r.Words 53 | } 54 | 55 | return chart{ 56 | ID: "reviewWords", 57 | Title: "Most Helpful", 58 | Metric: "# of words written in merged PRs", 59 | Items: topItems(mapToItems(uMap)), 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/print/print.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package print 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | 21 | "github.com/gocarina/gocsv" 22 | "k8s.io/klog/v2" 23 | ) 24 | 25 | // Print the values in "data" interface to standatrd output in the format specified by "out_type", either JSON/CSV 26 | func Print(data interface{}, outType string) error { 27 | var ( 28 | err error 29 | out string 30 | ) 31 | 32 | if outType == "JSON" { 33 | var jsonvar []byte 34 | jsonvar, err = json.Marshal(data) 35 | out = string(jsonvar) 36 | } else if outType == "CSV" { 37 | out, err = gocsv.MarshalString(data) 38 | } 39 | 40 | if err != nil { 41 | return err 42 | } 43 | 44 | klog.Infof("%d bytes of reviews output", len(out)) 45 | fmt.Print(out) 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/repo/files.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package repo 16 | 17 | import ( 18 | "context" 19 | "path/filepath" 20 | "strings" 21 | "time" 22 | 23 | "github.com/google/go-github/v33/github" 24 | "k8s.io/klog/v2" 25 | 26 | "github.com/google/pullsheet/pkg/client" 27 | "github.com/google/pullsheet/pkg/ghcache" 28 | ) 29 | 30 | // FilteredFiles returns a list of commit files that matter 31 | func FilteredFiles(ctx context.Context, c *client.Client, t time.Time, org string, project string, num int) ([]*github.CommitFile, error) { 32 | klog.Infof("Fetching file list for #%d", num) 33 | 34 | changed, err := ghcache.PullRequestsListFiles(ctx, c.Cache, c.GitHubClient, t, org, project, num) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | klog.Infof("%s/%s #%d had %d changed files", org, project, num, len(changed)) 40 | 41 | files := []*github.CommitFile{} 42 | for _, cf := range changed { 43 | if ignorePathRe.MatchString(cf.GetFilename()) { 44 | klog.Infof("ignoring %s", cf.GetFilename()) 45 | continue 46 | } 47 | klog.Errorf("#%d changed: %s", num, cf.GetFilename()) 48 | 49 | files = append(files, cf) 50 | } 51 | 52 | return files, err 53 | } 54 | 55 | // prType returns what kind of PR it thinks this may be 56 | func prType(files []github.CommitFile) string { 57 | result := "" 58 | for _, cf := range files { 59 | f := cf.GetFilename() 60 | ext := strings.TrimLeft(filepath.Ext(f), ".") 61 | 62 | if strings.Contains(filepath.Dir(f), "docs/") || strings.Contains(filepath.Dir(f), "examples/") || strings.Contains(filepath.Dir(f), "site/") { 63 | if result == "" { 64 | result = "docs" 65 | } 66 | klog.Infof("%s: %s", f, result) 67 | continue 68 | } 69 | 70 | if strings.Contains(f, "test") || strings.Contains(f, "integration") { 71 | if result == "" { 72 | result = "tests" 73 | } 74 | klog.Infof("%s: %s", f, result) 75 | continue 76 | } 77 | 78 | if ext == "md" && result == "" { 79 | result = "docs" 80 | } 81 | 82 | if ext == "go" || ext == "java" || ext == "cpp" || ext == "py" || ext == "c" || ext == "rs" { 83 | result = "backend" 84 | } 85 | 86 | if ext == "ts" || ext == "js" || ext == "html" { 87 | result = "frontend" 88 | } 89 | 90 | klog.Infof("%s (ext=%s): %s", f, ext, result) 91 | } 92 | 93 | if result == "" { 94 | return "unknown" 95 | } 96 | 97 | return result 98 | } 99 | -------------------------------------------------------------------------------- /pkg/repo/issue.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package repo 16 | 17 | import ( 18 | "context" 19 | "strings" 20 | "time" 21 | 22 | "github.com/google/go-github/v33/github" 23 | "k8s.io/klog/v2" 24 | 25 | "github.com/google/pullsheet/pkg/client" 26 | "github.com/google/pullsheet/pkg/ghcache" 27 | ) 28 | 29 | // IssueSummary is a summary of a single PR 30 | type IssueSummary struct { 31 | URL string 32 | Date string 33 | Author string 34 | Closer string 35 | Project string 36 | Type string 37 | Title string 38 | } 39 | 40 | // ClosedIssues returns a list of closed issues within a project 41 | func ClosedIssues(ctx context.Context, c *client.Client, org string, project string, since time.Time, until time.Time, users []string) ([]*IssueSummary, error) { 42 | closed, err := issues(ctx, c, org, project, since, until, users, "closed") 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | result := make([]*IssueSummary, 0, len(closed)) 48 | for _, i := range closed { 49 | result = append(result, &IssueSummary{ 50 | URL: i.GetHTMLURL(), 51 | Date: i.GetClosedAt().Format(dateForm), 52 | Author: i.GetUser().GetLogin(), 53 | Closer: i.GetClosedBy().GetLogin(), 54 | Project: project, 55 | Title: i.GetTitle(), 56 | }) 57 | } 58 | 59 | return result, nil 60 | } 61 | 62 | // issues returns a list of issues in a project 63 | func issues(ctx context.Context, c *client.Client, org string, project string, since time.Time, until time.Time, users []string, state string) ([]*github.Issue, error) { 64 | result := []*github.Issue{} 65 | opts := &github.IssueListByRepoOptions{ 66 | State: state, 67 | Sort: "updated", 68 | Direction: "desc", 69 | ListOptions: github.ListOptions{ 70 | PerPage: 100, 71 | }, 72 | } 73 | 74 | matchUser := map[string]bool{} 75 | for _, u := range users { 76 | matchUser[strings.ToLower(u)] = true 77 | } 78 | 79 | klog.Infof("Gathering issues for %s/%s, users=%q: %+v", org, project, users, opts) 80 | for page := 1; page != 0; { 81 | opts.ListOptions.Page = page 82 | issues, resp, err := c.GitHubClient.Issues.ListByRepo(ctx, org, project, opts) 83 | if err != nil { 84 | return result, err 85 | } 86 | if len(issues) == 0 { 87 | klog.Infof("There isn't any issue in %s/%s since %s", org, project, since) 88 | break 89 | } 90 | 91 | klog.Infof("Processing page %d of %s/%s issue results ...", page, org, project) 92 | 93 | page = resp.NextPage 94 | klog.Infof("Current issue updated at %s", issues[0].GetUpdatedAt()) 95 | 96 | for _, i := range issues { 97 | if i.IsPullRequest() { 98 | continue 99 | } 100 | if i.GetClosedAt().After(until) { 101 | klog.Infof("issue #%d closed at %s", i.GetNumber(), i.GetUpdatedAt()) 102 | continue 103 | } 104 | 105 | if i.GetUpdatedAt().Before(since) { 106 | klog.Infof("Hit issue #%d updated at %s", i.GetNumber(), i.GetUpdatedAt()) 107 | page = 0 108 | break 109 | } 110 | 111 | if !i.GetClosedAt().IsZero() && i.GetClosedAt().Before(since) { 112 | continue 113 | } 114 | 115 | if state != "" && i.GetState() != state { 116 | klog.Infof("Skipping issue #%d (state=%q)", i.GetNumber(), i.GetState()) 117 | continue 118 | } 119 | 120 | t := issueDate(i) 121 | 122 | klog.Infof("Fetching #%d (closed %s, updated %s): %q", i.GetNumber(), i.GetClosedAt().Format(dateForm), i.GetUpdatedAt().Format(dateForm), i.GetTitle()) 123 | 124 | full, err := ghcache.IssuesGet(ctx, c.Cache, c.GitHubClient, t, org, project, i.GetNumber()) 125 | if err != nil { 126 | time.Sleep(1 * time.Second) 127 | full, err = ghcache.IssuesGet(ctx, c.Cache, c.GitHubClient, t, org, project, i.GetNumber()) 128 | } 129 | if err != nil { 130 | klog.Errorf("failed IssuesGet: %v", err) 131 | break 132 | } 133 | 134 | creator := strings.ToLower(full.GetUser().GetLogin()) 135 | closer := strings.ToLower(full.GetClosedBy().GetLogin()) 136 | if len(matchUser) > 0 && !matchUser[creator] && !matchUser[closer] { 137 | continue 138 | } 139 | 140 | result = append(result, full) 141 | } 142 | } 143 | 144 | klog.Infof("Returning %d issues", len(result)) 145 | return result, nil 146 | } 147 | 148 | func issueDate(i *github.Issue) time.Time { 149 | t := i.GetClosedAt() 150 | if t.IsZero() { 151 | t = i.GetUpdatedAt() 152 | } 153 | if t.IsZero() { 154 | t = i.GetCreatedAt() 155 | } 156 | 157 | return t 158 | } 159 | -------------------------------------------------------------------------------- /pkg/repo/issue_comments.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package repo 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strings" 21 | "time" 22 | 23 | "k8s.io/klog/v2" 24 | 25 | "github.com/google/pullsheet/pkg/client" 26 | "github.com/google/pullsheet/pkg/ghcache" 27 | ) 28 | 29 | // CommentSummary a summary of a users reviews on an issue 30 | type CommentSummary struct { 31 | URL string 32 | Date string 33 | Project string 34 | Commenter string 35 | IssueAuthor string 36 | IssueState string 37 | Comments int 38 | Words int 39 | Title string 40 | } 41 | 42 | // IssueComments returns a list of issue comment summaries 43 | func IssueComments(ctx context.Context, c *client.Client, org string, project string, since time.Time, until time.Time, users []string) ([]*CommentSummary, error) { 44 | is, err := issues(ctx, c, org, project, since, until, nil, "") 45 | if err != nil { 46 | return nil, fmt.Errorf("issues: %v", err) 47 | } 48 | 49 | klog.Infof("found %d issues to check comments on", len(is)) 50 | reviews := []*CommentSummary{} 51 | 52 | matchUser := map[string]bool{} 53 | for _, u := range users { 54 | matchUser[strings.ToLower(u)] = true 55 | } 56 | 57 | for _, i := range is { 58 | if i.IsPullRequest() { 59 | continue 60 | } 61 | 62 | // username -> summary 63 | iMap := map[string]*CommentSummary{} 64 | 65 | cs, err := ghcache.IssuesListComments(ctx, c.Cache, c.GitHubClient, issueDate(i), org, project, i.GetNumber()) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | for _, c := range cs { 71 | commenter := c.GetUser().GetLogin() 72 | if c.CreatedAt.After(until) { 73 | continue 74 | } 75 | 76 | if c.CreatedAt.Before(since) { 77 | continue 78 | } 79 | 80 | if len(matchUser) > 0 && !matchUser[strings.ToLower(commenter)] { 81 | continue 82 | } 83 | 84 | if commenter == i.GetUser().GetLogin() { 85 | continue 86 | } 87 | 88 | if isBot(c.GetUser()) { 89 | continue 90 | } 91 | 92 | body := strings.TrimSpace(i.GetBody()) 93 | if (strings.HasPrefix(body, "/") || strings.HasPrefix(body, "cc")) && len(body) < 64 { 94 | klog.Infof("ignoring tag comment: %q", body) 95 | continue 96 | } 97 | 98 | wordCount := wordCount(c.GetBody()) 99 | 100 | if iMap[commenter] == nil { 101 | iMap[commenter] = &CommentSummary{ 102 | URL: i.GetHTMLURL(), 103 | IssueAuthor: i.GetUser().GetLogin(), 104 | IssueState: i.GetState(), 105 | Commenter: commenter, 106 | Project: project, 107 | Title: strings.TrimSpace(i.GetTitle()), 108 | } 109 | } 110 | 111 | iMap[commenter].Comments++ 112 | iMap[commenter].Date = c.CreatedAt.Format(dateForm) 113 | iMap[commenter].Words += wordCount 114 | klog.Infof("%d word comment by %s: %q for %s/%s #%d", wordCount, commenter, strings.TrimSpace(c.GetBody()), org, project, i.GetNumber()) 115 | } 116 | 117 | for _, rs := range iMap { 118 | reviews = append(reviews, rs) 119 | } 120 | } 121 | 122 | return reviews, err 123 | } 124 | -------------------------------------------------------------------------------- /pkg/repo/pr.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package repo 16 | 17 | import ( 18 | "context" 19 | "regexp" 20 | "strings" 21 | "time" 22 | 23 | "github.com/google/go-github/v33/github" 24 | "k8s.io/klog/v2" 25 | 26 | "github.com/google/pullsheet/pkg/client" 27 | "github.com/google/pullsheet/pkg/ghcache" 28 | ) 29 | 30 | const dateForm = "2006-01-02" 31 | 32 | var ( 33 | ignorePathRe = regexp.MustCompile(`go\.mod|go\.sum|vendor/|third_party|ignore|schemas/v\d|schema/v\d|Gopkg.lock|.DS_Store|\.json$|\.pb\.go|references/api/grpc|docs/commands/|pb\.gw\.go|proto/.*\.tmpl|proto/.*\.md`) 34 | truncRe = regexp.MustCompile(`changelog|CHANGELOG|Gopkg.toml`) 35 | commentRe = regexp.MustCompile(`