commit b5c1189de8eaf553114d0ab44f79952ff3c15e66 Author: Olivier Tremblay Date: Wed May 11 09:44:12 2016 -0400 Lots of small stuff diff --git a/cmd/jkl/comment.go b/cmd/jkl/comment.go new file mode 100644 index 0000000..bfb313f --- /dev/null +++ b/cmd/jkl/comment.go @@ -0,0 +1,52 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "io" + + "otremblay.com/jkl" +) + +type CommentCmd struct { + args []string + file string + issueKey string +} + +func NewCommentCmd(args []string) (*CommentCmd, error) { + ccmd := &CommentCmd{} + f := flag.NewFlagSet("comments", flag.ExitOnError) + f.StringVar(&ccmd.file, "f", "", "File to get issue comment from") + f.Parse(args) + if len(f.Args()) < 1 { + return nil, ErrNotEnoughArgs + } + ccmd.issueKey = f.Arg(0) + return ccmd, nil +} + +var ErrNotEnoughArgs = errors.New("Not enough arguments") + +func (ccmd *CommentCmd) Comment() error { + var b = bytes.NewBufferString("") + var comment io.Reader + var err error + if ccmd.file != "" { + comment, err = GetTextFromSpecifiedFile(ccmd.file, b) + if err != nil { + return err + } + } else { + comment, err = GetTextFromTmpFile(b) + if err != nil { + return err + } + + } + + io.Copy(b, comment) + + return jkl.AddComment(ccmd.issueKey, b.String()) +} diff --git a/cmd/jkl/create.go b/cmd/jkl/create.go new file mode 100644 index 0000000..716e298 --- /dev/null +++ b/cmd/jkl/create.go @@ -0,0 +1,54 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "os" + + "otremblay.com/jkl" +) + +type CreateCmd struct { + args []string + project string + file string +} + +func NewCreateCmd(args []string) (*CreateCmd, error) { + ccmd := &CreateCmd{project: os.Getenv("JIRA_PROJECT")} + f := flag.NewFlagSet("x", flag.ExitOnError) + f.StringVar(&ccmd.project, "p", "", "Jira project key") + f.StringVar(&ccmd.file, "f", "", "File to get issue description from") + f.Parse(args) + return ccmd, nil +} + +var ErrCcmdJiraProjectRequired = errors.New("Jira project needs to be set") + +func (ccmd *CreateCmd) Create() error { + var b = bytes.NewBufferString(CREATE_TEMPLATE) + var iss *jkl.Issue + var err error + if ccmd.file != "" { + iss, err = GetIssueFromFile(ccmd.file, b) + if err != nil { + return err + } + } else { + iss, err = GetIssueFromTmpFile(b) + if err != nil { + return err + } + + } + if iss.Fields != nil && + (iss.Fields.Project == nil || iss.Fields.Project.Key == "") { + iss.Fields.Project = &jkl.Project{Key: ccmd.project} + } + return jkl.Create(iss) +} + +const CREATE_TEMPLATE = `Issue Type: +Summary: +Description:` diff --git a/cmd/jkl/edit.go b/cmd/jkl/edit.go new file mode 100644 index 0000000..e5ae795 --- /dev/null +++ b/cmd/jkl/edit.go @@ -0,0 +1,59 @@ +package main + +import ( + "bytes" + "flag" + "os" + + "text/template" + + "otremblay.com/jkl" +) + +type EditCmd struct { + args []string + project string + file string +} + +func NewEditCmd(args []string) (*CreateCmd, error) { + ccmd := &CreateCmd{project: os.Getenv("JIRA_PROJECT")} + f := flag.NewFlagSet("x", flag.ExitOnError) + f.StringVar(&ccmd.project, "p", "", "Jira project key") + f.StringVar(&ccmd.file, "f", "filename", "File to get issue description from") + f.Parse(args) + return ccmd, nil +} + +func (ecmd *EditCmd) Edit(taskKey string) error { + b := bytes.NewBuffer(nil) + iss, err := jkl.GetIssue(taskKey) + if err != nil { + return err + } + err = editTmpl.Execute(b, iss) + if err != nil { + return err + } + + if ecmd.file != "" { + iss, err = GetIssueFromFile(ecmd.file, b) + + if err != nil { + return err + } + } else { + iss, err = GetIssueFromTmpFile(b) + if err != nil { + return err + } + + } + iss.Key = taskKey + return jkl.Edit(iss) +} + +const EDIT_TEMPLATE = `Summary: {{.Fields.Summary}} +Description: {{.Fields.Description}}` + +var editTmpl = template.Must(template.New("editTmpl").Parse(EDIT_TEMPLATE)) diff --git a/cmd/jkl/editor.go b/cmd/jkl/editor.go new file mode 100644 index 0000000..ac8a234 --- /dev/null +++ b/cmd/jkl/editor.go @@ -0,0 +1,148 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "regexp" + "strings" + + "reflect" + + "bufio" + + "otremblay.com/jkl" +) + +// def get_editor do +// [System.get_env("EDITOR"), "nano", "vim", "vi"] +// |> Enum.find(nil, fn (ed) -> System.find_executable(ed) != nil end) +// end +var editors = []string{os.Getenv("EDITOR"), "nano", "vim", "vi"} + +// GetEditor returns the path to an editor, taking $EDITOR in account +func GetEditor() string { + for _, ed := range editors { + if p, err := exec.LookPath(ed); err == nil { + return p + } + } + log.Fatal("No editor available; use flags.") + return "" +} + +func copyInitial(dst io.WriteSeeker, initial io.Reader) { + io.Copy(dst, initial) + dst.Seek(0, 0) +} + +func GetIssueFromTmpFile(initial io.Reader) (*jkl.Issue, error) { + f, err := ioutil.TempFile(os.TempDir(), "jkl") + if err != nil { + return nil, err + } + copyInitial(f, initial) + f2, err := GetTextFromFile(f) + if err != nil { + return nil, err + } + return IssueFromFile(f2), nil +} + +func GetTextFromTmpFile(initial io.Reader) (io.Reader, error) { + f, err := ioutil.TempFile(os.TempDir(), "jkl") + if err != nil { + return nil, err + } + copyInitial(f, initial) + return GetTextFromFile(f) +} + +func GetTextFromSpecifiedFile(filename string, initial io.Reader) (io.Reader, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + if fi, err := f.Stat(); err == nil && fi.Size() == 0 { + copyInitial(f, initial) + } + return GetTextFromFile(f) +} + +func GetTextFromFile(file *os.File) (io.Reader, error) { + cmd := exec.Command(GetEditor(), file.Name()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + fmt.Println(err) + return nil, err + } + _, err = file.Seek(0, 0) + return file, err +} + +func GetIssueFromFile(filename string, initial io.Reader) (*jkl.Issue, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + if fi, err := f.Stat(); err == nil && fi.Size() == 0 { + copyInitial(f, initial) + } + f2, err := GetTextFromFile(f) + if err != nil { + return nil, err + } + return IssueFromFile(f2), nil +} + +var spacex = regexp.MustCompile(`\s`) + +func IssueFromFile(f io.Reader) *jkl.Issue { + iss := &jkl.Issue{Fields: &jkl.Fields{}} + riss := reflect.ValueOf(iss).Elem() + fieldsField := riss.FieldByName("Fields").Elem() + currentField := reflect.Value{} + brd := bufio.NewReader(f) + for { + b, _, err := brd.ReadLine() + if err != nil { + break + } + parts := strings.Split(string(b), ":") + potentialField := spacex.ReplaceAllString(parts[0], "") + + if newfield := fieldsField.FieldByName(potentialField); newfield.IsValid() { + parts = parts[1:len(parts)] + if potentialField == "IssueType" { + iss.Fields.IssueType = &jkl.IssueType{} + currentField = reflect.Value{} + f2 := newfield.Elem() + f3 := f2.FieldByName("Name") + f3.SetString(strings.TrimSpace(strings.Join(parts, ":"))) + } else if potentialField == "Project" { + iss.Fields.Project = &jkl.Project{} + currentField = reflect.Value{} + f2 := newfield.Elem() + f3 := f2.FieldByName("Key") + f3.SetString(strings.TrimSpace(strings.Join(parts, ":"))) + } else { + currentField = newfield + } + } + if currentField.IsValid() { + currentField.SetString(strings.TrimSpace(currentField.String() + "\n" + strings.Join(parts, ":"))) + } + } + return iss +} + +func IssueFromList(list []string) *jkl.Issue { + return IssueFromFile(bytes.NewBufferString(strings.Join(list, "\n"))) +} diff --git a/cmd/jkl/editor_test.go b/cmd/jkl/editor_test.go new file mode 100644 index 0000000..35aa64f --- /dev/null +++ b/cmd/jkl/editor_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "strings" + "testing" +) + +func TestIssueFromList(t *testing.T) { + iss := IssueFromList(strings.Split(`Description: Cowboys + from +hell +Issue Type: Sometype +this is ignored +Summary: Dookienator +also ignored`, "\n")) + AssertEqual(t, `Cowboys + from +hell`, iss.Fields.Description) + AssertEqual(t, "Sometype", iss.Fields.IssueType.Name) +} + +func TestSpacex(t *testing.T) { + AssertEqual(t, "Something", spacex.ReplaceAllString("Some thing", "")) +} + +func AssertEqual(t *testing.T, expected interface{}, actual interface{}) { + if expected != actual { + t.Errorf(`Assertation failed! +Asserted: %v +Actual: %v`, expected, actual) + } +} + +func Assert(t *testing.T, fn func() bool) { + +} diff --git a/cmd/jkl/jkl.go b/cmd/jkl/jkl.go new file mode 100644 index 0000000..77f317d --- /dev/null +++ b/cmd/jkl/jkl.go @@ -0,0 +1,63 @@ +package main + +import ( + "flag" + "fmt" + "log" + + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load(".jklrc", fmt.Sprintf("%s/.jklrc", os.Getenv("HOME"))) + if err != nil { + log.Fatalln(err) + } + flag.Parse() + if len(flag.Args()) == 0 { + fmt.Print(usage) + return + } + if err := runcmd(flag.Args()); err != nil { + log.Println(err) + } +} + +func runcmd(args []string) error { + switch args[0] { + case "list": + return List(flag.Args()[1:]) + case "create": + ccmd, err := NewCreateCmd(flag.Args()[1:]) + if err != nil { + return err + } + return ccmd.Create() + case "task": + tcmd := &TaskCmd{} + return tcmd.Handle(flag.Args()[1:]) + case "edit": + ecmd := &EditCmd{} + return ecmd.Edit(flag.Arg(1)) + case "comment": + ccmd, err := NewCommentCmd(flag.Args()[1:]) + if err != nil { + return err + } + return ccmd.Comment() + } + fmt.Println(usage) + return nil +} + +const usage = `Usage: +jkl [options] [args] + +Available commands: + +list +create +edit +` diff --git a/cmd/jkl/list.go b/cmd/jkl/list.go new file mode 100644 index 0000000..f3dde3a --- /dev/null +++ b/cmd/jkl/list.go @@ -0,0 +1,30 @@ +package main + +import ( + "flag" + "os" + "strings" + + "text/template" + + "otremblay.com/jkl" +) + +var listTemplateStr string +var listTemplate *template.Template + +func init() { + flag.StringVar(&listTemplateStr, "listTemplate", "{{.Key}}\t({{.Fields.IssueType.Name}}{{if .Fields.Parent}} of {{.Fields.Parent.Key}}{{end}})\t{{.Fields.Summary}}\n", "Go template used in list command") + listTemplate = template.Must(template.New("listTemplate").Parse(listTemplateStr)) +} + +func List(args []string) error { + if issues, err := jkl.List(strings.Join(args, " ")); err != nil { + return err + } else { + for _, issue := range issues { + listTemplate.Execute(os.Stdout, issue) + } + } + return nil +} diff --git a/cmd/jkl/task.go b/cmd/jkl/task.go new file mode 100644 index 0000000..6fea0aa --- /dev/null +++ b/cmd/jkl/task.go @@ -0,0 +1,28 @@ +package main + +import ( + "errors" + "fmt" + + "otremblay.com/jkl" +) + +type TaskCmd struct{} + +func (t *TaskCmd) Handle(args []string) error { + if len(args) == 1 { + return t.Get(args[0]) + } + return ErrTaskSubCommandNotFound +} + +var ErrTaskSubCommandNotFound = errors.New("Subcommand not found.") + +func (t *TaskCmd) Get(taskKey string) error { + issue, err := jkl.GetIssue(taskKey) + if err != nil { + return err + } + fmt.Println(issue) + return nil +} diff --git a/cmd/jklfs/jklfile.go b/cmd/jklfs/jklfile.go new file mode 100644 index 0000000..64548b3 --- /dev/null +++ b/cmd/jklfs/jklfile.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "os" + "time" +) +import "github.com/hanwen/go-fuse/fuse" +import "github.com/hanwen/go-fuse/fuse/nodefs" +import "io/ioutil" + +func NewJklfsFile() (nodefs.File, error) { + f, err := ioutil.TempFile("", "jklfile") + if err != nil { + return nil, err + } + return &jklfile{f}, nil +} + +type jklfile struct { + *os.File +} + +func (f *jklfile) InnerFile() nodefs.File { + return nil +} + +func (f *jklfile) String() string { + return fmt.Sprintf("jklfile(%s)", f.Name()) +} + +func (f *jklfile) Write(data []byte, off int64) (uint32, fuse.Status) { + n, err := f.File.WriteAt(data, off) + if err != nil { + return fuse.EACCES + } + return uint32(n), fuse.OK +} + +func (f *jklfile) Fsync(flag int) (code fuse.Status) { + return fuse.OK +} + +func (f *jklfile) Truncate(size uint64) fuse.Status { + return fuse.EPERM +} + +func (f *jklfile) Chmod(mode uint32) fuse.Status { + return fuse.EPERM +} + +func (f *jklfile) Chown(uid uint32, gid uint32) fuse.Status { + return fuse.EPERM +} + +func (f *jklfile) Allocate(off uint64, sz uint64, mode uint32) fuse.Status { + return fuse.EPERM +} + +func (f *jklfile) Flush() fuse.Status { + return fuse.OK +} + +func (f *jklfile) GetAttr(out *fuse.Attr) fuse.Status { + return fuse.OK +} + +func (f *jklfile) Read(dest []byte, off int64) (fuse.ReadResult, fuse.Status) { + return nil, fuse.OK +} +func (f *jklfile) Release() { + +} +func (f *jklfile) SetInode(i *nodefs.Inode) {} + +func (f *jklfile) Utimens(atime *time.Time, mtime *time.Time) fuse.Status { + return fuse.EPERM +} diff --git a/cmd/jklfs/jklfs b/cmd/jklfs/jklfs new file mode 100755 index 0000000..7b42802 Binary files /dev/null and b/cmd/jklfs/jklfs differ diff --git a/cmd/jklfs/jklfs.go b/cmd/jklfs/jklfs.go new file mode 100644 index 0000000..735fab5 --- /dev/null +++ b/cmd/jklfs/jklfs.go @@ -0,0 +1,99 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "strings" + + "otremblay.com/jkl" + + "github.com/hanwen/go-fuse/fuse" + "github.com/hanwen/go-fuse/fuse/nodefs" + "github.com/hanwen/go-fuse/fuse/pathfs" +) + +type jklfs struct { + pathfs.FileSystem + issuePerDirs map[string]*jkl.Issue +} + +func (j *jklfs) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) { + switch name { + case "current_sprint": + return &fuse.Attr{ + Mode: fuse.S_IFDIR | 0755, + }, fuse.OK + case "": + return &fuse.Attr{ + Mode: fuse.S_IFDIR | 0755, + }, fuse.OK + } + if _, ok := j.issuePerDirs[name]; ok { + return &fuse.Attr{ + Mode: fuse.S_IFDIR | 0755, + }, fuse.OK + } + pathPieces := strings.Split(name, "/") + path := strings.Join(pathPieces[0:2], "/") + if i, ok := j.issuePerDirs[path]; ok { + if path+"/description" == name { + return &fuse.Attr{ + Mode: fuse.S_IFREG | 0644, Size: uint64(len(i.Fields.Description)), + }, fuse.OK + } + } + return nil, fuse.ENOENT +} + +func (j *jklfs) OpenDir(name string, context *fuse.Context) (c []fuse.DirEntry, code fuse.Status) { + if name == "" { + c = []fuse.DirEntry{{Name: "current_sprint", Mode: fuse.S_IFDIR}} + return c, fuse.OK + } + if name == "current_sprint" { + issues, err := jkl.List("sprint in openSprints() and project = 'DO'") + if err != nil { + fmt.Fprintln(os.Stderr, err) + return nil, fuse.ENOENT + } + c = make([]fuse.DirEntry, len(issues)) + for i, issue := range issues { + c[i] = fuse.DirEntry{Name: issue.Key, Mode: fuse.S_IFDIR} + j.issuePerDirs["current_sprint/"+issue.Key] = issue + } + return c, fuse.OK + } + + if _, ok := j.issuePerDirs[name]; ok { + c = []fuse.DirEntry{fuse.DirEntry{Name: "description", Mode: fuse.S_IFREG}} + return c, fuse.OK + } + + return nil, fuse.ENOENT +} + +func (j *jklfs) Open(name string, flags uint32, context *fuse.Context) (file nodefs.File, code fuse.Status) { + pathPieces := strings.Split(name, "/") + path := strings.Join(pathPieces[0:2], "/") + if i, ok := j.issuePerDirs[path]; ok { + if path+"/description" == name { + return nodefs.NewDataFile([]byte(i.Fields.Description)), fuse.OK + } + } + return nil, fuse.ENOENT +} + +func main() { + flag.Parse() + if len(flag.Args()) < 1 { + log.Fatal("Usage:\n jklfs MOUNTPOINT") + } + nfs := pathfs.NewPathNodeFs(&jklfs{pathfs.NewDefaultFileSystem(), map[string]*jkl.Issue{}}, nil) + server, _, err := nodefs.MountRoot(flag.Arg(0), nfs.Root(), nil) + if err != nil { + log.Fatalf("Mount fail: %v\n", err) + } + server.Serve() +} diff --git a/issue.go b/issue.go new file mode 100644 index 0000000..2123309 --- /dev/null +++ b/issue.go @@ -0,0 +1,68 @@ +package jkl + +import ( + "bytes" + "log" + "text/template" +) + +type Search struct { + Issues []*Issue `json:"issues"` +} + +type IssueType struct { + Name string `json:"name"` +} +type Project struct { + Key string `json:"key,omitempty"` +} + +type Author struct { + Name string + DisplayName string +} + +type Comment struct { + Author *Author + Body string +} + +type CommentColl struct { + Comments []Comment +} + +type Fields struct { + *IssueType `json:"issuetype,omitempty"` + Project *Project `json:"project,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Comment *CommentColl `json:"comment,omitempty"` + Parent *Issue `json:",omitempty"` +} +type Issue struct { + Key string `json:"key,omitempty"` + Fields *Fields `json:"fields"` +} + +func (i *Issue) String() string { + var b = bytes.NewBuffer(nil) + err := issueTmpl.Execute(b, i) + if err != nil { + log.Fatalln(err) + } + + return b.String() +} + +var commentTemplate = `{{if .Fields.Comment }}{{range .Fields.Comment.Comments}}{{.Author.DisplayName}}: +----------------- +{{.Body}} +----------------- + +{{end}}{{end}}` + +var issueTmplTxt = "\x1b[1m{{.Key}}\x1b[0m\t[{{.Fields.IssueType.Name}}]\t{{.Fields.Summary}}\n\n" + + "\x1b[1mDescription:\x1b[0m {{.Fields.Description}} \n\n" + + "\x1b[1mComments:\x1b[0m\n\n" + commentTemplate + +var issueTmpl = template.Must(template.New("issueTmpl").Parse(issueTmplTxt)) diff --git a/jiraclient.go b/jiraclient.go new file mode 100644 index 0000000..1554b59 --- /dev/null +++ b/jiraclient.go @@ -0,0 +1,66 @@ +package jkl + +import ( + "io" + "net/http" + "net/http/cookiejar" + "net/url" + "os" +) + +var j, _ = cookiejar.New(nil) + +var httpClient *JiraClient + +type JiraClient struct { + *http.Client + jiraRoot string +} + +func NewJiraClient(jiraRoot string) *JiraClient { + j := &JiraClient{ + &http.Client{ + Jar: j, + }, + jiraRoot, + } + if j.jiraRoot == "" { + j.jiraRoot = os.Getenv("JIRA_ROOT") + } + return j +} + +func (j *JiraClient) Do(req *http.Request) (*http.Response, error) { + var err error + req.SetBasicAuth(os.Getenv("JIRA_USER"), os.Getenv("JIRA_PASSWORD")) + req.Header.Add("Content-Type", "application/json") + req.URL, err = url.Parse(j.jiraRoot + "rest/" + req.URL.RequestURI()) + if err != nil { + return nil, err + } + return j.Client.Do(req) +} + +func (j *JiraClient) Put(path string, payload io.Reader) (*http.Response, error) { + req, err := http.NewRequest("PUT", path, payload) + if err != nil { + return nil, err + } + return j.Do(req) +} + +func (j *JiraClient) Post(path string, payload io.Reader) (*http.Response, error) { + req, err := http.NewRequest("POST", path, payload) + if err != nil { + return nil, err + } + return j.Do(req) +} + +func (j *JiraClient) Get(path string) (*http.Response, error) { + req, err := http.NewRequest("GET", path, nil) + if err != nil { + return nil, err + } + return j.Do(req) +} diff --git a/jkl.go b/jkl.go new file mode 100644 index 0000000..4557063 --- /dev/null +++ b/jkl.go @@ -0,0 +1,130 @@ +package jkl + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" +) + +var defaultIssue = &Issue{} + +func bootHttpClient() { + if httpClient == nil { + httpClient = NewJiraClient("") + } +} + +func Create(issue *Issue) error { + bootHttpClient() + payload, err := formatPayload(issue) + if err != nil { + return err + } + fmt.Println(issue) + resp, err := httpClient.Post("api/2/issue", payload) + if err != nil { + fmt.Println(resp.StatusCode) + return err + } + if resp.StatusCode >= 400 { + io.Copy(os.Stderr, resp.Body) + } + return nil +} + +func Edit(issue *Issue) error { + bootHttpClient() + payload, err := formatPayload(issue) + if err != nil { + return err + } + resp, err := httpClient.Put("api/2/issue/"+issue.Key, payload) + if err != nil { + fmt.Println(resp.StatusCode) + return err + } + if resp.StatusCode >= 400 { + io.Copy(os.Stderr, resp.Body) + } + return nil +} + +func List(jql string) ([]*Issue, error) { + bootHttpClient() + path := "api/2/search?fields=*all&maxResults=1000" + if jql != "" { + path += "&jql=" + url.QueryEscape(jql) + } + resp, err := httpClient.Get(path) + if err != nil { + fmt.Println(err) + return nil, err + } + dec := json.NewDecoder(resp.Body) + var issues = &Search{} + err = dec.Decode(issues) + if err != nil { + b, _ := ioutil.ReadAll(resp.Body) + fmt.Println(string(b)) + return nil, err + } + return issues.Issues, nil +} + +func GetIssue(taskKey string) (*Issue, error) { + bootHttpClient() + path := "api/2/issue/" + taskKey + resp, err := httpClient.Get(path) + if err != nil { + fmt.Println(resp.StatusCode) + return nil, err + } + dec := json.NewDecoder(resp.Body) + var issue = &Issue{} + err = dec.Decode(issue) + if err != nil { + b, _ := ioutil.ReadAll(resp.Body) + fmt.Println(string(b)) + return nil, err + } + return issue, nil +} + +func AddComment(taskKey string, comment string) error { + bootHttpClient() + var b []byte + payload := bytes.NewBuffer(b) + enc := json.NewEncoder(payload) + enc.Encode(map[string]string{"body": comment}) + resp, err := httpClient.Post("api/2/issue/"+taskKey+"/comment", payload) + if err != nil { + fmt.Println(resp.StatusCode) + return err + } + if resp.StatusCode >= 400 { + io.Copy(os.Stderr, resp.Body) + } + return nil +} + +func formatPayload(issue *Issue) (io.Reader, error) { + if issue.Fields != nil && + issue.Fields.Project != nil && + issue.Fields.Project.Key == "" { + issue.Fields.Project.Key = os.Getenv("JIRA_PROJECT") + } + var b []byte + payload := bytes.NewBuffer(b) + enc := json.NewEncoder(payload) + err := enc.Encode(issue) + fmt.Println(payload.String()) + if err != nil { + fmt.Println(err) + return nil, err + } + return payload, nil +}