├── .gitignore ├── .travis.yml ├── examples ├── config.sample.yml ├── users │ └── main.go └── issues │ └── main.go ├── README.md ├── users.go └── jira.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | examples/config.yml 3 | .idea 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 1.1 3 | install: 4 | - go get github.com/stretchr/testify/assert 5 | script: 6 | - go test -v -------------------------------------------------------------------------------- /examples/config.sample.yml: -------------------------------------------------------------------------------- 1 | host: https://jira.domain.com 2 | api_path: /rest/api/2 3 | activity_path: /activity 4 | login: LOGIN 5 | password: PASSWORD 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-jira-client 2 | ============== 3 | 4 | go-jira-client is a simple client written in golang to consume jira API... and Tokyo. 5 | 6 | [![Build Status](https://travis-ci.org/plouc/go-jira-client.png?branch=master)](https://travis-ci.org/plouc/go-jira-client) 7 | -------------------------------------------------------------------------------- /examples/users/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/plouc/go-jira-client" 7 | "io/ioutil" 8 | "launchpad.net/goyaml" 9 | "os" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type Config struct { 15 | Host string `yaml:"host"` 16 | ApiPath string `yaml:"api_path"` 17 | ActivityPath string `yaml:"activity_path"` 18 | Login string `yaml:"login"` 19 | Password string `yaml:"password"` 20 | } 21 | 22 | func main() { 23 | startedAt := time.Now() 24 | defer func() { 25 | fmt.Printf("processed in %v\n", time.Now().Sub(startedAt)) 26 | }() 27 | 28 | help := flag.Bool("help", false, "Show usage") 29 | 30 | // read config file 31 | file, e := ioutil.ReadFile("../config.yml") 32 | if e != nil { 33 | fmt.Printf("Config file error: %v\n", e) 34 | os.Exit(1) 35 | } 36 | 37 | // parse config file 38 | config := new(Config) 39 | err := goyaml.Unmarshal(file, &config) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | jira := gojira.NewJira( 45 | config.Host, 46 | config.ApiPath, 47 | config.ActivityPath, 48 | &gojira.Auth{config.Login, config.Password}, 49 | ) 50 | 51 | var method string 52 | flag.StringVar(&method, "m", "", "Specify method to retrieve user(s) data, available methods:\n"+ 53 | " > -m user -u USERNAME") 54 | 55 | var username string 56 | flag.StringVar(&username, "u", "", "Specify username") 57 | 58 | flag.Usage = func() { 59 | fmt.Printf("Usage:\n") 60 | flag.PrintDefaults() 61 | } 62 | flag.Parse() 63 | 64 | if *help == true || method == "" { 65 | flag.Usage() 66 | return 67 | } 68 | 69 | switch method { 70 | case "user": 71 | if username == "" { 72 | flag.Usage() 73 | return 74 | } 75 | 76 | user, err := jira.User(username) 77 | if err != nil { 78 | fmt.Println(err.Error()) 79 | return 80 | } 81 | 82 | format := "> %-14s: %s\n" 83 | 84 | fmt.Printf("%s\n", user.Name) 85 | fmt.Printf(format, "self", user.Self) 86 | fmt.Printf(format, "email address", user.EmailAddress) 87 | fmt.Printf(format, "display name", user.DisplayName) 88 | fmt.Printf(format, "active", strconv.FormatBool(user.Active)) 89 | fmt.Printf(format, "time zone", user.TimeZone) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /examples/issues/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/plouc/go-jira-client" 7 | "io/ioutil" 8 | "launchpad.net/goyaml" 9 | "os" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type Config struct { 15 | Host string `yaml:"host"` 16 | ApiPath string `yaml:"api_path"` 17 | ActivityPath string `yaml:"activity_path"` 18 | Login string `yaml:"login"` 19 | Password string `yaml:"password"` 20 | } 21 | 22 | func main() { 23 | startedAt := time.Now() 24 | defer func() { 25 | fmt.Printf("processed in %v\n", time.Now().Sub(startedAt)) 26 | }() 27 | 28 | help := flag.Bool("help", false, "Show usage") 29 | 30 | // read config file 31 | file, e := ioutil.ReadFile("../config.yml") 32 | if e != nil { 33 | fmt.Printf("Config file error: %v\n", e) 34 | os.Exit(1) 35 | } 36 | 37 | // parse config file 38 | config := new(Config) 39 | err := goyaml.Unmarshal(file, &config) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | jira := gojira.NewJira( 45 | config.Host, 46 | config.ApiPath, 47 | config.ActivityPath, 48 | &gojira.Auth{config.Login, config.Password}, 49 | ) 50 | 51 | var method string 52 | flag.StringVar(&method, "m", "", "Specify method to retrieve issue(s) data, available methods:\n"+ 53 | " > -m issue -id ISSUE_ID") 54 | 55 | var id string 56 | flag.StringVar(&id, "id", "", "Specify issue id") 57 | 58 | flag.Usage = func() { 59 | fmt.Printf("Usage:\n") 60 | flag.PrintDefaults() 61 | } 62 | flag.Parse() 63 | 64 | if *help == true || method == "" { 65 | flag.Usage() 66 | return 67 | } 68 | 69 | switch method { 70 | case "issue": 71 | if id == "" { 72 | flag.Usage() 73 | return 74 | } 75 | 76 | issue := jira.Issue(id) 77 | 78 | format := "> %-14s: %s\n" 79 | 80 | fmt.Printf("%s\n", issue.Id) 81 | fmt.Printf(format, "self", issue.Self) 82 | fmt.Printf(format, "key", issue.Key) 83 | fmt.Printf(format, "expand", issue.Expand) 84 | fields := issue.Fields 85 | fmt.Printf(format, "summary", fields.Summary) 86 | fmt.Printf(format, "reporter", fields.Reporter.Name) 87 | fmt.Printf(format, "assignee", fields.Assignee.Name) 88 | fmt.Printf(format, "is subtask?", strconv.FormatBool(fields.IssueType.Subtask)) 89 | //fmt.Printf(format, "created at", issue.CreatedAt) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /users.go: -------------------------------------------------------------------------------- 1 | package gojira 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | user_url = "/user" 10 | user_search_url = "/user/search" 11 | // http://example.com:8080/jira/rest/api/2/user/assignable/multiProjectSearch [GET] 12 | // http://example.com:8080/jira/rest/api/2/user/assignable/search [GET] 13 | // http://example.com:8080/jira/rest/api/2/user/avatar [POST, PUT] 14 | // http://example.com:8080/jira/rest/api/2/user/avatar/temporary [POST, POST] 15 | // http://example.com:8080/jira/rest/api/2/user/avatar/{id} [DELETE] 16 | // http://example.com:8080/jira/rest/api/2/user/avatars [GET] 17 | // http://example.com:8080/jira/rest/api/2/user/picker [GET] 18 | // http://example.com:8080/jira/rest/api/2/user/viewissue/search [GET] 19 | ) 20 | 21 | type User struct { 22 | Self string `json:"self"` 23 | Name string `json:"name"` 24 | EmailAddress string `json:"emailAddress"` 25 | DisplayName string `json:"displayName"` 26 | Active bool `json:"active"` 27 | TimeZone string `json:"timeZone"` 28 | AvatarUrls map[string]string `json:"avatarUrls"` 29 | Expand string `json:"expand"` 30 | // "groups": { 31 | // "size": 3, 32 | // "items": [ 33 | // { 34 | // "name": "jira-user", 35 | // "self": "http://www.example.com/jira/rest/api/2/group?groupname=jira-user" 36 | // }, 37 | // { 38 | // "name": "jira-admin", 39 | // "self": "http://www.example.com/jira/rest/api/2/group?groupname=jira-admin" 40 | // }, 41 | // { 42 | // "name": "important", 43 | // "self": "http://www.example.com/jira/rest/api/2/group?groupname=important" 44 | // } 45 | // ] 46 | // } 47 | } 48 | 49 | /* 50 | Returns a user. This resource cannot be accessed anonymously. 51 | 52 | GET http://example.com:8080/jira/rest/api/2/user?username=USERNAME 53 | 54 | Parameters 55 | 56 | username string The username 57 | 58 | Usage 59 | 60 | user, err := jira.User("username") 61 | if err != nil { 62 | fmt.Println(err.Error()) 63 | } 64 | fmt.Printf("%+v\n", user) 65 | */ 66 | func (j *Jira) User(username string) (*User, error) { 67 | url := j.BaseUrl + j.ApiPath + user_url + "?username=" + username 68 | contents := j.buildAndExecRequest("GET", url) 69 | 70 | user := new(User) 71 | err := json.Unmarshal(contents, &user) 72 | if err != nil { 73 | fmt.Println("%s", err) 74 | } 75 | 76 | return user, err 77 | } 78 | 79 | /* 80 | Returns a list of users that match the search string. This resource cannot be accessed anonymously. 81 | 82 | GET http://example.com:8080/jira/rest/api/2/user/search 83 | 84 | Parameters 85 | 86 | username string A query string used to search username, name or e-mail address 87 | startAt int The index of the first user to return (0-based) 88 | maxResults int The maximum number of users to return (defaults to 50). 89 | The maximum allowed value is 1000. 90 | If you specify a value that is higher than this number, 91 | your search results will be truncated. 92 | includeActive boolean If true, then active users are included in the results (default true) 93 | includeInactive boolean If true, then inactive users are included in the results (default false) 94 | 95 | */ 96 | func (j *Jira) SearchUser(username string, startAt int, maxResults int, includeActive bool, includeInactive bool) { 97 | url := j.BaseUrl + j.ApiPath + user_url + "?username=" + username 98 | contents := j.buildAndExecRequest("GET", url) 99 | fmt.Println(string(contents)) 100 | 101 | // @todo 102 | } 103 | -------------------------------------------------------------------------------- /jira.go: -------------------------------------------------------------------------------- 1 | package gojira 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "fmt" 7 | "io/ioutil" 8 | "math" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | type Jira struct { 16 | BaseUrl string 17 | ApiPath string 18 | ActivityPath string 19 | Client *http.Client 20 | Auth *Auth 21 | } 22 | 23 | type Auth struct { 24 | Login string 25 | Password string 26 | } 27 | 28 | type Pagination struct { 29 | Total int 30 | StartAt int 31 | MaxResults int 32 | Page int 33 | PageCount int 34 | Pages []int 35 | } 36 | 37 | func (p *Pagination) Compute() { 38 | p.PageCount = int(math.Ceil(float64(p.Total) / float64(p.MaxResults))) 39 | p.Page = int(math.Ceil(float64(p.StartAt) / float64(p.MaxResults))) 40 | 41 | p.Pages = make([]int, p.PageCount) 42 | for i := range p.Pages { 43 | p.Pages[i] = i 44 | } 45 | } 46 | 47 | type Issue struct { 48 | Id string 49 | Key string 50 | Self string 51 | Expand string 52 | Fields *IssueFields 53 | CreatedAt time.Time 54 | } 55 | 56 | type IssueList struct { 57 | Expand string 58 | StartAt int 59 | MaxResults int 60 | Total int 61 | Issues []*Issue 62 | Pagination *Pagination 63 | } 64 | 65 | type IssueFields struct { 66 | IssueType *IssueType 67 | Summary string 68 | Description string 69 | Reporter *User 70 | Assignee *User 71 | Project *JiraProject 72 | Created string 73 | Status *IssueStatus 74 | } 75 | 76 | type IssueType struct { 77 | Self string 78 | Id string 79 | Description string 80 | IconUrl string 81 | Name string 82 | Subtask bool 83 | } 84 | 85 | type IssueStatus struct { 86 | Name string 87 | } 88 | 89 | type JiraProject struct { 90 | Self string 91 | Id string 92 | Key string 93 | Name string 94 | AvatarUrls map[string]string 95 | } 96 | 97 | type ActivityItem struct { 98 | Title string `xml:"title"json:"title"` 99 | Id string `xml:"id"json:"id"` 100 | Link []Link `xml:"link"json:"link"` 101 | Updated time.Time `xml:"updated"json:"updated"` 102 | Author Person `xml:"author"json:"author"` 103 | Summary Text `xml:"summary"json:"summary"` 104 | Category Category `xml:"category"json:"category"` 105 | } 106 | 107 | type ActivityFeed struct { 108 | XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"json:"xml_name"` 109 | Title string `xml:"title"json:"title"` 110 | Id string `xml:"id"json:"id"` 111 | Link []Link `xml:"link"json:"link"` 112 | Updated time.Time `xml:"updated,attr"json:"updated"` 113 | Author Person `xml:"author"json:"author"` 114 | Entries []*ActivityItem `xml:"entry"json:"entries"` 115 | } 116 | 117 | type Category struct { 118 | Term string `xml:"term,attr"json:"term"` 119 | } 120 | 121 | type Link struct { 122 | Rel string `xml:"rel,attr,omitempty"json:"rel"` 123 | Href string `xml:"href,attr"json:"href"` 124 | } 125 | 126 | type Person struct { 127 | Name string `xml:"name"json:"name"` 128 | URI string `xml:"uri"json:"uri"` 129 | Email string `xml:"email"json:"email"` 130 | InnerXML string `xml:",innerxml"json:"inner_xml"` 131 | } 132 | 133 | type Text struct { 134 | Type string `xml:"type,attr,omitempty"json:"type"` 135 | Body string `xml:",chardata"json:"body"` 136 | } 137 | 138 | func NewJira(baseUrl string, apiPath string, activityPath string, auth *Auth) *Jira { 139 | 140 | client := &http.Client{} 141 | 142 | return &Jira{ 143 | BaseUrl: baseUrl, 144 | ApiPath: apiPath, 145 | ActivityPath: activityPath, 146 | Client: client, 147 | Auth: auth, 148 | } 149 | } 150 | 151 | const ( 152 | dateLayout = "2006-01-02T15:04:05.000-0700" 153 | ) 154 | 155 | func (j *Jira) buildAndExecRequest(method string, url string) []byte { 156 | 157 | req, err := http.NewRequest(method, url, nil) 158 | if err != nil { 159 | panic("Error while building jira request") 160 | } 161 | req.SetBasicAuth(j.Auth.Login, j.Auth.Password) 162 | 163 | resp, err := j.Client.Do(req) 164 | defer resp.Body.Close() 165 | contents, err := ioutil.ReadAll(resp.Body) 166 | if err != nil { 167 | fmt.Printf("%s", err) 168 | } 169 | 170 | return contents 171 | } 172 | 173 | func (j *Jira) UserActivity(user string) (ActivityFeed, error) { 174 | url := j.BaseUrl + j.ActivityPath + "?streams=" + url.QueryEscape("user IS "+user) 175 | 176 | return j.Activity(url) 177 | } 178 | 179 | func (j *Jira) Activity(url string) (ActivityFeed, error) { 180 | 181 | contents := j.buildAndExecRequest("GET", url) 182 | 183 | var activity ActivityFeed 184 | err := xml.Unmarshal(contents, &activity) 185 | if err != nil { 186 | fmt.Println("%s", err) 187 | } 188 | 189 | return activity, err 190 | } 191 | 192 | // search issues assigned to given user 193 | func (j *Jira) IssuesAssignedTo(user string, maxResults int, startAt int) IssueList { 194 | url := j.BaseUrl + j.ApiPath + "/search?jql=assignee=\"" + url.QueryEscape(user) + "\"&startAt=" + strconv.Itoa(startAt) + "&maxResults=" + strconv.Itoa(maxResults) 195 | return j.queryToIssueList(url) 196 | } 197 | 198 | func (j *Jira) IssuesByRawJQL(jql string) IssueList { 199 | url := j.BaseUrl + j.ApiPath + "/search?jql=" + url.QueryEscape(jql) 200 | return j.queryToIssueList(url) 201 | } 202 | 203 | func (j *Jira) queryToIssueList(url string) IssueList { 204 | contents := j.buildAndExecRequest("GET", url) 205 | 206 | var issues IssueList 207 | err := json.Unmarshal(contents, &issues) 208 | if err != nil { 209 | fmt.Println("%s", err) 210 | } 211 | 212 | for _, issue := range issues.Issues { 213 | t, _ := time.Parse(dateLayout, issue.Fields.Created) 214 | issue.CreatedAt = t 215 | } 216 | 217 | pagination := Pagination{ 218 | Total: issues.Total, 219 | StartAt: issues.StartAt, 220 | MaxResults: issues.MaxResults, 221 | } 222 | pagination.Compute() 223 | 224 | issues.Pagination = &pagination 225 | 226 | return issues 227 | } 228 | 229 | // search an issue by its id 230 | func (j *Jira) Issue(id string) Issue { 231 | 232 | url := j.BaseUrl + j.ApiPath + "/issue/" + id 233 | contents := j.buildAndExecRequest("GET", url) 234 | 235 | var issue Issue 236 | err := json.Unmarshal(contents, &issue) 237 | if err != nil { 238 | fmt.Println("%s", err) 239 | } 240 | 241 | return issue 242 | } 243 | --------------------------------------------------------------------------------