grim/youtrack-import
Initial import. Bitbucket does a basic but imcomplete ticket import
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,4 @@
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bitbucket/archive.go Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,34 @@
+func loadArchive(archive string) (*Archive, error) { + r, err := zip.OpenReader(archive) + for _, f := range r.File { + if f.Name == "db-2.0.json" { + if err := json.NewDecoder(rc).Decode(archive); err != nil { + return nil, fmt.Errorf("failed to find db-2.0.json in the archive") --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bitbucket/cmd.go Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,48 @@
+ "hg.sr.ht/~grim/youtrack-import/globals" + "hg.sr.ht/~grim/youtrack-import/youtrack" + Archive string `kong:"arg,name='archive',help='The zip file containing the archive'"` + Project string `kong:"arg,name='project',help='The name of the project'"` + ProjectID string `kong:"flag,name='project-id',help='The ID of the proejct'"` +func (c *Cmd) Run(g *globals.Globals) error { + archive, err := loadArchive(c.Archive) + client, err := youtrack.NewClient(g.URL, g.Token) + fmt.Printf("Loaded %d issues\n", len(archive.Issues)) + for _, issue := range archive.Issues { + vals.Set("project", c.Project) + resp, err := client.Put("/issue", vals) + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("expected %d got %d", http.StatusCreated, resp.StatusCode) + fmt.Printf("%#v\n", client) --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bitbucket/types.go Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,111 @@
+ DisplayName string `json:"display_name"` + AccountID string `json:"account_id"` + Assignee Author `json:"assignee"` + Component string `json:"component"` + Content string `json:"content"` + ContentUpdatedOn time.Time `json:"content_updated_on"` + CreatedOn time.Time `json:"created_on"` + EditedOn time.Time `json:"edited_on"` + Kind string `json:"kind"` + Milestone string `json:"milestone"` + Priority string `json:"priority"` + Reporter Author `json:"reporter"` + Status string `json:"status"` + Title string `json:"title"` + UpdatedOn time.Time `json:"updated_on"` + Version string `json:"version"` + Watchers []Author `json:"watchers"` + Voters []Author `json:"voters"` +func (i *Issue) Encode() url.Values { + // we check each value before adding it because we want to omit blanks + if i.Assignee.DisplayName != "" { + vals.Set("assignee", i.Assignee.DisplayName) + vals.Set("subsystem", i.Component) + vals.Set("description", i.Content) + vals.Set("summary", i.Title) + Content string `json:"content"` + CreatedOn time.Time `json:"created_on"` + Issue int `json:"issue"` + UpdatedOn time.Time `json:"updated_on"` + User Author `json:"user"` +type Attachment struct { + Filename string `json:"filename"` + Issue int `json:"issue"` + Path string `json:"path"` + User Author `json:"user"` + ChangedFrom string `json:"changed_from"` + ChangedTo string `json:"changed_to"` + Comment int `json:"comment"` + CreatedOn time.Time `json:"created_on"` + Field string `json:"field"` + Issue int `json:"issue"` + User Author `json:"user"` + DefaultAssignee Author `json:"default_assignee"` + DefaultComponent string `json:"default_component"` + DefaultKind string `json:"default_kind"` + DefaultMilestone string `json:"default_milestone"` + DefaultVersion string `json:"default_version"` + Name string `json:"name"` + Name string `json:"name"` + Name string `json:"name"` + Issues []Issue `json:"issues"` + Comments []Comment `json:"comments"` + Attachments []Attachment `json:"attachments"` + Logs []Log `json:"logs"` + Meta Meta `json:"meta"` + Components []Component `json:"components"` + Milestones []Milestone `json:"milestones"` + Versions []Version `json:"versions"` --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/globals/globals.go Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,6 @@
+ URL string `kong:"flag,name='url',required=True,help='The URL to the YouTrack server',env='YOUTRACK_URL'"` + Token string `kong:"flag,name='token',required=True,help='The token used to authenticate to the YouTrack server',env='YOUTRACK_TOKEN'"` --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/go.mod Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,5 @@
+module hg.sr.ht/~grim/youtrack-import +require github.com/alecthomas/kong v0.2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/go.sum Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,8 @@
+github.com/alecthomas/kong v0.2.1 h1:V1tLBhyQBC4rsbXbcOvm3GBaytJSwRNX69fp1WJxbqQ= +github.com/alecthomas/kong v0.2.1/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/main.go Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,27 @@
+ "github.com/alecthomas/kong" + "hg.sr.ht/~grim/youtrack-import/bitbucket" + "hg.sr.ht/~grim/youtrack-import/globals" + Bitbucket bitbucket.Cmd `kong:"cmd,help='Import a Bitbucket Cloud issue archive'"` + kong.Name("youtrack-import"), + kong.Description("A tool to import issues into YouTrack"), + err := ctx.Run(&cli.Globals) --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/youtrack/client.go Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,47 @@
+func NewClient(uri, token string) (*Client, error) { + parts, err := url.Parse(uri) + if filepath.Base(parts.Path) != "rest" { + parts.Path = filepath.Join(parts.Path, "rest") + Transport: NewTransport(token), +func (c *Client) Put(uri string, vals url.Values) (*http.Response, error) { + body := strings.NewReader(raw) + req, err := http.NewRequest(http.MethodPut, c.uri+uri, body) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(raw))) + return c.client.Do(req) --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/youtrack/issues.go Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,20 @@
+func (c *Client) CreateIssue(vals url.Values) error { + resp, err := c.Put("/issue", vals) + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("unexpected status code %d", resp.StatusCode) --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/youtrack/transport.go Wed Dec 11 14:22:12 2019 -0600
@@ -0,0 +1,24 @@
+ Transport http.RoundTripper +func NewTransport(token string) *Transport { + Transport: http.DefaultTransport, +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.token)) + return t.Transport.RoundTrip(req)