Commit Diff


commit - 43b3d76ef021d1f016352a1ba0f57430d33dc923
commit + 2fe99c0f775659ffbbd6683ed755964934a8300f
blob - be2c7243af97955bd988833827120bab346d0c2b (mode 644)
blob + /dev/null
--- jira/issue.json
+++ /dev/null
@@ -1,204 +0,0 @@
-{
-    "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 + fcca0d1aae37cbbabf2d1b8d0549f889a724e6d9 (mode 644)
--- /dev/null
+++ jira/acme.go
@@ -0,0 +1,159 @@
+package main
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
+	"log"
+	"os"
+	"path"
+	"strings"
+
+	"9fans.net/go/acme"
+)
+
+func init() {
+	log.SetFlags(0)
+	log.SetPrefix("Jira: ")
+}
+
+type awin struct {
+	*acme.Win
+	fsys fs.FS
+}
+
+func (w *awin) name() string {
+	b, err := w.ReadAll("tag")
+	if err != nil {
+		w.Err(err.Error())
+		return ""
+	}
+	fields := strings.Fields(string(b))
+	return strings.TrimPrefix(fields[0], "/jira/")
+}
+
+func (w *awin) Look(text string) bool {
+	text = strings.TrimSpace(text)
+	fname := path.Join(path.Dir(w.name()), text)
+	if strings.HasSuffix(text, "/") {
+		fname = path.Join(w.name(), text)
+	}
+	f, err := w.fsys.Open(fname)
+	if errors.Is(err, fs.ErrNotExist) {
+		return false
+	} else if err != nil {
+		w.Err(err.Error())
+		return false
+	}
+	stat, err := f.Stat()
+	if err != nil {
+		w.Err(err.Error())
+		return true
+	}
+
+	win, err := acme.New()
+	if err != nil {
+		w.Err(err.Error())
+		return true
+	}
+
+	wname := path.Join("/jira", w.name(), path.Base(fname))
+	if stat.IsDir() {
+		wname += "/"
+	}
+	win.Name(wname)
+	go func() {
+		defer f.Close()
+		buf := &bytes.Buffer{}
+		if stat.IsDir() {
+			dirs, err := f.(fs.ReadDirFile).ReadDir(-1)
+			if err != nil {
+				win.Err(err.Error())
+				return
+			}
+			for _, d := range dirs {
+				if d.IsDir() {
+					fmt.Fprintln(buf, d.Name()+"/")
+					continue
+				}
+				fmt.Fprintln(buf, d.Name())
+			}
+		} else {
+			if _, err := io.Copy(buf, f); err != nil {
+				win.Err(err.Error())
+				return
+			}
+		}
+		w := &awin{win, w.fsys}
+		if _, err := w.Write("body", buf.Bytes()); err != nil {
+			win.Err(err.Error())
+			return
+		}
+		win.Ctl("clean")
+		go w.EventLoop(w)
+	}()
+	return true
+}
+
+func (w *awin) Execute(cmd string) bool {
+	fields := strings.Fields(strings.TrimSpace(cmd))
+	switch fields[0] {
+	case "Get":
+		return false
+	case "Search":
+		if len(fields) == 1 {
+			return false
+		}
+		query := strings.Join(fields[1:], " ")
+		go newSearch("TODO", query)
+		return true
+	}
+	return false
+}
+
+func newSearch(apiRoot, query string) {
+	win, err := acme.New()
+	if err != nil {
+		acme.Errf("new window: %v", err.Error())
+		return
+	}
+	win.Name("/jira/search")
+	issues, err := searchIssues(apiRoot, query)
+	if err != nil {
+		win.Errf("search %q: %v", query, err)
+		return
+	}
+	_, err = win.Write("body", []byte(printIssues(issues)))
+	if err != nil {
+		win.Err(err.Error())
+	}
+}
+
+func main() {
+	fsys := &FS{apiRoot: "https://jira.atlassian.com/rest/api/2"}
+
+	acme.AutoExit(true)
+	win, err := acme.New()
+	if err != nil {
+		log.Fatal(err)
+	}
+	root := &awin{win, fsys}
+
+	dirs, err := fs.ReadDir(fsys, ".")
+	if err != nil {
+		log.Fatal(err)
+	}
+	buf := &bytes.Buffer{}
+	for _, d := range dirs {
+		fmt.Fprintln(buf, d.Name()+"/")
+	}
+	if _, err := root.Write("body", buf.Bytes()); err != nil {
+		log.Fatal(err)
+	}
+	root.Name("/jira/")
+	win.Ctl("clean")
+	root.EventLoop(root)
+	os.Exit(0)
+}
blob - 0e557dbc3006d3655385cd682f92608262fdf8e2
blob + 83ab784511d0ffd42f946f59f422538349276f2b
--- jira/jira.go
+++ jira/jira.go
@@ -1,5 +1,5 @@
 // https://developer.atlassian.com/cloud/jira/platform/rest/v2/
