169 lines
3.4 KiB
Go
169 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/ankitpokhrel/jira-cli/pkg/jira"
|
|
"github.com/davecgh/go-spew/spew"
|
|
)
|
|
|
|
var (
|
|
server = flag.String("server", "", "server endpoint")
|
|
token = flag.String("token", "", "path to token")
|
|
debug = flag.Bool("debug", false, "debug")
|
|
epic = flag.String("epic", "", "epic to build graph of")
|
|
out = flag.String("out", "", "")
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
c := newClient(*server, *token)
|
|
|
|
if *out != "" {
|
|
ret, err := c.Me()
|
|
failOnError(err)
|
|
fmt.Printf("Hello, %s (%s)\n", ret.Login, ret.Email)
|
|
}
|
|
|
|
res, err := c.Structure()
|
|
spew.Config.DisableMethods = true
|
|
spew.Dump(res, err)
|
|
return
|
|
|
|
issues := epicIssues(c, *epic)
|
|
|
|
const height = 120
|
|
const width = 300
|
|
var canvas Canvas
|
|
|
|
sorted := topologicalSort(issues)
|
|
column := 0
|
|
row := 0
|
|
columns := make(map[string]int)
|
|
for _, issue := range sorted {
|
|
key := issue.Key
|
|
fmt.Fprintln(os.Stderr, "%s %s %s", issue.Key, issue.Fields.Watches)
|
|
|
|
for _, link := range issue.Fields.IssueLinks {
|
|
switch link.LinkType.Name {
|
|
case "Depends":
|
|
if link.OutwardIssue != nil {
|
|
from := link.OutwardIssue.Key
|
|
if _, ok := issues[from]; !ok {
|
|
continue
|
|
}
|
|
|
|
fromSide := "right"
|
|
to := issue.Key
|
|
toSide := "left"
|
|
|
|
col, ok := columns[from]
|
|
assert(ok)
|
|
|
|
if col == column {
|
|
column++
|
|
row = 0
|
|
}
|
|
edge := Edge{
|
|
ID: fmt.Sprintf("%s-%s", from, to),
|
|
FromNode: from,
|
|
FromSide: fromSide,
|
|
ToNode: to,
|
|
ToSide: toSide,
|
|
}
|
|
canvas.Edges = append(canvas.Edges, edge)
|
|
}
|
|
}
|
|
}
|
|
|
|
var node Node
|
|
node.ID = key
|
|
node.Type = Text
|
|
node.X = column * (width + 100)
|
|
node.Y = row * (height + 40)
|
|
node.Width = 300
|
|
node.Height = height
|
|
node.Text = fmt.Sprintf("[%s](%s/browse/%s): %s", issue.Key, *server, issue.Key, issue.Fields.Summary)
|
|
switch issue.Fields.Priority.Name {
|
|
case "Highest":
|
|
node.Color = Red
|
|
case "High":
|
|
node.Color = Orange
|
|
case "Medium":
|
|
// Medium priority is the most common, ignore coloring.
|
|
// node.Color = Yellow
|
|
case "Low":
|
|
node.Color = Cyan
|
|
}
|
|
|
|
canvas.Nodes = append(canvas.Nodes, node)
|
|
columns[key] = column
|
|
row++
|
|
}
|
|
|
|
data, err := json.Marshal(canvas)
|
|
failOnError(err)
|
|
|
|
fmt.Println(string(data))
|
|
}
|
|
|
|
func topologicalSort(issues map[string]*jira.Issue) []*jira.Issue {
|
|
type compactIssue struct {
|
|
key string
|
|
dependsOn []string
|
|
}
|
|
|
|
var list []compactIssue
|
|
for _, issue := range issues {
|
|
var ci compactIssue
|
|
ci.key = issue.Key
|
|
for _, link := range issue.Fields.IssueLinks {
|
|
switch link.LinkType.Name {
|
|
case "Depends":
|
|
if link.OutwardIssue != nil {
|
|
_, inEpic := issues[link.OutwardIssue.Key]
|
|
if inEpic {
|
|
ci.dependsOn = append(ci.dependsOn, link.OutwardIssue.Key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
list = append(list, ci)
|
|
}
|
|
|
|
// Kahn's algorithm.
|
|
set := make(map[string]int)
|
|
for i, ci := range list {
|
|
if len(ci.dependsOn) == 0 {
|
|
set[ci.key] = i
|
|
}
|
|
}
|
|
|
|
var sorted []*jira.Issue
|
|
for len(set) != 0 {
|
|
var key string
|
|
var index int
|
|
for key, index = range set {
|
|
break
|
|
}
|
|
delete(set, key)
|
|
|
|
sorted = append(sorted, issues[key])
|
|
n := list[index]
|
|
for _, m := range list {
|
|
for i, inward := range m.dependsOn {
|
|
if inward == n.key {
|
|
m.dependsOn = append(m.dependsOn[:i], m.dependsOn[i+1:]...)
|
|
if len(m.dependsOn) == 0 {
|
|
set[m.key] = i
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return sorted
|
|
}
|