grim/youtrack-import

Parents
Children 7b898931bf51
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 @@
+syntax: glob
+*.zip
+youtrack-import
+
--- /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 @@
+package bitbucket
+
+import (
+ "archive/zip"
+ "encoding/json"
+ "fmt"
+)
+
+func loadArchive(archive string) (*Archive, error) {
+ r, err := zip.OpenReader(archive)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+
+ for _, f := range r.File {
+ if f.Name == "db-2.0.json" {
+ rc, err := f.Open()
+ if err != nil {
+ return nil, err
+ }
+ defer rc.Close()
+
+ archive := &Archive{}
+ if err := json.NewDecoder(rc).Decode(archive); err != nil {
+ return nil, err
+ }
+
+ return archive, 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 @@
+package bitbucket
+
+import (
+ "fmt"
+ "net/http"
+
+ "hg.sr.ht/~grim/youtrack-import/globals"
+ "hg.sr.ht/~grim/youtrack-import/youtrack"
+)
+
+type Cmd struct {
+ 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)
+ if err != nil {
+ return err
+ }
+
+ client, err := youtrack.NewClient(g.URL, g.Token)
+ if err != nil {
+ return err
+ }
+
+ fmt.Printf("Loaded %d issues\n", len(archive.Issues))
+
+ for _, issue := range archive.Issues {
+ vals := issue.Encode()
+
+ vals.Set("project", c.Project)
+
+ resp, err := client.Put("/issue", vals)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode != http.StatusCreated {
+ return fmt.Errorf("expected %d got %d", http.StatusCreated, resp.StatusCode)
+ }
+ }
+
+ fmt.Printf("%#v\n", client)
+
+ return nil
+}
--- /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 @@
+package bitbucket
+
+import (
+ "net/url"
+ "time"
+)
+
+type Author struct {
+ DisplayName string `json:"display_name"`
+ AccountID string `json:"account_id"`
+}
+
+type Issue struct {
+ 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"`
+ ID int `json:"id"`
+ 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 {
+ vals := 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)
+ }
+
+ if i.Component != "" {
+ vals.Set("subsystem", i.Component)
+ }
+
+ if i.Content != "" {
+ vals.Set("description", i.Content)
+ }
+
+ if i.Title != "" {
+ vals.Set("summary", i.Title)
+ }
+
+ return vals
+}
+
+type Comment struct {
+ Content string `json:"content"`
+ CreatedOn time.Time `json:"created_on"`
+ ID int `json:"id"`
+ 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"`
+}
+
+type Log struct {
+ 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"`
+}
+
+type Meta struct {
+ 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"`
+}
+
+type Component struct {
+ Name string `json:"name"`
+}
+
+type Milestone struct {
+ Name string `json:"name"`
+}
+
+type Version struct {
+ Name string `json:"name"`
+}
+
+type Archive struct {
+ 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 @@
+package globals
+
+type Globals struct {
+ 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
+
+go 1.13
+
+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 @@
+package main
+
+import (
+ "github.com/alecthomas/kong"
+
+ "hg.sr.ht/~grim/youtrack-import/bitbucket"
+ "hg.sr.ht/~grim/youtrack-import/globals"
+)
+
+type cli struct {
+ globals.Globals
+
+ Bitbucket bitbucket.Cmd `kong:"cmd,help='Import a Bitbucket Cloud issue archive'"`
+}
+
+func main() {
+ cli := cli{}
+ ctx := kong.Parse(
+ &cli,
+ kong.Name("youtrack-import"),
+ kong.Description("A tool to import issues into YouTrack"),
+ kong.UsageOnError(),
+ )
+
+ err := ctx.Run(&cli.Globals)
+ ctx.FatalIfErrorf(err)
+}
--- /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 @@
+package youtrack
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "strings"
+)
+
+type Client struct {
+ client *http.Client
+ uri string
+}
+
+func NewClient(uri, token string) (*Client, error) {
+ parts, err := url.Parse(uri)
+ if err != nil {
+ return nil, err
+ }
+
+ if filepath.Base(parts.Path) != "rest" {
+ parts.Path = filepath.Join(parts.Path, "rest")
+ }
+
+ return &Client{
+ client: &http.Client{
+ Transport: NewTransport(token),
+ },
+ uri: parts.String(),
+ }, nil
+}
+
+func (c *Client) Put(uri string, vals url.Values) (*http.Response, error) {
+ raw := vals.Encode()
+ body := strings.NewReader(raw)
+
+ req, err := http.NewRequest(http.MethodPut, c.uri+uri, body)
+ if err != nil {
+ return nil, err
+ }
+
+ 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 @@
+package youtrack
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+)
+
+func (c *Client) CreateIssue(vals url.Values) error {
+ resp, err := c.Put("/issue", vals)
+ if err != nil {
+ return err
+ }
+
+ if resp.StatusCode != http.StatusCreated {
+ return fmt.Errorf("unexpected status code %d", resp.StatusCode)
+ }
+
+ return nil
+}
--- /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 @@
+package youtrack
+
+import (
+ "fmt"
+ "net/http"
+)
+
+type Transport struct {
+ Transport http.RoundTripper
+ token string
+}
+
+func NewTransport(token string) *Transport {
+ return &Transport{
+ Transport: http.DefaultTransport,
+ token: token,
+ }
+}
+
+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)
+}