Lots of small stuff
This commit is contained in:
commit
b5c1189de8
14 changed files with 911 additions and 0 deletions
52
cmd/jkl/comment.go
Normal file
52
cmd/jkl/comment.go
Normal file
|
|
@ -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())
|
||||
}
|
||||
54
cmd/jkl/create.go
Normal file
54
cmd/jkl/create.go
Normal file
|
|
@ -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:`
|
||||
59
cmd/jkl/edit.go
Normal file
59
cmd/jkl/edit.go
Normal file
|
|
@ -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))
|
||||
148
cmd/jkl/editor.go
Normal file
148
cmd/jkl/editor.go
Normal file
|
|
@ -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")))
|
||||
}
|
||||
36
cmd/jkl/editor_test.go
Normal file
36
cmd/jkl/editor_test.go
Normal file
|
|
@ -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) {
|
||||
|
||||
}
|
||||
63
cmd/jkl/jkl.go
Normal file
63
cmd/jkl/jkl.go
Normal file
|
|
@ -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] <command> [args]
|
||||
|
||||
Available commands:
|
||||
|
||||
list
|
||||
create
|
||||
edit
|
||||
`
|
||||
30
cmd/jkl/list.go
Normal file
30
cmd/jkl/list.go
Normal file
|
|
@ -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
|
||||
}
|
||||
28
cmd/jkl/task.go
Normal file
28
cmd/jkl/task.go
Normal file
|
|
@ -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
|
||||
}
|
||||
78
cmd/jklfs/jklfile.go
Normal file
78
cmd/jklfs/jklfile.go
Normal file
|
|
@ -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
|
||||
}
|
||||
BIN
cmd/jklfs/jklfs
Executable file
BIN
cmd/jklfs/jklfs
Executable file
Binary file not shown.
99
cmd/jklfs/jklfs.go
Normal file
99
cmd/jklfs/jklfs.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
68
issue.go
Normal file
68
issue.go
Normal file
|
|
@ -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))
|
||||
66
jiraclient.go
Normal file
66
jiraclient.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
130
jkl.go
Normal file
130
jkl.go
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue