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