commit 8d9dbdfc8a07bca44bcef1a030dc1e248a6ddeaf from: Oliver Lowe date: Sun Nov 03 22:21:57 2024 UTC add initial jira support commit - 72ce55ca9db6e1433ac2c235c5e72c3f2feab374 commit + 8d9dbdfc8a07bca44bcef1a030dc1e248a6ddeaf blob - /dev/null blob + be2c7243af97955bd988833827120bab346d0c2b (mode 644) --- /dev/null +++ jira/issue.json @@ -0,0 +1,204 @@ +{ + "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations", + "id": "10002", + "self": "http://www.example.com/jira/rest/api/2/issue/10002", + "key": "EX-1", + "names": { + "watcher": "watcher", + "attachment": "attachment", + "sub-tasks": "sub-tasks", + "description": "description", + "project": "project", + "comment": "comment", + "issuelinks": "issuelinks", + "worklog": "worklog", + "updated": "updated", + "timetracking": "timetracking" + }, + "schema": {}, + "fields": { + "watcher": { + "self": "http://www.example.com/jira/rest/api/2/issue/EX-1/watchers", + "isWatching": false, + "watchCount": 1, + "watchers": [ + { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "name": "fred", + "displayName": "Fred F. User", + "active": false + } + ] + }, + "attachment": [ + { + "self": "http://www.example.com/jira/rest/api/2.0/attachments/10000", + "filename": "picture.jpg", + "author": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "key": "fred", + "accountId": "99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", + "name": "fred", + "avatarUrls": { + "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred", + "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred", + "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred", + "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred" + }, + "displayName": "Fred F. User", + "active": false + }, + "created": "2017-03-08T03:29:43.525+0000", + "size": 23123, + "mimeType": "image/jpeg", + "content": "http://www.example.com/jira/attachments/10000", + "thumbnail": "http://www.example.com/jira/secure/thumbnail/10000" + } + ], + "sub-tasks": [ + { + "id": "10000", + "type": { + "id": "10000", + "name": "", + "inward": "Parent", + "outward": "Sub-task" + }, + "outwardIssue": { + "id": "10003", + "key": "EX-2", + "self": "http://www.example.com/jira/rest/api/2/issue/EX-2", + "fields": { + "status": { + "iconUrl": "http://www.example.com/jira//images/icons/statuses/open.png", + "name": "Open" + } + } + } + } + ], + "description": "example bug report", + "project": { + "self": "http://www.example.com/jira/rest/api/2/project/EX", + "id": "10000", + "key": "EX", + "name": "Example", + "avatarUrls": { + "48x48": "http://www.example.com/jira/secure/projectavatar?size=large&pid=10000", + "24x24": "http://www.example.com/jira/secure/projectavatar?size=small&pid=10000", + "16x16": "http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000", + "32x32": "http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000" + }, + "projectCategory": { + "self": "http://www.example.com/jira/rest/api/2/projectCategory/10000", + "id": "10000", + "name": "FIRST", + "description": "First Project Category" + } + }, + "comment": [ + { + "self": "http://www.example.com/jira/rest/api/2/issue/10010/comment/10000", + "id": "10000", + "author": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "name": "fred", + "displayName": "Fred F. User", + "active": false + }, + "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eget venenatis elit. Duis eu justo eget augue iaculis fermentum. Sed semper quam laoreet nisi egestas at posuere augue semper.", + "updateAuthor": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "name": "fred", + "displayName": "Fred F. User", + "active": false + }, + "created": "2017-03-08T03:29:43.383+0000", + "updated": "2017-03-08T03:29:43.383+0000", + "visibility": { + "type": "role", + "value": "Administrators" + } + } + ], + "issuelinks": [ + { + "id": "10001", + "type": { + "id": "10000", + "name": "Dependent", + "inward": "depends on", + "outward": "is depended by" + }, + "outwardIssue": { + "id": "10004L", + "key": "PRJ-2", + "self": "http://www.example.com/jira/rest/api/2/issue/PRJ-2", + "fields": { + "status": { + "iconUrl": "http://www.example.com/jira//images/icons/statuses/open.png", + "name": "Open" + } + } + } + }, + { + "id": "10002", + "type": { + "id": "10000", + "name": "Dependent", + "inward": "depends on", + "outward": "is depended by" + }, + "inwardIssue": { + "id": "10004", + "key": "PRJ-3", + "self": "http://www.example.com/jira/rest/api/2/issue/PRJ-3", + "fields": { + "status": { + "iconUrl": "http://www.example.com/jira//images/icons/statuses/open.png", + "name": "Open" + } + } + } + } + ], + "worklog": [ + { + "self": "http://www.example.com/jira/rest/api/2/issue/10010/worklog/10000", + "author": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "name": "fred", + "displayName": "Fred F. User", + "active": false + }, + "updateAuthor": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "name": "fred", + "displayName": "Fred F. User", + "active": false + }, + "comment": "I did some work here.", + "updated": "2017-03-08T03:29:43.684+0000", + "visibility": { + "type": "group", + "value": "jira-developers" + }, + "started": "2017-03-08T03:29:43.684+0000", + "timeSpent": "3h20m", + "timeSpentSeconds": 12000, + "id": "100028", + "issueId": "10002" + } + ], + "updated": 1, + "timetracking": { + "originalEstimate": "10m", + "remainingEstimate": "3m", + "timeSpent": "6m", + "originalEstimateSeconds": 600, + "remainingEstimateSeconds": 200, + "timeSpentSeconds": 400 + } + } +} blob - /dev/null blob + b83bcf3e4b98697cc1693190dc78ba336010b40a (mode 644) --- /dev/null +++ jira/jira.go @@ -0,0 +1,84 @@ +// +package main + +import ( + "encoding/json" + "fmt" + "time" +) + +type Issue struct { + ID string `json:"id"` // TODO(otl): int? + URL string `json:"self"` + Key string `json:"key"` + Description string + Project Project + Comments []Comment +} + +type Project struct { + ID string `json:"id"` // TODO(otl): int? + Name string `json:"name"` + URL string `json:"self"` +} + +type Comment struct { + URL string `json:"self"` + ID string `json:"id"` // TODO(otl): int? + Body string `json:"body"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Author User `json:"author"` + UpdateAuthor User `json:"updateAuthor"` +} + +func (c *Comment) UnmarshalJSON(b []byte) error { + type alias Comment + aux := &struct { + Created string `json:"created"` + Updated string `json:"updated"` + *alias + }{ + alias: (*alias)(c), + } + if err := json.Unmarshal(b, aux); err != nil { + return err + } + var err error + tstamp := "2006-01-02T15:04:05.999-0700" + c.Created, err = time.Parse(tstamp, aux.Created) + if err != nil { + return fmt.Errorf("parse created time: %w", err) + } + c.Updated, err = time.Parse(tstamp, aux.Updated) + if err != nil { + return fmt.Errorf("parse updated time: %w", err) + } + return nil +} + +type User struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` +} + +func (issue *Issue) UnmarshalJSON(b []byte) error { + type alias Issue + aux := &struct { + Fields struct { + Description string + Comment []Comment + Project Project + } + *alias + }{ + alias: (*alias)(issue), + } + if err := json.Unmarshal(b, aux); err != nil { + return err + } + issue.Comments = aux.Fields.Comment + issue.Description = aux.Fields.Description + issue.Project = aux.Fields.Project + return nil +} blob - /dev/null blob + 7b7992b9e2273f94ef78eb36746b8d7bc5e7d6e2 (mode 644) --- /dev/null +++ jira/jira_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "encoding/json" + "os" + "testing" +) + +func TestIssue(t *testing.T) { + f, err := os.Open("issue.json") + if err != nil { + t.Fatal(err) + } + defer f.Close() + var issue Issue + if err := json.NewDecoder(f).Decode(&issue); err != nil { + t.Fatal(err) + } +} + +func TestPrint(t *testing.T) { + f, err := os.Open("issue.json") + if err != nil { + t.Fatal(err) + } + defer f.Close() + var issue Issue + if err := json.NewDecoder(f).Decode(&issue); err != nil { + t.Fatal(err) + } + printIssue(os.Stdout, &issue) +} blob - /dev/null blob + b8598ddab249a4c773596c165ff8186cd28c2ab6 (mode 644) --- /dev/null +++ jira/print.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "io" + "strings" +) + +func printIssue(w io.Writer, i *Issue) (n int, err error) { + buf := &strings.Builder{} + fmt.Fprintln(buf, "URL:", i.URL) + fmt.Fprintln(buf) + + fmt.Fprintln(buf, i.Description) + fmt.Fprintln(buf) + + for _, c := range i.Comments { + buf.WriteString("\t") + printComment(buf, &c) + } + return w.Write([]byte(buf.String())) +} + +func printComment(w io.Writer, c *Comment) (n int, err error) { + buf := &strings.Builder{} + date := c.Created + if !c.Updated.IsZero() { + date = c.Updated + } + fmt.Fprintln(buf, "Date:", date) + fmt.Fprintln(buf, "From:", c.Author.Name) + fmt.Fprintln(buf) + fmt.Fprintln(buf, c.Body) + return w.Write([]byte(buf.String())) +} blob - /dev/null blob + 30254dd4d27efe5f9765de36b1994068c770b813 (mode 644) --- /dev/null +++ jira/project.json @@ -0,0 +1,116 @@ +{ + "expand": "description,lead,issueTypes,url,projectKeys", + "self": "http://www.example.com/jira/rest/api/2/project/EX", + "id": "10000", + "key": "EX", + "description": "This project was created as an example for REST.", + "lead": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "key": "fred", + "accountId": "99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", + "name": "fred", + "avatarUrls": { + "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred", + "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred", + "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred", + "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred" + }, + "displayName": "Fred F. User", + "active": false + }, + "components": [ + { + "self": "http://www.example.com/jira/rest/api/2/component/10000", + "id": "10000", + "name": "Component 1", + "description": "This is a JIRA component", + "lead": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "key": "fred", + "accountId": "99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", + "name": "fred", + "avatarUrls": { + "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred", + "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred", + "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred", + "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred" + }, + "displayName": "Fred F.User", + "active": false + }, + "assigneeType": "PROJECT_LEAD", + "assignee": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "key": "fred", + "accountId": "99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", + "name": "fred", + "avatarUrls": { + "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred", + "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred", + "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred", + "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred" + }, + "displayName": "Fred F. User", + "active": false + }, + "realAssigneeType": "PROJECT_LEAD", + "realAssignee": { + "self": "http://www.example.com/jira/rest/api/2/user?username=fred", + "key": "fred", + "accountId": "99:27935d01-92a7-4687-8272-a9b8d3b2ae2e", + "name": "fred", + "avatarUrls": { + "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred", + "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred", + "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred", + "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred" + }, + "displayName": "Fred F. User", + "active": false + }, + "isAssigneeTypeValid": false, + "project": "HSP", + "projectId": 10000 + } + ], + "issueTypes": [ + { + "self": "http://localhost:8090/jira/rest/api/2.0/issueType/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "http://localhost:8090/jira/images/icons/issuetypes/task.png", + "name": "Task", + "subtask": false, + "avatarId": 1 + }, + { + "self": "http://localhost:8090/jira/rest/api/2.0/issueType/1", + "id": "1", + "description": "A problem with the software.", + "iconUrl": "http://localhost:8090/jira/images/icons/issuetypes/bug.png", + "name": "Bug", + "subtask": false, + "avatarId": 10002 + } + ], + "url": "http://www.example.com/jira/browse/EX", + "email": "from-jira@example.com", + "assigneeType": "PROJECT_LEAD", + "versions": [], + "name": "Example", + "roles": { + "Developers": "http://www.example.com/jira/rest/api/2/project/EX/role/10000" + }, + "avatarUrls": { + "48x48": "http://www.example.com/jira/secure/projectavatar?size=large&pid=10000", + "24x24": "http://www.example.com/jira/secure/projectavatar?size=small&pid=10000", + "16x16": "http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000", + "32x32": "http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000" + }, + "projectCategory": { + "self": "http://www.example.com/jira/rest/api/2/projectCategory/10000", + "id": "10000", + "name": "FIRST", + "description": "First Project Category" + } +}