diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 0000000..c1fa090 --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,12 @@ +{ + "ImportPath": "github.com/avanier/jkl", + "GoVersion": "go1.8", + "GodepVersion": "v79", + "Deps": [ + { + "ImportPath": "github.com/joho/godotenv", + "Comment": "v1.1-23-gc9360df", + "Rev": "c9360df4d16dc0e391ea2f28da2d31a9ede2e26f" + } + ] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 0000000..4cdaa53 --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/README.md b/README.md index 199076b..eb6d017 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,14 @@ # jkl -jkl is a library for programmatically interacting with a JIRA installation. It -comes with a command line program (also called `jkl`) which allows you to -interact with JIRA via the command line. + +TODO: Write a project description ## Installation -To use the library, simply import it into your application: -`import "github.com/otremblay/jkl"` -To install the command line application: -First, make sure you have a working go environment: -https://golang.org/doc/install - -Then, execute the following command from your shell: - -`$ go get github.com/otremblay/jkl/cmd/jkl` +TODO: Describe the installation process ## Usage -Make sure you create a `~/.jklrc` file in your home directory, it should contain -at a minimum: - -``` -JIRA_ROOT="https://jira.example.com/" -JIRA_USER="myusername" -JIRA_PASSWORD="mypassword" -JIRA_PROJECT="DPK" -``` -Those values are for example only, your setup will be different. - -TODO: Finish writing usage instructions +TODO: Write usage instructions ## Contributing diff --git a/cmd/jkl/assign.go b/cmd/jkl/assign.go deleted file mode 100644 index d498b63..0000000 --- a/cmd/jkl/assign.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "errors" - "flag" - - "otremblay.com/jkl" -) - -type AssignCmd struct { - args []string - assignee string - issueKey string -} - -func NewAssignCmd(args []string) (*AssignCmd, error) { - ccmd := &AssignCmd{} - f := flag.NewFlagSet("assign", flag.ExitOnError) - f.Parse(args) - if len(f.Args()) < 2 { - return nil, ErrAssignNotEnoughArgs - } - ccmd.issueKey = f.Arg(0) - ccmd.assignee = f.Arg(1) - return ccmd, nil -} - -var ErrAssignNotEnoughArgs = errors.New("Not enough arguments, need issue key + assignee") - -func (ccmd *AssignCmd) Assign() error { - return jkl.Assign(ccmd.issueKey, ccmd.assignee) -} - -func (ccmd *AssignCmd) Run() error { - return ccmd.Assign() -} diff --git a/cmd/jkl/attach.go b/cmd/jkl/attach.go deleted file mode 100644 index c62f0a1..0000000 --- a/cmd/jkl/attach.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "flag" - - "otremblay.com/jkl" -) - -type AttachCmd struct { - args []string - file string - taskKey string -} - -func NewAttachCmd(args []string) (*AttachCmd, error) { - ccmd := &AttachCmd{} - f := flag.NewFlagSet("x", flag.ExitOnError) - f.Parse(args) - if len(f.Args()) < 2 { - return nil, ErrNotEnoughArgs - } - ccmd.taskKey = f.Arg(0) - ccmd.file = f.Arg(1) - return ccmd, nil -} - -func (ecmd *AttachCmd) Attach() error { - return jkl.Attach(ecmd.taskKey, ecmd.file) -} - -func (ecmd *AttachCmd) Run() error { - return ecmd.Attach() -} diff --git a/cmd/jkl/create.go b/cmd/jkl/create.go index 0306cb8..d36e0c7 100644 --- a/cmd/jkl/create.go +++ b/cmd/jkl/create.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "strings" "text/template" "otremblay.com/jkl" @@ -18,6 +17,7 @@ type CreateCmd struct { project string file string issuetype string + silent bool } func NewCreateCmd(args []string) (*CreateCmd, error) { @@ -44,7 +44,7 @@ func (ccmd *CreateCmd) Create() error { } } - + if ccmd.project == "" { return ErrCcmdJiraProjectRequired } @@ -56,22 +56,13 @@ func (ccmd *CreateCmd) Create() error { if err != nil { fmt.Fprintln(os.Stderr, fmt.Sprintf("Error getting the CreateMeta for project [%s] and issue types [%s]", ccmd.project, isstype), err) } - + if !readfile { createTemplate.Execute(b, cm) } var iss *jkl.JiraIssue // TODO: Evil badbad don't do this. - var isst = cm.Projects[0].IssueTypes[0].Fields - for _, v := range cm.Projects[0].IssueTypes { - if strings.ToLower(isstype) == strings.ToLower(v.Name) { - isst = v.Fields - break - } - } - - em := &jkl.EditMeta{Fields: isst} - + em := &jkl.EditMeta{Fields: cm.Projects[0].IssueTypes[0].Fields} if ccmd.file != "" { iss, err = GetIssueFromFile(ccmd.file, b, em) if err != nil { diff --git a/cmd/jkl/edit.go b/cmd/jkl/edit.go index 0e3ff3b..c467433 100644 --- a/cmd/jkl/edit.go +++ b/cmd/jkl/edit.go @@ -3,6 +3,7 @@ package main import ( "bytes" "flag" + "fmt" "os" "text/template" @@ -31,7 +32,7 @@ func (ecmd *EditCmd) Edit() error { b := bytes.NewBuffer(nil) iss, err := jkl.GetIssue(ecmd.taskKey) if err != nil { - return err + return fmt.Errorf("Edit failed: %v", err) } err = editTmpl.Execute(b, iss) if err != nil { @@ -42,12 +43,12 @@ func (ecmd *EditCmd) Edit() error { iss, err = GetIssueFromFile(ecmd.file, b, iss.EditMeta) if err != nil { - return err + return fmt.Errorf("Error getting issue from file: %v", err) } } else { iss, err = GetIssueFromTmpFile(b, iss.EditMeta) if err != nil { - return err + return fmt.Errorf("Error getting issue from temp file: %v", err) } } diff --git a/cmd/jkl/editor.go b/cmd/jkl/editor.go index 9c59ea5..3d45bb0 100644 --- a/cmd/jkl/editor.go +++ b/cmd/jkl/editor.go @@ -5,10 +5,12 @@ import ( "fmt" "io" "io/ioutil" + "log" "os" "os/exec" "regexp" "strings" + "unicode" "reflect" @@ -21,21 +23,16 @@ import ( // [System.get_env("EDITOR"), "nano", "vim", "vi"] // |> Enum.find(nil, fn (ed) -> System.find_executable(ed) != nil end) // end -var editors = []string{"nano", "vim", "vi"} +var editors = []string{os.Getenv("EDITOR"), "nano", "vim", "vi"} // GetEditor returns the path to an editor, taking $EDITOR in account func GetEditor() string { - if ed := os.Getenv("EDITOR"); ed != "" { - return ed - } - if ed := os.Getenv("VISUAL"); ed != "" { - return ed - } for _, ed := range editors { if p, err := exec.LookPath(ed); err == nil { return p } } + log.Fatal("No editor available; use flags.") return "" } @@ -47,12 +44,12 @@ func copyInitial(dst io.WriteSeeker, initial io.Reader) { func GetIssueFromTmpFile(initial io.Reader, editMeta *jkl.EditMeta) (*jkl.JiraIssue, error) { f, err := ioutil.TempFile(os.TempDir(), "jkl") if err != nil { - return nil, err + return nil, fmt.Errorf("Error opening tempfile: %v", err) } copyInitial(f, initial) f2, err := GetTextFromFile(f) if err != nil { - return nil, err + return nil, fmt.Errorf("Error reading tempfile: %v", err) } return IssueFromReader(f2, editMeta), nil } @@ -78,16 +75,19 @@ func GetTextFromSpecifiedFile(filename string, initial io.Reader) (io.Reader, er } 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 + var err error + if !*SilentMode { + 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) } - _, err = file.Seek(0, 0) return file, err } @@ -109,10 +109,11 @@ func GetIssueFromFile(filename string, initial io.Reader, editMeta *jkl.EditMeta var spacex = regexp.MustCompile(`\s`) func IssueFromReader(f io.Reader, editMeta *jkl.EditMeta) *jkl.JiraIssue { - iss := &jkl.JiraIssue{Fields: &jkl.Fields{ExtraFields: map[string]interface{}{}}} + iss := &jkl.JiraIssue{Fields: &jkl.Fields{}} riss := reflect.ValueOf(iss).Elem() fieldsField := riss.FieldByName("Fields").Elem() currentField := reflect.Value{} + currFieldName := "" brd := bufio.NewReader(f) for { b, _, err := brd.ReadLine() @@ -136,6 +137,7 @@ func IssueFromReader(f io.Reader, editMeta *jkl.EditMeta) *jkl.JiraIssue { if len(parts) > 0 { iss.Fields.IssueType = &jkl.IssueType{} currentField = reflect.Value{} + currFieldName = potentialField f2 := newfield.Elem() f3 := f2.FieldByName("Name") f3.SetString(strings.TrimSpace(strings.Join(parts, ":"))) @@ -144,6 +146,7 @@ func IssueFromReader(f io.Reader, editMeta *jkl.EditMeta) *jkl.JiraIssue { if len(parts) > 0 { iss.Fields.Project = &jkl.Project{} currentField = reflect.Value{} + currFieldName = potentialField f2 := newfield.Elem() f3 := f2.FieldByName("Key") f3.SetString(strings.TrimSpace(strings.Join(parts, ":"))) @@ -152,47 +155,29 @@ func IssueFromReader(f io.Reader, editMeta *jkl.EditMeta) *jkl.JiraIssue { if len(parts) > 0 { iss.Fields.Parent = &jkl.JiraIssue{} currentField = reflect.Value{} + currFieldName = potentialField f2 := newfield.Elem() f3 := f2.FieldByName("Key") f3.SetString(strings.TrimSpace(strings.Join(parts, ":"))) } } else { + currFieldName = potentialField currentField = newfield } } else if editMeta != nil { // If it's not valid, throw it at the createmeta. It will probably end up in ExtraFields. - val := strings.TrimSpace(strings.Join(parts[1:], ":")) - for fieldname, m := range editMeta.Fields { - var something interface{} = val - if strings.ToLower(m.Name) == strings.ToLower(potentialField) { - name := fieldname - for _, av := range m.AllowedValues { - if strings.ToLower(av.Name) == strings.ToLower(val) { - something = av - break - } - } - if m.Schema.CustomId > 0 { - name = fmt.Sprintf("custom_%d", m.Schema.CustomId) - } - iss.Fields.ExtraFields[name] = something - - break - } - } } if currentField.IsValid() { - newpart := strings.Join(parts, ":") - newvalue := currentField.String() + "\n" + newpart - if strings.TrimSpace(newpart) != "" { - newvalue = strings.TrimSpace(newvalue) + newString := currentField.String() + "\n" + strings.Join(parts, ":") + if currFieldName != "Description" { + newString = strings.TrimSpace(newString) + } else if currentField.String() == "" { + newString = strings.TrimLeftFunc(newString, unicode.IsSpace) } - currentField.SetString(newvalue) + currentField.SetString(newString) } } - - iss.EditMeta = editMeta return iss } diff --git a/cmd/jkl/editor_test.go b/cmd/jkl/editor_test.go index b9a9745..4ca6988 100644 --- a/cmd/jkl/editor_test.go +++ b/cmd/jkl/editor_test.go @@ -13,6 +13,7 @@ Issue Type: Sometype this is ignored Summary: Dookienator also ignored`, "\n"), nil) + AssertEqual(t, `Cowboys from hell`, iss.Fields.Description) @@ -34,3 +35,13 @@ Actual: %v`, expected, actual) func Assert(t *testing.T, fn func() bool) { } + +func TestIssueFromFile(t *testing.T) { + *SilentMode = true + issue, err := GetIssueFromFile("editor_test_file.issue", nil, nil) + if err != nil { + t.Errorf("Apparently we effed up some! --> %v", err) + } + AssertEqual(t, "Dookie", issue.Fields.Summary) + AssertEqual(t, "McDoogal", issue.Fields.Description) +} diff --git a/cmd/jkl/editor_test_file.issue b/cmd/jkl/editor_test_file.issue new file mode 100644 index 0000000..d558aee --- /dev/null +++ b/cmd/jkl/editor_test_file.issue @@ -0,0 +1,2 @@ +Summary: Dookie +Description: McDoogal \ No newline at end of file diff --git a/cmd/jkl/flag.go b/cmd/jkl/flag.go deleted file mode 100644 index d49e1b5..0000000 --- a/cmd/jkl/flag.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "errors" - "flag" - - "otremblay.com/jkl" -) - -type FlagCmd struct { - args []string - flg bool -} - -func NewFlagCmd(args []string, flg bool) (*FlagCmd, error) { - ccmd := &FlagCmd{flg: flg} - f := flag.NewFlagSet("flag", flag.ExitOnError) - f.Parse(args) - if len(f.Args()) < 1 { - return nil, ErrFlagNotEnoughArgs - } - ccmd.args = f.Args() - return ccmd, nil -} - -var ErrFlagNotEnoughArgs = errors.New("Not enough arguments, need at least one issue key") - -func (ccmd *FlagCmd) Flag() error { - return jkl.FlagIssue(ccmd.args, ccmd.flg) -} - -func (ccmd *FlagCmd) Run() error { - return ccmd.Flag() -} diff --git a/cmd/jkl/jkl.go b/cmd/jkl/jkl.go index 68065bc..0df6212 100644 --- a/cmd/jkl/jkl.go +++ b/cmd/jkl/jkl.go @@ -14,6 +14,7 @@ import ( var verbose = flag.Bool("v", false, "Output debug information about jkl") var help = flag.Bool("h", false, "Outputs usage information message") +var SilentMode = flag.Bool("s", false, "Silent execution.") func main() { jkl.FindRCFile() @@ -55,22 +56,12 @@ func getCmd(args []string, depth int) (Runner, error) { case "edit": return NewEditCmd(args[1:]) case "comment": - if strings.Contains(strings.Join(args, ""), jkl.CommentIdSeparator) { + if strings.Contains(strings.Join(args,""),jkl.CommentIdSeparator){ return NewEditCommentCmd(args[1:]) } return NewCommentCmd(args[1:]) case "edit-comment": return NewEditCommentCmd(args[1:]) - case "assign": - return NewAssignCmd(args[1:]) - case "flag": - return NewFlagCmd(args[1:], true) - case "unflag": - return NewFlagCmd(args[1:], false) - case "link": - return NewLinkCmd(args[1:]) - case "attach": - return NewAttachCmd(args[1:]) default: // Think about this real hard. // I want `jkl JIRA-1234 done` to move it to done. @@ -102,10 +93,9 @@ func getCmd(args []string, depth int) (Runner, error) { return nil, ErrTaskSubCommandNotFound } -var verbs = []string{"list", "create", "task", "edit", "comment", "edit-comment", "attach"} - -func init() { - sort.Strings(verbs) +var verbs = []string{"list", "create", "task", "edit", "comment","edit-comment"} +func init(){ +sort.Strings(verbs) } const usage = `Usage: diff --git a/cmd/jkl/link.go b/cmd/jkl/link.go deleted file mode 100644 index 76b7d00..0000000 --- a/cmd/jkl/link.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "errors" - "flag" - - "otremblay.com/jkl" -) - -type LinkCmd struct { - args []string -} - -func NewLinkCmd(args []string) (*LinkCmd, error) { - ccmd := &LinkCmd{} - f := flag.NewFlagSet("Link", flag.ExitOnError) - f.Parse(args) - ccmd.args = f.Args() - return ccmd, nil -} - -var ErrLinkNotEnoughArgs = errors.New("Not enough arguments, need at least two issue keys and a reason") - -func (ccmd *LinkCmd) Link() error { - return jkl.LinkIssue(ccmd.args) -} - -func (ccmd *LinkCmd) Run() error { - return ccmd.Link() -} \ No newline at end of file diff --git a/cmd/jkl/list.go b/cmd/jkl/list.go index 18e193e..b5c5a61 100644 --- a/cmd/jkl/list.go +++ b/cmd/jkl/list.go @@ -21,9 +21,6 @@ func (l *listissue) URL() string { } func (l *listissue) Color() string { - if l.Fields == nil || l.Fields.Status == nil { - return "" - } if os.Getenv("JKLNOCOLOR") == "true" || !terminal.IsTerminal(int(os.Stdout.Fd())) { return "" } @@ -42,6 +39,9 @@ func (l *listissue) Color() string { } return "" } +func (l *listissue) EFByName(name string) string { + return (*jkl.JiraIssue)(l).EFByName(name) +} type ListCmd struct { args []string @@ -55,7 +55,7 @@ func NewListCmd(args []string) (*ListCmd, error) { if *verbose { fmt.Println(&ccmd.tmplstr) } - f.StringVar(&ccmd.tmplstr, "listTemplate", "{{.Color}}{{.Key}}{{if .Color}}\x1b[39m{{end}}\t({{if .Fields.IssueType}}{{.Fields.IssueType.Name}}{{end}}{{if .Fields.Parent}} of {{.Fields.Parent.Key}}{{end}})\t{{.Fields.Summary}}\t{{if .Fields.Assignee}}[{{.Fields.Assignee.Name}}]{{end}}\n", "Go template used in list command") + f.StringVar(&ccmd.tmplstr, "listTemplate", "{{.Color}}{{.Key}}{{if .Color}}\x1b[39m{{end}}\t({{.Fields.IssueType.Name}}{{if .Fields.Parent}} of {{.Fields.Parent.Key}}{{end}})\t{{.Fields.Summary}}\t{{if .Fields.Assignee}}[{{.Fields.Assignee.Name}}]{{end}}\n", "Go template used in list command") f.Parse(args) ccmd.args = f.Args() if len(ccmd.args) == 0 { @@ -79,7 +79,7 @@ func (l *ListCmd) List() error { if issues, err := jkl.List(strings.Join(l.args, " ")); err != nil { return err } else { - for _, issue := range issues { + for issue := range issues { var li listissue li = listissue(*issue) err := l.tmpl.Execute(os.Stdout, &li) diff --git a/cmd/jkl/task.go b/cmd/jkl/task.go index a24ccaa..b9279a3 100644 --- a/cmd/jkl/task.go +++ b/cmd/jkl/task.go @@ -19,11 +19,11 @@ func (t *TaskCmd) Handle() error { return t.Get(t.args[0]) } if c == 2 { - // fmt.Println(t.args) + fmt.Println(t.args) err := t.Transition(t.args[0], t.args[1]) if err != nil { - //fmt.Println(err) - return t.Log(t.args[0], strings.Join(t.args[1:], " ")) + fmt.Println(err) + return t.Log(t.args[0], strings.Join(t.args[1:]," ")) } } return ErrTaskSubCommandNotFound @@ -45,7 +45,7 @@ func (t *TaskCmd) Transition(taskKey, transition string) error { } func (t *TaskCmd) Log(taskKey, time string) error { - return jkl.LogWork(taskKey, time) +return jkl.LogWork(taskKey, time) } func (t *TaskCmd) Run() error { diff --git a/cmd/jklfs/jklfile.go b/cmd/jklfs/jklfile.go index e34d3fc..ad5bbc3 100644 --- a/cmd/jklfs/jklfile.go +++ b/cmd/jklfs/jklfile.go @@ -32,7 +32,7 @@ func (f *jklfile) String() string { func (f *jklfile) Write(data []byte, off int64) (uint32, fuse.Status) { n, err := f.File.WriteAt(data, off) if err != nil { - return uint32(0), fuse.EACCES + return uint32(0),fuse.EACCES } return uint32(n), fuse.OK } @@ -76,7 +76,3 @@ func (f *jklfile) SetInode(i *nodefs.Inode) {} func (f *jklfile) Utimens(atime *time.Time, mtime *time.Time) fuse.Status { return fuse.EPERM } - -func (f *jklfile) Flock(flags int) fuse.Status { - return fuse.ENOSYS -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 7da4219..0000000 --- a/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module github.com/otremblay/jkl - -go 1.13 - -require ( - github.com/hanwen/go-fuse v0.0.0-20170609101909-5690be47d614 - github.com/joho/godotenv v1.2.0 - golang.org/x/crypto v0.0.0-20171019172325-541b9d50ad47 - golang.org/x/sys v0.0.0-20171017063910-8dbc5d05d6ed -) diff --git a/go.sum b/go.sum deleted file mode 100644 index ef396af..0000000 --- a/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/hanwen/go-fuse v0.0.0-20170609101909-5690be47d614/go.mod h1:4ZJ05v9yt5k/mcFkGvSPKJB5T8G/6nuumL63ZqlrPvI= -github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -golang.org/x/crypto v0.0.0-20171019172325-541b9d50ad47/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/sys v0.0.0-20171017063910-8dbc5d05d6ed/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/issue.go b/issue.go index ba51508..6e6d596 100644 --- a/issue.go +++ b/issue.go @@ -15,7 +15,9 @@ import ( ) type Search struct { - Issues []*JiraIssue `json:"issues"` + Issues []*JiraIssue `json:"issues"` + Total int + MaxResults int } type IssueType struct { @@ -38,10 +40,9 @@ func (it *IssueType) RangeFieldSpecs() string { } type AllowedValue struct { - Id string `json:"id"` - Self string `json:"self"` - Value string `json:"value"` - Name string `json:"name"` + Id string + Self string + Value string } func (a *AllowedValue) String() string { @@ -156,14 +157,7 @@ func (f *Fields) UnmarshalJSON(b []byte) error { return err } f.rawExtraFields = map[string]json.RawMessage{} - if reflect.ValueOf(f) == reflect.Zero(reflect.TypeOf(f)) { - fmt.Println("wtf") - } - v := reflect.ValueOf(f) - if !v.IsValid() { - fmt.Println("What all the fucks") - } - vf := v.Elem() + vf := reflect.ValueOf(f).Elem() for key, mess := range f.rawFields { field := vf.FieldByNameFunc(func(s string) bool { return strings.ToLower(key) == strings.ToLower(s) }) if field.IsValid() { @@ -174,11 +168,8 @@ func (f *Fields) UnmarshalJSON(b []byte) error { fmt.Fprintln(os.Stderr, objType, obj, string(mess)) fmt.Fprintln(os.Stderr, errors.New(fmt.Sprintf("%s [%s]: %s", "Error allocating field", key, err))) } - val := reflect.ValueOf(obj) - if val == reflect.Zero(reflect.TypeOf(val)) || !val.IsValid() { - field.Set(reflect.Zero(objType)) - } else { - field.Set(val.Elem()) + if obj != nil && !(reflect.ValueOf(obj) == reflect.Zero(reflect.TypeOf(obj))) { + field.Set(reflect.ValueOf(obj).Elem()) } } else { f.rawExtraFields[key] = mess @@ -253,91 +244,6 @@ type JiraIssue struct { var sprintRegexp = regexp.MustCompile(`name=([^,]+),`) -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Ptr: - return v.IsNil() || isEmptyValue(reflect.Indirect(v)) - } - return false -} - -func (i *JiraIssue) MarshalJSON() ([]byte, error) { - fields := map[string]interface{}{} - vf := reflect.ValueOf(*(i.Fields)) - if i.EditMeta != nil && i.EditMeta.Fields != nil { - for k, f := range i.EditMeta.Fields { - name := k - if f.Schema.CustomId > 0 { - name = fmt.Sprintf("custom_%d", f.Schema.CustomId) - } - if val, ok := i.Fields.ExtraFields[name]; ok && val != nil { - if f.Schema.Type == "array" { - fields[name] = []interface{}{val} - continue - } - fields[name] = val - } else if val == nil { - delete(fields, name) - } - } - } - - for i := 0; i < vf.NumField(); i++ { - ft := vf.Type().Field(i) - fv := vf.Field(i) - - if ft.Name != "ExtraFields" && (fv.CanSet() || fv.CanInterface() || fv.CanAddr()) && fv.IsValid() { - name := strings.ToLower(ft.Name) - if alias, ok := ft.Tag.Lookup("json"); ok { - if alias != "" { - name = strings.Split(alias, ",")[0] - } - } - value := fv.Interface() - if value != nil { - fields[name] = value - - } else { - delete(fields, name) - } - } - } - fmt.Println(fields) - for k, v := range fields { - v2 := reflect.ValueOf(v) - for { - if v2.Kind() != reflect.Ptr && v2.Kind() != reflect.Interface { - break - } - v2 = v2.Elem() - } - if !v2.IsValid() || (v2.Kind() == reflect.Slice && v2.Len() == 0) { - delete(fields, k) - } - } - em := i.EditMeta - i.EditMeta = nil - defer func() { i.EditMeta = em }() - type Alias JiraIssue - return json.Marshal(&struct { - *Alias - Fields map[string]interface{} `json:"fields,omitempty"` - }{ - Fields: fields, - Alias: (*Alias)(i), - }) -} - func (i *JiraIssue) UnmarshalJSON(b []byte) error { tmp := map[string]json.RawMessage{} if len(b) == 0 { @@ -349,14 +255,8 @@ func (i *JiraIssue) UnmarshalJSON(b []byte) error { if err != nil && *Verbose { fmt.Fprintln(os.Stderr, errors.New(fmt.Sprintf("%s: %s", "Error unpacking raw json", err))) } - if _, ok := tmp["fields"]; !ok { - fmt.Fprintln(os.Stderr, "Received no fields? wtf?") - fmt.Fprintln(os.Stderr, string(b)) - os.Exit(1) - } err = json.Unmarshal(tmp["fields"], &i.Fields) if err != nil && *Verbose { - fmt.Println(string(tmp["fields"])) fmt.Fprintln(os.Stderr, errors.New(fmt.Sprintf("%s: %s", "Error unpacking fields", err))) } err = json.Unmarshal(tmp["transitions"], &i.Transitions) @@ -406,7 +306,7 @@ func (i *JiraIssue) UnmarshalJSON(b []byte) error { fallthrough default: if string(v) != "null" { - i.Fields.ExtraFields[k] = strings.Replace(string(v), "\\r\\n", "\n", -1) + i.Fields.ExtraFields[k] = string(v) } } } @@ -426,9 +326,6 @@ func (i *JiraIssue) String() string { if os.Getenv("JKLNOCOLOR") == "true" { tmpl = issueTmplNoColor } - if customTmpl := os.Getenv("JKL_ISSUE_TMPL"); customTmpl != "" { - tmpl = template.Must(template.New("customIssueTmpl").Parse(customTmpl)) - } err := tmpl.Execute(b, i) if err != nil { log.Fatalln(err) @@ -437,6 +334,20 @@ func (i *JiraIssue) String() string { return b.String() } +func (i *JiraIssue) EFByName(name string) string { + for k, f := range i.EditMeta.Fields { + if f.Name == name { + return fmt.Sprint(i.Fields.ExtraFields[k]) + } + } + + return "" +} + +func (i *JiraIssue) StoryPoints() string { + return i.EFByName("Story Points") +} + func (i *JiraIssue) PrintExtraFields() string { sorter := map[string]string{} b := bytes.NewBuffer(nil) @@ -489,5 +400,15 @@ var issueTmplNoColorTxt = "{{.Key}}\t{{if .Fields.IssueType}}[{{.Fields.IssueTyp "Worklog:\n{{range .Fields.Worklog.Worklogs}}\t{{.}}\n{{end}}" var CommentIdSeparator = "~" -var issueTmpl = template.Must(template.New("issueTmpl").Parse(issueTmplTxt)) -var issueTmplNoColor = template.Must(template.New("issueTmplNoColor").Parse(issueTmplNoColorTxt)) +var issueTmpl, issueTmplNoColor *template.Template + +func SetupTmpl() { + isstmpl := os.Getenv("JKL_ISSUE_TMPL") + if isstmpl != "" { + issueTmpl = template.Must(template.New("issueTmpl").Parse(isstmpl)) + issueTmplNoColor = issueTmpl + } else { + issueTmpl = template.Must(template.New("issueTmpl").Parse(issueTmplTxt)) + issueTmplNoColor = template.Must(template.New("issueTmplNoColor").Parse(issueTmplNoColorTxt)) + } +} diff --git a/jiraclient.go b/jiraclient.go index 434dba8..ea9a3ed 100644 --- a/jiraclient.go +++ b/jiraclient.go @@ -1,8 +1,6 @@ package jkl import ( - "bytes" - "encoding/json" "errors" "fmt" "io" @@ -11,8 +9,6 @@ import ( "net/http/cookiejar" "net/url" "os" - "strings" - "time" ) var j, _ = cookiejar.New(nil) @@ -24,84 +20,39 @@ type JiraClient struct { jiraRoot string } -func init() { +func init(){ x := false Verbose = &x } func NewJiraClient(jiraRoot string) *JiraClient { - jc := &JiraClient{ + j := &JiraClient{ &http.Client{ Jar: j, }, jiraRoot, } - if jc.jiraRoot == "" { - jc.jiraRoot = os.Getenv("JIRA_ROOT") + if j.jiraRoot == "" { + j.jiraRoot = os.Getenv("JIRA_ROOT") } - if cookiefile := os.Getenv("JIRA_COOKIEFILE"); cookiefile != "" { - makeNewFile := false - f, err := os.Open(cookiefile) - server := jc.jiraRoot + "rest/gadget/1.0/login" - u, _ := url.Parse(server) - if err != nil { - makeNewFile = true - } else { - if stat, err := f.Stat(); err == nil { - if time.Now().Sub(stat.ModTime()).Minutes() > 60 { - makeNewFile = true - } else { - var cookies []*http.Cookie - dec := json.NewDecoder(f) - dec.Decode(&cookies) - u, _ = url.Parse(jc.jiraRoot) - jc.Jar.SetCookies(u, cookies) - } - } - f.Close() - } - if makeNewFile { - f, err = os.Create(cookiefile) - if err != nil { - panic(err) - } - - http.DefaultClient.Jar = j - form := url.Values{} - form.Add("os_username", os.Getenv("JIRA_USER")) - form.Add("os_password", os.Getenv("JIRA_PASSWORD")) - req, _ := http.NewRequest("POST", server, strings.NewReader(form.Encode())) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := http.DefaultClient.Do(req) - if err != nil || resp.StatusCode >= 400 { - fmt.Println(resp.Header) - fmt.Println(resp.Status) - fmt.Println(err) - } - b := bytes.NewBuffer(nil) - enc := json.NewEncoder(b) - enc.Encode(j.Cookies(u)) - io.Copy(f, b) - f.Close() - } - } - if *Verbose { - fmt.Println("Jira root:", jc.jiraRoot) + fmt.Println("Jira root:", j.jiraRoot) } - return jc + return j } -func (j *JiraClient) DoLess(req *http.Request) (*http.Response, error) { +func (j *JiraClient) Do(req *http.Request) (*http.Response, error) { + var err error + req.SetBasicAuth(os.Getenv("JIRA_USER"), os.Getenv("JIRA_PASSWORD")) + if *Verbose { + fmt.Println("Jira User: ", os.Getenv("JIRA_USER")) + fmt.Println("Jira Password: ", os.Getenv("JIRA_PASSWORD")) + } req.Header.Add("Content-Type", "application/json") req.Header.Add("Accept", "application/json, text/plain, text/html") - return j.DoEvenLess(req) -} - -func (j *JiraClient) DoEvenLess(req *http.Request) (*http.Response, error) { - var err error - if os.Getenv("JIRA_COOKIEFILE") == "" { - req.SetBasicAuth(os.Getenv("JIRA_USER"), os.Getenv("JIRA_PASSWORD")) + req.URL, err = url.Parse(j.jiraRoot + "rest/" + req.URL.RequestURI()) + if err != nil { + return nil, err } resp, err := j.Client.Do(req) if err != nil { @@ -124,15 +75,6 @@ func (j *JiraClient) DoEvenLess(req *http.Request) (*http.Response, error) { return resp, nil } -func (j *JiraClient) Do(req *http.Request) (*http.Response, error) { - var err error - req.URL, err = url.Parse(j.jiraRoot + "rest/" + req.URL.RequestURI()) - if err != nil { - return nil, err - } - return j.DoLess(req) -} - func (j *JiraClient) Put(path string, payload io.Reader) (*http.Response, error) { req, err := http.NewRequest("PUT", path, payload) if err != nil { diff --git a/jkl.go b/jkl.go index 6d53e76..bc1f855 100644 --- a/jkl.go +++ b/jkl.go @@ -8,8 +8,6 @@ import ( "io" "io/ioutil" "log" - "mime/multipart" - "net/http" "net/url" "os" "strings" @@ -34,7 +32,6 @@ func bootHttpClient() { func Create(issue *JiraIssue) (*JiraIssue, error) { bootHttpClient() payload, err := formatPayload(issue) - if err != nil { return nil, err } @@ -219,7 +216,7 @@ func DoTransition(taskKey string, transitionName string) error { return err } var t *Transition - //fmt.Println(iss.Transitions) + fmt.Println(iss.Transitions) for _, transition := range iss.Transitions { if strings.ToLower(transition.Name) == strings.ToLower(transitionName) { t = transition @@ -242,6 +239,7 @@ func DoTransition(taskKey string, transitionName string) error { } func LogWork(taskKey string, workAmount string) error { + bootHttpClient() payload, err := serializePayload(map[string]interface{}{"timeSpent": workAmount}) resp, err := httpClient.Post("api/2/issue/"+taskKey+"/worklog", payload) if err != nil { @@ -254,120 +252,6 @@ func LogWork(taskKey string, workAmount string) error { return nil } -func Assign(taskKey string, user string) error { - bootHttpClient() - payload, err := serializePayload(map[string]interface{}{"name": user}) - resp, err := httpClient.Put("api/2/issue/"+taskKey+"/assignee", payload) - if err != nil { - fmt.Println(resp.StatusCode) - return err - } - if resp.StatusCode >= 400 { - io.Copy(os.Stderr, resp.Body) - } - return nil -} - -func FlagIssue(taskKeys []string, flg bool) error { - bootHttpClient() - payload, err := serializePayload(map[string]interface{}{"issueKeys": taskKeys, "flag": flg}) - req, err := http.NewRequest("POST", "", payload) - - if err != nil { - return err - } - req.URL, err = url.Parse(httpClient.jiraRoot + "rest/" + "greenhopper/1.0/xboard/issue/flag/flag.json") - if err != nil { - return err - } - resp, err := httpClient.DoLess(req) - if err != nil { - fmt.Println(resp.StatusCode) - return err - } - if resp.StatusCode >= 400 { - io.Copy(os.Stderr, resp.Body) - } - return nil -} - -type msi map[string]interface{} - -func LinkIssue(params []string) error { - bootHttpClient() - if len(params) == 0 { - resp, err := httpClient.Get("api/2/issueLinkType") - if err != nil { - if resp != nil { - fmt.Println(resp.StatusCode) - } - return err - } - io.Copy(os.Stdout, resp.Body) - return nil - } - payload, err := serializePayload(msi{ - "type": msi{"name": strings.Join(params[1:len(params)-1], " ")}, - "inwardIssue": msi{"key": params[len(params)-1]}, - "outwardIssue": msi{"key": params[0]}, - }) - resp, err := httpClient.Post("api/2/issueLink", payload) - if err != nil { - if resp != nil { - fmt.Println(resp.StatusCode) - } - return err - } - if resp.StatusCode >= 400 { - io.Copy(os.Stderr, resp.Body) - } - return nil -} - -func Attach(issueKey string, filename string) error { - bootHttpClient() - - // Prepare a form that you will submit to that URL. - var b bytes.Buffer - w := multipart.NewWriter(&b) - // Add your image file - f, err := os.Open(filename) - if err != nil { - return err - } - fi, err := os.Lstat(filename) - fw, err := w.CreateFormFile("file", fi.Name()) - if err != nil { - return err - } - if _, err = io.Copy(fw, f); err != nil { - return err - } - // Don't forget to close the multipart writer. - // If you don't close it, your request will be missing the terminating boundary. - w.Close() - - req, err := http.NewRequest("POST", "", &b) - - if err != nil { - return err - } - req.URL, err = url.Parse(httpClient.jiraRoot + "rest/" + fmt.Sprintf("api/2/issue/%s/attachments", issueKey)) - if err != nil { - return err - } - req.Header.Add("X-Atlassian-Token", "no-check") - req.Header.Add("Content-Type", w.FormDataContentType()) - res, err := httpClient.DoEvenLess(req) - - if err != nil { - s, _ := ioutil.ReadAll(res.Body) - fmt.Println(string(s)) - return err - } - return nil -} - func formatPayload(issue *JiraIssue) (io.Reader, error) { if issue.Fields != nil && issue.Fields.Project != nil && @@ -382,7 +266,7 @@ func serializePayload(i interface{}) (io.Reader, error) { payload := bytes.NewBuffer(b) enc := json.NewEncoder(payload) err := enc.Encode(i) - //fmt.Println("payload: ", payload.String()) + fmt.Println(payload.String()) if err != nil { fmt.Println(err) return nil, err diff --git a/jkl_test.go b/jkl_test.go index d1665bd..70f796c 100644 --- a/jkl_test.go +++ b/jkl_test.go @@ -1,11 +1,34 @@ package jkl import ( + "encoding/json" + "fmt" "os" "testing" "text/template" ) +func TestUnmarshalProjects(t *testing.T) { + f, err := os.Open("projects.json") + if err != nil { + t.Error(err) + } + dec := json.NewDecoder(f) + x := struct{ Projects []Project }{} + + err = dec.Decode(&x) + if err != nil { + t.Error(err) + } + for _, p := range x.Projects { + for _, it := range p.IssueTypes { + for sn, f := range it.Fields { + fmt.Println(it.Name, sn, f.Name, f.Required, f.Schema.Type) + } + } + } +} + type TestType struct { Field string }