ref: 460d2d0fcf6cb75fabd5001bec5d0eb96dcc4458
parent: 4b410a5ffd714421418b90186dede34fae8e8f7e
author: Michael Misch <michaelmisch1985@gmail.com>
date: Fri Nov 29 12:07:49 PST 2019
Update parser for new format and write list and dot outputs
--- /dev/null
+++ b/dag.go
@@ -1,0 +1,28 @@
+package main
+
+import (
+ "github.com/goombaio/dag"
+)
+
+func dagFromLayout(l *Layout) *dag.DAG {
+ dg := dag.NewDAG()
+ dm := make(map[string]*dag.Vertex)
+ for _, job := range l.Jobs {
+ dm[job.Key] = dag.NewVertex(job.Key, job)
+ dg.AddVertex(dm[job.Key])
+ }
+ for _, job := range l.Jobs {
+ for _, req := range job.Requires {
+ for _, other := range l.Jobs {
+ if tagsMatch(other.Tags, job.Tags) {
+ continue
+ }
+ if !contains(other.Tags, req) {
+ continue
+ }
+ dg.AddEdge(dm[job.Key], dm[other.Key])
+ }
+ }
+ }
+ return dg
+}
--- a/generate.go
+++ b/generate.go
@@ -1,27 +1,35 @@
package main
-// WalkFunc through and search each file. We do want to early exit on an unsupported mime
-// We'll use a codified list of mappings for now, PR to update.
-// use http.DetectContentType(data) and send off to a go routine when it's a known type
-// Return on channel to run l.command on for the given types, run commands in main thread until done
-// In the end, we'll have a workable layout
+import "os"
type generator struct {
+ existing *Layout
}
-func newGenerator() *generator {
- return nil
-}
-
func (g *generator) dotTodoExists() bool {
- return false
+ if _, err := os.Stat(".todo"); err != nil {
+ return false
+ }
+ return true
}
-func (g *generator) parseTodo() {
-
+func (g *generator) parseTodo() error {
+ var err error
+ g.existing, err = layoutFromTodoFile()
+ if err != nil {
+ return err
+ }
+ return nil
}
+// WalkFunc through and search each file. We do want to early exit on an unsupported mime
+// We'll use a codified list of mappings for now, PR to update.
+// use http.DetectContentType(data) and send off to a go routine when it's a known type
+// Return on channel to run l.command on for the given types, run commands in main thread until done
+// In the end, we'll have a workable layout
// In our scrub function we need tags, so if there isn't one in a TODO entry, etc we will assume `[general]`
func (g *generator) toLayout() *Layout {
+ // Use g.existing as a starting point and walk through our file path, add anything we can find.
+ //go g.parseFile(filename)
return nil
}
--- a/layout.go
+++ b/layout.go
@@ -4,7 +4,6 @@
"bufio"
"errors"
"fmt"
- "log"
"os"
"regexp"
"strings"
@@ -17,6 +16,7 @@
// Job - A tagged collection of tasks
type Job struct {
+ Key string
Tags []string
Requires []string
Tasks []*Task
@@ -45,28 +45,18 @@
}
sc := bufio.NewScanner(fl)
for sc.Scan() {
- txt := sc.Text()
- if txt == "" {
+ if sc.Text() == "" {
continue
}
// Build out entries in a stepwise-fashion. A proper parser grammer would be better here
// but this is quick and assumes machine-written input
j := &Job{}
- j.Tags = parseTags(txt)
- j.Requires = parseRequires(txt)
- sc.Scan()
- t := &Task{}
- txt = sc.Text()
- if txt == "" {
- continue
- }
- if txt[:1] != "[" {
- t.Title = txt
- sc.Scan()
- }
- t.Entries = findEntries(sc)
- j.Tasks = append(j.Tasks, t)
+ j.Tags = parseTags(sc)
+ j.Key = strings.Join(j.Tags, ", ")
+ j.Requires = parseRequires(sc)
+ j.Tasks = parseTasks(sc)
l.Jobs = append(l.Jobs, j)
+
}
if err := sc.Err(); err != nil {
return nil, err
@@ -74,18 +64,38 @@
return l, nil
}
-func parseTags(txt string) []string {
+func parseTags(sc *bufio.Scanner) []string {
+ txt := sc.Text()
n := strings.Index(txt, ":")
return stringToTags(txt[:n])
}
-func parseRequires(txt string) []string {
- n := strings.Index(txt, ": releases ")
- if len(txt) > n+len(": releases [ ]") {
- return stringToTags(txt[n+len(": releases "):])
+func parseRequires(sc *bufio.Scanner) []string {
+ txt := sc.Text()
+ defer sc.Scan()
+ n := strings.Index(txt, ": requires ")
+ if len(txt) > n+len(": requires [ ]") {
+ return stringToTags(txt[n+len(": requires "):])
}
return nil
}
+
+func parseTasks(sc *bufio.Scanner) []*Task {
+ var tasks []*Task
+ for {
+ title := sc.Text()
+ if len(sc.Text()) == 0 {
+ return tasks
+ }
+ sc.Scan()
+ t := &Task{
+ Title: title,
+ Entries: findEntries(sc),
+ }
+ tasks = append(tasks, t)
+ }
+}
+
func findEntries(sc *bufio.Scanner) []*Entry {
en := []*Entry{}
for {
@@ -93,9 +103,6 @@
if len(d) < 4 {
return en
}
- if err := sc.Err(); err != nil {
- log.Fatal(err)
- }
switch d[:3] {
case "[ ]":
en = append(en, &Entry{
@@ -136,13 +143,25 @@
return true
}
+func isCompleted(job *Job) bool {
+ for _, task := range job.Tasks {
+ for _, entry := range task.Entries {
+ if !entry.Done {
+ return false
+ }
+ }
+ }
+ return true
+}
+
// This is adequate for our needs.
func stringToJobs(incoming string) (*Job, error) {
r := regexp.MustCompile(`([^\[\]]*)\s?(\[.*\])+\s?(.*)`)
matches := r.FindAllStringSubmatch(incoming, -1)
- if len(matches[0]) < 3 {
+ if matches != nil && len(matches[0]) < 3 {
return nil, errors.New("Could not parse string")
}
+
title := matches[0][1]
if len(matches[0]) == 4 {
title = fmt.Sprintf("%s%s", matches[0][1], matches[0][3])
@@ -181,10 +200,22 @@
tmp += string(b)
}
}
- if tags[0] == "" {
+ if tags == nil || tags[0] == "" {
return nil
}
return tags
+}
+
+func (l *Layout) removeCompleted() {
+ for i, job := range l.Jobs {
+ if !isCompleted(job) {
+ continue
+ }
+ if i < len(l.Jobs)-1 {
+ copy(l.Jobs[i:], l.Jobs[i+1:])
+ }
+ l.Jobs = l.Jobs[:len(l.Jobs)-1]
+ }
}
func (l *Layout) destroy(title string) error {
--- a/runners.go
+++ b/runners.go
@@ -10,8 +10,6 @@
if err != nil {
return err
}
- //d := dagFromLayout(l)
- //writeList(d)
writeList(l)
return nil
}
@@ -21,8 +19,6 @@
if err != nil {
return err
}
- //d := dagFromLayout(l)
- //writeListAll(d)
writeListAll(l)
return nil
}
@@ -78,9 +74,12 @@
// generate walks the file looking for a handful of known tokens
func generate(c *command) error {
- g := newGenerator()
+ g := &generator{}
if g.dotTodoExists() {
- g.parseTodo()
+ err := g.parseTodo()
+ if err != nil {
+ return err
+ }
}
l := g.toLayout()
writeTodo(l)
--- a/samples/.todo
+++ b/samples/.todo
@@ -1,25 +1,25 @@
-[#1]:
-TODO(halfwit) - Write all the code
+[1]:
+TODO(halfwit): Write all the code
[ ] general: Show what things look like when added manually
[ ] general: Show what auto generated looks like as well
-[#2]: requires [#1]
-TODO(halfwit) - remember how to write c code on normal systems
+[2]: requires [1]
+TODO(halfwit): remember how to write c code on normal systems
[ ] sample.c: write the stuff
[ ] sample.c: don't get hung up on the details
-[#3]: requires [#1]
-TODO(halfwit) - Make a demo of something interesting to demo `todo generate`
+[3]: requires [1] [2]
+TODO(halfwit): Make a demo of something interesting to demo todo generate
[ ] sample.go: write the code
[ ] sample.go: take the time
-[#4]: requires [#1] [example]
-TODO(halfwit) - Write some sample code
-[ ] sample.sh: get to the choppa
+[4]: requires [1] [example] [2] [3]
+TODO(halfwit): Write some sample code
+[x] sample.sh: get to the choppa
[ ] sample.sh: return
-INFO - Show adding multiple components to a single task from other source files
-[x] file2.sh: Add another todo entry for the same tag
[example]:
+I guess they shouldn't be optional
[x] general: Show how headers are optional
-[x] general: Show something silly
\ No newline at end of file
+[x] general: Show something silly
+
--- a/write.go
+++ b/write.go
@@ -1,16 +1,21 @@
package main
import (
+ "fmt"
"log"
"os"
+ "strings"
"text/template"
+
+ "github.com/goombaio/dag"
)
// Templates are so fun.
-const todoFile = `{{range .Jobs}}{{range $n, $t := .Tags}}{{if $n}} {{end}}[{{$t}}]{{end}}:{{ $length := len .Requires}}{{if gt $length 0}} requires{{range .Requires}} [{{.}}]{{end}}{{end}}
+const todoTmpl = `{{range .Jobs}}{{range $n, $t := .Tags}}{{if $n}} {{end}}[{{$t}}]{{end}}:{{ $length := len .Requires}}{{if gt $length 0}} requires{{range .Requires}} [{{.}}]{{end}}{{end}}
{{range .Tasks}}{{if .Title}}{{.Title}}
-{{end}}{{range .Entries}}{{if .Done}}[x] {{.Desc}}{{else}}[ ] {{.Desc}}{{end}}
-{{end}}
+{{end}}{{range .Entries}}{{if .Done}}[x] {{.Desc}}
+{{else}}[ ] {{.Desc}}
+{{end}}{{end}}
{{end}}{{end}}`
// Writer that creates our normal file
@@ -20,7 +25,7 @@
if err != nil {
log.Fatal(err)
}
- t := template.Must(template.New("todoFile").Parse(todoFile))
+ t := template.Must(template.New("todoTmpl").Parse(todoTmpl))
err = t.Execute(wr, l)
if err != nil {
log.Fatal(err)
@@ -29,15 +34,87 @@
// Writer that outputs in dot format
func writeDot(l *Layout) {
+ var sb strings.Builder
+ sb.WriteString("digraph depgraph {\n\trankdir=RL;\n")
+ // Labels
+ for _, job := range l.Jobs {
+ sb.WriteString(fmt.Sprintf("%v", job.Key))
+ sb.WriteString(` [label="`)
+ if len(job.Tasks) > 1 {
+ for _, task := range job.Tasks {
+ sb.WriteString(task.Title)
+ for _, entry := range task.Entries {
+ sb.WriteString("\n")
+ sb.WriteString(entry.Desc)
+ if entry.Done {
+ sb.WriteString(" ✓")
+ }
+ }
+ sb.WriteString("\n")
+ }
+ } else {
+ sb.WriteString(job.Tasks[0].Title)
+ for _, entry := range job.Tasks[0].Entries {
+ sb.WriteString("\n")
+ sb.WriteString(entry.Desc)
+ if entry.Done {
+ sb.WriteString(" ✓")
+ }
+ }
+ }
+ sb.WriteString(`"];`)
+ sb.WriteString("\r\n")
+ }
+ // Links
+ for _, job := range l.Jobs {
+ if len(job.Requires) == 0 {
+ continue
+ }
+ for _, req := range job.Requires {
+ sb.WriteString(fmt.Sprintf("%s -> %s;\n", job.Key, req))
+ }
+ }
+ sb.WriteString("}\n")
+ fmt.Println(sb.String())
}
// Writer that outputs only leaves
func writeList(l *Layout) {
-
+ l.removeCompleted()
+ d := dagFromLayout(l)
+ leaves := d.SinkVertices()
+ for _, leaf := range leaves {
+ job := leaf.Value.(*Job)
+ for _, t := range job.Tasks {
+ fmt.Printf("%v\t%s\n", job.Tags, t.Title)
+ }
+ }
}
// Writer that outputs all nodes
func writeListAll(l *Layout) {
-
+ d := dagFromLayout(l)
+ seen := map[*Job]bool{}
+ var walk func(*dag.Vertex)
+ walk = func(v *dag.Vertex) {
+ job := v.Value.(*Job)
+ if seen[job] {
+ return
+ }
+ seen[job] = true
+ children, err := d.Successors(v)
+ if err != nil {
+ log.Fatal(err)
+ }
+ for _, child := range children {
+ walk(child)
+ }
+ for _, t := range job.Tasks {
+ fmt.Printf("[%v]\t%s\n", job.Key, t.Title)
+ }
+ }
+ for _, t := range d.SourceVertices() {
+ walk(t)
+ }
}