├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd └── tickgit │ ├── commands │ ├── helpers.go │ ├── root.go │ └── todos.go │ └── tickgit.go ├── docs └── API.md ├── go.mod ├── go.sum ├── pkg ├── blame │ └── blame.go ├── comments │ ├── comments.go │ ├── comments_test.go │ ├── languages.go │ └── testdata │ │ ├── elixir │ │ └── simple.exs │ │ ├── haskell │ │ └── hello_world.hs │ │ ├── javascript │ │ ├── hello_world.js │ │ ├── node_modules │ │ │ └── ignored.js │ │ └── subdir │ │ │ └── index.js │ │ ├── julia │ │ └── example.jl │ │ ├── kotlin │ │ └── hello_world.kt │ │ ├── lisp │ │ └── hello_world.lisp │ │ ├── php │ │ └── test.php │ │ └── rust │ │ └── comments.rs └── todos │ ├── report.go │ ├── todos.go │ └── todos_test.go └── testdata └── repos └── repo-001 └── commit-001 └── test.tickgit /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | create: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v1 15 | - 16 | name: Set up Go 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: '1.13.1' 20 | - 21 | name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@v1 23 | with: 24 | version: latest 25 | args: release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.ACTION_GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 1.14 9 | uses: actions/setup-go@v1 10 | with: 11 | go-version: 1.14.3 12 | id: go 13 | 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v1 16 | 17 | - name: Vet 18 | run: go vet -v ./... 19 | 20 | - name: Test 21 | run: go test -v ./... 22 | 23 | lint: 24 | name: Lint 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: golangci-lint 29 | uses: golangci/golangci-lint-action@v1 30 | with: 31 | version: v1.26 32 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: tickgit 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - 7 | main: ./cmd/tickgit/tickgit.go 8 | env: 9 | - CGO_ENABLED=0 10 | brews: 11 | - 12 | github: 13 | owner: augmentable-dev 14 | name: homebrew-tickgit 15 | commit_author: 16 | name: tickgit 17 | email: support@tickgit.com 18 | homepage: "https://www.tickgit.com/" 19 | description: "Surface TODO comments in any codebase" 20 | 21 | archives: 22 | - replacements: 23 | darwin: Darwin 24 | linux: Linux 25 | windows: Windows 26 | 386: i386 27 | amd64: x86_64 28 | checksum: 29 | name_template: 'checksums.txt' 30 | snapshot: 31 | name_template: "{{ .Tag }}-next" 32 | changelog: 33 | sort: asc 34 | filters: 35 | exclude: 36 | - '^docs:' 37 | - '^test:' 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Patrick DeVivo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/augmentable-dev/tickgit?status.svg)](https://godoc.org/github.com/augmentable-dev/tickgit) 2 | [![BuildStatus](https://github.com/augmentable-dev/tickgit/workflows/tests/badge.svg)](https://github.com/augmentable-dev/tickgit/actions?workflow=tests) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/augmentable-dev/tickgit)](https://goreportcard.com/report/github.com/augmentable-dev/tickgit) 4 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/augmentable-dev/tickgit) 5 | [![Coverage](http://gocover.io/_badge/github.com/augmentable-dev/tickgit)](http://gocover.io/github.com/augmentable-dev/tickgit) 6 | [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/augmentable-dev/tickgit)](https://www.tickgit.com/browse?repo=github.com/augmentable-dev/tickgit) 7 | 8 | ## tickgit 🎟️ 9 | 10 | `tickgit` is a tool to help you manage latent work in a codebase. Use the `tickgit` command to view pending tasks, progress reports, completion summaries and historical data (using `git` history). 11 | 12 | It's not meant to replace full-fledged project management tools such as JIRA or Trello. It will, hopefully, be a useful way to augment those tools with project management patterns that coexist with your code. As such, it's primary audience is software engineers. 13 | 14 | ### TODOs 15 | 16 | `tickgit` will scan a codebase and identify any TODO items in the comments. It will output a report like so: 17 | 18 | ``` 19 | # tickgit ~/Desktop/facebook/react 20 | ... 21 | TODO: 22 | => packages/scheduler/src/__tests__/SchedulerBrowser-test.js:85:9 23 | => added 1 month ago by Andrew Clark in a2e05b6c148b25590884e8911d4d4acfcb76a487 24 | 25 | TODO: Scheduler no longer requires these methods to be polyfilled. But 26 | => packages/scheduler/src/__tests__/SchedulerBrowser-test.js:77:7 27 | => added 1 month ago by Andrew Clark in a2e05b6c148b25590884e8911d4d4acfcb76a487 28 | 29 | TODO: Scheduler no longer requires these methods to be polyfilled. But 30 | => packages/scheduler/src/forks/SchedulerHostConfig.default.js:77:7 31 | => added 1 month ago by Andrew Clark in a2e05b6c148b25590884e8911d4d4acfcb76a487 32 | 33 | TODO: useTransition hook instead. 34 | => fixtures/concurrent/time-slicing/src/index.js:110:11 35 | => added 3 weeks ago by Sebastian Markbåge in 3ad076472ce9108b9b8a6a6fe039244b74a34392 36 | 37 | 128 TODOs Found 📝 38 | ``` 39 | 40 | Check out [an example](https://www.tickgit.com/browse?repo=github.com/kubernetes/kubernetes) of the TODOs tickgit will surface for the Kubernetes codebase. 41 | 42 | #### Coming Soon 43 | 44 | - [x] Blame - get a better sense of how old TODOs are, when they were introduced and by whom 45 | - [ ] Context - more visibility into the lines of code _around_ a TODO for greater context 46 | - [ ] More `TODO` type phrases to match, such as `FIXME`, `XXX`, `HACK`, or customized alternatives. 47 | - [ ] More configurability (e.g. custom ignore paths) 48 | - [ ] Markdown parsing 49 | - [ ] More thorough historical stats 50 | 51 | ### Installation 52 | 53 | #### Homebrew 54 | 55 | ``` 56 | brew tap augmentable-dev/tickgit 57 | brew install tickgit 58 | ``` 59 | 60 | ### Usage 61 | 62 | The most up to date usage will be the output of `tickgit --help`. 63 | 64 | ### API 65 | 66 | To find information about using the tickgit API, see [this file](https://github.com/augmentable-dev/tickgit/blob/master/docs/API.md). 67 | -------------------------------------------------------------------------------- /cmd/tickgit/commands/helpers.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func validateDir(dir string) { 10 | if dir == "" { 11 | cwd, err := os.Getwd() 12 | handleError(err, nil) 13 | dir = cwd 14 | } 15 | 16 | abs, err := filepath.Abs(filepath.Join(dir, ".git")) 17 | handleError(err, nil) 18 | 19 | if _, err := os.Stat(abs); os.IsNotExist(err) { 20 | handleError(fmt.Errorf("%s is not a git repository", abs), nil) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/tickgit/commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/briandowns/spinner" 8 | ) 9 | 10 | // TODO clean this up 11 | func handleError(err error, spinner *spinner.Spinner) { 12 | if err != nil { 13 | if spinner != nil { 14 | // spinner.Suffix = "" 15 | spinner.FinalMSG = err.Error() 16 | spinner.Stop() 17 | } else { 18 | fmt.Println(err) 19 | } 20 | os.Exit(1) 21 | } 22 | } 23 | 24 | // Execute adds all child commands to the root command and sets flags appropriately. 25 | func Execute() { 26 | if err := todosCmd.Execute(); err != nil { 27 | fmt.Println(err) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/tickgit/commands/todos.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/csv" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/augmentable-dev/tickgit/pkg/comments" 14 | "github.com/augmentable-dev/tickgit/pkg/todos" 15 | "github.com/briandowns/spinner" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var csvOutput bool 20 | 21 | func init() { 22 | todosCmd.Flags().BoolVar(&csvOutput, "csv-output", false, "specify whether or not output should be in CSV format") 23 | } 24 | 25 | var todosCmd = &cobra.Command{ 26 | Use: "todos", 27 | Short: "Print a report of current TODOs", 28 | Long: `Scans a given git repository looking for any code comments with TODOs. Displays a report of all the TODO items found.`, 29 | Args: cobra.MaximumNArgs(1), 30 | Run: func(cmd *cobra.Command, args []string) { 31 | s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) 32 | s.HideCursor = true 33 | s.Suffix = " finding TODOs" 34 | s.Writer = os.Stderr 35 | s.Start() 36 | 37 | cwd, err := os.Getwd() 38 | handleError(err, s) 39 | 40 | dir := cwd 41 | if len(args) == 1 { 42 | dir, err = filepath.Rel(cwd, args[0]) 43 | handleError(err, s) 44 | } 45 | 46 | validateDir(dir) 47 | 48 | foundToDos := make(todos.ToDos, 0) 49 | err = comments.SearchDir(dir, func(comment *comments.Comment) { 50 | todo := todos.NewToDo(*comment) 51 | if todo != nil { 52 | foundToDos = append(foundToDos, todo) 53 | s.Suffix = fmt.Sprintf(" %d TODOs found", len(foundToDos)) 54 | } 55 | }) 56 | handleError(err, s) 57 | 58 | s.Suffix = fmt.Sprintf(" blaming %d TODOs", len(foundToDos)) 59 | ctx := context.Background() 60 | // timeout after 30 seconds 61 | // ctx, cancel := context.WithTimeout(ctx, 30*time.Second) 62 | // defer cancel() 63 | err = foundToDos.FindBlame(ctx, dir) 64 | sort.Sort(&foundToDos) 65 | 66 | handleError(err, s) 67 | 68 | s.Stop() 69 | 70 | if csvOutput { 71 | w := csv.NewWriter(os.Stdout) 72 | err := w.Write([]string{ 73 | "text", "file_path", "start_line", "start_position", "end_line", "end_position", "author", "author_email", "author_sha", "author_time", 74 | }) 75 | handleError(err, s) 76 | 77 | for _, todo := range foundToDos { 78 | err := w.Write([]string{ 79 | todo.String, 80 | todo.FilePath, 81 | strconv.Itoa(todo.StartLocation.Line), 82 | strconv.Itoa(todo.StartLocation.Pos), 83 | strconv.Itoa(todo.EndLocation.Line), 84 | strconv.Itoa(todo.EndLocation.Pos), 85 | todo.Blame.Author.Name, 86 | todo.Blame.Author.Email, 87 | todo.Blame.SHA, 88 | todo.Blame.Author.When.Format(time.RFC3339), 89 | }) 90 | handleError(err, s) 91 | } 92 | 93 | // Write any buffered data to the underlying writer (standard output). 94 | w.Flush() 95 | 96 | err = w.Error() 97 | handleError(err, s) 98 | 99 | } else { 100 | err := todos.WriteTodos(foundToDos, os.Stdout) 101 | handleError(err, s) 102 | } 103 | 104 | }, 105 | } 106 | -------------------------------------------------------------------------------- /cmd/tickgit/tickgit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/augmentable-dev/tickgit/cmd/tickgit/commands" 5 | ) 6 | 7 | func main() { 8 | commands.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | ### TODOs Badge 4 | 5 | `GET` requests to `https://api.tickgit.com/badgen` with a `repo` path segment: 6 | 7 | ``` 8 | https://api.tickgit.com/badgen/github.com/facebook/react 9 | ``` 10 | 11 | Supplying a `branch` segment will lookup a specific branch. `master` is the branch used if none is specified. 12 | 13 | ``` 14 | https://api.tickgit.com/badgen/github.com/facebook/react/branch-name 15 | ``` 16 | 17 | Will return JSON that can be fed into a badgen badge: [https://badgen.net/https](https://badgen.net/https) 18 | 19 | ``` 20 | [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/augmentable-dev/tickgit)](https://www.tickgit.com/browse?repo=github.com/augmentable-dev/tickgit) 21 | ``` 22 | 23 | [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/augmentable-dev/tickgit)](https://www.tickgit.com/browse?repo=github.com/augmentable-dev/tickgit) 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/augmentable-dev/tickgit 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/augmentable-dev/lege v0.0.0-20191028004410-79cb985065a1 7 | github.com/briandowns/spinner v1.11.1 8 | github.com/dustin/go-humanize v1.0.0 9 | github.com/fatih/color v1.9.0 // indirect 10 | github.com/go-enry/go-enry/v2 v2.5.2 11 | github.com/karrick/godirwalk v1.15.6 12 | github.com/mattn/go-colorable v0.1.6 // indirect 13 | github.com/sergi/go-diff v1.1.0 // indirect 14 | github.com/spf13/cobra v1.0.0 15 | github.com/spf13/pflag v1.0.5 // indirect 16 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect 17 | golang.org/x/net v0.0.0-20200528225125-3c3fba18258b // indirect 18 | golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect 19 | gopkg.in/src-d/go-git.v4 v4.13.1 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 5 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 9 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 10 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 12 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 13 | github.com/augmentable-dev/lege v0.0.0-20191028004410-79cb985065a1 h1:NBe2//2MA/Z7X4wuKnHSIN+xI/oBTLYMJVc8VCwXK4o= 14 | github.com/augmentable-dev/lege v0.0.0-20191028004410-79cb985065a1/go.mod h1:DtuvAW6+SE9e44O6eLaMJp8PFiadmk6NfXslCKYCiB0= 15 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 16 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 17 | github.com/briandowns/spinner v1.11.1 h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0= 18 | github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= 19 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 20 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 21 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 22 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 23 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 24 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 25 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 26 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 27 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 28 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 30 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 32 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 33 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 34 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 35 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 36 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 37 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 38 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 39 | github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= 40 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 41 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 42 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 43 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 44 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 45 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 46 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 47 | github.com/go-enry/go-enry/v2 v2.5.2 h1:3f3PFAO6JitWkPi1GQ5/m6Xu4gNL1U5soJ8QaYqJ0YQ= 48 | github.com/go-enry/go-enry/v2 v2.5.2/go.mod h1:GVzIiAytiS5uT/QiuakK7TF1u4xDab87Y8V5EJRpsIQ= 49 | github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= 50 | github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= 51 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 52 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 53 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 54 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 55 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 56 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 57 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 58 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 59 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 60 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 61 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 62 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 63 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 64 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 65 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 66 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 67 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 68 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 69 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 70 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 71 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 72 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 73 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 74 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 75 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 76 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 77 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 78 | github.com/karrick/godirwalk v1.15.6 h1:Yf2mmR8TJy+8Fa0SuQVto5SYap6IF7lNVX4Jdl8G1qA= 79 | github.com/karrick/godirwalk v1.15.6/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= 80 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 81 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 82 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 83 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 84 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 85 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 86 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 87 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 88 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 89 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 90 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 91 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 92 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 93 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 94 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 95 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 96 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 97 | github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= 98 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 99 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 100 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 101 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 102 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 103 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 104 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 105 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 106 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 107 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 108 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 109 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 110 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 111 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 112 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 113 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 114 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 115 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 116 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 117 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 118 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 119 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 120 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 121 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 122 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 123 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 124 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 125 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 126 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 127 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 128 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 129 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 130 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 131 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 132 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 133 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 134 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 135 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 136 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 137 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 138 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 139 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 140 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 141 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 142 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 143 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 144 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 145 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 146 | github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= 147 | github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 148 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 149 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 150 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 151 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 152 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 153 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 154 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 155 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 156 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 157 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 158 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 159 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 160 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 161 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 162 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 163 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 164 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 165 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 166 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 167 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 168 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 169 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 170 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 171 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= 172 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 173 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 174 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 175 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 176 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 177 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 178 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 179 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 180 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 181 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 182 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= 183 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 184 | golang.org/x/net v0.0.0-20200528225125-3c3fba18258b h1:IYiJPiJfzktmDAO1HQiwjMjwjlYKHAL7KzeD544RJPs= 185 | golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 186 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 187 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 188 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 189 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 190 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 191 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 192 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 193 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 194 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 195 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 196 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 197 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 198 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 199 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= 200 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 201 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= 206 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 208 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 209 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 210 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 211 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 212 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 213 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 214 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 215 | golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 216 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 217 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 218 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 219 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 220 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 221 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 222 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 223 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 224 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 225 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 226 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 227 | gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= 228 | gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 229 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= 230 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 231 | gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= 232 | gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= 233 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 234 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 235 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 236 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 237 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 238 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 239 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 240 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 241 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 242 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 243 | -------------------------------------------------------------------------------- /pkg/blame/blame.go: -------------------------------------------------------------------------------- 1 | package blame 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // Options are options to determine what and how to blame 15 | type Options struct { 16 | Directory string 17 | SHA string 18 | Lines []int 19 | } 20 | 21 | // Blame represents the "blame" of a particular line or range of lines 22 | type Blame struct { 23 | SHA string 24 | Author Event 25 | Committer Event 26 | Range [2]int 27 | } 28 | 29 | // Event represents the who and when of a commit event 30 | type Event struct { 31 | Name string 32 | Email string 33 | When time.Time 34 | } 35 | 36 | func (blame *Blame) String() string { 37 | return fmt.Sprintf("%s: %s <%s>", blame.SHA, blame.Author.Name, blame.Author.Email) 38 | } 39 | 40 | func (event *Event) String() string { 41 | return fmt.Sprintf("%s <%s>", event.Name, event.Email) 42 | } 43 | 44 | // Result is a mapping of line numbers to blames for a given file 45 | type Result map[int]Blame 46 | 47 | func (options *Options) argsFromOptions(filePath string) []string { 48 | args := []string{"blame"} 49 | if options.SHA != "" { 50 | args = append(args, options.SHA) 51 | } 52 | 53 | for _, line := range options.Lines { 54 | args = append(args, fmt.Sprintf("-L %d,%d", line, line)) 55 | } 56 | 57 | args = append(args, "--porcelain", "--incremental") 58 | 59 | args = append(args, filePath) 60 | return args 61 | } 62 | 63 | func parsePorcelain(reader io.Reader) (Result, error) { 64 | scanner := bufio.NewScanner(reader) 65 | res := make(Result) 66 | 67 | const ( 68 | author = "author " 69 | authorMail = "author-mail " 70 | authorTime = "author-time " 71 | authorTZ = "author-tz " 72 | 73 | committer = "committer " 74 | committerMail = "committer-mail " 75 | committerTime = "committer-time " 76 | committerTZ = "committer-tz " 77 | ) 78 | 79 | seenCommits := make(map[string]Blame) 80 | var currentCommit Blame 81 | for scanner.Scan() { 82 | line := scanner.Text() 83 | switch { 84 | case strings.HasPrefix(line, author): 85 | currentCommit.Author.Name = strings.TrimPrefix(line, author) 86 | case strings.HasPrefix(line, authorMail): 87 | s := strings.TrimPrefix(line, authorMail) 88 | currentCommit.Author.Email = strings.Trim(s, "<>") 89 | case strings.HasPrefix(line, authorTime): 90 | timeString := strings.TrimPrefix(line, authorTime) 91 | i, err := strconv.ParseInt(timeString, 10, 64) 92 | if err != nil { 93 | return nil, err 94 | } 95 | currentCommit.Author.When = time.Unix(i, 0) 96 | case strings.HasPrefix(line, authorTZ): 97 | tzString := strings.TrimPrefix(line, authorTZ) 98 | parsed, err := time.Parse("-0700", tzString) 99 | if err != nil { 100 | return nil, err 101 | } 102 | loc := parsed.Location() 103 | currentCommit.Author.When = currentCommit.Author.When.In(loc) 104 | case strings.HasPrefix(line, committer): 105 | currentCommit.Committer.Name = strings.TrimPrefix(line, committer) 106 | case strings.HasPrefix(line, committerMail): 107 | s := strings.TrimPrefix(line, committer) 108 | currentCommit.Committer.Email = strings.Trim(s, "<>") 109 | case strings.HasPrefix(line, committerTime): 110 | timeString := strings.TrimPrefix(line, committerTime) 111 | i, err := strconv.ParseInt(timeString, 10, 64) 112 | if err != nil { 113 | return nil, err 114 | } 115 | currentCommit.Committer.When = time.Unix(i, 0) 116 | case strings.HasPrefix(line, committerTZ): 117 | tzString := strings.TrimPrefix(line, committerTZ) 118 | parsed, err := time.Parse("-0700", tzString) 119 | if err != nil { 120 | return nil, err 121 | } 122 | loc := parsed.Location() 123 | currentCommit.Committer.When = currentCommit.Committer.When.In(loc) 124 | case len(strings.Split(line, " ")[0]) == 40: // if the first string sep by a space is 40 chars long, it's probably the commit header 125 | split := strings.Split(line, " ") 126 | sha := split[0] 127 | 128 | // if we haven't seen this commit before, create an entry in the seen commits map that will get filled out in subsequent lines 129 | if _, ok := seenCommits[sha]; !ok { 130 | seenCommits[sha] = Blame{SHA: sha} 131 | } 132 | 133 | // update the current commit to be this new one we've just encountered 134 | currentCommit.SHA = sha 135 | 136 | // pull out the line information 137 | line := split[2] 138 | l, err := strconv.ParseInt(line, 10, 64) // the starting line of the range 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | var c int64 144 | if len(split) > 3 { 145 | c, err = strconv.ParseInt(split[3], 10, 64) // the number of lines in the range 146 | if err != nil { 147 | return nil, err 148 | } 149 | } 150 | for i := l; i < l+c; i++ { 151 | res[int(i)] = Blame{SHA: sha} 152 | } 153 | } 154 | // after every line, make sure the current commit in the seen commits map is updated 155 | seenCommits[currentCommit.SHA] = currentCommit 156 | } 157 | for line, blame := range res { 158 | res[line] = seenCommits[blame.SHA] 159 | } 160 | if err := scanner.Err(); err != nil { 161 | return nil, err 162 | } 163 | 164 | return res, nil 165 | } 166 | 167 | // Exec uses git to lookup the blame of a file, given the supplied options 168 | func Exec(ctx context.Context, filePath string, options *Options) (Result, error) { 169 | gitPath, err := exec.LookPath("git") 170 | if err != nil { 171 | return nil, fmt.Errorf("could not find git: %w", err) 172 | } 173 | 174 | args := options.argsFromOptions(filePath) 175 | 176 | cmd := exec.CommandContext(ctx, gitPath, args...) 177 | cmd.Dir = options.Directory 178 | 179 | stdout, err := cmd.StdoutPipe() 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | if err := cmd.Start(); err != nil { 185 | return nil, err 186 | } 187 | 188 | res, err := parsePorcelain(stdout) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | if err := cmd.Wait(); err != nil { 194 | return nil, err 195 | } 196 | 197 | return res, nil 198 | } 199 | -------------------------------------------------------------------------------- /pkg/comments/comments.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/augmentable-dev/lege" 13 | "github.com/go-enry/go-enry/v2" 14 | "github.com/karrick/godirwalk" 15 | "gopkg.in/src-d/go-git.v4/plumbing/object" 16 | ) 17 | 18 | // Comments is a list of comments 19 | type Comments []*Comment 20 | 21 | // Comment represents a comment in a source code file 22 | type Comment struct { 23 | lege.Collection 24 | FilePath string 25 | } 26 | 27 | // SearchFile searches a file for comments. It infers the language 28 | func SearchFile(filePath string, reader io.Reader, cb func(*Comment)) error { 29 | // create a preview reader that reads in some of the file for enry to better identify the language 30 | var buf bytes.Buffer 31 | tee := io.TeeReader(reader, &buf) 32 | previewReader := io.LimitReader(tee, 1000) 33 | preview, err := ioutil.ReadAll(previewReader) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | // create a new reader concatenating the preview and the original reader (which has now been read from) 39 | fullReader := io.MultiReader(strings.NewReader(buf.String()), reader) 40 | 41 | lang := Language(enry.GetLanguage(filepath.Base(filePath), preview)) 42 | if enry.IsVendor(filePath) { 43 | return nil 44 | } 45 | options, ok := LanguageParseOptions[lang] 46 | if !ok { // TODO provide a default parse option for when we don't know how to handle a language? I.e. default to CStyle comments say 47 | return nil 48 | } 49 | commentParser, err := lege.NewParser(options) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | collections, err := commentParser.Parse(fullReader) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | for _, c := range collections { 60 | comment := Comment{*c, filePath} 61 | cb(&comment) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // SearchDir searches a directory for comments 68 | func SearchDir(dirPath string, cb func(comment *Comment)) error { 69 | err := godirwalk.Walk(dirPath, &godirwalk.Options{ 70 | Callback: func(path string, de *godirwalk.Dirent) error { 71 | localPath, err := filepath.Rel(dirPath, path) 72 | if err != nil { 73 | return err 74 | } 75 | pathComponents := strings.Split(localPath, string(os.PathSeparator)) 76 | // let's ignore git directories TODO: figure out a more generic way to set ignores 77 | matched, err := filepath.Match(".git", pathComponents[0]) 78 | if err != nil { 79 | return err 80 | } 81 | if matched { 82 | return nil 83 | } 84 | if de.IsRegular() { 85 | p, err := filepath.Abs(path) 86 | if err != nil { 87 | return err 88 | } 89 | f, err := os.Open(p) 90 | if err != nil { 91 | return err 92 | } 93 | err = SearchFile(localPath, f, cb) 94 | if err != nil { 95 | return err 96 | } 97 | f.Close() 98 | } 99 | return nil 100 | }, 101 | Unsorted: true, 102 | }) 103 | if err != nil { 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | // SearchCommit searches all files in the tree of a given commit 110 | func SearchCommit(commit *object.Commit, cb func(*Comment)) error { 111 | var wg sync.WaitGroup 112 | errs := make(chan error) 113 | 114 | fileIter, err := commit.Files() 115 | if err != nil { 116 | return err 117 | } 118 | defer fileIter.Close() 119 | err = fileIter.ForEach(func(file *object.File) error { 120 | if file.Mode.IsFile() { 121 | wg.Add(1) 122 | go func() { 123 | defer wg.Done() 124 | 125 | r, err := file.Reader() 126 | if err != nil { 127 | errs <- err 128 | return 129 | } 130 | err = SearchFile(file.Name, r, cb) 131 | if err != nil { 132 | errs <- err 133 | return 134 | } 135 | 136 | }() 137 | } 138 | return nil 139 | }) 140 | 141 | if err != nil { 142 | return err 143 | } 144 | 145 | wg.Wait() 146 | return nil 147 | } 148 | -------------------------------------------------------------------------------- /pkg/comments/comments_test.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestJSFiles(t *testing.T) { 8 | var comments Comments 9 | err := SearchDir("testdata/javascript", func(comment *Comment) { 10 | comments = append(comments, comment) 11 | }) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | if len(comments) != 3 { 17 | t.Fail() 18 | } 19 | } 20 | 21 | func TestLispFiles(t *testing.T) { 22 | var comments Comments 23 | err := SearchDir("testdata/lisp", func(comment *Comment) { 24 | comments = append(comments, comment) 25 | }) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | if len(comments) != 1 { 31 | t.Fail() 32 | } 33 | } 34 | 35 | func TestRustFiles(t *testing.T) { 36 | var comments Comments 37 | err := SearchDir("testdata/rust", func(comment *Comment) { 38 | comments = append(comments, comment) 39 | }) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | // TODO: break the different comment types out into separate files? 45 | // once the issue with lege is worked out for handling the different comment types 46 | if len(comments) != 21 { 47 | t.Fail() 48 | } 49 | } 50 | 51 | func TestPHPFiles(t *testing.T) { 52 | var comments Comments 53 | err := SearchDir("testdata/php", func(comment *Comment) { 54 | comments = append(comments, comment) 55 | }) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if len(comments) != 3 { 61 | t.Fail() 62 | } 63 | } 64 | 65 | func TestKotlinFiles(t *testing.T) { 66 | var comments Comments 67 | err := SearchDir("testdata/kotlin", func(comment *Comment) { 68 | comments = append(comments, comment) 69 | }) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | if len(comments) != 2 { 75 | t.Fail() 76 | } 77 | } 78 | 79 | func TestJuliaFiles(t *testing.T) { 80 | var comments Comments 81 | err := SearchDir("testdata/julia", func(comment *Comment) { 82 | comments = append(comments, comment) 83 | }) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | if len(comments) != 3 { 89 | t.Fail() 90 | } 91 | } 92 | 93 | func TestElixirFiles(t *testing.T) { 94 | var comments Comments 95 | err := SearchDir("testdata/elixir", func(comment *Comment) { 96 | comments = append(comments, comment) 97 | }) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | if len(comments) != 2 { 103 | t.Fail() 104 | } 105 | } 106 | 107 | func TestHaskellFiles(t *testing.T) { 108 | var comments Comments 109 | err := SearchDir("testdata/haskell", func(comment *Comment) { 110 | comments = append(comments, comment) 111 | }) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | 116 | if len(comments) != 2 { 117 | t.Fail() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /pkg/comments/languages.go: -------------------------------------------------------------------------------- 1 | package comments 2 | 3 | import "github.com/augmentable-dev/lege" 4 | 5 | // CStyleCommentOptions ... 6 | var CStyleCommentOptions *lege.ParseOptions = &lege.ParseOptions{ 7 | Boundaries: []lege.Boundary{ 8 | { 9 | Start: "//", 10 | End: "\n", 11 | }, 12 | { 13 | Start: "/*", 14 | End: "*/", 15 | }, 16 | }, 17 | } 18 | 19 | // HashStyleCommentOptions ... 20 | var HashStyleCommentOptions *lege.ParseOptions = &lege.ParseOptions{ 21 | Boundaries: []lege.Boundary{ 22 | { 23 | Start: "#", 24 | End: "\n", 25 | }, 26 | }, 27 | } 28 | 29 | // LispStyleCommentOptions .. 30 | var LispStyleCommentOptions *lege.ParseOptions = &lege.ParseOptions{ 31 | Boundaries: []lege.Boundary{ 32 | { 33 | Start: ";", 34 | End: "\n", 35 | }, 36 | }, 37 | } 38 | 39 | // Language is a source language (i.e. "Go") 40 | type Language string 41 | 42 | // LanguageParseOptions keeps track of source languages and their corresponding comment options 43 | var LanguageParseOptions map[Language]*lege.ParseOptions = map[Language]*lege.ParseOptions{ 44 | "C": CStyleCommentOptions, 45 | "C#": CStyleCommentOptions, 46 | "C++": CStyleCommentOptions, 47 | "Common Lisp": LispStyleCommentOptions, 48 | "Emacs Lisp": LispStyleCommentOptions, 49 | "Go": CStyleCommentOptions, 50 | "Groovy": CStyleCommentOptions, 51 | "Haskell": {Boundaries: []lege.Boundary{{Start: "--", End: "\n"}, {Start: "{-", End: "-}"}}}, 52 | "Java": CStyleCommentOptions, 53 | "JavaScript": CStyleCommentOptions, 54 | "Objective-C": CStyleCommentOptions, 55 | "PHP": {Boundaries: append(CStyleCommentOptions.Boundaries, HashStyleCommentOptions.Boundaries...)}, 56 | "Python": HashStyleCommentOptions, 57 | "R": HashStyleCommentOptions, 58 | "Ruby": HashStyleCommentOptions, 59 | "Shell": HashStyleCommentOptions, 60 | "Swift": CStyleCommentOptions, 61 | "TypeScript": CStyleCommentOptions, 62 | "Visual Basic": {Boundaries: []lege.Boundary{{Start: "'", End: "\n"}}}, 63 | // TODO Currently, the underlying pkg that does the parsing/plucking (lege) doesn't properly support precedance 64 | // so lines beginning with /// or //! will be picked up by this start // and include a / or ! preceding the comment 65 | "Kotlin": CStyleCommentOptions, 66 | "Rust": {Boundaries: []lege.Boundary{{Start: "///", End: "\n"}, {Start: "//!", End: "\n"}, {Start: "//", End: "\n"}}}, 67 | 68 | // TODO unfortunately, lege does't seem to handle the below boundaries very well, similar issue as to above I believe. Something with precendance? 69 | // Multi-line comments are not getting picked up... 70 | "Elixir": HashStyleCommentOptions, 71 | "Julia": {Boundaries: []lege.Boundary{{Start: "#=", End: "=#"}, {Start: "#", End: "\n"}}}, 72 | } 73 | 74 | -------------------------------------------------------------------------------- /pkg/comments/testdata/elixir/simple.exs: -------------------------------------------------------------------------------- 1 | 2 | # this is a comment 3 | # this is another 4 | IO.puts "Hello world from Elixir" 5 | -------------------------------------------------------------------------------- /pkg/comments/testdata/haskell/hello_world.hs: -------------------------------------------------------------------------------- 1 | {- 2 | This is a test multiline comment 3 | -} 4 | module Main where 5 | 6 | -- Test single line comment 7 | main :: IO () 8 | main = putStrLn "Hello World" 9 | -------------------------------------------------------------------------------- /pkg/comments/testdata/javascript/hello_world.js: -------------------------------------------------------------------------------- 1 | // this is a js comment 2 | 3 | function helloWorld() { 4 | return "hello world" 5 | } 6 | 7 | /* this is a block comment 8 | that spans multiple lines 9 | */ 10 | -------------------------------------------------------------------------------- /pkg/comments/testdata/javascript/node_modules/ignored.js: -------------------------------------------------------------------------------- 1 | // the comments in this file should be ignored, since it's in a "vendor" path 2 | 3 | -------------------------------------------------------------------------------- /pkg/comments/testdata/javascript/subdir/index.js: -------------------------------------------------------------------------------- 1 | // this comment should be found 2 | 3 | -------------------------------------------------------------------------------- /pkg/comments/testdata/julia/example.jl: -------------------------------------------------------------------------------- 1 | 2 | # function to calculate the volume of a sphere 3 | function sphere_vol(r) 4 | # julia allows Unicode names (in UTF-8 encoding) 5 | # so either "pi" or the symbol π can be used 6 | 7 | return 4/3*pi*r^3 8 | end 9 | -------------------------------------------------------------------------------- /pkg/comments/testdata/kotlin/hello_world.kt: -------------------------------------------------------------------------------- 1 | // this is a comment 2 | fun main() { 3 | println("Hello world!") 4 | /* 5 | Here's another comment 6 | */ 7 | } 8 | -------------------------------------------------------------------------------- /pkg/comments/testdata/lisp/hello_world.lisp: -------------------------------------------------------------------------------- 1 | ; Comment 2 | (+ 1 1) -------------------------------------------------------------------------------- /pkg/comments/testdata/php/test.php: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /pkg/comments/testdata/rust/comments.rs: -------------------------------------------------------------------------------- 1 | // Line comments are anything after ‘//’ and extend to the end of the line. 2 | 3 | let x = 5; // This is also a line comment. 4 | 5 | // If you have a long explanation for something, you can put line comments next 6 | // to each other. Put a space between the // and your comment so that it’s 7 | // more readable. 8 | 9 | 10 | /// Adds one to the number given. 11 | /// 12 | /// # Examples 13 | /// 14 | /// ``` 15 | /// let five = 5; 16 | /// 17 | /// assert_eq!(6, add_one(5)); 18 | /// # fn add_one(x: i32) -> i32 { 19 | /// # x + 1 20 | /// # } 21 | /// ``` 22 | fn add_one(x: i32) -> i32 { 23 | x + 1 24 | } 25 | 26 | 27 | //! # The Rust Standard Library 28 | //! 29 | //! The Rust Standard Library provides the essential runtime 30 | //! functionality for building portable Rust software. 31 | 32 | -------------------------------------------------------------------------------- /pkg/todos/report.go: -------------------------------------------------------------------------------- 1 | package todos 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | "text/template" 7 | ) 8 | 9 | const defaultTemplate = ` 10 | {{- range $index, $todo := . }} 11 | {{ .String }} 12 | => {{ .Comment.FilePath }}:{{ .Comment.StartLocation.Line }}:{{ .Comment.StartLocation.Pos }} 13 | {{- if .Blame }} 14 | => added {{ .TimeAgo }} by {{ .Blame.Author }} in {{ .Blame.SHA }} 15 | {{- end }} 16 | {{ else }} 17 | no todos 🎉 18 | {{- end }} 19 | {{ len . }} TODOs Found 📝 20 | ` 21 | 22 | // WriteTodos renders a report of todos 23 | func WriteTodos(todos ToDos, writer io.Writer) error { 24 | 25 | t, err := template.New("todos").Parse(defaultTemplate) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | // replace the phrase in the todo string with a "highlighted" version for console output 31 | // TODO eventually make this configurable, for NO_COLOR output (or customization of color?) 32 | for _, todo := range todos { 33 | todo.String = strings.Replace(todo.String, todo.Phrase, "\u001b[33m"+todo.Phrase+"\u001b[0m", 1) 34 | } 35 | 36 | err = t.Execute(writer, todos) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/todos/todos.go: -------------------------------------------------------------------------------- 1 | package todos 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/augmentable-dev/tickgit/pkg/blame" 8 | "github.com/augmentable-dev/tickgit/pkg/comments" 9 | "github.com/dustin/go-humanize" 10 | ) 11 | 12 | // ToDo represents a ToDo item 13 | type ToDo struct { 14 | comments.Comment 15 | String string 16 | Phrase string 17 | Blame *blame.Blame 18 | } 19 | 20 | // ToDos represents a list of ToDo items 21 | type ToDos []*ToDo 22 | 23 | // TimeAgo returns a human readable string indicating the time since the todo was added 24 | func (t *ToDo) TimeAgo() string { 25 | if t.Blame == nil { 26 | return "" 27 | } 28 | return humanize.Time(t.Blame.Author.When) 29 | } 30 | 31 | // NewToDo produces a pointer to a ToDo from a comment 32 | func NewToDo(comment comments.Comment) *ToDo { 33 | // FIXME this should be configurable and probably NOT hardcoded here 34 | // in fact, this list might be too expansive for a sensible default 35 | startingMatchPhrases := []string{"TODO", "FIXME", "OPTIMIZE", "HACK", "XXX", "WTF", "LEGACY"} 36 | var matchPhrases []string 37 | for _, phrase := range startingMatchPhrases { 38 | // populates matchPhrases with the contents of startingMatchPhrases plus the @+lowerCase version of each phrase 39 | matchPhrases = append(matchPhrases, phrase, "@"+strings.ToLower(phrase)) 40 | } 41 | 42 | for _, phrase := range matchPhrases { 43 | s := comment.String() 44 | if strings.Contains(s, phrase) { 45 | todo := ToDo{ 46 | Comment: comment, 47 | String: strings.Trim(s, " "), 48 | Phrase: phrase, 49 | } 50 | return &todo 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // NewToDos produces a list of ToDos from a list of comments 58 | func NewToDos(comments comments.Comments) ToDos { 59 | todos := make(ToDos, 0) 60 | for _, comment := range comments { 61 | todo := NewToDo(*comment) 62 | if todo != nil { 63 | todos = append(todos, todo) 64 | } 65 | } 66 | return todos 67 | } 68 | 69 | // Len returns the number of todos 70 | func (t ToDos) Len() int { 71 | return len(t) 72 | } 73 | 74 | // Less compares two todos by their creation time 75 | func (t ToDos) Less(i, j int) bool { 76 | first := t[i] 77 | second := t[j] 78 | if first.Blame == nil || second.Blame == nil { 79 | return false 80 | } 81 | return first.Blame.Author.When.Before(second.Blame.Author.When) 82 | } 83 | 84 | // Swap swaps two todos 85 | func (t ToDos) Swap(i, j int) { 86 | temp := t[i] 87 | t[i] = t[j] 88 | t[j] = temp 89 | } 90 | 91 | // CountWithCommits returns the number of todos with an associated commit (in which that todo was added) 92 | func (t ToDos) CountWithCommits() (count int) { 93 | for _, todo := range t { 94 | if todo.Blame != nil { 95 | count++ 96 | } 97 | } 98 | return count 99 | } 100 | 101 | // FindBlame sets the blame information on each todo in a set of todos 102 | func (t *ToDos) FindBlame(ctx context.Context, dir string) error { 103 | fileMap := make(map[string]ToDos) 104 | for _, todo := range *t { 105 | filePath := todo.FilePath 106 | if _, ok := fileMap[filePath]; !ok { 107 | fileMap[filePath] = make(ToDos, 0) 108 | } 109 | fileMap[filePath] = append(fileMap[filePath], todo) 110 | } 111 | 112 | for filePath, todos := range fileMap { 113 | lines := make([]int, 0) 114 | 115 | for _, todo := range todos { 116 | lines = append(lines, todo.StartLocation.Line) 117 | } 118 | blames, err := blame.Exec(ctx, filePath, &blame.Options{ 119 | Directory: dir, 120 | Lines: lines, 121 | }) 122 | if err != nil { 123 | // TODO (patrickdevivo) report this error 124 | continue 125 | } 126 | for line, blame := range blames { 127 | for _, todo := range todos { 128 | if todo.StartLocation.Line == line { 129 | b := blame 130 | todo.Blame = &b 131 | } 132 | } 133 | } 134 | } 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /pkg/todos/todos_test.go: -------------------------------------------------------------------------------- 1 | package todos 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/augmentable-dev/lege" 7 | "github.com/augmentable-dev/tickgit/pkg/comments" 8 | ) 9 | 10 | func TestNewToDoNil(t *testing.T) { 11 | collection := lege.NewCollection(lege.Location{}, lege.Location{}, lege.Boundary{}, "Hello World") 12 | comment := comments.Comment{ 13 | Collection: *collection, 14 | } 15 | todo := NewToDo(comment) 16 | 17 | if todo != nil { 18 | t.Fatalf("did not expect a TODO, got: %v", todo) 19 | } 20 | } 21 | 22 | func TestNewToDo(t *testing.T) { 23 | collection := lege.NewCollection(lege.Location{}, lege.Location{}, lege.Boundary{}, "TODO Hello World") 24 | comment := comments.Comment{ 25 | Collection: *collection, 26 | } 27 | todo := NewToDo(comment) 28 | 29 | if todo == nil { 30 | t.Fatalf("expected a TODO, got: %v", todo) 31 | } 32 | 33 | if todo.Phrase != "TODO" { 34 | t.Fatalf("expected matched phrase to be TODO, got: %s", todo.Phrase) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /testdata/repos/repo-001/commit-001/test.tickgit: -------------------------------------------------------------------------------- 1 | goal "Goal 1" { 2 | task "Step 1" { 3 | status = "done" 4 | description = "rejigger the what's eehoosit" 5 | } 6 | 7 | task "Step 2" { 8 | status = "pending" 9 | } 10 | 11 | task "Step 3" { 12 | status = "pending" 13 | } 14 | } 15 | 16 | goal "Goal 2" { 17 | task "Step 1" { 18 | status = "done" 19 | } 20 | 21 | task "Step 2" { 22 | status = "done" 23 | } 24 | 25 | task "Step 3" { 26 | status = "done" 27 | } 28 | } --------------------------------------------------------------------------------