-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from mpittkin/first-version
Initial version
- Loading branch information
Showing
9 changed files
with
377 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"github.com/mpittkin/go-todo/output" | ||
"github.com/mpittkin/go-todo/todo" | ||
"io/fs" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
) | ||
|
||
type config struct { | ||
RootPath string | ||
OutputType string | ||
SlackWebhookURL string | ||
RepoTitle string | ||
} | ||
|
||
const ( | ||
outTypeConsole = "console" | ||
outTypeSlackWebhook = "slack-webhook" | ||
) | ||
|
||
const ( | ||
defaultRootPath = "." | ||
defaultOutputType = "console" | ||
defaultSlackWebhookURL = "" | ||
) | ||
|
||
func main() { | ||
cfg := config{ | ||
RootPath: defaultRootPath, | ||
OutputType: defaultOutputType, | ||
SlackWebhookURL: defaultSlackWebhookURL, | ||
} | ||
if r := os.Getenv("GOTODO_ROOT_PATH"); r != "" { | ||
cfg.RootPath = r | ||
} | ||
if o := os.Getenv("GOTODO_OUTPUT_TYPE"); o != "" { | ||
cfg.OutputType = o | ||
} | ||
if u := os.Getenv("GOTODO_SLACK_URL"); u != "" { | ||
cfg.SlackWebhookURL = u | ||
} | ||
cfg.RepoTitle = os.Getenv("GOTODO_REPO_TITLE") | ||
|
||
pathFlag := flag.String("root-path", "", "the root path from which directories will be traversed looking for files to parse") | ||
outputFlag := flag.String("output-type", "", "the output type (console, json, or slack-webhook") | ||
webhookFlag := flag.String("slack-webhook-url", "", "when output type is set to 'slack-webhook' defines the url to send the POST request") | ||
titleFlag := flag.String("repo-title", "", "when output type is set to slack-webhook, this is included in the report to indicate to the reader the source of the todos") | ||
|
||
flag.Parse() | ||
if *pathFlag != "" { | ||
cfg.RootPath = *pathFlag | ||
} | ||
if *outputFlag != "" { | ||
cfg.OutputType = *outputFlag | ||
} | ||
if *webhookFlag != "" { | ||
cfg.SlackWebhookURL = *webhookFlag | ||
} | ||
if *titleFlag != "" { | ||
cfg.SlackWebhookURL = *titleFlag | ||
} | ||
|
||
rootPath := "." | ||
if len(os.Args) > 1 { | ||
rootPath = os.Args[1] | ||
if err := os.Chdir(rootPath); err != nil { | ||
log.Fatalf("Unable to change directory to %s\n", rootPath) | ||
} | ||
} | ||
|
||
absPath, err := filepath.Abs(rootPath) | ||
if err != nil { | ||
log.Fatalf("Unable to convert path '%s' to absolute path: %s", rootPath, err) | ||
} | ||
fmt.Println("Starting at " + absPath) | ||
|
||
var result []todo.Todo | ||
|
||
// Walk through the files and process all .go files | ||
err = fs.WalkDir(os.DirFS(absPath), ".", func(path string, d fs.DirEntry, err error) error { | ||
// Skip walking .git because it contains so many small files it slows down the program substantially | ||
if d.IsDir() { | ||
if d.Name() == ".git" { | ||
return fs.SkipDir | ||
} | ||
return nil | ||
} | ||
|
||
ext := filepath.Ext(path) | ||
if ext != ".go" { | ||
return nil | ||
} | ||
|
||
todos, err := todo.ParseGo(path) | ||
if err != nil { | ||
log.Fatalf("parse %s: %s", path, err) | ||
} | ||
|
||
result = append(result, todos...) | ||
|
||
return nil | ||
}) | ||
if err != nil { | ||
log.Fatalf("walk path %s: %s", absPath, err) | ||
} | ||
|
||
switch cfg.OutputType { | ||
case outTypeConsole: | ||
output.ToConsole(result) | ||
case outTypeSlackWebhook: | ||
if err := output.ToSlackWebhook(result, cfg.SlackWebhookURL, cfg.RepoTitle); err != nil { | ||
log.Fatalf("error posting result to slack webhook: %s", err) | ||
} | ||
fmt.Println("Todo report sent successfully to Slack") | ||
default: | ||
log.Printf("invalid output type %s\n", cfg.OutputType) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package output | ||
|
||
import ( | ||
"fmt" | ||
"github.com/mpittkin/go-todo/todo" | ||
) | ||
|
||
func ToConsole(todos []todo.Todo) { | ||
for _, todo := range todos { | ||
fmt.Println(todo) | ||
} | ||
|
||
fmt.Printf("Found %d todos\n", len(todos)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package output | ||
|
||
import ( | ||
"fmt" | ||
"github.com/mpittkin/go-todo/todo" | ||
"io" | ||
"log" | ||
"net/http" | ||
"sort" | ||
"strings" | ||
) | ||
|
||
type AuthorTodos struct { | ||
AuthorMail string | ||
Todos []todo.Todo | ||
} | ||
|
||
func ToSlackWebhook(todos []todo.Todo, webhookUrl string, repo string) error { | ||
byAuthor := make(map[string][]todo.Todo) | ||
for _, td := range todos { | ||
authorTodos := byAuthor[td.Mail] | ||
authorTodos = append(authorTodos, td) | ||
byAuthor[td.Mail] = authorTodos | ||
} | ||
|
||
var byAuthorSl []AuthorTodos | ||
for authorMail, authorTodos := range byAuthor { | ||
byAuthorSl = append(byAuthorSl, AuthorTodos{ | ||
AuthorMail: authorMail, | ||
Todos: authorTodos, | ||
}) | ||
} | ||
|
||
sort.Slice(byAuthorSl, func(i, j int) bool { | ||
return byAuthorSl[i].AuthorMail < byAuthorSl[j].AuthorMail | ||
}) | ||
|
||
message := fmt.Sprintf(` | ||
Todo Report: %s | ||
Total Todos: %d\n | ||
`, repo, len(todos)) | ||
|
||
for _, auth := range byAuthorSl { | ||
authorBlock := fmt.Sprintf(` | ||
*%s* | ||
`, auth.AuthorMail) | ||
for _, td := range auth.Todos { | ||
|
||
authorBlock += fmt.Sprintf("%s:%d (%v) `%s`\\n\n", td.Path, td.Line, td.Time.Format("2006-01-02"), td.Text) | ||
} | ||
|
||
message += authorBlock | ||
} | ||
|
||
body := fmt.Sprintf(`{ "text": "%s"}`, message) | ||
|
||
if err := PostToWebhook(webhookUrl, strings.NewReader(body)); err != nil { | ||
return fmt.Errorf("post to slack webhook %s: %w", webhookUrl, err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func PostToWebhook(url string, body io.Reader) error { | ||
req, err := http.NewRequest(http.MethodPost, url, body) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
resp, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { | ||
err := resp.Body.Close() | ||
if err != nil { | ||
log.Printf("close response body: %s", err) | ||
} | ||
}() | ||
if resp.StatusCode < 200 || resp.StatusCode >= 300 { | ||
return fmt.Errorf("http response error %s", resp.Status) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package todo | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"os/exec" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const ( | ||
pfxAuthorName = "author " | ||
pfxAuthorMail = "author-mail " | ||
pfxTime = "author-time " | ||
) | ||
|
||
func Blame(path string, lineNo int) (BlameInfo, error) { | ||
var blameInfo BlameInfo | ||
lineArg := fmt.Sprintf("-L %d,%d", lineNo, lineNo) | ||
args := []string{ | ||
"blame", | ||
"--porcelain", | ||
"--incremental", | ||
lineArg, | ||
path, | ||
} | ||
|
||
cmd := exec.Command("git", args...) | ||
|
||
stdOut, err := cmd.StdoutPipe() | ||
if err != nil { | ||
return blameInfo, err | ||
} | ||
if err := cmd.Start(); err != nil { | ||
return blameInfo, err | ||
} | ||
|
||
scanner := bufio.NewScanner(bufio.NewReader(stdOut)) | ||
for scanner.Scan() { | ||
line := scanner.Text() | ||
switch { | ||
case strings.HasPrefix(line, pfxAuthorMail): | ||
value := strings.TrimPrefix(line, pfxAuthorMail) | ||
blameInfo.Mail = strings.Trim(value, "<>") | ||
case strings.HasPrefix(line, pfxAuthorName): | ||
blameInfo.Name = strings.TrimPrefix(line, pfxAuthorName) | ||
case strings.HasPrefix(line, pfxTime): | ||
value := strings.TrimPrefix(line, pfxTime) | ||
unixTime, err := strconv.ParseInt(value, 10, 64) | ||
if err != nil { | ||
return blameInfo, err | ||
} | ||
blameInfo.Time = time.Unix(unixTime, 0) | ||
} | ||
} | ||
return blameInfo, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package todo | ||
|
||
import "time" | ||
|
||
type Todo struct { | ||
Path string | ||
Line int | ||
Text string | ||
BlameInfo | ||
} | ||
|
||
type BlameInfo struct { | ||
Name string | ||
Mail string | ||
Time time.Time | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package todo | ||
|
||
import ( | ||
"go/parser" | ||
"go/token" | ||
"log" | ||
) | ||
|
||
func ParseGo(path string) ([]Todo, error) { | ||
fset := token.NewFileSet() | ||
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var todos []Todo | ||
for _, c := range f.Comments { | ||
for _, sc := range c.List { | ||
isTodo := todoLineMatcher.MatchString(sc.Text) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
if isTodo { | ||
pos := fset.Position(sc.Pos()) | ||
todos = append(todos, Todo{ | ||
Path: path, | ||
Line: pos.Line, | ||
Text: sc.Text, | ||
}) | ||
} | ||
} | ||
} | ||
|
||
for i := 0; i < len(todos); i++ { | ||
blame, err := Blame(path, todos[i].Line) | ||
if err != nil { | ||
return nil, err | ||
} | ||
todos[i].BlameInfo = blame | ||
} | ||
|
||
return todos, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package todo | ||
|
||
import "regexp" | ||
|
||
//Match any line that contains the word t-o-d-o, case-insensitive | ||
|
||
const todoLineRegex = "(?i)\\btodo\\b" | ||
|
||
var todoLineMatcher = regexp.MustCompile(todoLineRegex) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package todo | ||
|
||
import ( | ||
"regexp" | ||
"testing" | ||
) | ||
|
||
func TestMatch(t *testing.T) { | ||
var isTodo = regexp.MustCompile(todoLineRegex) | ||
|
||
tests := []struct { | ||
str string | ||
wantMatch bool | ||
}{ | ||
{"todo: do stuff", true}, | ||
{"TODO: do stuff", true}, | ||
{"ToDo - do stuff", true}, | ||
{"we should todo later", true}, | ||
{"look at the rest of our todos", false}, | ||
{"regular old normal comment line", false}, | ||
} | ||
|
||
for _, tt := range tests { | ||
gotMatch := isTodo.MatchString(tt.str) | ||
if gotMatch != tt.wantMatch { | ||
t.Errorf("\"%s\" wanted match %t got %t", tt.str, tt.wantMatch, gotMatch) | ||
} | ||
} | ||
} |