-// https://jira.atlassian.com/rest/api/latest/issue/JRA-9
+// https://jira.atlassian.com/rest/api/2/issue/JRA-9
 package main
 
 import (
@@ -24,9 +24,10 @@ type Issue struct {
 }
 
 type Project struct {
-	ID   string `json:"id"` // TODO(otl): int?
-	Name string `json:"name"`
-	URL  string `json:"self"`
+	ID string `json:"id"` // TODO(otl): int?
+	// Name string `json:"name"`
+	Key string `json:"key"`
+	URL string `json:"self"`
 }
 
 type Comment struct {
blob - /dev/null
blob + 392894548760bc2cc46ea7f1e8c014a30882d6d0 (mode 644)
--- /dev/null
+++ jira/fileinfo.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+	"io/fs"
+	"strings"
+	"time"
+)
+
+func (is *Issue) Name() string {
+	_, number, found := strings.Cut(is.Key, "-")
+	if !found {
+		return is.Key
+	}
+	return number
+}
+
+func (is *Issue) Size() int64        { return int64(len(printIssue(is))) }
+func (is *Issue) Mode() fs.FileMode  { return 0o444 | fs.ModeDir }
+func (is *Issue) ModTime() time.Time { return is.Updated }
+func (is *Issue) IsDir() bool        { return is.Mode().IsDir() }
+func (is *Issue) Sys() any           { return nil }
+
+func (c *Comment) Name() string       { return c.ID }
+func (c *Comment) Size() int64        { return int64(len(printComment(c))) }
+func (c *Comment) Mode() fs.FileMode  { return 0o444 }
+func (c *Comment) ModTime() time.Time { return c.Updated }
+func (c *Comment) IsDir() bool        { return c.Mode().IsDir() }
+func (c *Comment) Sys() any           { return nil }
+
+func (p *Project) Name() string       { return p.Key }
+func (p *Project) Size() int64        { return -1 }
+func (p *Project) Mode() fs.FileMode  { return 0o444 | fs.ModeDir }
+func (p *Project) ModTime() time.Time { return time.Time{} }
+func (p *Project) IsDir() bool        { return p.Mode().IsDir() }
+func (p *Project) Sys() any           { return nil }
+
+type stat struct {
+	name  string
+	size  int64
+	mode  fs.FileMode
+	mtime time.Time
+}
+
+func (s stat) Name() string       { return s.name }
+func (s stat) Size() int64        { return s.size }
+func (s stat) Mode() fs.FileMode  { return s.mode }
+func (s stat) ModTime() time.Time { return s.mtime }
+func (s stat) IsDir() bool        { return s.Mode().IsDir() }
+func (s stat) Sys() any           { return nil }
blob - 228ee2633969341e9376cc0ca2f9173fa531c45a
blob + 170637a16a565d81a410b6fe2dedcf1f2e298c52
--- jira/jira_test.go
+++ jira/jira_test.go
@@ -3,24 +3,23 @@ package main
 import (
 	"encoding/json"
 	"os"
-	"path/filepath"
 	"testing"
 )
 
-func TestIssue(t *testing.T) {
-	names, err := filepath.Glob("issue*.json")
+func TestDecode(t *testing.T) {
+	dents, err := os.ReadDir("testdata/issue")
 	if err != nil {
 		t.Fatal(err)
 	}
-	for _, name := range names {
-		f, err := os.Open(name)
+	for _, d := range dents {
+		f, err := os.Open("testdata/issue/"+d.Name())
 		if err != nil {
-			t.Fatalf("%s: %v", name, err)
+			t.Fatal(err)
 		}
-		defer f.Close()
-		var issue Issue
-		if err := json.NewDecoder(f).Decode(&issue); err != nil {
-			t.Fatalf("%s: %v", name, err)
+		var i Issue
+		if err := json.NewDecoder(f).Decode(&i); err != nil {
+			t.Errorf("decode %s: %v", f.Name(), err)
 		}
+		f.Close()
 	}
 }
blob - /dev/null
blob + 3861ee42f7106b70150c8c2ee0e265ba49360177 (mode 644)
--- /dev/null
+++ jira/fs.go
@@ -0,0 +1,346 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"io/fs"
+	"log"
+	"path"
+	"strings"
+	"time"
+)
+
+type FS struct {
+	apiRoot string
+	root    *fid
+}
+
+// JRASERVER/1234/issue
+// JRASERVER/1234/5678
+
+const (
+	ftypeRoot int = iota
+	ftypeProject
+	ftypeIssue
+	ftypeIssueDir
+	ftypeComment
+)
+
+type fid struct {
+	apiRoot string
+	name    string
+	typ     int
+	rd      io.Reader
+	parent  *fid
+
+	// May not be set.
+	stat *stat
+
+	// directories only
+	children []fs.DirEntry
+	dirp     int
+}
+
+func (f *fid) Name() string { return f.name }
+func (f *fid) IsDir() bool  { return f.Type().IsDir() }
+
+func (f *fid) Type() fs.FileMode {
+	switch f.typ {
+	case ftypeRoot, ftypeProject, ftypeIssueDir:
+		return fs.ModeDir
+	}
+	return 0
+}
+
+func (f *fid) Info() (fs.FileInfo, error) { return f.Stat() }
+
+func (f *fid) Stat() (fs.FileInfo, error) {
+	if debug {
+		log.Println("stat", f.name)
+	}
+	if f.stat != nil {
+		return f.stat, nil
+	}
+
+	switch f.typ {
+	case ftypeRoot:
+		return &stat{".", int64(len(f.children)), 0o444 | fs.ModeDir, time.Time{}}, nil
+	case ftypeProject:
+		p, err := getProject(f.apiRoot, f.name)
+		if err != nil {
+			return nil, &fs.PathError{"stat", f.name, err}
+		}
+		return p, nil
+	case ftypeIssueDir, ftypeIssue:
+		is, err := getIssue(f.apiRoot, f.issueKey())
+		if err != nil {
+			return nil, &fs.PathError{"stat", f.name, err}
+		}
+		if f.typ == ftypeIssueDir {
+			return is, nil
+		}
+		return &stat{f.name, int64(len(printIssue(is))), 0o444, is.Updated}, nil
+	case ftypeComment:
+		c, err := getComment(f.apiRoot, f.issueKey(), f.name)
+		if err != nil {
+			return nil, &fs.PathError{"stat", f.name, err}
+		}
+		return c, nil
+	}
+	err := fmt.Errorf("unexpected fid type %d", f.typ)
+	return nil, &fs.PathError{"stat", f.name, err}
+}
+
+func (f *fid) Read(p []byte) (n int, err error) {
+	if f.rd == nil {
+		switch f.typ {
+		case ftypeComment:
+			c, err := getComment(f.apiRoot, f.issueKey(), f.name)
+			if err != nil {
+				err = fmt.Errorf("get comment %s: %w", f.issueKey(), err)
+				return 0, &fs.PathError{"read", f.name, err}
+			}
+			f.rd = strings.NewReader(printComment(c))
+		case ftypeIssue:
+			is, err := getIssue(f.apiRoot, f.issueKey())
+			if err != nil {
+				err = fmt.Errorf("get issue %s: %w", f.issueKey(), err)
+				return 0, &fs.PathError{"read", f.name, err}
+			}
+			f.rd = strings.NewReader(printIssue(is))
+		default:
+			var err error
+			if f.children == nil {
+				f.children, err = f.ReadDir(-1)
+				if err != nil {
+					return 0, &fs.PathError{"read", f.name, err}
+				}
+			}
+			buf := &strings.Builder{}
+			for _, d := range f.children {
+				fmt.Fprintln(buf, fs.FormatDirEntry(d))
+			}
+			f.rd = strings.NewReader(buf.String())
+		}
+	}
+	return f.rd.Read(p)
+}
+
+func (f *fid) Close() error {
+	f.rd = nil
+	return nil
+}
+
+func (f *fid) ReadDir(n int) ([]fs.DirEntry, error) {
+	if !f.IsDir() {
+		return nil, fmt.Errorf("not a directory")
+	}
+	if debug {
+		log.Println("readdir", f.name)
+	}
+	if f.children == nil {
+		switch f.typ {
+		case ftypeRoot:
+			return nil, fmt.Errorf("root initialised incorrectly: no dir entries")
+		case ftypeProject:
+			issues, err := getIssues(f.apiRoot, f.name)
+			if err != nil {
+				return nil, fmt.Errorf("get issues: %w", err)
+			}
+			f.children = make([]fs.DirEntry, len(issues))
+			for i, issue := range issues {
+				f.children[i] = &fid{
+					apiRoot: f.apiRoot,
+					name:    issue.Name(),
+					typ:     ftypeIssueDir,
+					parent:  f,
+				}
+			}
+		case ftypeIssueDir:
+			issue, err := getIssue(f.apiRoot, f.issueKey())
+			if err != nil {
+				return nil, fmt.Errorf("get issue %s: %w", f.name, err)
+			}
+			f.children = make([]fs.DirEntry, len(issue.Comments)+1)
+			for i, c := range issue.Comments {
+				f.children[i] = &fid{
+					apiRoot: f.apiRoot,
+					name:    c.ID,
+					typ:     ftypeComment,
+					rd:      strings.NewReader(printComment(&c)),
+					parent:  f,
+				}
+			}
+			f.children[len(f.children)-1] = &fid{
+				name:    "issue",
+				apiRoot: f.apiRoot,
+				typ:     ftypeIssue,
+				rd:      strings.NewReader(issue.Summary),
+				parent:  f,
+				stat:    &stat{"issue", int64(len(printIssue(issue))), 0o444, issue.Updated},
+			}
+		}
+	}
+
+	if f.dirp >= len(f.children) {
+		if n <= 0 {
+			return nil, nil
+		}
+		return nil, io.EOF
+	}
+	if n <= 0 {
+		f.dirp = len(f.children)
+		return f.children, nil
+	}
+
+	var err error
+	d := f.children[f.dirp:]
+	if len(d) >= n {
+		d = d[:n]
+	} else if len(d) <= n {
+		err = io.EOF
+	}
+	f.dirp += n
+	return d, err
+}
+
+func (f *fid) issueKey() string {
+	// to make the issue key e.g. "EXAMPLE-42"
+	// we need the name of the issue (parent name, "42")
+	// and the name of the project (the issue's parent's name, "EXAMPLE")
+	var project, issueNumber string
+	switch f.typ {
+	default:
+		return ""
+	case ftypeComment, ftypeIssue:
+		project = f.parent.parent.name
+		issueNumber = f.parent.name
+	case ftypeIssueDir:
+		project = f.parent.name
+		issueNumber = f.name
+	}
+	return project + "-" + issueNumber
+}
+
+func (fsys *FS) Open(name string) (fs.File, error) {
+	if !fs.ValidPath(name) {
+		return nil, &fs.PathError{"open", name, fs.ErrInvalid}
+	}
+	name = path.Clean(name)
+	if strings.Contains(name, "\\") {
+		return nil, fs.ErrNotExist
+	}
+
+	var err error
+	if fsys.root == nil {
+		fsys.root, err = makeRoot(fsys.apiRoot)
+		if err != nil {
+			return nil, fmt.Errorf("make root file: %w", err)
+		}
+	}
+	if debug {
+		log.Println("open", name)
+	}
+	if name == "." {
+		f := *fsys.root
+		return &f, nil
+	}
+
+	elems := strings.Split(name, "/")
+	if elems[0] == "." && len(elems) > 1 {
+		elems = elems[1:]
+	}
+
+	f := fsys.root
+	for _, elem := range elems {
+		dir, err := find(f, elem)
+		if err != nil {
+			return nil, &fs.PathError{"open", name, err}
+		}
+		f = dir
+	}
+	g := *f
+	return &g, nil
+}
+
+func makeRoot(apiRoot string) (*fid, error) {
+	projects, err := getProjects(apiRoot)
+	if err != nil {
+		return nil, err
+	}
+	root := &fid{
+		apiRoot:  apiRoot,
+		name:     ".",
+		typ:      ftypeRoot,
+		children: make([]fs.DirEntry, len(projects)),
+	}
+	for i, p := range projects {
+		root.children[i] = &fid{
+			apiRoot: apiRoot,
+			name:    p.Key,
+			typ:     ftypeProject,
+		}
+	}
+	return root, nil
+}
+
+func find(dir *fid, name string) (*fid, error) {
+	if !dir.IsDir() {
+		return nil, fs.ErrNotExist
+	}
+	child := &fid{apiRoot: dir.apiRoot, parent: dir}
+	switch dir.typ {
+	case ftypeRoot:
+		for _, d := range dir.children {
+			if d.Name() == name {
+				child, ok := d.(*fid)
+				if !ok {
+					return nil, fmt.Errorf("unexpected dir entry type %T", d)
+				}
+				return child, nil
+			}
+		}
+		return nil, fs.ErrNotExist
+	case ftypeProject:
+		key := fmt.Sprintf("%s-%s", dir.name, name)
+		ok, err := checkIssue(dir.apiRoot, key)
+		if err != nil {
+			return nil, err
+		}
+		if !ok {
+			return nil, fs.ErrNotExist
+		}
+		child.name = name
+		child.typ = ftypeIssueDir
+		return child, nil
+	case ftypeIssueDir:
+		if name == "issue" {
+			child.name = name
+			child.typ = ftypeIssue
+			return child, nil
+		}
+		// we may have already loaded the dir entries (comments)
+		// when we loaded the parent (issue).
+		/*
+			for _, d := range dir.children {
+				if d.Name() == name {
+					c, ok := d.(*fid)
+					if !ok {
+						break
+					}
+					return c, nil
+				}
+			}
+		*/
+		ok, err := checkComment(dir.apiRoot, dir.issueKey(), name)
+		if err != nil {
+			return nil, err
+		} else if !ok {
+			return nil, fs.ErrNotExist
+		}
+		child.name = name
+		child.typ = ftypeComment
+		return child, nil
+	}
+	return nil, fs.ErrNotExist
+}
blob - 6a0a9f61dbdc6dfb8597452dab7845f69dbab1e4
blob + 8f98e5761509808ede8a89fd018f9217261d2ac5
--- jira/print.go
+++ jira/print.go
@@ -2,15 +2,24 @@ package main
 
 import (
 	"fmt"
-	"io"
 	"strings"
 	"time"
 )
 
-func printIssue(w io.Writer, i *Issue) (n int, err error) {
+func printIssues(issues []Issue) string {
 	buf := &strings.Builder{}
+	for _, ii := range issues {
+		name := strings.TrimPrefix(ii.Key, ii.Project.Key+"-")
+		fmt.Fprintf(buf, "%s/\t%s\n", name, ii.Summary)
+	}
+	return buf.String()
+}
+
+func printIssue(i *Issue) string {
+	buf := &strings.Builder{}
 	fmt.Fprintln(buf, "From:", i.Reporter.Name)
 	fmt.Fprintln(buf, "URL:", i.URL)
+	fmt.Fprintln(buf, "Date", i.Updated.Format(time.RFC1123Z))
 	fmt.Fprintln(buf, "Subject:", i.Summary)
 	fmt.Fprintln(buf)
 
@@ -21,31 +30,30 @@ func printIssue(w io.Writer, i *Issue) (n int, err err
 		if !c.Updated.IsZero() {
 			date = c.Updated
 		}
-		fmt.Fprintf(buf, "%s/\t%s\t%s (%s)\n", c.ID, summarise(c.Body), c.Author.Name, date.Format(time.DateTime))
+		fmt.Fprintf(buf, "%s\t%s\t%s (%s)\n", c.ID, summarise(c.Body, 36), c.Author.Name, date.Format(time.DateTime))
 	}
-	return w.Write([]byte(buf.String()))
+	return buf.String()
 }
 
-func printComment(w io.Writer, c *Comment) (n int, err error) {
+func printComment(c *Comment) string {
 	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, "Date:", date)
 	fmt.Fprintln(buf)
 	fmt.Fprintln(buf, c.Body)
-	return w.Write([]byte(buf.String()))
+	return buf.String()
 }
 
-func summarise(body string) string {
-	max := 36
-	if len(body) < max {
+func summarise(body string, length int) string {
+	if len(body) < length {
 		body = strings.ReplaceAll(body, "\n", " ")
 		return strings.TrimSpace(body)
 	}
-	body = body[:max]
+	body = body[:length]
 	body = strings.ReplaceAll(body, "\r", "")
 	body = strings.ReplaceAll(body, "\n", " ")
 	body = strings.TrimSpace(body)
blob - /dev/null
blob + 3385371a5b6e8fb9a46702a7497ecb7b4ae2779c (mode 644)
--- /dev/null
+++ jira/fs_test.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+	"encoding/json"
+	"io/fs"
+	"os"
+	"testing"
+)
+
+const atlassianRoot = "https://jira.atlassian.com/rest/api/2"
+
+func TestIssueName(t *testing.T) {
+	f, err := os.Open("testdata/issue/TEST-1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+	var issue Issue
+	if err := json.NewDecoder(f).Decode(&issue); err != nil {
+		t.Fatal(err)
+	}
+	want := "1"
+	if issue.Name() != want {
+		t.Errorf("issue.Name() = %q, want %q", issue.Name(), want)
+	}
+}
+
+func TestIssueKey(t *testing.T) {
+	comment := &fid{name: "69", typ: ftypeComment}
+	issue := &fid{name: "issue", typ: ftypeIssue}
+	issueDir := &fid{name: "1", typ: ftypeIssueDir, children: []fs.DirEntry{issue, comment}}
+	project := &fid{name: "TEST", typ: ftypeProject, children: []fs.DirEntry{issueDir}}
+
+	comment.parent = issueDir
+	issue.parent = issueDir
+	issueDir.parent = project
+
+	want := "TEST-1"
+	for _, f := range []*fid{comment, issue, issueDir} {
+		if f.issueKey() != want {
+			t.Errorf("fid %s issueKey = %q, want %q", f.name, f.issueKey(), want)
+		}
+	}
+}
blob - /dev/null
blob + 9e13590de1b26661edb73de98faba407b6221474 (mode 644)
--- /dev/null
+++ jira/http.go
@@ -0,0 +1,154 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+)
+
+const debug = false
+
+func getProjects(apiRoot string) ([]Project, error) {
+	u := fmt.Sprintf("%s/project", apiRoot)
+	if debug {
+		fmt.Fprintln(os.Stderr, "GET", u)
+	}
+	resp, err := http.Get(u)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("non-ok status: %s", resp.Status)
+	}
+	defer resp.Body.Close()
+	var p []Project
+	if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
+		return nil, fmt.Errorf("decode project: %w", err)
+	}
+	return p, nil
+}
+
+func getProject(apiRoot, name string) (*Project, error) {
+	u := fmt.Sprintf("%s/project/%s", apiRoot, name)
+	if debug {
+		fmt.Fprintln(os.Stderr, "GET", u)
+	}
+	resp, err := http.Get(u)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("non-ok status: %s", resp.Status)
+	}
+	defer resp.Body.Close()
+	var p Project
+	if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
+		return nil, fmt.Errorf("decode project: %w", err)
+	}
+	return &p, nil
+}
+
+func getIssues(apiRoot, project string) ([]Issue, error) {
+	q := fmt.Sprintf("project = %q", project)
+	return searchIssues(apiRoot, q)
+}
+
+func searchIssues(apiRoot, query string) ([]Issue, error) {
+	u, err := url.Parse(apiRoot + "/search")
+	if err != nil {
+		return nil, err
+	}
+	q := make(url.Values)
+	q.Add("jql", query)
+	u.RawQuery = q.Encode()
+	if debug {
+		fmt.Fprintln(os.Stderr, "GET", u)
+	}
+	resp, err := http.Get(u.String())
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("non-ok status: %s", resp.Status)
+	}
+	t := struct {
+		Issues []Issue
+	}{}
+	if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
+		return nil, fmt.Errorf("decode issues: %w", err)
+	}
+	return t.Issues, nil
+}
+
+func checkIssue(apiRoot, name string) (bool, error) {
+	u := fmt.Sprintf("%s/issue/%s", apiRoot, name)
+	if debug {
+		log.Println("HEAD", u)
+	}
+	resp, err := http.Head(u)
+	if err != nil {
+		return false, err
+	}
+	if resp.StatusCode == http.StatusOK {
+		return true, nil
+	}
+	return false, nil
+}
+
+func getIssue(apiRoot, name string) (*Issue, error) {
+	u := fmt.Sprintf("%s/issue/%s", apiRoot, name)
+	if debug {
+		log.Println("GET", u)
+	}
+	resp, err := http.Get(u)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("non-ok status: %s", resp.Status)
+	}
+	defer resp.Body.Close()
+	var is Issue
+	if err := json.NewDecoder(resp.Body).Decode(&is); err != nil {
+		return nil, fmt.Errorf("decode issue: %w", err)
+	}
+	return &is, nil
+}
+
+func checkComment(apiRoot, ikey, id string) (bool, error) {
+	u := fmt.Sprintf("%s/issue/%s/comment/%s", apiRoot, ikey, id)
+	if debug {
+		log.Println("HEAD", u)
+	}
+	resp, err := http.Head(u)
+	if err != nil {
+		return false, err
+	}
+	if resp.StatusCode == http.StatusOK {
+		return true, nil
+	}
+	return false, nil
+}
+
+func getComment(apiRoot string, ikey, id string) (*Comment, error) {
+	u := fmt.Sprintf("%s/issue/%s/comment/%s", apiRoot, ikey, id)
+	if debug {
+		fmt.Fprintln(os.Stderr, "GET", u)
+	}
+	resp, err := http.Get(u)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("non-ok status: %s", resp.Status)
+	}
+	defer resp.Body.Close()
+	var c Comment
+	if err := json.NewDecoder(resp.Body).Decode(&c); err != nil {
+		return nil, fmt.Errorf("decode comment: %w", err)
+	}
+	return &c, nil
+}
blob - /dev/null
blob + be15175872d22159c4e9f7409fd2432da54e3145 (mode 644)
--- /dev/null
+++ jira/http_test.go
@@ -0,0 +1,111 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path"
+	"testing"
+	"testing/fstest"
+)
+
+func serveFakeList(dir string) http.HandlerFunc {
+	return func(w http.ResponseWriter, req *http.Request) {
+		var prefix string
+		typ := path.Base(dir)
+		switch typ {
+		case "issue":
+			prefix = `{"issues": [`
+		case "project":
+			prefix = "["
+		default:
+			http.NotFound(w, req)
+			return
+		}
+		dirs, err := os.ReadDir(dir)
+		if err != nil {
+			log.Println(err)
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		fmt.Fprintln(w, prefix)
+		for i, d := range dirs {
+			f, err := os.Open(path.Join(dir, d.Name()))
+			if err != nil {
+				log.Println(err)
+				return
+			}
+			if _, err := io.Copy(w, f); err != nil {
+				log.Printf("copy %s: %v", f.Name(), err)
+				f.Close()
+			}
+			f.Close()
+			if i == len(dirs)-1 {
+				break
+			}
+			fmt.Fprintln(w, ",")
+		}
+		fmt.Fprintln(w, "]}")
+	}
+}
+
+func handleComment(w http.ResponseWriter, req *http.Request) {
+	id := path.Base(req.URL.Path)
+	http.ServeFile(w, req, "testdata/comment/"+id)
+}
+
+func TestGet(t *testing.T) {
+	http.HandleFunc("/project", serveFakeList("testdata/project"))
+	http.HandleFunc("/search", serveFakeList("testdata/issue"))
+	http.HandleFunc("/issue", serveFakeList("testdata/issue"))
+	http.HandleFunc("/issue/TEST-1/comment/", handleComment)
+	http.Handle("/", http.FileServer(http.Dir("testdata")))
+	srv := httptest.NewServer(nil)
+	defer srv.Close()
+
+	project := "TEST"
+	issue := "TEST-1"
+	comment := "69"
+	if _, err := getProject(srv.URL, project); err != nil {
+		t.Fatalf("get project %s: %v", project, err)
+	}
+	if _, err := getIssues(srv.URL, project); err != nil {
+		t.Fatalf("get %s issues: %v", project, err)
+	}
+	if _, err := getIssue(srv.URL, issue); err != nil {
+		t.Fatalf("get issue %s: %v", issue, err)
+	}
+	c, err := getComment(srv.URL, issue, comment)
+	if err != nil {
+		t.Fatalf("get comment %s from %s: %v", comment, issue, err)
+	}
+	if c.ID != "69" {
+		t.Fatalf("wanted comment id %s, got %s", "69", c.ID)
+	}
+
+	fsys := &FS{apiRoot: srv.URL}
+	f, err := fsys.Open("TEST/1/69")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if _, err := f.Stat(); err != nil {
+		t.Fatal(err)
+	}
+	if _, err := io.Copy(io.Discard, f); err != nil {
+		t.Fatal(err)
+	}
+	f.Close()
+
+	expected := []string{
+		"TEST",
+		"TEST/1",
+		"TEST/1/issue",
+		"TEST/1/69",
+	}
+	if err := fstest.TestFS(fsys, expected...); err != nil {
+		t.Error(err)
+	}
+}
blob - /dev/null
blob + afa177739515fe9b83c8948093f5d7596b0ef6f5 (mode 644)
--- /dev/null
+++ jira/testdata/comment/69
@@ -0,0 +1,35 @@
+{
+    "self": "https://jira.atlassian.com/rest/api/2/issue/10148/comment/17800",
+    "id": "69",
+    "author": {
+        "self": "https://jira.atlassian.com/rest/api/2/user?username=owen%40atlassian.com",
+        "name": "owen@atlassian.com",
+        "key": "owen@atlassian.com",
+        "avatarUrls": {
+            "48x48": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=48",
+            "24x24": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=24",
+            "16x16": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=16",
+            "32x32": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=32"
+        },
+        "displayName": "Owen Fellows",
+        "active": true,
+        "timeZone": "UTC"
+    },
+    "body": "This is due to users not having a timezone set, see linked Issue.",
+    "updateAuthor": {
+        "self": "https://jira.atlassian.com/rest/api/2/user?username=owen%40atlassian.com",
+        "name": "owen@atlassian.com",
+        "key": "owen@atlassian.com",
+        "avatarUrls": {
+            "48x48": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=48",
+            "24x24": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=24",
+            "16x16": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=16",
+            "32x32": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=32"
+        },
+        "displayName": "Owen Fellows",
+        "active": true,
+        "timeZone": "UTC"
+    },
+    "created": "2003-11-17T01:55:10.760+0000",
+    "updated": "2003-11-17T01:55:10.760+0000"
+}
blob - /dev/null
blob + 72af386aea83edb5619ec766b813cdd8b9929e9a (mode 644)
--- /dev/null
+++ jira/testdata/issue/TEST-1
@@ -0,0 +1,877 @@
+{
+    "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations",
+    "id": "10148",
+    "self": "https://jira.atlassian.com/rest/api/latest/issue/10148",
+    "key": "TEST-1",
+    "fields": {
+        "fixVersions": [
+            {
+                "self": "https://jira.atlassian.com/rest/api/2/version/15918",
+                "id": "15918",
+                "description": "",
+                "name": "4.4",
+                "archived": false,
+                "released": true,
+                "releaseDate": "2011-08-02"
+            }
+        ],
+        "resolution": {
+            "self": "https://jira.atlassian.com/rest/api/2/resolution/1",
+            "id": "1",
+            "description": "A fix for this issue is checked into the tree and tested.",
+            "name": "Fixed"
+        },
+        "labels": [
+            "affects-cloud",
+            "affects-server"
+        ],
+        "aggregatetimeoriginalestimate": 2160000,
+        "timeestimate": 2160000,
+        "issuelinks": [
+            {
+                "id": "50308",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/50308",
+                "type": {
+                    "id": "10020",
+                    "name": "Blocker",
+                    "inward": "is blocked by",
+                    "outward": "blocks",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10020"
+                },
+                "outwardIssue": {
+                    "id": "126823",
+                    "key": "JSWSERVER-2855",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/126823",
+                    "fields": {
+                        "summary": "GreenHopper Hourly Burndown Gadget - Update Every 15 Minutes or as Scheduled",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "37511",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/37511",
+                "type": {
+                    "id": "10050",
+                    "name": "Cause",
+                    "inward": "causes",
+                    "outward": "is caused by",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10050"
+                },
+                "inwardIssue": {
+                    "id": "85626",
+                    "key": "JRASERVER-17359",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/85626",
+                    "fields": {
+                        "summary": "Add timezone support to JQL dates.",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "28680",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/28680",
+                "type": {
+                    "id": "10001",
+                    "name": "Duplicate",
+                    "inward": "is duplicated by",
+                    "outward": "duplicates",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10001"
+                },
+                "inwardIssue": {
+                    "id": "42259",
+                    "key": "JRASERVER-11253",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/42259",
+                    "fields": {
+                        "summary": "Date stamp does not compensate for time zone differences",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "priority": {
+                            "self": "https://jira.atlassian.com/rest/api/2/priority/4",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/priorities/low.svg",
+                            "name": "Low",
+                            "id": "4"
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/1",
+                            "id": "1",
+                            "description": "A problem which impairs or prevents the functions of the product.",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51493&avatarType=issuetype",
+                            "name": "Bug",
+                            "subtask": false,
+                            "avatarId": 51493
+                        }
+                    }
+                }
+            },
+            {
+                "id": "10942",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/10942",
+                "type": {
+                    "id": "10001",
+                    "name": "Duplicate",
+                    "inward": "is duplicated by",
+                    "outward": "duplicates",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10001"
+                },
+                "inwardIssue": {
+                    "id": "15168",
+                    "key": "JRASERVER-2677",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/15168",
+                    "fields": {
+                        "summary": "time on jira.atlassian.com?",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "11779",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/11779",
+                "type": {
+                    "id": "10001",
+                    "name": "Duplicate",
+                    "inward": "is duplicated by",
+                    "outward": "duplicates",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10001"
+                },
+                "inwardIssue": {
+                    "id": "16922",
+                    "key": "JRASERVER-3316",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/16922",
+                    "fields": {
+                        "summary": "Created/Updated times are shown in server time zone, not the client's one",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "12982",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/12982",
+                "type": {
+                    "id": "10001",
+                    "name": "Duplicate",
+                    "inward": "is duplicated by",
+                    "outward": "duplicates",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10001"
+                },
+                "inwardIssue": {
+                    "id": "22426",
+                    "key": "JRASERVER-5539",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/22426",
+                    "fields": {
+                        "summary": "TimeZone preference",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "22245",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/22245",
+                "type": {
+                    "id": "10001",
+                    "name": "Duplicate",
+                    "inward": "is duplicated by",
+                    "outward": "duplicates",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10001"
+                },
+                "inwardIssue": {
+                    "id": "46356",
+                    "key": "JRASERVER-11930",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/46356",
+                    "fields": {
+                        "summary": "Display Time in Current User TZ",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "31127",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/31127",
+                "type": {
+                    "id": "10001",
+                    "name": "Duplicate",
+                    "inward": "is duplicated by",
+                    "outward": "duplicates",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10001"
+                },
+                "inwardIssue": {
+                    "id": "70205",
+                    "key": "JRASERVER-15149",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/70205",
+                    "fields": {
+                        "summary": "Date/Times to reflect local time zone",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "32100",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/32100",
+                "type": {
+                    "id": "10001",
+                    "name": "Duplicate",
+                    "inward": "is duplicated by",
+                    "outward": "duplicates",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10001"
+                },
+                "inwardIssue": {
+                    "id": "73554",
+                    "key": "JRASERVER-15528",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/73554",
+                    "fields": {
+                        "summary": "JIRA enterprise does not honour system TZ nor seems to provide option to override, all stamps are and keep on being 1h out of sync after migration to other JIRA instance",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "41981",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/41981",
+                "type": {
+                    "id": "10001",
+                    "name": "Duplicate",
+                    "inward": "is duplicated by",
+                    "outward": "duplicates",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10001"
+                },
+                "inwardIssue": {
+                    "id": "104230",
+                    "key": "JRASERVER-20802",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/104230",
+                    "fields": {
+                        "summary": "User Profiles > Time Zones - the ability to select a time zone per user profile",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "11606",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/11606",
+                "type": {
+                    "id": "10000",
+                    "name": "Reference",
+                    "inward": "is related to",
+                    "outward": "relates to",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10000"
+                },
+                "outwardIssue": {
+                    "id": "13031",
+                    "key": "JRASERVER-1519",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/13031",
+                    "fields": {
+                        "summary": "Allow arbitrary fields (company, phone#, etc) in user profile",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/11772",
+                            "description": "This suggestion needs more unique domain votes and comments before being reviewed by our team.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/generic.png",
+                            "name": "Gathering Interest",
+                            "id": "11772",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/2",
+                                "id": 2,
+                                "key": "new",
+                                "colorName": "default",
+                                "name": "To Do"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "27174",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/27174",
+                "type": {
+                    "id": "10000",
+                    "name": "Reference",
+                    "inward": "is related to",
+                    "outward": "relates to",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10000"
+                },
+                "outwardIssue": {
+                    "id": "39309",
+                    "key": "JRASERVER-10613",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/39309",
+                    "fields": {
+                        "summary": "Capability to customize date/time format per user",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/11772",
+                            "description": "This suggestion needs more unique domain votes and comments before being reviewed by our team.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/generic.png",
+                            "name": "Gathering Interest",
+                            "id": "11772",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/2",
+                                "id": 2,
+                                "key": "new",
+                                "colorName": "default",
+                                "name": "To Do"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            },
+            {
+                "id": "71195",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/71195",
+                "type": {
+                    "id": "10000",
+                    "name": "Reference",
+                    "inward": "is related to",
+                    "outward": "relates to",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10000"
+                },
+                "inwardIssue": {
+                    "id": "192343",
+                    "key": "JRASERVER-28939",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/192343",
+                    "fields": {
+                        "summary": "Add time-zone support for rendering date fields",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/12072",
+                            "description": "This issue has been reviewed, but needs more supporting information to gauge how pervasive the problem is.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/generic.png",
+                            "name": "Gathering Impact",
+                            "id": "12072",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/2",
+                                "id": 2,
+                                "key": "new",
+                                "colorName": "default",
+                                "name": "To Do"
+                            }
+                        },
+                        "priority": {
+                            "self": "https://jira.atlassian.com/rest/api/2/priority/4",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/priorities/low.svg",
+                            "name": "Low",
+                            "id": "4"
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/1",
+                            "id": "1",
+                            "description": "A problem which impairs or prevents the functions of the product.",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51493&avatarType=issuetype",
+                            "name": "Bug",
+                            "subtask": false,
+                            "avatarId": 51493
+                        }
+                    }
+                }
+            },
+            {
+                "id": "57958",
+                "self": "https://jira.atlassian.com/rest/api/2/issueLink/57958",
+                "type": {
+                    "id": "10000",
+                    "name": "Reference",
+                    "inward": "is related to",
+                    "outward": "relates to",
+                    "self": "https://jira.atlassian.com/rest/api/2/issueLinkType/10000"
+                },
+                "inwardIssue": {
+                    "id": "144072",
+                    "key": "JRASERVER-25303",
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/144072",
+                    "fields": {
+                        "summary": "Administer Timezone settings",
+                        "status": {
+                            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+                            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+                            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+                            "name": "Closed",
+                            "id": "6",
+                            "statusCategory": {
+                                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                                "id": 3,
+                                "key": "done",
+                                "colorName": "success",
+                                "name": "Done"
+                            }
+                        },
+                        "issuetype": {
+                            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+                            "id": "10000",
+                            "description": "",
+                            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+                            "name": "Suggestion",
+                            "subtask": false,
+                            "avatarId": 51505
+                        }
+                    }
+                }
+            }
+        ],
+        "assignee": null,
+        "status": {
+            "self": "https://jira.atlassian.com/rest/api/2/status/6",
+            "description": "Work on this issue is complete.\r\n\r\nIf it\u2019s fixed in a Server product, the resolution will be \u2018Fixed\u2019 and the Fix Version field will indicate the product version that contains the fix.\r\n\r\nIf no code changes were required, the resolution will be \u2018Duplicate', 'Won't fix', 'Handled by support', 'Timed out', or similar.",
+            "iconUrl": "https://jira.atlassian.com/images/icons/statuses/closed.png",
+            "name": "Closed",
+            "id": "6",
+            "statusCategory": {
+                "self": "https://jira.atlassian.com/rest/api/2/statuscategory/3",
+                "id": 3,
+                "key": "done",
+                "colorName": "success",
+                "name": "Done"
+            }
+        },
+        "components": [
+            {
+                "self": "https://jira.atlassian.com/rest/api/2/component/10125",
+                "id": "10125",
+                "name": "User Management - Others"
+            }
+        ],
+       "archiveddate": null,
+        "aggregatetimeestimate": 2160000,
+        "creator": {
+            "self": "https://jira.atlassian.com/rest/api/2/user?username=mike%40atlassian.com",
+            "name": "mike@atlassian.com",
+            "key": "mike@atlassian.com",
+            "avatarUrls": {
+                "48x48": "https://avatar-cdn.atlassian.com/b1a3871fb560dc486b6636467e9802fe?d=mm&s=48",
+                "24x24": "https://avatar-cdn.atlassian.com/b1a3871fb560dc486b6636467e9802fe?d=mm&s=24",
+                "16x16": "https://avatar-cdn.atlassian.com/b1a3871fb560dc486b6636467e9802fe?d=mm&s=16",
+                "32x32": "https://avatar-cdn.atlassian.com/b1a3871fb560dc486b6636467e9802fe?d=mm&s=32"
+            },
+            "displayName": "Mike Cannon-Brookes",
+            "active": true,
+            "timeZone": "Australia/Sydney"
+        },
+        "subtasks": [],
+        "reporter": {
+            "self": "https://jira.atlassian.com/rest/api/2/user?username=mike%40atlassian.com",
+            "name": "mike@atlassian.com",
+            "key": "mike@atlassian.com",
+            "avatarUrls": {
+                "48x48": "https://avatar-cdn.atlassian.com/b1a3871fb560dc486b6636467e9802fe?d=mm&s=48",
+                "24x24": "https://avatar-cdn.atlassian.com/b1a3871fb560dc486b6636467e9802fe?d=mm&s=24",
+                "16x16": "https://avatar-cdn.atlassian.com/b1a3871fb560dc486b6636467e9802fe?d=mm&s=16",
+                "32x32": "https://avatar-cdn.atlassian.com/b1a3871fb560dc486b6636467e9802fe?d=mm&s=32"
+            },
+            "displayName": "Mike Cannon-Brookes",
+            "active": true,
+            "timeZone": "Australia/Sydney"
+        },
+        "aggregateprogress": {
+            "progress": 1500,
+            "total": 2161500,
+            "percent": 0
+        },
+        "progress": {
+            "progress": 1500,
+            "total": 2161500,
+            "percent": 0
+        },
+        "votes": {
+            "self": "https://jira.atlassian.com/rest/api/2/issue/JRASERVER-9/votes",
+            "votes": 454,
+            "hasVoted": false
+        },
+        "worklog": {
+            "startAt": 0,
+            "maxResults": 20,
+            "total": 1,
+            "worklogs": [
+                {
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/10148/worklog/96804",
+                    "author": {
+                        "self": "https://jira.atlassian.com/rest/api/2/user?username=tim%40atlassian.com",
+                        "name": "tim@atlassian.com",
+                        "key": "tim@atlassian.com",
+                        "avatarUrls": {
+                            "48x48": "https://jira.atlassian.com/secure/useravatar?ownerId=tim%40atlassian.com&avatarId=2465661",
+                            "24x24": "https://jira.atlassian.com/secure/useravatar?size=small&ownerId=tim%40atlassian.com&avatarId=2465661",
+                            "16x16": "https://jira.atlassian.com/secure/useravatar?size=xsmall&ownerId=tim%40atlassian.com&avatarId=2465661",
+                            "32x32": "https://jira.atlassian.com/secure/useravatar?size=medium&ownerId=tim%40atlassian.com&avatarId=2465661"
+                        },
+                        "displayName": "TimP",
+                        "active": true,
+                        "timeZone": "America/Los_Angeles"
+                    },
+                    "updateAuthor": {
+                        "self": "https://jira.atlassian.com/rest/api/2/user?username=tim%40atlassian.com",
+                        "name": "tim@atlassian.com",
+                        "key": "tim@atlassian.com",
+                        "avatarUrls": {
+                            "48x48": "https://jira.atlassian.com/secure/useravatar?ownerId=tim%40atlassian.com&avatarId=2465661",
+                            "24x24": "https://jira.atlassian.com/secure/useravatar?size=small&ownerId=tim%40atlassian.com&avatarId=2465661",
+                            "16x16": "https://jira.atlassian.com/secure/useravatar?size=xsmall&ownerId=tim%40atlassian.com&avatarId=2465661",
+                            "32x32": "https://jira.atlassian.com/secure/useravatar?size=medium&ownerId=tim%40atlassian.com&avatarId=2465661"
+                        },
+                        "displayName": "TimP",
+                        "active": true,
+                        "timeZone": "America/Los_Angeles"
+                    },
+                    "comment": "Time submitted by matt for review CR-2",
+                    "created": "2011-05-20T05:55:22.952+0000",
+                    "updated": "2011-05-20T05:55:22.952+0000",
+                    "started": "2011-05-20T05:55:18.528+0000",
+                    "timeSpent": "25m",
+                    "timeSpentSeconds": 1500,
+                    "id": "96804",
+                    "issueId": "10148"
+                }
+            ]
+        },
+        "archivedby": null,
+        "issuetype": {
+            "self": "https://jira.atlassian.com/rest/api/2/issuetype/10000",
+            "id": "10000",
+            "description": "",
+            "iconUrl": "https://jira.atlassian.com/secure/viewavatar?size=xsmall&avatarId=51505&avatarType=issuetype",
+            "name": "Suggestion",
+            "subtask": false,
+            "avatarId": 51505
+        },
+        "timespent": 1500,
+        "project": {
+            "self": "https://jira.atlassian.com/rest/api/2/project/10240",
+            "id": "10240",
+            "key": "JRASERVER",
+            "name": "Jira Data Center",
+            "projectTypeKey": "software",
+            "avatarUrls": {
+                "48x48": "https://jira.atlassian.com/secure/projectavatar?pid=10240&avatarId=105190",
+                "24x24": "https://jira.atlassian.com/secure/projectavatar?size=small&pid=10240&avatarId=105190",
+                "16x16": "https://jira.atlassian.com/secure/projectavatar?size=xsmall&pid=10240&avatarId=105190",
+                "32x32": "https://jira.atlassian.com/secure/projectavatar?size=medium&pid=10240&avatarId=105190"
+            },
+            "projectCategory": {
+                "self": "https://jira.atlassian.com/rest/api/2/projectCategory/10031",
+                "id": "10031",
+                "description": "",
+                "name": "Atlassian Products"
+            }
+        },
+        "aggregatetimespent": 1500,
+        "resolutiondate": "2011-06-07T15:31:26.782+0000",
+        "workratio": 0,
+        "watches": {
+            "self": "https://jira.atlassian.com/rest/api/2/issue/JRASERVER-9/watchers",
+            "watchCount": 213,
+            "isWatching": false
+        },
+        "created": "2002-02-08T05:08:00.000+0000",
+         "updated": "2022-05-18T18:35:49.533+0000",
+         "timeoriginalestimate": 2160000,
+        "description": "Add time zones to user profile.     That way the dates displayed to a user are always contiguous with their local time zone, rather than the server's time zone.",
+        "timetracking": {
+            "originalEstimate": "600h",
+            "remainingEstimate": "600h",
+            "timeSpent": "25m",
+            "originalEstimateSeconds": 2160000,
+            "remainingEstimateSeconds": 2160000,
+            "timeSpentSeconds": 1500
+        },
+        "attachment": [
+            {
+                "self": "https://jira.atlassian.com/rest/api/2/attachment/45565",
+                "id": "45565",
+                "filename": "log_110314_115720______.csv",
+                "created": "2011-03-26T15:38:11.072+0000",
+                "size": 24576,
+                "mimeType": "text/csv",
+                "content": "https://jira.atlassian.com/secure/attachment/45565/log_110314_115720______.csv"
+            },
+            {
+                "self": "https://jira.atlassian.com/rest/api/2/attachment/40875",
+                "id": "40875",
+                "filename": "times.png",
+                "author": {
+                    "self": "https://jira.atlassian.com/rest/api/2/user?username=rkrishna",
+                    "name": "rkrishna",
+                    "key": "rkrishna",
+                    "avatarUrls": {
+                        "48x48": "https://jira.atlassian.com/secure/useravatar?ownerId=rkrishna&avatarId=2478834",
+                        "24x24": "https://jira.atlassian.com/secure/useravatar?size=small&ownerId=rkrishna&avatarId=2478834",
+                        "16x16": "https://jira.atlassian.com/secure/useravatar?size=xsmall&ownerId=rkrishna&avatarId=2478834",
+                        "32x32": "https://jira.atlassian.com/secure/useravatar?size=medium&ownerId=rkrishna&avatarId=2478834"
+                    },
+                    "displayName": "Roy Krishna",
+                    "active": false,
+                    "timeZone": "Australia/Sydney"
+                },
+                "created": "2010-09-21T05:31:32.198+0000",
+                "size": 39226,
+                "mimeType": "image/png",
+                "content": "https://jira.atlassian.com/secure/attachment/40875/times.png",
+                "thumbnail": "https://jira.atlassian.com/secure/thumbnail/40875/_thumb_40875.png"
+            }
+        ],
+        "summary": "User Preference: User Time Zones",
+        "environment": null,
+        "duedate": null,
+        "comment": {
+            "comments": [
+                {
+                    "self": "https://jira.atlassian.com/rest/api/2/issue/10148/comment/17800",
+                    "id": "69",
+                    "author": {
+                        "self": "https://jira.atlassian.com/rest/api/2/user?username=owen%40atlassian.com",
+                        "name": "owen@atlassian.com",
+                        "key": "owen@atlassian.com",
+                        "avatarUrls": {
+                            "48x48": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=48",
+                            "24x24": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=24",
+                            "16x16": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=16",
+                            "32x32": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=32"
+                        },
+                        "displayName": "Owen Fellows",
+                        "active": true,
+                        "timeZone": "UTC"
+                    },
+                    "body": "This is due to users not having a timezone set, see linked Issue.",
+                    "updateAuthor": {
+                        "self": "https://jira.atlassian.com/rest/api/2/user?username=owen%40atlassian.com",
+                        "name": "owen@atlassian.com",
+                        "key": "owen@atlassian.com",
+                        "avatarUrls": {
+                            "48x48": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=48",
+                            "24x24": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=24",
+                            "16x16": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=16",
+                            "32x32": "https://avatar-cdn.atlassian.com/5aa96f52989b58639779e66325f190a0?d=mm&s=32"
+                        },
+                        "displayName": "Owen Fellows",
+                        "active": true,
+                        "timeZone": "UTC"
+                    },
+                    "created": "2003-11-17T01:55:10.760+0000",
+                    "updated": "2003-11-17T01:55:10.760+0000"
+                }
+            ],
+            "maxResults": 143,
+            "total": 143,
+            "startAt": 0
+        }
+    }
+}
blob - /dev/null
blob + b7a01c69a5f5e79a2d8b2c6395e5df33c2a157e2 (mode 644)
--- /dev/null
+++ jira/testdata/project/TEST
@@ -0,0 +1,116 @@
+{
+    "expand": "description,lead,issueTypes,url,projectKeys",
+    "self": "http://www.example.com/jira/rest/api/2/project/EX",
+    "id": "10000",
+    "key": "TEST",
+    "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"
+    }
+}