package jiracli import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "os" "path/filepath" "reflect" "strconv" "strings" "text/template" yaml "gopkg.in/coryb/yaml.v2" "github.com/coryb/figtree" "github.com/mgutz/ansi" "golang.org/x/crypto/ssh/terminal" ) func findTemplate(name string) ([]byte, error) { if file, err := findClosestParentPath(filepath.Join(".jira.d", "templates", name)); err == nil { b, err := ioutil.ReadFile(file) if err != nil { return nil, err } return b, nil } return nil, nil } func getTemplate(name string) (string, error) { if _, err := os.Stat(name); err == nil { b, err := ioutil.ReadFile(name) if err != nil { return "", err } return string(b), nil } b, err := findTemplate(name) if err != nil { return "", err } else if b != nil { return string(b), nil } if s, ok := AllTemplates[name]; ok { return s, nil } return "", fmt.Errorf("No Template found for %q", name) } func tmpTemplate(templateName string, data interface{}) (string, error) { tmpFile, err := tmpYml(templateName) if err != nil { return "", err } defer tmpFile.Close() return tmpFile.Name(), RunTemplate(templateName, data, tmpFile) } func TemplateProcessor() *template.Template { funcs := map[string]interface{}{ "jira": func() string { return os.Args[0] }, "env": func() map[string]string { out := map[string]string{} for _, env := range os.Environ() { kv := strings.SplitN(env, "=", 2) out[kv[0]] = kv[1] } return out }, "toJson": func(content interface{}) (string, error) { bytes, err := json.MarshalIndent(content, "", " ") if err != nil { return "", err } return string(bytes), nil }, "termWidth": func() int { w, _, err := terminal.GetSize(int(os.Stdout.Fd())) if err == nil { return w } if os.Getenv("COLUMNS") != "" { w, err = strconv.Atoi(os.Getenv("COLUMNS")) } if err == nil { return w } return 120 }, "sub": func(a, b int) int { return a - b }, "append": func(more string, content interface{}) (string, error) { switch value := content.(type) { case string: return string(append([]byte(content.(string)), []byte(more)...)), nil case []byte: return string(append(content.([]byte), []byte(more)...)), nil default: return "", fmt.Errorf("Unknown type: %s", value) } }, "indent": func(spaces int, content string) string { indent := make([]rune, spaces+1, spaces+1) indent[0] = '\n' for i := 1; i < spaces+1; i++ { indent[i] = ' ' } lineSeps := []rune{'\n', '\u0085', '\u2028', '\u2029'} for _, sep := range lineSeps { indent[0] = sep content = strings.Replace(content, string(sep), string(indent), -1) } return content }, "comment": func(content string) string { lineSeps := []rune{'\n', '\u0085', '\u2028', '\u2029'} for _, sep := range lineSeps { content = strings.Replace(content, string(sep), string([]rune{sep, '#', ' '}), -1) } return content }, "color": func(color string) string { return ansi.ColorCode(color) }, "split": func(sep string, content string) []string { return strings.Split(content, sep) }, "join": func(sep string, content []interface{}) string { vals := make([]string, len(content)) for i, v := range content { vals[i] = v.(string) } return strings.Join(vals, sep) }, "abbrev": func(max int, content string) string { if len(content) > max { var buffer bytes.Buffer buffer.WriteString(content[:max-3]) buffer.WriteString("...") return buffer.String() } return content }, "rep": func(count int, content string) string { var buffer bytes.Buffer for i := 0; i < count; i++ { buffer.WriteString(content) } return buffer.String() }, "age": func(content string) (string, error) { return fuzzyAge(content) }, "dateFormat": func(format string, content string) (string, error) { return dateFormat(format, content) }, } return template.New("gojira").Funcs(funcs) } func ConfigTemplate(fig *figtree.FigTree, template, command string, opts interface{}) (string, error) { var tmp map[string]interface{} err := ConvertType(opts, &tmp) if err != nil { return "", err } fig.LoadAllConfigs(command+".yml", &tmp) fig.LoadAllConfigs("config.yml", &tmp) tmpl, err := TemplateProcessor().Parse(template) if err != nil { return "", err } buf := bytes.NewBufferString("") if err := tmpl.Execute(buf, &tmp); err != nil { return "", err } return buf.String(), nil } func ConvertType(input interface{}, output interface{}) error { // HACK HACK HACK: convert data formats to json for backwards compatibilty with templates jsonData, err := json.Marshal(input) if err != nil { return err } defer func(mapType, iface reflect.Type) { yaml.DefaultMapType = mapType yaml.IfaceType = iface }(yaml.DefaultMapType, yaml.IfaceType) yaml.DefaultMapType = reflect.TypeOf(map[string]interface{}{}) yaml.IfaceType = yaml.DefaultMapType.Elem() if err := yaml.Unmarshal(jsonData, output); err != nil { return err } return nil } func RunTemplate(templateName string, data interface{}, out io.Writer) error { templateContent, err := getTemplate(templateName) if err != nil { return err } if out == nil { out = os.Stdout } var rawData interface{} err = ConvertType(data, &rawData) if err != nil { return err } tmpl, err := TemplateProcessor().Parse(templateContent) if err != nil { return err } if err := tmpl.Execute(out, rawData); err != nil { return err } return nil } var AllTemplates = map[string]string{ "attach-list": defaultAttachListTemplate, "comment": defaultCommentTemplate, "component-add": defaultComponentAddTemplate, "components": defaultComponentsTemplate, "create": defaultCreateTemplate, "createmeta": defaultDebugTemplate, "debug": defaultDebugTemplate, "edit": defaultEditTemplate, "editmeta": defaultDebugTemplate, "epic-create": defaultEpicCreateTemplate, "epic-list": defaultTableTemplate, "fields": defaultDebugTemplate, "issuelinktypes": defaultDebugTemplate, "issuetypes": defaultIssuetypesTemplate, "json": defaultDebugTemplate, "list": defaultListTemplate, "request": defaultDebugTemplate, "subtask": defaultSubtaskTemplate, "table": defaultTableTemplate, "transition": defaultTransitionTemplate, "transitions": defaultTransitionsTemplate, "transmeta": defaultDebugTemplate, "view": defaultViewTemplate, "worklog": defaultWorklogTemplate, "worklogs": defaultWorklogsTemplate, } const defaultDebugTemplate = "{{ . | toJson}}\n" const defaultListTemplate = "{{ range .issues }}{{ .key | append \":\" | printf \"%-12s\"}} {{ .fields.summary }}\n{{ end }}" const defaultTableTemplate = `{{/* table template */ -}} {{$w := sub termWidth 107 -}} +{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+ | {{ "Issue" | printf "%-14s" }} | {{ "Summary" | printf (printf "%%-%ds" (sub $w 2)) }} | {{ "Type" | printf "%-12s"}} | {{ "Priority" | printf "%-12s" }} | {{ "Status" | printf "%-12s" }} | {{ "Age" | printf "%-10s" }} | {{ "Reporter" | printf "%-12s" }} | {{ "Assignee" | printf "%-12s" }} | +{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+ {{ range .issues -}} | {{ .key | printf "%-14s"}} | {{ .fields.summary | abbrev (sub $w 2) | printf (printf "%%-%ds" (sub $w 2)) }} | {{.fields.issuetype.name | printf "%-12s" }} | {{.fields.priority.name | printf "%-12s" }} | {{.fields.status.name | printf "%-12s" }} | {{.fields.created | age | printf "%-10s" }} | {{if .fields.reporter}}{{ .fields.reporter.name | printf "%-12s"}}{{else}}{{end}} | {{if .fields.assignee }}{{.fields.assignee.name | printf "%-12s" }}{{else}}{{end}} | {{ end -}} +{{ "-" | rep 16 }}+{{ "-" | rep $w }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+ ` const defaultAttachListTemplate = `{{/* table template */ -}} +{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+ | {{printf "%-10s" "id"}} | {{printf "%-28s" "filename"}} | {{printf "%-10s" "bytes"}} | {{printf "%-12s" "user"}} | {{printf "%-12s" "created"}} | +{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+ {{range . -}} | {{.id | printf "%10d" }} | {{.filename | printf "%-28s"}} | {{.size | printf "%10d"}} | {{.author.name | printf "%-12s"}} | {{.created | age | printf "%-12s"}} | {{end -}} +{{ "-" | rep 12 }}+{{ "-" | rep 30 }}+{{ "-" | rep 12 }}+{{ "-" | rep 14 }}+{{ "-" | rep 14 }}+ ` const defaultViewTemplate = `{{/* view template */ -}} issue: {{ .key }} {{if .fields.created -}} created: {{ .fields.created | age }} ago {{end -}} {{if .fields.status -}} status: {{ .fields.status.name }} {{end -}} summary: {{ .fields.summary }} project: {{ .fields.project.key }} {{if .fields.components -}} components: {{ range .fields.components }}{{ .name }} {{end}} {{end -}} {{if .fields.issuetype -}} issuetype: {{ .fields.issuetype.name }} {{end -}} {{if .fields.assignee -}} assignee: {{ .fields.assignee.name }} {{end -}} reporter: {{ if .fields.reporter }}{{ .fields.reporter.name }}{{end}} {{if .fields.customfield_10110 -}} watchers: {{ range .fields.customfield_10110 }}{{ .name }} {{end}} {{end -}} {{if .fields.issuelinks -}} blockers: {{ range .fields.issuelinks }}{{if .outwardIssue}}{{ .outwardIssue.key }}[{{.outwardIssue.fields.status.name}}]{{end}}{{end}} depends: {{ range .fields.issuelinks }}{{if .inwardIssue}}{{ .inwardIssue.key }}[{{.inwardIssue.fields.status.name}}]{{end}}{{end}} {{end -}} {{if .fields.priority -}} priority: {{ .fields.priority.name }} {{end -}} {{if .fields.votes -}} votes: {{ .fields.votes.votes}} {{end -}} {{if .fields.labels -}} labels: {{ join ", " .fields.labels }} {{end -}} description: | {{ or .fields.description "" | indent 2 }} {{if .fields.comment.comments}} comments: {{ range .fields.comment.comments }} - | # {{.author.name}}, {{.created | age}} ago {{ or .body "" | indent 4}} {{end}} {{end -}} ` const defaultEditTemplate = `{{/* edit template */ -}} # issue: {{ .key }} - created: {{ .fields.created | age}} ago update: comment: - add: body: |~ {{ or .overrides.comment "" | indent 10 }} fields: summary: >- {{ or .overrides.summary .fields.summary }} {{- if and .meta.fields.components .meta.fields.components.allowedValues }} components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}} - name: {{.}}{{end}}{{else}}{{ range .fields.components }} - name: {{ .name }}{{end}}{{end}}{{end}} {{- if .meta.fields.assignee}} assignee: name: {{ if .overrides.assignee }}{{.overrides.assignee}}{{else}}{{if .fields.assignee }}{{ .fields.assignee.name }}{{end}}{{end}}{{end}} {{- if .meta.fields.reporter}} reporter: name: {{ if .overrides.reporter }}{{ .overrides.reporter }}{{else if .fields.reporter}}{{ .fields.reporter.name }}{{end}}{{end}} {{- if .meta.fields.customfield_10110}} # watchers customfield_10110: {{ range .fields.customfield_10110 }} - name: {{ .name }}{{end}}{{if .overrides.watcher}} - name: {{ .overrides.watcher}}{{end}}{{end}} {{- if .meta.fields.priority }} priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}} name: {{ or .overrides.priority .fields.priority.name "" }}{{end}} description: |~ {{ or .overrides.description .fields.description "" | indent 4 }} # votes: {{ .fields.votes.votes }} # comments: # {{ range .fields.comment.comments }} - | # {{.author.name}}, {{.created | age}} ago # {{ or .body "" | indent 4 | comment}} # {{end}} ` const defaultTransitionsTemplate = `{{ range .transitions }}{{.id }}: {{.name}} {{end}}` const defaultComponentsTemplate = `{{ range . }}{{.id }}: {{.name}} {{end}}` const defaultComponentAddTemplate = `{{/* compoinent add template */ -}} project: {{or .project ""}} name: {{or .name ""}} description: {{or .description ""}} leadUserName: {{or .leadUserName ""}} ` const defaultIssuetypesTemplate = `{{/* issuetypes template */ -}} {{ range .issuetypes }}{{color "+bh"}}{{.name | append ":" | printf "%-13s" }}{{color "reset"}} {{.description}} {{end}}` const defaultCreateTemplate = `{{/* create template */ -}} fields: project: key: {{ or .overrides.project "" }} issuetype: name: {{ or .overrides.issuetype "" }} summary: >- {{ or .overrides.summary "" }}{{if .meta.fields.priority.allowedValues}} priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}} name: {{ or .overrides.priority ""}}{{end}}{{if .meta.fields.components.allowedValues}} components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}} - name: {{ . }}{{end}}{{end}} description: |~ {{ or .overrides.description "" | indent 4 }}{{if .meta.fields.assignee}} assignee: name: {{ or .overrides.assignee "" }}{{end}}{{if .meta.fields.reporter}} reporter: name: {{ or .overrides.reporter .overrides.user }}{{end}}{{if .meta.fields.customfield_10110}} # watchers customfield_10110: {{ range split "," (or .overrides.watchers "")}} - name: {{.}}{{end}} - name:{{end}}` const defaultEpicCreateTemplate = `{{/* epic create template */ -}} fields: project: key: {{ or .overrides.project "" }} # Epic Name customfield_10120: {{ or (index .overrides "epic-name") "" }} summary: >- {{ or .overrides.summary "" }}{{if .meta.fields.priority.allowedValues}} priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}} name: {{ or .overrides.priority ""}}{{end}}{{if .meta.fields.components.allowedValues}} components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}} - name: {{ . }}{{end}}{{end}} description: |~ {{ or .overrides.description "" | indent 4 }}{{if .meta.fields.assignee}} assignee: name: {{ or .overrides.assignee "" }}{{end}}{{if .meta.fields.reporter}} reporter: name: {{ or .overrides.reporter .overrides.user }}{{end}}{{if .meta.fields.customfield_10110}} # watchers customfield_10110: {{ range split "," (or .overrides.watchers "")}} - name: {{.}}{{end}} - name:{{end}} issuetype: name: Epic` const defaultSubtaskTemplate = `{{/* create subtask template */ -}} fields: project: key: {{ .parent.fields.project.key }} summary: >- {{ or .overrides.summary "" }}{{if .meta.fields.priority.allowedValues}} priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}} name: {{ or .overrides.priority ""}}{{end}}{{if .meta.fields.components.allowedValues}} components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{ range split "," (or .overrides.components "")}} - name: {{ . }}{{end}}{{end}} description: |~ {{ or .overrides.description "" | indent 4 }}{{if .meta.fields.assignee}} assignee: name: {{ or .overrides.assignee "" }}{{end}}{{if .meta.fields.reporter}} reporter: name: {{ or .overrides.reporter .overrides.user }}{{end}}{{if .meta.fields.customfield_10110}} # watchers customfield_10110: {{ range split "," (or .overrides.watchers "")}} - name: {{.}}{{end}} - name:{{end}} issuetype: name: Sub-task parent: key: {{ .parent.key }}` const defaultCommentTemplate = `body: |~ {{ or .overrides.comment "" | indent 2 }} ` const defaultTransitionTemplate = `{{/* transition template */ -}} {{- if .meta.fields.comment }} update: comment: - add: body: |~ {{ or .overrides.comment "" | indent 10 }} {{- end -}} fields: {{- if .meta.fields.assignee}} assignee: name: {{if .overrides.assignee}}{{.overrides.assignee}}{{else}}{{if .fields.assignee}}{{.fields.assignee.name}}{{end}}{{end}} {{- end -}} {{if .meta.fields.components}} components: # Values: {{ range .meta.fields.components.allowedValues }}{{.name}}, {{end}}{{if .overrides.components }}{{ range (split "," .overrides.components)}} - name: {{.}}{{end}}{{else}}{{ range .fields.components }} - name: {{ .name }}{{end}}{{end}} {{- end -}} {{if .meta.fields.description}} description: |~ {{ or .fields.description "" | indent 4 }} {{- end -}} {{if .meta.fields.fixVersions -}} {{if .meta.fields.fixVersions.allowedValues}} fixVersions: # Values: {{ range .meta.fields.fixVersions.allowedValues }}{{.name}}, {{end}}{{if .overrides.fixVersions}}{{ range (split "," .overrides.fixVersions)}} - name: {{.name}}{{end}}{{else}}{{range .fields.fixVersions}} - name: {{.name}}{{end}}{{end}} {{- end -}} {{- end -}} {{if .meta.fields.issuetype}} issuetype: # Values: {{ range .meta.fields.issuetype.allowedValues }}{{.name}}, {{end}} name: {{if .overrides.issuetype}}{{.overrides.issuetype}}{{else}}{{if .fields.issuetype}}{{.fields.issuetype.name}}{{end}}{{end}} {{- end -}} {{if .meta.fields.labels}} labels: {{range .fields.labels}} - {{.}}{{end}}{{if .overrides.labels}}{{range (split "," .overrides.labels)}} - {{.}}{{end}}{{end}} {{- end -}} {{if .meta.fields.priority}} priority: # Values: {{ range .meta.fields.priority.allowedValues }}{{.name}}, {{end}} name: {{ or .overrides.priority "unassigned" }} {{- end -}} {{if .meta.fields.reporter}} reporter: name: {{if .overrides.reporter}}{{.overrides.reporter}}{{else}}{{if .fields.reporter}}{{.fields.reporter.name}}{{end}}{{end}} {{- end -}} {{if .meta.fields.resolution}} resolution: # Values: {{ range .meta.fields.resolution.allowedValues }}{{.name}}, {{end}} name: {{if .overrides.resolution}}{{.overrides.resolution}}{{else if .fields.resolution}}{{.fields.resolution.name}}{{else}}{{or .overrides.defaultResolution "Fixed"}}{{end}} {{- end -}} {{if .meta.fields.summary}} summary: >- {{or .overrides.summary .fields.summary}} {{- end -}} {{if .meta.fields.versions.allowedValues}} versions: # Values: {{ range .meta.fields.versions.allowedValues }}{{.name}}, {{end}}{{if .overrides.versions}}{{ range (split "," .overrides.versions)}} - name: {{.}}{{end}}{{else}}{{range .fields.versions}} - name: {{.}}{{end}}{{end}} {{- end}} transition: id: {{ .transition.id }} name: {{ .transition.name }} ` const defaultWorklogTemplate = `{{/* worklog template */ -}} # issue: {{ .issue }} comment: |~ {{ or .comment "" }} timeSpent: {{ or .timeSpent "" }} started: {{ or .started "" }} ` const defaultWorklogsTemplate = `{{/* worklogs template */ -}} {{ range .worklogs }}- # {{.author.name}}, {{.created | age}} ago comment: {{ or .comment "" }} started: {{ .started }} timeSpent: {{ .timeSpent }} {{end}}`