grim/youtrack-import
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 @@
"hg.sr.ht/~grim/youtrack-import/youtrack"
- typeMap = map[string]string{
+ kindMap = map[string]string{ "enhancement": "Feature",
- stateMap = map[string]string{
+ statusMap = map[string]string{ @@ -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 { + Description: bb.Content, + Reporter: bb.Reporter.String(), - 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 + // map field values from bitbucket to youtrack + if replace, found := priorityMap[yt.Priority]; found { + if replace, found := kindMap[yt.Type]; found { + if replace, found := statusMap[yt.State]; found { - if val, found := vals["priority"]; found {
- if replace, found := priorityMap[val[0]]; found {
- vals["priority"] = []string{replace}
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{
--- 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 { 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 @@
@@ -32,15 +34,30 @@
-func (c *Client) checkStatus(resp *http.Response, expected int) error {
- if resp.StatusCode != expected {
- body, err := ioutil.ReadAll(resp.Body)
- return fmt.Errorf("%s", string(body))
+func (c *Client) checkStatuses(resp *http.Response, expected []int) error { + for _, acceptable := range expected { + if resp.StatusCode == acceptable {
+ fmt.Printf("failed to find an acceptable status code\n") + body, err := ioutil.ReadAll(resp.Body) + if err := xml.NewDecoder(bytes.NewReader(body)).Decode(&xmlErr); err != nil { + 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 @@
+func (c *Client) xmlRequest(method, url string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, url, body) + 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 @@
+ if err := client.ImportIssues(p); err != 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 @@
+ 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"` +func (i *Issue) encode() xmlIssue { + 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) + if i.Description != "" { + x.AddField("description", i.Description) + if !i.Updated.IsZero() { + x.AddField("updated", fmt.Sprintf("%d", i.Updated.Unix())) + x.AddField("updaterName", i.UpdatedBy) + if !i.Resolved.IsZero() { + x.AddField("resolved", fmt.Sprintf("%d", i.Resolved.Unix())) + 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) +func (c *Client) importIssuesRange(p *Project, s, e int) error { + 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) + return fmt.Errorf("wut: %v", err) + // create the reader for the put request + putBody := bytes.NewReader(data) + resp, err := c.xmlRequest( + c.uri+"/import/"+p.ID+"/issues", + 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) +func (c *Client) ImportIssues(p *Project) error { + for s := 0; s < len(p.Issues); s += r { + fmt.Printf("importing issues %d-%d ... ", s, e-1) + if err := c.importIssuesRange(p, s, e); err != nil { + fmt.Printf("failed.\n") --- 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 @@
- 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 @@
func (c *Client) CreateProject(p *Project) error {
req, err := http.NewRequest(
--- 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 @@
-func (i *Issue) encode() xmlIssue {
- 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)
- if i.Description != "" {
- x.AddField("description", i.Description)
- if !i.Updated.IsZero() {
- x.AddField("updated", fmt.Sprintf("%d", i.Updated.Unix()))
- x.AddField("updaterName", i.UpdatedBy)
- if !i.Resolved.IsZero() {
- x.AddField("resolved", fmt.Sprintf("%d", i.Resolved.Unix()))
- 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)
--- 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 @@
- 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"`
+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"` + XMLName xml.Name `xml:"error"` + Message string `xml:",cdata"`