grim/youtrack-import

75076ea0a1ee
Parents c7b965f00b18
Children fab48894ef61
Issues almost work, need to create a user map yet
--- a/bitbucket/converter.go Tue Dec 17 04:38:10 2019 -0600
+++ b/bitbucket/converter.go Mon Dec 30 20:17:16 2019 -0600
@@ -1,20 +1,18 @@
package bitbucket
import (
- "net/url"
-
"hg.sr.ht/~grim/youtrack-import/youtrack"
)
var (
- typeMap = map[string]string{
+ kindMap = map[string]string{
"bug": "Bug",
"enhancement": "Feature",
"proposal": "Feature",
"task": "Task",
}
- stateMap = map[string]string{
+ statusMap = map[string]string{
"new": "Submitted",
"open": "Open",
"resolved": "Fixed",
@@ -34,24 +32,36 @@
}
)
-func convertIssue(vals url.Values) {
- if val, found := vals["type"]; found {
- if replace, found := typeMap[val[0]]; found {
- vals["type"] = []string{replace}
- }
+func (a *Archive) convertIssue(bb Issue) *youtrack.Issue {
+ yt := &youtrack.Issue{
+ Number: bb.ID,
+ Summary: bb.Title,
+ Description: bb.Content,
+ Created: bb.CreatedOn,
+ Updated: bb.UpdatedOn,
+ Reporter: bb.Reporter.String(),
+
+ Priority: bb.Priority,
+ Type: bb.Kind,
+ State: bb.Status,
}
- if val, found := vals["state"]; found {
- if replace, found := stateMap[val[0]]; found {
- vals["state"] = []string{replace}
- }
+ // TODO: check if state is resovled then set the Resolved field to the
+ // UpdatedOn timestamp
+
+ // map field values from bitbucket to youtrack
+ if replace, found := priorityMap[yt.Priority]; found {
+ yt.Priority = replace
+ }
+ if replace, found := kindMap[yt.Type]; found {
+ yt.Type = replace
+ }
+ if replace, found := statusMap[yt.State]; found {
+ yt.State = replace
}
- if val, found := vals["priority"]; found {
- if replace, found := priorityMap[val[0]]; found {
- vals["priority"] = []string{replace}
- }
- }
+ return yt
+
}
func (a *Archive) convert() (*youtrack.Project, error) {
@@ -67,9 +77,16 @@
versions[i] = a.Versions[i].Name
}
+ // convert all of the issues
+ issues := make([]*youtrack.Issue, len(a.Issues))
+ for i := 0; i < len(a.Issues); i++ {
+ issues[i] = a.convertIssue(a.Issues[i])
+ }
+
project := &youtrack.Project{
Subsystems: components,
Versions: versions,
+ Issues: issues,
}
return project, nil
--- a/bitbucket/types.go Tue Dec 17 04:38:10 2019 -0600
+++ b/bitbucket/types.go Mon Dec 30 20:17:16 2019 -0600
@@ -9,6 +9,10 @@
AccountID string `json:"account_id"`
}
+func (a Author) String() string {
+ return a.DisplayName
+}
+
type Issue struct {
Assignee Author `json:"assignee"`
Component string `json:"component"`
--- a/youtrack/client.go Tue Dec 17 04:38:10 2019 -0600
+++ b/youtrack/client.go Mon Dec 30 20:17:16 2019 -0600
@@ -1,6 +1,8 @@
package youtrack
import (
+ "bytes"
+ "encoding/xml"
"fmt"
"io"
"io/ioutil"
@@ -32,15 +34,30 @@
}, nil
}
-func (c *Client) checkStatus(resp *http.Response, expected int) error {
- if resp.StatusCode != expected {
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return fmt.Errorf("%s", string(body))
+func (c *Client) checkStatuses(resp *http.Response, expected []int) error {
+ for _, acceptable := range expected {
+ if resp.StatusCode == acceptable {
+ return nil
}
}
- return nil
+ fmt.Printf("failed to find an acceptable status code\n")
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ xmlErr := xmlError{}
+ if err := xml.NewDecoder(bytes.NewReader(body)).Decode(&xmlErr); err != nil {
+ return err
+ }
+
+ return fmt.Errorf(xmlErr.Message)
+}
+
+func (c *Client) checkStatus(resp *http.Response, expected int) error {
+ return c.checkStatuses(resp, []int{expected})
}
func (c *Client) request(method, url string, body io.Reader) (*http.Response, error) {
@@ -51,3 +68,15 @@
return c.client.Do(req)
}
+
+func (c *Client) xmlRequest(method, url string, body io.Reader) (*http.Response, error) {
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", "application/xml")
+
+ return c.client.Do(req)
+
+}
--- a/youtrack/import.go Tue Dec 17 04:38:10 2019 -0600
+++ b/youtrack/import.go Mon Dec 30 20:17:16 2019 -0600
@@ -41,5 +41,10 @@
return err
}
+ // create the issues
+ if err := client.ImportIssues(p); err != nil {
+ return err
+ }
+
return nil
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/youtrack/issue.go Mon Dec 30 20:17:16 2019 -0600
@@ -0,0 +1,139 @@
+package youtrack
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+type Issue struct {
+ Number int `yt"numberInProject"`
+ Summary string `yt:"summary"`
+ Description string `yt:"description"`
+ Created time.Time `yt:"created"`
+ Updated time.Time `yt:"updated"`
+ UpdatedBy string `yt:"updaterName"`
+ Resolved time.Time `yt:"resolved"`
+ Reporter string `yt:"reporter"`
+ Voters []string `yt:"voterName"`
+ Watchers []string `yt:"watcherName"`
+ PermittedGroup string `yt:"permittedGroup"`
+
+ Priority string `yt:"priority"`
+ Type string `yt:"type"`
+ State string `yt:"state"`
+
+ Comments []Comment
+}
+
+func (i *Issue) encode() xmlIssue {
+ x := xmlIssue{}
+
+ // required fields
+ x.AddField("numberInProject", fmt.Sprintf("%d", i.Number))
+ x.AddField("summary", i.Summary)
+ x.AddField("created", fmt.Sprintf("%d", i.Created.Unix()))
+ x.AddField("reporterName", i.Reporter)
+
+ // optional fields
+ if i.Description != "" {
+ x.AddField("description", i.Description)
+ }
+
+ if !i.Updated.IsZero() {
+ x.AddField("updated", fmt.Sprintf("%d", i.Updated.Unix()))
+ }
+
+ if i.UpdatedBy != "" {
+ x.AddField("updaterName", i.UpdatedBy)
+ }
+
+ if !i.Resolved.IsZero() {
+ x.AddField("resolved", fmt.Sprintf("%d", i.Resolved.Unix()))
+ }
+
+ if len(i.Voters) > 0 {
+ x.AddFieldSlice("voterName", i.Voters)
+ }
+
+ if len(i.Watchers) > 0 {
+ x.AddFieldSlice("watcherName", i.Watchers)
+ }
+
+ if i.PermittedGroup != "" {
+ x.AddField("permittedGroup", i.PermittedGroup)
+ }
+
+ for _, c := range i.Comments {
+ x.AddComment(c.Author, c.Text, c.Created, c.Updated)
+ }
+
+ return x
+}
+
+func (c *Client) importIssuesRange(p *Project, s, e int) error {
+ issues := &xmlIssues{}
+
+ issues.Issues = make([]xmlIssue, e-s)
+
+ for i := s; i < e; i++ {
+ issues.Issues[i-s] = p.Issues[i].encode()
+ }
+
+ // convert the datastructure to an xml string
+ data, err := xml.Marshal(issues)
+ if err != nil {
+ return fmt.Errorf("wut: %v", err)
+ }
+
+ // create the reader for the put request
+ putBody := bytes.NewReader(data)
+
+ resp, err := c.xmlRequest(
+ http.MethodPut,
+ c.uri+"/import/"+p.ID+"/issues",
+ putBody,
+ )
+ if err != nil {
+ return fmt.Errorf("wtf %v", err)
+ }
+ defer resp.Body.Close()
+
+ if err := c.checkStatuses(resp, []int{http.StatusOK, http.StatusBadRequest}); err != nil {
+ return fmt.Errorf("failed to import issues: %v", err)
+ }
+
+ report := xmlImportReport{}
+ if err := xml.NewDecoder(resp.Body).Decode(&report); err != nil {
+ return fmt.Errorf("repo: %v", err)
+ }
+
+ fmt.Printf("%#v\n", report)
+
+ return nil
+}
+
+func (c *Client) ImportIssues(p *Project) error {
+ r := 10
+
+ for s := 0; s < len(p.Issues); s += r {
+ e := s + r
+ if e > len(p.Issues) {
+ e = len(p.Issues)
+ }
+
+ fmt.Printf("importing issues %d-%d ... ", s, e-1)
+
+ if err := c.importIssuesRange(p, s, e); err != nil {
+ fmt.Printf("failed.\n")
+
+ return err
+ }
+
+ fmt.Printf("done.\n")
+ }
+
+ return nil
+}
--- a/youtrack/ownedfield.go Tue Dec 17 04:38:10 2019 -0600
+++ b/youtrack/ownedfield.go Mon Dec 30 20:17:16 2019 -0600
@@ -51,7 +51,7 @@
defer resp.Body.Close()
- if err := c.checkStatus(resp, http.StatusOK); err != nil {
+ if err := c.checkStatus(resp, http.StatusCreated); err != nil {
return fmt.Errorf("failed to append %q to %q: %s", value, name, err)
}
--- a/youtrack/project.go Tue Dec 17 04:38:10 2019 -0600
+++ b/youtrack/project.go Mon Dec 30 20:17:16 2019 -0600
@@ -4,6 +4,17 @@
"net/http"
)
+type Project struct {
+ ID string
+ Name string
+ LeadLogin string
+
+ Subsystems []string
+ Versions []string
+
+ Issues []*Issue
+}
+
func (c *Client) CreateProject(p *Project) error {
req, err := http.NewRequest(
http.MethodPut,
--- a/youtrack/types.go Tue Dec 17 04:38:10 2019 -0600
+++ b/youtrack/types.go Mon Dec 30 20:17:16 2019 -0600
@@ -1,85 +1,12 @@
package youtrack
import (
- "fmt"
"time"
)
-type Project struct {
- ID string
- Name string
- LeadLogin string
-
- Subsystems []string
- Versions []string
-
- Issues []Issue
-}
-
type Comment struct {
Author string
Text string
Created time.Time
Updated time.Time
}
-
-type Issue struct {
- Number int
- Summary string
- Description string
- Created time.Time
- Updated time.Time
- UpdatedBy string
- Resolved time.Time
- Reporter string
- Voters []string
- Watchers []string
- PermittedGroup string
-
- Comments []Comment
-}
-
-func (i *Issue) encode() xmlIssue {
- x := xmlIssue{}
-
- // required fields
- x.AddField("numberInProject", fmt.Sprintf("%d", i.Number))
- x.AddField("summary", i.Summary)
- x.AddField("created", fmt.Sprintf("%d", i.Created.Unix()))
- x.AddField("reporterName", i.Reporter)
-
- // optional fields
- if i.Description != "" {
- x.AddField("description", i.Description)
- }
-
- if !i.Updated.IsZero() {
- x.AddField("updated", fmt.Sprintf("%d", i.Updated.Unix()))
- }
-
- if i.UpdatedBy != "" {
- x.AddField("updaterName", i.UpdatedBy)
- }
-
- if !i.Resolved.IsZero() {
- x.AddField("resolved", fmt.Sprintf("%d", i.Resolved.Unix()))
- }
-
- if len(i.Voters) > 0 {
- x.AddFieldSlice("voterName", i.Voters)
- }
-
- if len(i.Watchers) > 0 {
- x.AddFieldSlice("watcherName", i.Watchers)
- }
-
- if i.PermittedGroup != "" {
- x.AddField("permittedGroup", i.PermittedGroup)
- }
-
- for _, c := range i.Comments {
- x.AddComment(c.Author, c.Text, c.Created, c.Updated)
- }
-
- return x
-}
--- a/youtrack/version.go Tue Dec 17 04:38:10 2019 -0600
+++ b/youtrack/version.go Mon Dec 30 20:17:16 2019 -0600
@@ -67,7 +67,7 @@
defer resp.Body.Close()
- if err := c.checkStatus(resp, http.StatusOK); err != nil {
+ if err := c.checkStatus(resp, http.StatusCreated); err != nil {
return fmt.Errorf("failed to append %q to %q: %v", value, name, err)
}
--- a/youtrack/xml.go Tue Dec 17 04:38:10 2019 -0600
+++ b/youtrack/xml.go Mon Dec 30 20:17:16 2019 -0600
@@ -48,3 +48,25 @@
XMLName xml.Name `xml:"issues"`
Issues []xmlIssue
}
+
+type xmlImportReportItemError struct {
+ FieldName string `xml:"fieldName,attr"`
+ Value string `xml:"value,attr"`
+ Message string `xml:",cdata"`
+}
+
+type xmlImportReportItem struct {
+ Imported bool `xml:"imported,attr"`
+ ID string `xml:"id,attr"`
+ Errors []xmlImportReportItemError `xml:"error"`
+}
+
+type xmlImportReport struct {
+ XMLName xml.Name `xml:"importReport"`
+ Items []xmlImportReportItem `xml:"item"`
+}
+
+type xmlError struct {
+ XMLName xml.Name `xml:"error"`
+ Message string `xml:",cdata"`
+}