Compare commits

...
Sign in to create a new pull request.

17 commits

Author SHA1 Message Date
Olivier Tremblay
493275c780
Merge pull request #20 from gabeguz/config-docs
Start adding Usage docs to README
2020-01-20 10:53:37 -05:00
Olivier Tremblay
f5b3436243
Merge pull request #18 from gabeguz/install
Add installation instructions to README
2020-01-20 10:53:01 -05:00
Olivier Tremblay
2a6d302067
Merge pull request #17 from gabeguz/docs
Add project description to README
2020-01-20 10:52:33 -05:00
Olivier Tremblay
7526ce33cc
Merge pull request #23 from otremblay/no-mas-circles
Trash CCI from JKL.
2019-10-01 07:19:30 -04:00
Olivier Tremblay
672d42341c
Trash CCI from JKL. 2019-10-01 07:19:02 -04:00
Olivier Tremblay
0bc3cce7d7
Merge pull request #22 from otremblay/fix-gomod
Okay, I cave, no vanity url for now
2019-10-01 06:58:20 -04:00
Olivier Tremblay
1215ca9b05
Okay, I cave, no vanity url for now 2019-10-01 06:57:36 -04:00
Olivier Tremblay
2f862fa7d6
Merge pull request #21 from otremblay/fix-gomod
Remove Gopkg, switch to go mod.
2019-10-01 06:31:06 -04:00
Olivier Tremblay
4e2338e694
Remove Gopkg, switch to go mod. 2019-10-01 06:30:31 -04:00
Oliver Tremblay
197e28024b
It goes into the repo or it gets the hose again. 2019-02-26 09:10:07 -05:00
Olivier Tremblay
96cadaf4df
Implement attach command 2018-06-20 13:53:05 -04:00
Olivier Tremblay
22a1ba56e5
I forgot some files 2018-05-02 09:45:01 -04:00
Olivier Tremblay
9aa31ec22c
Added a bunch of stuff and I now need to store said stuff 2018-05-02 09:36:19 -04:00
Gabriel Guzman
1c44b282a8 Start adding Usage docs to README
Add a section on setting up your `~/.jklrc` file with required variables.
2017-10-30 14:11:18 -04:00
Gabriel Guzman
70716e847e Add installation instructions to README 2017-10-30 13:14:23 -04:00
Gabriel Guzman
a36986db32 Add project description 2017-10-30 13:07:17 -04:00
Olivier Tremblay
1458b63287
Canonical path is actually otremblay.com/jkl
Also we use go dep instead of godeps
2017-10-20 20:40:29 -04:00
21 changed files with 656 additions and 189 deletions

12
Godeps/Godeps.json generated
View file

@ -1,12 +0,0 @@
{
"ImportPath": "github.com/avanier/jkl",
"GoVersion": "go1.8",
"GodepVersion": "v79",
"Deps": [
{
"ImportPath": "github.com/joho/godotenv",
"Comment": "v1.1-23-gc9360df",
"Rev": "c9360df4d16dc0e391ea2f28da2d31a9ede2e26f"
}
]
}

5
Godeps/Readme generated
View file

@ -1,5 +0,0 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.

View file

@ -1,14 +1,34 @@
# jkl # jkl
jkl is a library for programmatically interacting with a JIRA installation. It
TODO: Write a project description comes with a command line program (also called `jkl`) which allows you to
interact with JIRA via the command line.
## Installation ## Installation
To use the library, simply import it into your application:
`import "github.com/otremblay/jkl"`
TODO: Describe the installation process 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`
## Usage ## Usage
TODO: Write usage instructions 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
## Contributing ## Contributing

36
cmd/jkl/assign.go Normal file
View file

@ -0,0 +1,36 @@
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()
}

33
cmd/jkl/attach.go Normal file
View file

@ -0,0 +1,33 @@
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()
}

View file

@ -4,9 +4,10 @@ import (
"bytes" "bytes"
"errors" "errors"
"flag" "flag"
"fmt"
"io" "io"
"os" "os"
"fmt" "strings"
"text/template" "text/template"
"otremblay.com/jkl" "otremblay.com/jkl"
@ -61,7 +62,16 @@ func (ccmd *CreateCmd) Create() error {
} }
var iss *jkl.JiraIssue var iss *jkl.JiraIssue
// TODO: Evil badbad don't do this. // TODO: Evil badbad don't do this.
em := &jkl.EditMeta{Fields: cm.Projects[0].IssueTypes[0].Fields} 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}
if ccmd.file != "" { if ccmd.file != "" {
iss, err = GetIssueFromFile(ccmd.file, b, em) iss, err = GetIssueFromFile(ccmd.file, b, em)
if err != nil { if err != nil {

View file

@ -21,9 +21,9 @@ func NewEditCmd(args []string) (*EditCmd, error) {
ccmd := &EditCmd{project: os.Getenv("JIRA_PROJECT")} ccmd := &EditCmd{project: os.Getenv("JIRA_PROJECT")}
f := flag.NewFlagSet("x", flag.ExitOnError) f := flag.NewFlagSet("x", flag.ExitOnError)
f.StringVar(&ccmd.project, "p", "", "Jira project key") f.StringVar(&ccmd.project, "p", "", "Jira project key")
f.StringVar(&ccmd.file, "f", "filename", "File to get issue description from") f.StringVar(&ccmd.file, "f", "", "File to get issue description from")
f.Parse(args) f.Parse(args)
ccmd.taskKey = flag.Arg(0) ccmd.taskKey = f.Arg(0)
return ccmd, nil return ccmd, nil
} }

View file

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
@ -17,20 +16,26 @@ import (
"otremblay.com/jkl" "otremblay.com/jkl"
) )
// def get_editor do // def get_editor do
// [System.get_env("EDITOR"), "nano", "vim", "vi"] // [System.get_env("EDITOR"), "nano", "vim", "vi"]
// |> Enum.find(nil, fn (ed) -> System.find_executable(ed) != nil end) // |> Enum.find(nil, fn (ed) -> System.find_executable(ed) != nil end)
// end // end
var editors = []string{os.Getenv("EDITOR"), "nano", "vim", "vi"} var editors = []string{"nano", "vim", "vi"}
// GetEditor returns the path to an editor, taking $EDITOR in account // GetEditor returns the path to an editor, taking $EDITOR in account
func GetEditor() string { func GetEditor() string {
if ed := os.Getenv("EDITOR"); ed != "" {
return ed
}
if ed := os.Getenv("VISUAL"); ed != "" {
return ed
}
for _, ed := range editors { for _, ed := range editors {
if p, err := exec.LookPath(ed); err == nil { if p, err := exec.LookPath(ed); err == nil {
return p return p
} }
} }
log.Fatal("No editor available; use flags.")
return "" return ""
} }
@ -104,7 +109,7 @@ func GetIssueFromFile(filename string, initial io.Reader, editMeta *jkl.EditMeta
var spacex = regexp.MustCompile(`\s`) var spacex = regexp.MustCompile(`\s`)
func IssueFromReader(f io.Reader, editMeta *jkl.EditMeta) *jkl.JiraIssue { func IssueFromReader(f io.Reader, editMeta *jkl.EditMeta) *jkl.JiraIssue {
iss := &jkl.JiraIssue{Fields: &jkl.Fields{}} iss := &jkl.JiraIssue{Fields: &jkl.Fields{ExtraFields: map[string]interface{}{}}}
riss := reflect.ValueOf(iss).Elem() riss := reflect.ValueOf(iss).Elem()
fieldsField := riss.FieldByName("Fields").Elem() fieldsField := riss.FieldByName("Fields").Elem()
currentField := reflect.Value{} currentField := reflect.Value{}
@ -156,12 +161,38 @@ func IssueFromReader(f io.Reader, editMeta *jkl.EditMeta) *jkl.JiraIssue {
} }
} else if editMeta != nil { } else if editMeta != nil {
// If it's not valid, throw it at the createmeta. It will probably end up in ExtraFields. // 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() { if currentField.IsValid() {
currentField.SetString(strings.TrimSpace(currentField.String() + "\n" + strings.Join(parts, ":"))) newpart := strings.Join(parts, ":")
newvalue := currentField.String() + "\n" + newpart
if strings.TrimSpace(newpart) != "" {
newvalue = strings.TrimSpace(newvalue)
}
currentField.SetString(newvalue)
} }
} }
iss.EditMeta = editMeta
return iss return iss
} }

View file

@ -12,7 +12,7 @@ hell
Issue Type: Sometype Issue Type: Sometype
this is ignored this is ignored
Summary: Dookienator Summary: Dookienator
also ignored`, "\n")) also ignored`, "\n"), nil)
AssertEqual(t, `Cowboys AssertEqual(t, `Cowboys
from from
hell`, iss.Fields.Description) hell`, iss.Fields.Description)

34
cmd/jkl/flag.go Normal file
View file

@ -0,0 +1,34 @@
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()
}

View file

@ -61,6 +61,16 @@ func getCmd(args []string, depth int) (Runner, error) {
return NewCommentCmd(args[1:]) return NewCommentCmd(args[1:])
case "edit-comment": case "edit-comment":
return NewEditCommentCmd(args[1:]) 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: default:
// Think about this real hard. // Think about this real hard.
// I want `jkl JIRA-1234 done` to move it to done. // I want `jkl JIRA-1234 done` to move it to done.
@ -92,7 +102,8 @@ func getCmd(args []string, depth int) (Runner, error) {
return nil, ErrTaskSubCommandNotFound return nil, ErrTaskSubCommandNotFound
} }
var verbs = []string{"list", "create", "task", "edit", "comment","edit-comment"} var verbs = []string{"list", "create", "task", "edit", "comment", "edit-comment", "attach"}
func init() { func init() {
sort.Strings(verbs) sort.Strings(verbs)
} }

30
cmd/jkl/link.go Normal file
View file

@ -0,0 +1,30 @@
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()
}

View file

@ -21,6 +21,9 @@ func (l *listissue) URL() string {
} }
func (l *listissue) Color() 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())) { if os.Getenv("JKLNOCOLOR") == "true" || !terminal.IsTerminal(int(os.Stdout.Fd())) {
return "" return ""
} }
@ -52,7 +55,7 @@ func NewListCmd(args []string) (*ListCmd, error) {
if *verbose { if *verbose {
fmt.Println(&ccmd.tmplstr) fmt.Println(&ccmd.tmplstr)
} }
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.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.Parse(args) f.Parse(args)
ccmd.args = f.Args() ccmd.args = f.Args()
if len(ccmd.args) == 0 { if len(ccmd.args) == 0 {

View file

@ -19,10 +19,10 @@ func (t *TaskCmd) Handle() error {
return t.Get(t.args[0]) return t.Get(t.args[0])
} }
if c == 2 { if c == 2 {
fmt.Println(t.args) // fmt.Println(t.args)
err := t.Transition(t.args[0], t.args[1]) err := t.Transition(t.args[0], t.args[1])
if err != nil { if err != nil {
fmt.Println(err) //fmt.Println(err)
return t.Log(t.args[0], strings.Join(t.args[1:], " ")) return t.Log(t.args[0], strings.Join(t.args[1:], " "))
} }
} }

View file

@ -76,3 +76,7 @@ func (f *jklfile) SetInode(i *nodefs.Inode) {}
func (f *jklfile) Utimens(atime *time.Time, mtime *time.Time) fuse.Status { func (f *jklfile) Utimens(atime *time.Time, mtime *time.Time) fuse.Status {
return fuse.EPERM return fuse.EPERM
} }
func (f *jklfile) Flock(flags int) fuse.Status {
return fuse.ENOSYS
}

10
go.mod Normal file
View file

@ -0,0 +1,10 @@
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
)

4
go.sum Normal file
View file

@ -0,0 +1,4 @@
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=

138
issue.go
View file

@ -38,9 +38,10 @@ func (it *IssueType) RangeFieldSpecs() string {
} }
type AllowedValue struct { type AllowedValue struct {
Id string Id string `json:"id"`
Self string Self string `json:"self"`
Value string Value string `json:"value"`
Name string `json:"name"`
} }
func (a *AllowedValue) String() string { func (a *AllowedValue) String() string {
@ -58,10 +59,8 @@ type FieldSpec struct {
} }
Operations []string Operations []string
AllowedValues []*AllowedValue AllowedValues []*AllowedValue
} }
type CreateMeta struct { type CreateMeta struct {
Projects []*Project Projects []*Project
} }
@ -157,7 +156,14 @@ func (f *Fields) UnmarshalJSON(b []byte) error{
return err return err
} }
f.rawExtraFields = map[string]json.RawMessage{} f.rawExtraFields = map[string]json.RawMessage{}
vf := reflect.ValueOf(f).Elem() 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()
for key, mess := range f.rawFields { for key, mess := range f.rawFields {
field := vf.FieldByNameFunc(func(s string) bool { return strings.ToLower(key) == strings.ToLower(s) }) field := vf.FieldByNameFunc(func(s string) bool { return strings.ToLower(key) == strings.ToLower(s) })
if field.IsValid() { if field.IsValid() {
@ -168,7 +174,12 @@ func (f *Fields) UnmarshalJSON(b []byte) error{
fmt.Fprintln(os.Stderr, objType, obj, string(mess)) fmt.Fprintln(os.Stderr, objType, obj, string(mess))
fmt.Fprintln(os.Stderr, errors.New(fmt.Sprintf("%s [%s]: %s", "Error allocating field", key, err))) fmt.Fprintln(os.Stderr, errors.New(fmt.Sprintf("%s [%s]: %s", "Error allocating field", key, err)))
} }
field.Set(reflect.ValueOf(obj).Elem()) val := reflect.ValueOf(obj)
if val == reflect.Zero(reflect.TypeOf(val)) || !val.IsValid() {
field.Set(reflect.Zero(objType))
} else {
field.Set(val.Elem())
}
} else { } else {
f.rawExtraFields[key] = mess f.rawExtraFields[key] = mess
} }
@ -223,20 +234,16 @@ type Transition struct{
Name string `json:"name"` Name string `json:"name"`
} }
type Schema struct { type Schema struct {
System string System string
Custom string Custom string
CustomId int CustomId int
} }
type EditMeta struct { type EditMeta struct {
Fields map[string]*FieldSpec Fields map[string]*FieldSpec
} }
type JiraIssue struct { type JiraIssue struct {
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
Fields *Fields `json:"fields,omitempty"` Fields *Fields `json:"fields,omitempty"`
@ -244,8 +251,93 @@ type JiraIssue struct {
EditMeta *EditMeta `json:"editmeta,omitempty"` EditMeta *EditMeta `json:"editmeta,omitempty"`
} }
var sprintRegexp = regexp.MustCompile(`name=([^,]+),`) 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 { func (i *JiraIssue) UnmarshalJSON(b []byte) error {
tmp := map[string]json.RawMessage{} tmp := map[string]json.RawMessage{}
if len(b) == 0 { if len(b) == 0 {
@ -257,8 +349,14 @@ func (i *JiraIssue) UnmarshalJSON(b []byte) error {
if err != nil && *Verbose { if err != nil && *Verbose {
fmt.Fprintln(os.Stderr, errors.New(fmt.Sprintf("%s: %s", "Error unpacking raw json", err))) 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) err = json.Unmarshal(tmp["fields"], &i.Fields)
if err != nil && *Verbose { if err != nil && *Verbose {
fmt.Println(string(tmp["fields"]))
fmt.Fprintln(os.Stderr, errors.New(fmt.Sprintf("%s: %s", "Error unpacking fields", err))) fmt.Fprintln(os.Stderr, errors.New(fmt.Sprintf("%s: %s", "Error unpacking fields", err)))
} }
err = json.Unmarshal(tmp["transitions"], &i.Transitions) err = json.Unmarshal(tmp["transitions"], &i.Transitions)
@ -282,7 +380,8 @@ func (i *JiraIssue) UnmarshalJSON(b []byte) error {
if len(results) == 2 { if len(results) == 2 {
i.Fields.ExtraFields[k] = results[1] i.Fields.ExtraFields[k] = results[1]
} }
} else {switch f.Schema.Type { } else {
switch f.Schema.Type {
case "user": case "user":
a := &Author{} a := &Author{}
json.Unmarshal(v, &a) json.Unmarshal(v, &a)
@ -290,20 +389,24 @@ func (i *JiraIssue) UnmarshalJSON(b []byte) error {
case "option": case "option":
val := &AllowedValue{} val := &AllowedValue{}
err = json.Unmarshal(v, &val) err = json.Unmarshal(v, &val)
if err != nil {panic(err)} if err != nil {
panic(err)
}
i.Fields.ExtraFields[k] = val i.Fields.ExtraFields[k] = val
case "array": case "array":
if f.Schema.Items == "option" { if f.Schema.Items == "option" {
val := []*AllowedValue{} val := []*AllowedValue{}
err = json.Unmarshal(v, &val) err = json.Unmarshal(v, &val)
if err != nil {panic(err)} if err != nil {
panic(err)
}
i.Fields.ExtraFields[k] = val i.Fields.ExtraFields[k] = val
continue continue
} }
fallthrough fallthrough
default: default:
if string(v) != "null" { if string(v) != "null" {
i.Fields.ExtraFields[k] = string(v) i.Fields.ExtraFields[k] = strings.Replace(string(v), "\\r\\n", "\n", -1)
} }
} }
} }
@ -323,6 +426,9 @@ func (i *JiraIssue) String() string {
if os.Getenv("JKLNOCOLOR") == "true" { if os.Getenv("JKLNOCOLOR") == "true" {
tmpl = issueTmplNoColor tmpl = issueTmplNoColor
} }
if customTmpl := os.Getenv("JKL_ISSUE_TMPL"); customTmpl != "" {
tmpl = template.Must(template.New("customIssueTmpl").Parse(customTmpl))
}
err := tmpl.Execute(b, i) err := tmpl.Execute(b, i)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)

View file

@ -1,6 +1,8 @@
package jkl package jkl
import ( import (
"bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -9,6 +11,8 @@ import (
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "net/url"
"os" "os"
"strings"
"time"
) )
var j, _ = cookiejar.New(nil) var j, _ = cookiejar.New(nil)
@ -26,33 +30,78 @@ func init(){
} }
func NewJiraClient(jiraRoot string) *JiraClient { func NewJiraClient(jiraRoot string) *JiraClient {
j := &JiraClient{ jc := &JiraClient{
&http.Client{ &http.Client{
Jar: j, Jar: j,
}, },
jiraRoot, jiraRoot,
} }
if j.jiraRoot == "" { if jc.jiraRoot == "" {
j.jiraRoot = os.Getenv("JIRA_ROOT") jc.jiraRoot = os.Getenv("JIRA_ROOT")
} }
if *Verbose { if cookiefile := os.Getenv("JIRA_COOKIEFILE"); cookiefile != "" {
fmt.Println("Jira root:", j.jiraRoot) 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)
} }
return j }
f.Close()
}
if makeNewFile {
f, err = os.Create(cookiefile)
if err != nil {
panic(err)
} }
func (j *JiraClient) Do(req *http.Request) (*http.Response, error) { http.DefaultClient.Jar = j
var err error form := url.Values{}
req.SetBasicAuth(os.Getenv("JIRA_USER"), os.Getenv("JIRA_PASSWORD")) form.Add("os_username", os.Getenv("JIRA_USER"))
if *Verbose { form.Add("os_password", os.Getenv("JIRA_PASSWORD"))
fmt.Println("Jira User: ", os.Getenv("JIRA_USER")) req, _ := http.NewRequest("POST", server, strings.NewReader(form.Encode()))
fmt.Println("Jira Password: ", os.Getenv("JIRA_PASSWORD")) 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)
}
return jc
}
func (j *JiraClient) DoLess(req *http.Request) (*http.Response, error) {
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json, text/plain, text/html") req.Header.Add("Accept", "application/json, text/plain, text/html")
req.URL, err = url.Parse(j.jiraRoot + "rest/" + req.URL.RequestURI()) return j.DoEvenLess(req)
if err != nil { }
return nil, err
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"))
} }
resp, err := j.Client.Do(req) resp, err := j.Client.Do(req)
if err != nil { if err != nil {
@ -75,6 +124,15 @@ func (j *JiraClient) Do(req *http.Request) (*http.Response, error) {
return resp, nil 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) { func (j *JiraClient) Put(path string, payload io.Reader) (*http.Response, error) {
req, err := http.NewRequest("PUT", path, payload) req, err := http.NewRequest("PUT", path, payload)
if err != nil { if err != nil {

121
jkl.go
View file

@ -8,6 +8,8 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"mime/multipart"
"net/http"
"net/url" "net/url"
"os" "os"
"strings" "strings"
@ -32,6 +34,7 @@ func bootHttpClient() {
func Create(issue *JiraIssue) (*JiraIssue, error) { func Create(issue *JiraIssue) (*JiraIssue, error) {
bootHttpClient() bootHttpClient()
payload, err := formatPayload(issue) payload, err := formatPayload(issue)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -216,7 +219,7 @@ func DoTransition(taskKey string, transitionName string) error {
return err return err
} }
var t *Transition var t *Transition
fmt.Println(iss.Transitions) //fmt.Println(iss.Transitions)
for _, transition := range iss.Transitions { for _, transition := range iss.Transitions {
if strings.ToLower(transition.Name) == strings.ToLower(transitionName) { if strings.ToLower(transition.Name) == strings.ToLower(transitionName) {
t = transition t = transition
@ -251,6 +254,120 @@ func LogWork(taskKey string, workAmount string) error {
return nil 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) { func formatPayload(issue *JiraIssue) (io.Reader, error) {
if issue.Fields != nil && if issue.Fields != nil &&
issue.Fields.Project != nil && issue.Fields.Project != nil &&
@ -265,7 +382,7 @@ func serializePayload(i interface{}) (io.Reader, error) {
payload := bytes.NewBuffer(b) payload := bytes.NewBuffer(b)
enc := json.NewEncoder(payload) enc := json.NewEncoder(payload)
err := enc.Encode(i) err := enc.Encode(i)
fmt.Println(payload.String()) //fmt.Println("payload: ", payload.String())
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, err return nil, err

View file

@ -1,34 +1,11 @@
package jkl package jkl
import ( import (
"encoding/json"
"fmt"
"os" "os"
"testing" "testing"
"text/template" "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 { type TestType struct {
Field string Field string
} }