shithub: jirafs

Download patch

ref: 6b44a1578f84079dca6b0896926ca2134374e1f5
parent: 84844799a48e02168bd927b0b3c518b248ba0180
author: Kenny Levinsen <w@kl.wtf>
date: Mon Jun 6 20:08:54 EDT 2016

Direct status assignment

Whenever a status assignment is made, the workflow graph is now
generated, and the shortest path to the target status is found.

Unfortunately, this seems to be the only way to just assign a
status in JIRA, and even requires using private API's.

--- a/jira.go
+++ b/jira.go
@@ -274,7 +274,7 @@
 		switch file {
 		case "key":
 			return nil
-		case "status", "transitions":
+		case "transitions":
 			sf.Lock()
 			str := string(sf.Content)
 			sf.Unlock()
@@ -281,6 +281,52 @@
 			str = strings.Replace(str, "\n", "", -1)
 
 			return TransitionIssue(jc, issue.Key, str)
+
+		case "status":
+			sf.Lock()
+			str := string(sf.Content)
+			sf.Unlock()
+			str = strings.Replace(str, "\n", "", -1)
+
+			issue, err := GetIssue(jc, fmt.Sprintf("%s-%s", iw.project, iw.issueNo))
+			if err != nil {
+				log.Printf("Could not fetch issue: %v", err)
+				return err
+			}
+			if issue.Fields == nil {
+				log.Printf("Issue missing fields")
+				return errors.New("oops")
+			}
+			if issue.Fields.Status == nil {
+				log.Printf("Issue missing status")
+				return errors.New("oops2")
+			}
+
+			wg, err := BuildWorkflow2(jc, iw.project, issue.Fields.Type.ID)
+			if err != nil {
+				log.Printf("Could not build workflow: %v", err)
+				return err
+			}
+
+			p, err := wg.Path(issue.Fields.Status.Name, str, 10000)
+			if err != nil {
+				log.Printf("Could not find path: %v", err)
+				log.Printf("Workflow: \n%s\n", wg.Dump())
+				return err
+			}
+
+			log.Printf("Workflow path: %s", strings.Join(p, ", "))
+
+			for _, s := range p {
+				err = TransitionIssue(jc, issue.Key, s)
+				if err != nil {
+					log.Printf("Could not transition issue: %v", err)
+					return err
+				}
+			}
+
+			return nil
+
 		default:
 			sf.Lock()
 			str := string(sf.Content)
--- a/utils.go
+++ b/utils.go
@@ -228,7 +228,9 @@
 }
 
 type CommentResult struct {
-	Comments []jira.Comment `json:"comments,omitempty"`
+	Comments []struct {
+		ID string `json:"id"`
+	} `json:"comments,omitempty"`
 }
 
 func GetCommentsForIssue(jc *jira.Client, issue string) ([]string, error) {
--- /dev/null
+++ b/workflow.go
@@ -1,0 +1,279 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"net/url"
+
+	"github.com/andygrunwald/go-jira"
+)
+
+type thing struct {
+	Name string `json:"name"`
+}
+
+func BuildWorkflow1(jc *jira.Client, project, issueTypeNo string) (*WorkflowGraph, error) {
+	req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
+	if err != nil {
+		return nil, fmt.Errorf("could not query JIRA 1: %v", err)
+	}
+
+	var t thing
+	if _, err = jc.Do(req, &t); err != nil {
+		return nil, fmt.Errorf("could not query JIRA 2: %v", err)
+	}
+
+	req, err = jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/workflow?workflowName=%s", url.QueryEscape(t.Name)), nil)
+	if err != nil {
+		return nil, fmt.Errorf("could not query JIRA 3: %v", err)
+	}
+
+	var wr WorkflowResponse1
+	if _, err = jc.Do(req, &wr); err != nil {
+		return nil, fmt.Errorf("could not query JIRA 4: %v", err)
+	}
+
+	var wg WorkflowGraph
+	wg.Build1(&wr)
+	return &wg, nil
+}
+
+func BuildWorkflow2(jc *jira.Client, project, issueTypeNo string) (*WorkflowGraph, error) {
+	req, err := jc.NewRequest("GET", fmt.Sprintf("/rest/projectconfig/latest/issuetype/%s/%s/workflow", project, issueTypeNo), nil)
+	if err != nil {
+		return nil, fmt.Errorf("could not query JIRA 1: %v", err)
+	}
+
+	var t thing
+	if _, err = jc.Do(req, &t); err != nil {
+		return nil, fmt.Errorf("could not query JIRA 2: %v", err)
+	}
+
+	req, err = jc.NewRequest("GET", fmt.Sprintf("/rest/workflowDesigner/latest/workflows?name=%s", url.QueryEscape(t.Name)), nil)
+	if err != nil {
+		return nil, fmt.Errorf("could not query JIRA 3: %v", err)
+	}
+
+	req.Header.Set("X-Atlassian-Token", "nocheck")
+
+	var wr WorkflowResponse2
+	if _, err = jc.Do(req, &wr); err != nil {
+		return nil, fmt.Errorf("could not query JIRA 4: %v", err)
+	}
+
+	var wg WorkflowGraph
+	wg.Build2(&wr)
+	return &wg, nil
+}
+
+type WorkflowResponse2 struct {
+	Layout struct {
+		Statuses []struct {
+			ID           string `json:"statusId"`
+			TransitionID string `json:"id"`
+			Name         string `json:"name"`
+			Description  string `json:"description"`
+			Initial      bool   `json:"initial"`
+		} `json:"statuses"`
+		Transitions []struct {
+			Name        string `json:"name"`
+			Description string `json:"description"`
+			SourceID    string `json:"sourceId"`
+			TargetID    string `json:"targetId"`
+			ActionID    int    `json:"actionId"`
+			Global      bool   `json:"globalTransition"`
+			Looped      bool   `json:"loopedTransition"`
+		} `json:"transitions"`
+	} `json:"layout"`
+}
+
+type WorkflowResponse1 struct {
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	ID          int    `json:"id"`
+	DisplayName string `json:"displayName"`
+	Admin       bool   `json:"admin"`
+	Sources     []struct {
+		FromStatus WorkflowStatus `json:"fromStatus"`
+		Targets    []struct {
+			ToStatus       WorkflowStatus `json:"toStatus"`
+			TransitionName string
+		} `json:"targets"`
+	} `json:"sources"`
+}
+
+type WorkflowStatus struct {
+	StatusCategory struct {
+		Sequence       int      `json:"sequence"`
+		PrimaryAlias   string   `json:"primaryAlias"`
+		TranslatedName string   `json:"translatedName"`
+		ColorName      string   `json:"colorName"`
+		Aliases        []string `json:"aliases"`
+		Name           string   `json:"name"`
+		Key            string   `json:"key"`
+		ID             int      `json:"id"`
+	} `json:"statusCategory"`
+	IconURL     string `json:"iconUrl"`
+	Description string `json:"description"`
+	Name        string `json:"name"`
+	ID          string `json:"id"`
+}
+
+func (wf *WorkflowStatus) Status() *Status {
+	return &Status{
+		Name:        wf.Name,
+		ID:          wf.ID,
+		Description: wf.Description,
+	}
+}
+
+type Status struct {
+	Name        string
+	Description string
+	ID          string
+	Edges       []StatusEdge
+}
+
+type StatusEdge struct {
+	Name   string
+	Status *Status
+}
+
+type WorkflowGraph struct {
+	verteces map[string]*Status
+}
+
+func (wg *WorkflowGraph) Build2(wr *WorkflowResponse2) {
+	if wg.verteces == nil {
+		wg.verteces = make(map[string]*Status)
+	}
+
+	local := make(map[string]*Status)
+	layout := wr.Layout
+
+	for _, s := range layout.Statuses {
+		l := &Status{
+			Name:        s.Name,
+			Description: s.Description,
+			ID:          s.ID,
+		}
+
+		wg.verteces[s.Name] = l
+		local[s.TransitionID] = l
+	}
+
+	for _, t := range layout.Transitions {
+		a := local[t.SourceID]
+		b := local[t.TargetID]
+		edge := StatusEdge{
+			Name:   t.Name,
+			Status: b,
+		}
+		if t.Global {
+			for _, v := range local {
+				v.Edges = append(v.Edges, edge)
+			}
+		} else {
+			a.Edges = append(a.Edges, edge)
+		}
+	}
+}
+
+func (wg *WorkflowGraph) Build1(wr *WorkflowResponse1) {
+	if wg.verteces == nil {
+		wg.verteces = make(map[string]*Status)
+	}
+	for _, elem := range wr.Sources {
+		name := elem.FromStatus.Name
+		fromStatus, exists := wg.verteces[name]
+		if !exists {
+			fromStatus = elem.FromStatus.Status()
+			wg.verteces[fromStatus.Name] = fromStatus
+		}
+
+		for _, target := range elem.Targets {
+			targetName := target.ToStatus.Name
+			targetStatus, exists := wg.verteces[targetName]
+			if !exists {
+				targetStatus = target.ToStatus.Status()
+				wg.verteces[targetStatus.Name] = targetStatus
+			}
+			targetEdge := StatusEdge{
+				Name:   target.TransitionName,
+				Status: targetStatus,
+			}
+
+			fromStatus.Edges = append(fromStatus.Edges, targetEdge)
+		}
+	}
+}
+
+func (wg *WorkflowGraph) Dump() string {
+	var ss string
+	for _, v := range wg.verteces {
+		var s string
+		for _, e := range v.Edges {
+			s += fmt.Sprintf("%s (%s), ", e.Status.Name, e.Name)
+		}
+		ss += fmt.Sprintf("Status: %s, edges: %s\n", v.Name, s)
+	}
+
+	return ss
+}
+
+type path struct {
+	from *path
+	edge StatusEdge
+}
+
+func (wg *WorkflowGraph) Path(A, B string, limit int) ([]string, error) {
+	statusA := wg.verteces[A]
+	statusB := wg.verteces[B]
+
+	if statusA == nil || statusB == nil {
+		return nil, errors.New("no such status")
+	}
+
+	var search []path
+	for _, edge := range statusA.Edges {
+		search = append(search, path{edge: edge})
+	}
+
+	for len(search) > 0 {
+		limit--
+		if limit == 0 {
+			break
+		}
+		p := search[0]
+		search = search[1:]
+
+		// FOUND!
+		if p.edge.Status == statusB {
+			var s []string
+			start := &p
+
+			for {
+				s = append(s, start.edge.Name)
+				if start.from == nil {
+					break
+				}
+
+				start = start.from
+			}
+
+			for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
+				s[i], s[j] = s[j], s[i]
+			}
+
+			return s, nil
+		}
+
+		// Add the edges to the search.
+		for _, edge := range p.edge.Status.Edges {
+			// log.Printf("%s -> %s", p.edge.Status.Name, edge.Name)
+			search = append(search, path{from: &p, edge: edge})
+		}
+	}
+
+	return nil, errors.New("path not found")
+}