diff --git a/.chglog/config.yml b/.chglog/config.yml
index abfce168..13bd904c 100755
--- a/.chglog/config.yml
+++ b/.chglog/config.yml
@@ -18,10 +18,21 @@ options:
perf: Performance Improvements
refactor: Code Refactoring
header:
- pattern: "^(\\w*)\\:\\s(.*)$"
+ pattern: "^(\\w*)[\\[(\\w*)\\]]\\:\\s(.*)$"
pattern_maps:
- Type
+ - JiraIssueId
- Subject
notes:
keywords:
- BREAKING CHANGE
+ jira:
+ info:
+ username: u
+ token: p
+ url: https://jira.com
+ issue:
+ type_maps:
+ Task: fix
+ Story: feat
+ description_pattern: "(.*)"
diff --git a/Gopkg.lock b/Gopkg.lock
index d07ec387..df90c648 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -2,96 +2,178 @@
[[projects]]
+ digest = "1:6b933ddaf9c30844438b732960021771842cd3a4a57d521e62de6e844bd55370"
+ name = "github.com/andygrunwald/go-jira"
+ packages = ["."]
+ pruneopts = "NUT"
+ revision = "7966e7f5ed3e453362a4deac213c0e6cf7563f4c"
+ version = "v1.10.0"
+
+[[projects]]
+ digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
+ pruneopts = "NUT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
+ digest = "1:7c4b5e23bb01e5945b0df8a5f181efa2d90abd19e368be7c7bd55a5737db0739"
name = "github.com/fatih/color"
packages = ["."]
+ pruneopts = "NUT"
revision = "507f6050b8568533fb3f5504de8e5205fa62a114"
version = "v1.6.0"
[[projects]]
+ digest = "1:aa3ed0a71c4e66e4ae6486bf97a3f4cab28edc78df2e50c5ad01dc7d91604b88"
+ name = "github.com/fatih/structs"
+ packages = ["."]
+ pruneopts = "NUT"
+ revision = "4966fc68f5b7593aafa6cbbba2d65ec6e1416047"
+ version = "v1.1.0"
+
+[[projects]]
+ digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690"
+ name = "github.com/google/go-querystring"
+ packages = ["query"]
+ pruneopts = "NUT"
+ revision = "44c6ddd0a2342c386950e880b658017258da92fc"
+ version = "v1.0.0"
+
+[[projects]]
+ digest = "1:cf327083982a19eae01f70be6663a935a02bac3a2dc79efbc19668c8378bfbb3"
name = "github.com/imdario/mergo"
packages = ["."]
+ pruneopts = "NUT"
revision = "163f41321a19dd09362d4c63cc2489db2015f1f4"
version = "0.3.2"
[[projects]]
+ digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
name = "github.com/mattn/go-colorable"
packages = ["."]
+ pruneopts = "NUT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
+ digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287"
name = "github.com/mattn/go-isatty"
packages = ["."]
+ pruneopts = "NUT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
branch = "master"
+ digest = "1:063d55b87e200bced5e2be658cc70acafb4c5bbc4afa04d4b82f66298b73d089"
name = "github.com/mgutz/ansi"
packages = ["."]
+ pruneopts = "NUT"
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
[[projects]]
+ digest = "1:14715f705ff5dfe0ffd6571d7d201dd8e921030f8070321a79380d8ca4ec1a24"
+ name = "github.com/pkg/errors"
+ packages = ["."]
+ pruneopts = "NUT"
+ revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
+ version = "v0.8.1"
+
+[[projects]]
+ digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
+ pruneopts = "NUT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
+ digest = "1:a852b1ad03ca063d2c57866d9f94dcb1cb2e111415c5902ce0586fc2d207221b"
name = "github.com/stretchr/testify"
packages = ["assert"]
+ pruneopts = "NUT"
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
+ digest = "1:5e59b9b6d0c389f8b691d5caa2211dea04ac95c5285c95b3b8e1187042df3f26"
+ name = "github.com/trivago/tgo"
+ packages = [
+ "tcontainer",
+ "treflect",
+ ]
+ pruneopts = "NUT"
+ revision = "efdb64f40efe6e7cd3f50415710e7af6a7c316ad"
+ version = "v1.0.7"
+
+[[projects]]
+ digest = "1:bc6fc3b838718f65a91d63108426e69bbddf643c04f20e2bbbb0ea907d84b984"
name = "github.com/tsuyoshiwada/go-gitcmd"
packages = ["."]
+ pruneopts = "NUT"
revision = "5f1f5f9475df211f8b48620d704284d7375229c3"
version = "0.0.1"
[[projects]]
+ digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58"
name = "github.com/urfave/cli"
packages = ["."]
+ pruneopts = "NUT"
revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
version = "v1.20.0"
[[projects]]
branch = "master"
+ digest = "1:600bbc566231723dd6d13ccb6269a1c97235041281d796e03102ae4feab4d5d8"
name = "golang.org/x/sys"
packages = ["unix"]
+ pruneopts = "NUT"
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd"
[[projects]]
+ digest = "1:8f2948d3a161e4ef9591a3a465ef8fb94e12f458115ed00f9aed6c2c0a1d3025"
name = "gopkg.in/AlecAivazis/survey.v1"
packages = [
".",
"core",
- "terminal"
+ "terminal",
]
+ pruneopts = "NUT"
revision = "9f89d9dd66613216993dc300f9f3dbae9c3c9bda"
version = "v1.4.2"
[[projects]]
+ digest = "1:e21bd8ae4721734a5b9a79c7345aa528f0cc2e0cc18265691c37566f1c445a19"
name = "gopkg.in/kyokomi/emoji.v1"
packages = ["."]
+ pruneopts = "NUT"
revision = "7e06b236c489543f53868841f188a294e3383eab"
version = "v1.5"
[[projects]]
branch = "v2"
+ digest = "1:13e704c08924325be00f96e47e7efe0bfddf0913cdfc237423c83f9b183ff590"
name = "gopkg.in/yaml.v2"
packages = ["."]
+ pruneopts = "NUT"
revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
- inputs-digest = "da532fb70b3049dffe6ae9f73b3f0a742452639096a325ac5d2400be6ad0559e"
+ input-imports = [
+ "github.com/andygrunwald/go-jira",
+ "github.com/fatih/color",
+ "github.com/imdario/mergo",
+ "github.com/mattn/go-colorable",
+ "github.com/stretchr/testify/assert",
+ "github.com/tsuyoshiwada/go-gitcmd",
+ "github.com/urfave/cli",
+ "gopkg.in/AlecAivazis/survey.v1",
+ "gopkg.in/kyokomi/emoji.v1",
+ "gopkg.in/yaml.v2",
+ ]
solver-name = "gps-cdcl"
solver-version = 1
diff --git a/README.md b/README.md
index 2c69f0bc..98ff5637 100644
--- a/README.md
+++ b/README.md
@@ -480,6 +480,86 @@ See godoc [RenderData][doc-render-data] for available variables.
> :memo: Even with styles that are not yet supported, it is possible to make ordinary CHANGELOG.
+## Jira Integration
+
+
+Jira is a popular project management tool. When a project uses Jira to track feature development and bug fixes,
+it may also want to generate change log based information stored in Jira. With embedding a Jira story id in git
+commit header, the git-chglog tool may automatically fetch data of the story from Jira, those data then can be
+used to render the template.
+
+Take the following steps to add Jira integration:
+
+#### 1. Change the header parse pattern to recognize Jira issue id in the configure file.
+
+__Where Jira issue is identical Jira story.__
+
+The following is a sample pattern:
+
+ ```yaml
+ header:
+ pattern: "^(\\w*)[\\[(\\w*)\\]]\\:\\s(.*)$"
+ pattern_maps:
+ - Type
+ - JiraIssueId
+ - Subject
+ ```
+
+This sample pattern can match both forms of commit headers:
+
+* `feat: new feature of something`
+* `[JIRA-ID]: something`
+
+#### 2. Add Jira configuration to the configure file.
+
+The following is a sample:
+
+ ```yaml
+ jira:
+ info:
+ username: u
+ token: p
+ url: https://jira.com
+ issue:
+ type_maps:
+ Task: fix
+ Story: feat
+ description_pattern: "(.*)"
+ ```
+
+Here you need to define Jira URL, access username and token (password). If you don't want to
+write your Jira access credential in configure file, you may define them with environment variables:
+`JIRA_URL`, `JIRA_USERNAME` and `JIRA_TOKEN`.
+
+You also needs to define a issue type map. In above sample, Jira issue type `Task` will be
+mapped to `fix` and `Story` will be mapped to `feat`.
+
+As a Jira story's description could be very long, you might not want to include the entire
+description into change log. In that case, you may define `description_pattern` like above,
+so that only content embraced with ` ... ` will be included.
+
+#### 3. Update the template to show Jira data.
+
+In the template, if a commit contains a Jira issue id, then you may show Jira data. For example:
+
+```markdown
+{{ range .CommitGroups -}}
+### {{ .Title }}
+{{ range .Commits -}}
+- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
+{{ if .JiraIssue }} {{ .JiraIssue.Description }}
+{{ end }}
+{{ end }}
+{{ end -}}
+```
+
+Within a `Commit`, the following Jira data can be used in template:
+
+* `.JiraIssue.Summary` - Summary of the Jira story
+* `.JiraIssue.Description` - Description of the Jira story
+* `.JiraIssue.Type` - Original type of the Jira story, and `.Type` will be mapped type.
+* `.JiraIssue.Labels` - A list of strings, each is a Jira label.
+
## FAQ
diff --git a/chglog.go b/chglog.go
index a45f201f..d8b707dc 100644
--- a/chglog.go
+++ b/chglog.go
@@ -16,23 +16,28 @@ import (
// Options is an option used to process commits
type Options struct {
- Processor Processor
- NextTag string // Treat unreleased commits as specified tags (EXPERIMENTAL)
- TagFilterPattern string // Filter tag by regexp
- CommitFilters map[string][]string // Filter by using `Commit` properties and values. Filtering is not done by specifying an empty value
- CommitSortBy string // Property name to use for sorting `Commit` (e.g. `Scope`)
- CommitGroupBy string // Property name of `Commit` to be grouped into `CommitGroup` (e.g. `Type`)
- CommitGroupSortBy string // Property name to use for sorting `CommitGroup` (e.g. `Title`)
- CommitGroupTitleMaps map[string]string // Map for `CommitGroup` title conversion
- HeaderPattern string // A regular expression to use for parsing the commit header
- HeaderPatternMaps []string // A rule for mapping the result of `HeaderPattern` to the property of `Commit`
- IssuePrefix []string // Prefix used for issues (e.g. `#`, `gh-`)
- RefActions []string // Word list of `Ref.Action`
- MergePattern string // A regular expression to use for parsing the merge commit
- MergePatternMaps []string // Similar to `HeaderPatternMaps`
- RevertPattern string // A regular expression to use for parsing the revert commit
- RevertPatternMaps []string // Similar to `HeaderPatternMaps`
- NoteKeywords []string // Keyword list to find `Note`. A semicolon is a separator, like `:` (e.g. `BREAKING CHANGE`)
+ Processor Processor
+ NextTag string // Treat unreleased commits as specified tags (EXPERIMENTAL)
+ TagFilterPattern string // Filter tag by regexp
+ CommitFilters map[string][]string // Filter by using `Commit` properties and values. Filtering is not done by specifying an empty value
+ CommitSortBy string // Property name to use for sorting `Commit` (e.g. `Scope`)
+ CommitGroupBy string // Property name of `Commit` to be grouped into `CommitGroup` (e.g. `Type`)
+ CommitGroupSortBy string // Property name to use for sorting `CommitGroup` (e.g. `Title`)
+ CommitGroupTitleMaps map[string]string // Map for `CommitGroup` title conversion
+ HeaderPattern string // A regular expression to use for parsing the commit header
+ HeaderPatternMaps []string // A rule for mapping the result of `HeaderPattern` to the property of `Commit`
+ IssuePrefix []string // Prefix used for issues (e.g. `#`, `gh-`)
+ RefActions []string // Word list of `Ref.Action`
+ MergePattern string // A regular expression to use for parsing the merge commit
+ MergePatternMaps []string // Similar to `HeaderPatternMaps`
+ RevertPattern string // A regular expression to use for parsing the revert commit
+ RevertPatternMaps []string // Similar to `HeaderPatternMaps`
+ NoteKeywords []string // Keyword list to find `Note`. A semicolon is a separator, like `:` (e.g. `BREAKING CHANGE`)
+ JiraUsername string
+ JiraToken string
+ JiraUrl string
+ JiraTypeMaps map[string]string
+ JiraIssueDescriptionPattern string
}
// Info is metadata related to CHANGELOG
@@ -100,6 +105,8 @@ func NewGenerator(config *Config) *Generator {
Bin: config.Bin,
})
+ jiraClient := NewJiraClient(config)
+
if config.Options.Processor != nil {
config.Options.Processor.Bootstrap(config)
}
@@ -111,7 +118,7 @@ func NewGenerator(config *Config) *Generator {
config: config,
tagReader: newTagReader(client, config.Options.TagFilterPattern),
tagSelector: newTagSelector(),
- commitParser: newCommitParser(client, config),
+ commitParser: newCommitParser(client, jiraClient, config),
commitExtractor: newCommitExtractor(config.Options),
}
}
diff --git a/cmd/git-chglog/config.go b/cmd/git-chglog/config.go
index 6f13b723..e20fc93d 100644
--- a/cmd/git-chglog/config.go
+++ b/cmd/git-chglog/config.go
@@ -48,6 +48,24 @@ type NoteOptions struct {
Keywords []string `yaml:"keywords"`
}
+type JiraClientInfoOptions struct {
+ Username string `yaml:"username"`
+ Token string `yaml:"token"`
+ URL string `yaml:"url"`
+}
+
+// JiraIssueOptions ...
+type JiraIssueOptions struct {
+ TypeMaps map[string]string `yaml:"type_maps"`
+ DescriptionPattern string `yaml:"description_pattern"`
+}
+
+// JiraOptions ...
+type JiraOptions struct {
+ ClintInfo JiraClientInfoOptions `yaml:"info"`
+ Issue JiraIssueOptions `yaml:"issue"`
+}
+
// Options ...
type Options struct {
Commits CommitOptions `yaml:"commits"`
@@ -58,6 +76,7 @@ type Options struct {
Merges PatternOptions `yaml:"merges"`
Reverts PatternOptions `yaml:"reverts"`
Notes NoteOptions `yaml:"notes"`
+ Jira JiraOptions `yaml:"jira"`
}
// Config ...
@@ -243,6 +262,13 @@ func (config *Config) normalizeStyleOfBitbucket() {
config.Options = opts
}
+func or_value(str1 string, str2 string) string {
+ if str1 != "" {
+ return str1
+ }
+ return str2
+}
+
// Convert ...
func (config *Config) Convert(ctx *CLIContext) *chglog.Config {
info := config.Info
@@ -251,28 +277,33 @@ func (config *Config) Convert(ctx *CLIContext) *chglog.Config {
return &chglog.Config{
Bin: config.Bin,
WorkingDir: ctx.WorkingDir,
- Template: config.Template,
+ Template: or_value(ctx.Template, config.Template),
Info: &chglog.Info{
Title: info.Title,
- RepositoryURL: info.RepositoryURL,
+ RepositoryURL: or_value(ctx.RepositoryUrl, info.RepositoryURL),
},
Options: &chglog.Options{
- NextTag: ctx.NextTag,
- TagFilterPattern: ctx.TagFilterPattern,
- CommitFilters: opts.Commits.Filters,
- CommitSortBy: opts.Commits.SortBy,
- CommitGroupBy: opts.CommitGroups.GroupBy,
- CommitGroupSortBy: opts.CommitGroups.SortBy,
- CommitGroupTitleMaps: opts.CommitGroups.TitleMaps,
- HeaderPattern: opts.Header.Pattern,
- HeaderPatternMaps: opts.Header.PatternMaps,
- IssuePrefix: opts.Issues.Prefix,
- RefActions: opts.Refs.Actions,
- MergePattern: opts.Merges.Pattern,
- MergePatternMaps: opts.Merges.PatternMaps,
- RevertPattern: opts.Reverts.Pattern,
- RevertPatternMaps: opts.Reverts.PatternMaps,
- NoteKeywords: opts.Notes.Keywords,
+ NextTag: ctx.NextTag,
+ TagFilterPattern: ctx.TagFilterPattern,
+ CommitFilters: opts.Commits.Filters,
+ CommitSortBy: opts.Commits.SortBy,
+ CommitGroupBy: opts.CommitGroups.GroupBy,
+ CommitGroupSortBy: opts.CommitGroups.SortBy,
+ CommitGroupTitleMaps: opts.CommitGroups.TitleMaps,
+ HeaderPattern: opts.Header.Pattern,
+ HeaderPatternMaps: opts.Header.PatternMaps,
+ IssuePrefix: opts.Issues.Prefix,
+ RefActions: opts.Refs.Actions,
+ MergePattern: opts.Merges.Pattern,
+ MergePatternMaps: opts.Merges.PatternMaps,
+ RevertPattern: opts.Reverts.Pattern,
+ RevertPatternMaps: opts.Reverts.PatternMaps,
+ NoteKeywords: opts.Notes.Keywords,
+ JiraUsername: or_value(ctx.JiraUsername, opts.Jira.ClintInfo.Username),
+ JiraToken: or_value(ctx.JiraToken, opts.Jira.ClintInfo.Token),
+ JiraUrl: or_value(ctx.JiraUrl, opts.Jira.ClintInfo.URL),
+ JiraTypeMaps: opts.Jira.Issue.TypeMaps,
+ JiraIssueDescriptionPattern: opts.Jira.Issue.DescriptionPattern,
},
}
}
diff --git a/cmd/git-chglog/context.go b/cmd/git-chglog/context.go
index 79b497e3..bcb110f9 100644
--- a/cmd/git-chglog/context.go
+++ b/cmd/git-chglog/context.go
@@ -10,6 +10,8 @@ type CLIContext struct {
Stdout io.Writer
Stderr io.Writer
ConfigPath string
+ Template string
+ RepositoryUrl string
OutputPath string
Silent bool
NoColor bool
@@ -17,6 +19,9 @@ type CLIContext struct {
Query string
NextTag string
TagFilterPattern string
+ JiraUsername string
+ JiraToken string
+ JiraUrl string
}
// InitContext ...
diff --git a/cmd/git-chglog/main.go b/cmd/git-chglog/main.go
index d4582ba8..efc6a474 100644
--- a/cmd/git-chglog/main.go
+++ b/cmd/git-chglog/main.go
@@ -83,6 +83,18 @@ func CreateApp(actionFunc cli.ActionFunc) *cli.App {
Value: ".chglog/config.yml",
},
+ // template
+ cli.StringFlag{
+ Name: "template, t",
+ Usage: "specifies a template file to pick up. If not specified, use the one in config",
+ },
+
+ // repository url
+ cli.StringFlag{
+ Name: "repository-url",
+ Usage: "specifies git repo URL. If not specified, use 'repository_url' in config",
+ },
+
// output
cli.StringFlag{
Name: "output, o",
@@ -120,6 +132,24 @@ func CreateApp(actionFunc cli.ActionFunc) *cli.App {
Usage: "Regular expression of tag filter. Is specified, only matched tags will be picked",
},
+ cli.StringFlag{
+ Name: "jira-url",
+ Usage: "Jira URL",
+ EnvVar: "JIRA_URL",
+ },
+
+ cli.StringFlag{
+ Name: "jira-username",
+ Usage: "Jira username",
+ EnvVar: "JIRA_USERNAME",
+ },
+
+ cli.StringFlag{
+ Name: "jira-token",
+ Usage: "Jira token",
+ EnvVar: "JIRA_TOKEN",
+ },
+
// help & version
cli.HelpFlag,
cli.VersionFlag,
@@ -162,17 +192,20 @@ func AppAction(c *cli.Context) error {
// chglog
chglogCLI := NewCLI(
&CLIContext{
- WorkingDir: wd,
- Stdout: colorable.NewColorableStdout(),
- Stderr: colorable.NewColorableStderr(),
- ConfigPath: c.String("config"),
- OutputPath: c.String("output"),
- Silent: c.Bool("silent"),
- NoColor: c.Bool("no-color"),
- NoEmoji: c.Bool("no-emoji"),
- Query: c.Args().First(),
- NextTag: c.String("next-tag"),
+ WorkingDir: wd,
+ Stdout: colorable.NewColorableStdout(),
+ Stderr: colorable.NewColorableStderr(),
+ ConfigPath: c.String("config"),
+ OutputPath: c.String("output"),
+ Silent: c.Bool("silent"),
+ NoColor: c.Bool("no-color"),
+ NoEmoji: c.Bool("no-emoji"),
+ Query: c.Args().First(),
+ NextTag: c.String("next-tag"),
TagFilterPattern: c.String("tag-filter-pattern"),
+ JiraUsername: c.String("jira-username"),
+ JiraToken: c.String("jira-token"),
+ JiraUrl: c.String("jira-url"),
},
fs,
NewConfigLoader(),
diff --git a/commit_parser.go b/commit_parser.go
index 5054b227..a4942499 100644
--- a/commit_parser.go
+++ b/commit_parser.go
@@ -1,12 +1,14 @@
package chglog
import (
+ "fmt"
+ "os"
"regexp"
"strconv"
"strings"
"time"
- gitcmd "github.com/tsuyoshiwada/go-gitcmd"
+ "github.com/tsuyoshiwada/go-gitcmd"
)
var (
@@ -47,18 +49,20 @@ func joinAndQuoteMeta(list []string, sep string) string {
}
type commitParser struct {
- client gitcmd.Client
- config *Config
- reHeader *regexp.Regexp
- reMerge *regexp.Regexp
- reRevert *regexp.Regexp
- reRef *regexp.Regexp
- reIssue *regexp.Regexp
- reNotes *regexp.Regexp
- reMention *regexp.Regexp
+ client gitcmd.Client
+ jiraClient JiraClient
+ config *Config
+ reHeader *regexp.Regexp
+ reMerge *regexp.Regexp
+ reRevert *regexp.Regexp
+ reRef *regexp.Regexp
+ reIssue *regexp.Regexp
+ reNotes *regexp.Regexp
+ reMention *regexp.Regexp
+ reJiraIssueDescription *regexp.Regexp
}
-func newCommitParser(client gitcmd.Client, config *Config) *commitParser {
+func newCommitParser(client gitcmd.Client, jiraClient JiraClient, config *Config) *commitParser {
opts := config.Options
joinedRefActions := joinAndQuoteMeta(opts.RefActions, "|")
@@ -66,15 +70,17 @@ func newCommitParser(client gitcmd.Client, config *Config) *commitParser {
joinedNoteKeywords := joinAndQuoteMeta(opts.NoteKeywords, "|")
return &commitParser{
- client: client,
- config: config,
- reHeader: regexp.MustCompile(opts.HeaderPattern),
- reMerge: regexp.MustCompile(opts.MergePattern),
- reRevert: regexp.MustCompile(opts.RevertPattern),
- reRef: regexp.MustCompile("(?i)(" + joinedRefActions + ")\\s?([\\w/\\.\\-]+)?(?:" + joinedIssuePrefix + ")(\\d+)"),
- reIssue: regexp.MustCompile("(?:" + joinedIssuePrefix + ")(\\d+)"),
- reNotes: regexp.MustCompile("^(?i)\\s*(" + joinedNoteKeywords + ")[:\\s]+(.*)"),
- reMention: regexp.MustCompile("@([\\w-]+)"),
+ client: client,
+ jiraClient: jiraClient,
+ config: config,
+ reHeader: regexp.MustCompile(opts.HeaderPattern),
+ reMerge: regexp.MustCompile(opts.MergePattern),
+ reRevert: regexp.MustCompile(opts.RevertPattern),
+ reRef: regexp.MustCompile("(?i)(" + joinedRefActions + ")\\s?([\\w/\\.\\-]+)?(?:" + joinedIssuePrefix + ")(\\d+)"),
+ reIssue: regexp.MustCompile("(?:" + joinedIssuePrefix + ")(\\d+)"),
+ reNotes: regexp.MustCompile("^(?i)\\s*(" + joinedNoteKeywords + ")[:\\s]+(.*)"),
+ reMention: regexp.MustCompile("@([\\w-]+)"),
+ reJiraIssueDescription: regexp.MustCompile(opts.JiraIssueDescriptionPattern),
}
}
@@ -206,6 +212,11 @@ func (p *commitParser) processHeader(commit *Commit, input string) {
// refs & mentions
commit.Refs = p.parseRefs(input)
commit.Mentions = p.parseMentions(input)
+
+ // Jira
+ if commit.JiraIssueId != "" {
+ p.processJiraIssue(commit, commit.JiraIssueId)
+ }
}
func (p *commitParser) processBody(commit *Commit, input string) {
@@ -344,6 +355,28 @@ func (p *commitParser) uniqMentions(mentions []string) []string {
return arr
}
+func (p *commitParser) processJiraIssue(commit *Commit, issueId string) {
+ issue, err := p.jiraClient.GetJiraIssue(commit.JiraIssueId)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to parse Jira story %s: %s\n", issueId, err)
+ return
+ }
+ commit.Type = p.config.Options.JiraTypeMaps[issue.Fields.Type.Name]
+ commit.JiraIssue = &JiraIssue{
+ Type: issue.Fields.Type.Name,
+ Summary: issue.Fields.Summary,
+ Description: issue.Fields.Description,
+ Labels: issue.Fields.Labels,
+ }
+
+ if p.config.Options.JiraIssueDescriptionPattern != "" {
+ res := p.reJiraIssueDescription.FindStringSubmatch(commit.JiraIssue.Description)
+ if len(res) > 1 {
+ commit.JiraIssue.Description = res[1]
+ }
+ }
+}
+
var (
fenceTypes = []string{
"```",
diff --git a/commit_parser_test.go b/commit_parser_test.go
index 2df79531..1a70c674 100644
--- a/commit_parser_test.go
+++ b/commit_parser_test.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
+ agjira "github.com/andygrunwald/go-jira"
)
func TestCommitParserParse(t *testing.T) {
@@ -27,7 +28,7 @@ func TestCommitParserParse(t *testing.T) {
},
}
- parser := newCommitParser(mock, &Config{
+ parser := newCommitParser(mock, nil, &Config{
Options: &Options{
CommitFilters: map[string][]string{
"Type": []string{
@@ -308,3 +309,102 @@ Closes username/repository#456`, "```", "```"),
},
}, commits)
}
+
+
+type mockJiraClient struct {
+}
+
+func (jira mockJiraClient) GetJiraIssue(id string) (*agjira.Issue, error) {
+ return &agjira.Issue {
+ ID: id,
+ Fields: &agjira.IssueFields{
+ Expand: "",
+ Type: agjira.IssueType{Name: "Story"},
+ Project: agjira.Project{},
+ Resolution: nil,
+ Priority: nil,
+ Resolutiondate: agjira.Time{},
+ Created: agjira.Time{},
+ Duedate: agjira.Date{},
+ Watches: nil,
+ Assignee: nil,
+ Updated: agjira.Time{},
+ Description: fmt.Sprintf("description of %s", id),
+ Summary: fmt.Sprintf("summary of %s", id),
+ Creator: nil,
+ Reporter: nil,
+ Components: nil,
+ Status: nil,
+ Progress: nil,
+ AggregateProgress: nil,
+ TimeTracking: nil,
+ TimeSpent: 0,
+ TimeEstimate: 0,
+ TimeOriginalEstimate: 0,
+ Worklog: nil,
+ IssueLinks: nil,
+ Comments: nil,
+ FixVersions: nil,
+ AffectsVersions: nil,
+ Labels: []string{"GA"},
+ Subtasks: nil,
+ Attachments: nil,
+ Epic: nil,
+ Sprint: nil,
+ Parent: nil,
+ AggregateTimeOriginalEstimate: 0,
+ AggregateTimeSpent: 0,
+ AggregateTimeEstimate: 0,
+ Unknowns: nil,
+ },
+ }, nil
+}
+
+func TestCommitParserParseWithJira(t *testing.T) {
+ assert := assert.New(t)
+ assert.True(true)
+
+ mock := &mockClient{
+ ReturnExec: func(subcmd string, args ...string) (string, error) {
+ if subcmd != "log" {
+ return "", errors.New("")
+ }
+
+ bytes, _ := ioutil.ReadFile(filepath.Join("testdata", "gitlog_jira.txt"))
+
+ return string(bytes), nil
+ },
+ }
+
+ parser := newCommitParser(mock, mockJiraClient{}, &Config{
+ Options: &Options{
+ CommitFilters: map[string][]string{
+ "Type": []string{
+ "feat",
+ "fix",
+ "perf",
+ "refactor",
+ },
+ },
+ HeaderPattern: "^(?:(\\w*)|(?:\\[(.*)\\])?)\\:\\s(.*)$",
+ HeaderPatternMaps: []string{
+ "Type",
+ "JiraIssueId",
+ "Subject",
+ },
+ JiraTypeMaps: map[string]string {
+ "Story": "feat",
+ },
+ },
+ })
+
+ commits, err := parser.Parse("HEAD")
+ assert.Nil(err)
+ commit := commits[0]
+ assert.Equal(commit.JiraIssueId, "JIRA-1111")
+ assert.Equal(commit.JiraIssue.Type, "Story")
+ assert.Equal(commit.JiraIssue.Summary, "summary of JIRA-1111")
+ assert.Equal(commit.JiraIssue.Description, "description of JIRA-1111")
+ assert.Equal(commit.JiraIssue.Labels, []string{"GA"})
+ assert.Equal(commit.Type, "feat")
+}
diff --git a/fields.go b/fields.go
index 46cf8cc3..123da6c7 100644
--- a/fields.go
+++ b/fields.go
@@ -52,21 +52,31 @@ type NoteGroup struct {
Notes []*Note
}
+// JiraIssue
+type JiraIssue struct {
+ Type string
+ Summary string
+ Description string
+ Labels []string
+}
+
// Commit data
type Commit struct {
- Hash *Hash
- Author *Author
- Committer *Committer
- Merge *Merge // If it is not a merge commit, `nil` is assigned
- Revert *Revert // If it is not a revert commit, `nil` is assigned
- Refs []*Ref
- Notes []*Note
- Mentions []string // Name of the user included in the commit header or body
- Header string // (e.g. `feat(core): Add new feature`)
- Type string // (e.g. `feat`)
- Scope string // (e.g. `core`)
- Subject string // (e.g. `Add new feature`)
- Body string
+ Hash *Hash
+ Author *Author
+ Committer *Committer
+ Merge *Merge // If it is not a merge commit, `nil` is assigned
+ Revert *Revert // If it is not a revert commit, `nil` is assigned
+ Refs []*Ref
+ Notes []*Note
+ Mentions []string // Name of the user included in the commit header or body
+ JiraIssue *JiraIssue // If no issue id found in header, `nil` is assigned
+ Header string // (e.g. `feat(core)[RNWY-310]: Add new feature`)
+ Type string // (e.g. `feat`)
+ Scope string // (e.g. `core`)
+ Subject string // (e.g. `Add new feature`)
+ JiraIssueId string // (e.g. `RNWY-310`)
+ Body string
}
// CommitGroup is a collection of commits grouped according to the `CommitGroupBy` option
diff --git a/jira.go b/jira.go
new file mode 100644
index 00000000..78afbdd7
--- /dev/null
+++ b/jira.go
@@ -0,0 +1,36 @@
+package chglog
+
+import (
+ agjira "github.com/andygrunwald/go-jira"
+)
+
+type JiraClient interface {
+ GetJiraIssue(id string) (*agjira.Issue, error)
+}
+
+type jiraClient struct {
+ username string
+ token string
+ url string
+}
+
+func NewJiraClient(config *Config) JiraClient {
+ return jiraClient{
+ username: config.Options.JiraUsername,
+ token: config.Options.JiraToken,
+ url: config.Options.JiraUrl,
+ }
+}
+
+func (jira jiraClient) GetJiraIssue(id string) (*agjira.Issue, error) {
+ tp := agjira.BasicAuthTransport{
+ Username: jira.username,
+ Password: jira.token,
+ }
+ client, err := agjira.NewClient(tp.Client(), jira.url)
+ if err != nil {
+ return nil, err
+ }
+ issue, _, err := client.Issue.Get(id, nil)
+ return issue, err
+}
diff --git a/jira_test.go b/jira_test.go
new file mode 100644
index 00000000..30936e29
--- /dev/null
+++ b/jira_test.go
@@ -0,0 +1,42 @@
+package chglog
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestJira(t *testing.T) {
+ assert := assert.New(t)
+
+ config := &Config {
+ Options: &Options{
+ Processor: nil,
+ NextTag: "",
+ TagFilterPattern: "",
+ CommitFilters: nil,
+ CommitSortBy: "",
+ CommitGroupBy: "",
+ CommitGroupSortBy: "",
+ CommitGroupTitleMaps: nil,
+ HeaderPattern: "",
+ HeaderPatternMaps: nil,
+ IssuePrefix: nil,
+ RefActions: nil,
+ MergePattern: "",
+ MergePatternMaps: nil,
+ RevertPattern: "",
+ RevertPatternMaps: nil,
+ NoteKeywords: nil,
+ JiraUsername: "uuu",
+ JiraToken: "ppp",
+ JiraUrl: "http://jira.com",
+ JiraTypeMaps: nil,
+ JiraIssueDescriptionPattern: "",
+ },
+ }
+
+ jira := NewJiraClient(config)
+ issue, err := jira.GetJiraIssue("fake")
+ assert.Nil(issue)
+ assert.Error(err)
+}
diff --git a/testdata/gitlog_jira.txt b/testdata/gitlog_jira.txt
new file mode 100644
index 00000000..f535f2e1
--- /dev/null
+++ b/testdata/gitlog_jira.txt
@@ -0,0 +1 @@
+@@__CHGLOG__@@HASH:65cf1add9735dcc4810dda3312b0792236c97c4e 65cf1add@@__CHGLOG_DELIMITER__@@AUTHOR:tsuyoshi wada mail@example.com 1514808000@@__CHGLOG_DELIMITER__@@COMMITTER:tsuyoshi wada mail@example.com 1514808000@@__CHGLOG_DELIMITER__@@SUBJECT:[JIRA-1111]: Add new feature #123@@__CHGLOG_DELIMITER__@@BODY: This is body message.
diff --git a/vendor/github.com/andygrunwald/go-jira/LICENSE b/vendor/github.com/andygrunwald/go-jira/LICENSE
new file mode 100644
index 00000000..692f6bea
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Andy Grunwald
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/github.com/andygrunwald/go-jira/authentication.go b/vendor/github.com/andygrunwald/go-jira/authentication.go
new file mode 100644
index 00000000..f848a1d5
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/authentication.go
@@ -0,0 +1,187 @@
+package jira
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+)
+
+const (
+ // HTTP Basic Authentication
+ authTypeBasic = 1
+ // HTTP Session Authentication
+ authTypeSession = 2
+)
+
+// AuthenticationService handles authentication for the JIRA instance / API.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
+type AuthenticationService struct {
+ client *Client
+
+ // Authentication type
+ authType int
+
+ // Basic auth username
+ username string
+
+ // Basic auth password
+ password string
+}
+
+// Session represents a Session JSON response by the JIRA API.
+type Session struct {
+ Self string `json:"self,omitempty"`
+ Name string `json:"name,omitempty"`
+ Session struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ } `json:"session,omitempty"`
+ LoginInfo struct {
+ FailedLoginCount int `json:"failedLoginCount"`
+ LoginCount int `json:"loginCount"`
+ LastFailedLoginTime string `json:"lastFailedLoginTime"`
+ PreviousLoginTime string `json:"previousLoginTime"`
+ } `json:"loginInfo"`
+ Cookies []*http.Cookie
+}
+
+// AcquireSessionCookie creates a new session for a user in JIRA.
+// Once a session has been successfully created it can be used to access any of JIRA's remote APIs and also the web UI by passing the appropriate HTTP Cookie header.
+// The header will by automatically applied to every API request.
+// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
+// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
+//
+// Deprecated: Use CookieAuthTransport instead
+func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
+ apiEndpoint := "rest/auth/1/session"
+ body := struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ }{
+ username,
+ password,
+ }
+
+ req, err := s.client.NewRequest("POST", apiEndpoint, body)
+ if err != nil {
+ return false, err
+ }
+
+ session := new(Session)
+ resp, err := s.client.Do(req, session)
+
+ if resp != nil {
+ session.Cookies = resp.Cookies()
+ }
+
+ if err != nil {
+ return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
+ }
+ if resp != nil && resp.StatusCode != 200 {
+ return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
+ }
+
+ s.client.session = session
+ s.authType = authTypeSession
+
+ return true, nil
+}
+
+// SetBasicAuth sets username and password for the basic auth against the JIRA instance.
+//
+// Deprecated: Use BasicAuthTransport instead
+func (s *AuthenticationService) SetBasicAuth(username, password string) {
+ s.username = username
+ s.password = password
+ s.authType = authTypeBasic
+}
+
+// Authenticated reports if the current Client has authentication details for JIRA
+func (s *AuthenticationService) Authenticated() bool {
+ if s != nil {
+ if s.authType == authTypeSession {
+ return s.client.session != nil
+ } else if s.authType == authTypeBasic {
+ return s.username != ""
+ }
+
+ }
+ return false
+}
+
+// Logout logs out the current user that has been authenticated and the session in the client is destroyed.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
+//
+// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the
+// client anymore
+func (s *AuthenticationService) Logout() error {
+ if s.authType != authTypeSession || s.client.session == nil {
+ return fmt.Errorf("no user is authenticated")
+ }
+
+ apiEndpoint := "rest/auth/1/session"
+ req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
+ if err != nil {
+ return fmt.Errorf("Creating the request to log the user out failed : %s", err)
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ return fmt.Errorf("Error sending the logout request: %s", err)
+ }
+ if resp.StatusCode != 204 {
+ return fmt.Errorf("The logout was unsuccessful with status %d", resp.StatusCode)
+ }
+
+ // If logout successful, delete session
+ s.client.session = nil
+
+ return nil
+
+}
+
+// GetCurrentUser gets the details of the current user.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
+func (s *AuthenticationService) GetCurrentUser() (*Session, error) {
+ if s == nil {
+ return nil, fmt.Errorf("AUthenticaiton Service is not instantiated")
+ }
+ if s.authType != authTypeSession || s.client.session == nil {
+ return nil, fmt.Errorf("No user is authenticated yet")
+ }
+
+ apiEndpoint := "rest/auth/1/session"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, fmt.Errorf("Could not create request for getting user info : %s", err)
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ return nil, fmt.Errorf("Error sending request to get user info : %s", err)
+ }
+ if resp.StatusCode != 200 {
+ return nil, fmt.Errorf("Getting user info failed with status : %d", resp.StatusCode)
+ }
+
+ defer resp.Body.Close()
+ ret := new(Session)
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Couldn't read body from the response : %s", err)
+ }
+
+ err = json.Unmarshal(data, &ret)
+
+ if err != nil {
+ return nil, fmt.Errorf("Could not unmarshall received user info : %s", err)
+ }
+
+ return ret, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/board.go b/vendor/github.com/andygrunwald/go-jira/board.go
new file mode 100644
index 00000000..a691672c
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/board.go
@@ -0,0 +1,204 @@
+package jira
+
+import (
+ "fmt"
+ "strconv"
+ "time"
+)
+
+// BoardService handles Agile Boards for the JIRA instance / API.
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/server/
+type BoardService struct {
+ client *Client
+}
+
+// BoardsList reflects a list of agile boards
+type BoardsList struct {
+ MaxResults int `json:"maxResults" structs:"maxResults"`
+ StartAt int `json:"startAt" structs:"startAt"`
+ Total int `json:"total" structs:"total"`
+ IsLast bool `json:"isLast" structs:"isLast"`
+ Values []Board `json:"values" structs:"values"`
+}
+
+// Board represents a JIRA agile board
+type Board struct {
+ ID int `json:"id,omitempty" structs:"id,omitempty"`
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitemtpy"`
+ Type string `json:"type,omitempty" structs:"type,omitempty"`
+ FilterID int `json:"filterId,omitempty" structs:"filterId,omitempty"`
+}
+
+// BoardListOptions specifies the optional parameters to the BoardService.GetList
+type BoardListOptions struct {
+ // BoardType filters results to boards of the specified type.
+ // Valid values: scrum, kanban.
+ BoardType string `url:"type,omitempty"`
+ // Name filters results to boards that match or partially match the specified name.
+ Name string `url:"name,omitempty"`
+ // ProjectKeyOrID filters results to boards that are relevant to a project.
+ // Relevance meaning that the JQL filter defined in board contains a reference to a project.
+ ProjectKeyOrID string `url:"projectKeyOrId,omitempty"`
+
+ SearchOptions
+}
+
+// GetAllSprintsOptions specifies the optional parameters to the BoardService.GetList
+type GetAllSprintsOptions struct {
+ // State filters results to sprints in the specified states, comma-separate list
+ State string `url:"state,omitempty"`
+
+ SearchOptions
+}
+
+// SprintsList reflects a list of agile sprints
+type SprintsList struct {
+ MaxResults int `json:"maxResults" structs:"maxResults"`
+ StartAt int `json:"startAt" structs:"startAt"`
+ Total int `json:"total" structs:"total"`
+ IsLast bool `json:"isLast" structs:"isLast"`
+ Values []Sprint `json:"values" structs:"values"`
+}
+
+// Sprint represents a sprint on JIRA agile board
+type Sprint struct {
+ ID int `json:"id" structs:"id"`
+ Name string `json:"name" structs:"name"`
+ CompleteDate *time.Time `json:"completeDate" structs:"completeDate"`
+ EndDate *time.Time `json:"endDate" structs:"endDate"`
+ StartDate *time.Time `json:"startDate" structs:"startDate"`
+ OriginBoardID int `json:"originBoardId" structs:"originBoardId"`
+ Self string `json:"self" structs:"self"`
+ State string `json:"state" structs:"state"`
+}
+
+// GetAllBoards will returns all boards. This only includes boards that the user has permission to view.
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards
+func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) {
+ apiEndpoint := "rest/agile/1.0/board"
+ url, err := addOptions(apiEndpoint, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+ req, err := s.client.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ boards := new(BoardsList)
+ resp, err := s.client.Do(req, boards)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return boards, resp, err
+}
+
+// GetBoard will returns the board for the given boardID.
+// This board will only be returned if the user has permission to view it.
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard
+func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ board := new(Board)
+ resp, err := s.client.Do(req, board)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return board, resp, nil
+}
+
+// CreateBoard creates a new board. Board name, type and filter Id is required.
+// name - Must be less than 255 characters.
+// type - Valid values: scrum, kanban
+// filterId - Id of a filter that the user has permissions to view.
+// Note, if the user does not have the 'Create shared objects' permission and tries to create a shared board, a private
+// board will be created instead (remember that board sharing depends on the filter sharing).
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard
+func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) {
+ apiEndpoint := "rest/agile/1.0/board"
+ req, err := s.client.NewRequest("POST", apiEndpoint, board)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ responseBoard := new(Board)
+ resp, err := s.client.Do(req, responseBoard)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return responseBoard, resp, nil
+}
+
+// DeleteBoard will delete an agile board.
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard
+func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID)
+ req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+ return nil, resp, err
+}
+
+// GetAllSprints will return all sprints from a board, for a given board Id.
+// This only includes sprints that the user has permission to view.
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
+func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) {
+ id, err := strconv.Atoi(boardID)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ result, response, err := s.GetAllSprintsWithOptions(id, &GetAllSprintsOptions{})
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return result.Values, response, nil
+}
+
+// GetAllSprintsWithOptions will return sprints from a board, for a given board Id and filtering options
+// This only includes sprints that the user has permission to view.
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
+func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", boardID)
+ url, err := addOptions(apiEndpoint, options)
+ if err != nil {
+ return nil, nil, err
+ }
+ req, err := s.client.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ result := new(SprintsList)
+ resp, err := s.client.Do(req, result)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+
+ return result, resp, err
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/component.go b/vendor/github.com/andygrunwald/go-jira/component.go
new file mode 100644
index 00000000..407ad36a
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/component.go
@@ -0,0 +1,38 @@
+package jira
+
+// ComponentService handles components for the JIRA instance / API.
+//
+// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component
+type ComponentService struct {
+ client *Client
+}
+
+// CreateComponentOptions are passed to the ComponentService.Create function to create a new JIRA component
+type CreateComponentOptions struct {
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ Description string `json:"description,omitempty" structs:"description,omitempty"`
+ Lead *User `json:"lead,omitempty" structs:"lead,omitempty"`
+ LeadUserName string `json:"leadUserName,omitempty" structs:"leadUserName,omitempty"`
+ AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"`
+ Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"`
+ Project string `json:"project,omitempty" structs:"project,omitempty"`
+ ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"`
+}
+
+// Create creates a new JIRA component based on the given options.
+func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) {
+ apiEndpoint := "rest/api/2/component"
+ req, err := s.client.NewRequest("POST", apiEndpoint, options)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ component := new(ProjectComponent)
+ resp, err := s.client.Do(req, component)
+
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+
+ return component, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/error.go b/vendor/github.com/andygrunwald/go-jira/error.go
new file mode 100644
index 00000000..bd1a5b9a
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/error.go
@@ -0,0 +1,90 @@
+package jira
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+// Error message from JIRA
+// See https://docs.atlassian.com/jira/REST/cloud/#error-responses
+type Error struct {
+ HTTPError error
+ ErrorMessages []string `json:"errorMessages"`
+ Errors map[string]string `json:"errors"`
+}
+
+// NewJiraError creates a new jira Error
+func NewJiraError(resp *Response, httpError error) error {
+ if resp == nil {
+ return errors.Wrap(httpError, "No response returned")
+ }
+
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return errors.Wrap(err, httpError.Error())
+ }
+ jerr := Error{HTTPError: httpError}
+ contentType := resp.Header.Get("Content-Type")
+ if strings.HasPrefix(contentType, "application/json") {
+ err = json.Unmarshal(body, &jerr)
+ if err != nil {
+ httpError = errors.Wrap(errors.New("Could not parse JSON"), httpError.Error())
+ return errors.Wrap(err, httpError.Error())
+ }
+ } else {
+ if httpError == nil {
+ return fmt.Errorf("Got Response Status %s:%s", resp.Status, string(body))
+ }
+ return errors.Wrap(httpError, fmt.Sprintf("%s: %s", resp.Status, string(body)))
+ }
+
+ return &jerr
+}
+
+// Error is a short string representing the error
+func (e *Error) Error() string {
+ if len(e.ErrorMessages) > 0 {
+ // return fmt.Sprintf("%v", e.HTTPError)
+ return fmt.Sprintf("%s: %v", e.ErrorMessages[0], e.HTTPError)
+ }
+ if len(e.Errors) > 0 {
+ for key, value := range e.Errors {
+ return fmt.Sprintf("%s - %s: %v", key, value, e.HTTPError)
+ }
+ }
+ return e.HTTPError.Error()
+}
+
+// LongError is a full representation of the error as a string
+func (e *Error) LongError() string {
+ var msg bytes.Buffer
+ if e.HTTPError != nil {
+ msg.WriteString("Original:\n")
+ msg.WriteString(e.HTTPError.Error())
+ msg.WriteString("\n")
+ }
+ if len(e.ErrorMessages) > 0 {
+ msg.WriteString("Messages:\n")
+ for _, v := range e.ErrorMessages {
+ msg.WriteString(" - ")
+ msg.WriteString(v)
+ msg.WriteString("\n")
+ }
+ }
+ if len(e.Errors) > 0 {
+ for key, value := range e.Errors {
+ msg.WriteString(" - ")
+ msg.WriteString(key)
+ msg.WriteString(" - ")
+ msg.WriteString(value)
+ msg.WriteString("\n")
+ }
+ }
+ return msg.String()
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/field.go b/vendor/github.com/andygrunwald/go-jira/field.go
new file mode 100644
index 00000000..257d4f99
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/field.go
@@ -0,0 +1,43 @@
+package jira
+
+// FieldService handles fields for the JIRA instance / API.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field
+type FieldService struct {
+ client *Client
+}
+
+// Field represents a field of a JIRA issue.
+type Field struct {
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Key string `json:"key,omitempty" structs:"key,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ Custom bool `json:"custom,omitempty" structs:"custom,omitempty"`
+ Navigable bool `json:"navigable,omitempty" structs:"navigable,omitempty"`
+ Searchable bool `json:"searchable,omitempty" structs:"searchable,omitempty"`
+ ClauseNames []string `json:"clauseNames,omitempty" structs:"clauseNames,omitempty"`
+ Schema FieldSchema `json:"schema,omitempty" structs:"schema,omitempty"`
+}
+
+type FieldSchema struct {
+ Type string `json:"type,omitempty" structs:"type,omitempty"`
+ System string `json:"system,omitempty" structs:"system,omitempty"`
+}
+
+// GetList gets all fields from JIRA
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get
+func (s *FieldService) GetList() ([]Field, *Response, error) {
+ apiEndpoint := "rest/api/2/field"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ fieldList := []Field{}
+ resp, err := s.client.Do(req, &fieldList)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return fieldList, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/filter.go b/vendor/github.com/andygrunwald/go-jira/filter.go
new file mode 100644
index 00000000..209e63bd
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/filter.go
@@ -0,0 +1,93 @@
+package jira
+
+import "github.com/google/go-querystring/query"
+import "fmt"
+
+// FilterService handles fields for the JIRA instance / API.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Filter
+type FilterService struct {
+ client *Client
+}
+
+// Filter represents a Filter in Jira
+type Filter struct {
+ Self string `json:"self"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Owner User `json:"owner"`
+ Jql string `json:"jql"`
+ ViewURL string `json:"viewUrl"`
+ SearchURL string `json:"searchUrl"`
+ Favourite bool `json:"favourite"`
+ FavouritedCount int `json:"favouritedCount"`
+ SharePermissions []interface{} `json:"sharePermissions"`
+ Subscriptions struct {
+ Size int `json:"size"`
+ Items []interface{} `json:"items"`
+ MaxResults int `json:"max-results"`
+ StartIndex int `json:"start-index"`
+ EndIndex int `json:"end-index"`
+ } `json:"subscriptions"`
+}
+
+// GetList retrieves all filters from Jira
+func (fs *FilterService) GetList() ([]*Filter, *Response, error) {
+
+ options := &GetQueryOptions{}
+ apiEndpoint := "rest/api/2/filter"
+ req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if options != nil {
+ q, err := query.Values(options)
+ if err != nil {
+ return nil, nil, err
+ }
+ req.URL.RawQuery = q.Encode()
+ }
+
+ filters := []*Filter{}
+ resp, err := fs.client.Do(req, &filters)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+ return filters, resp, err
+}
+
+// GetFavouriteList retrieves the user's favourited filters from Jira
+func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) {
+ apiEndpoint := "rest/api/2/filter/favourite"
+ req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ filters := []*Filter{}
+ resp, err := fs.client.Do(req, &filters)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+ return filters, resp, err
+}
+
+// Get retrieves a single Filter from Jira
+func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/filter/%d", filterID)
+ req, err := fs.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ filter := new(Filter)
+ resp, err := fs.client.Do(req, filter)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return filter, resp, err
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/group.go b/vendor/github.com/andygrunwald/go-jira/group.go
new file mode 100644
index 00000000..8ceadc9b
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/group.go
@@ -0,0 +1,154 @@
+package jira
+
+import (
+ "fmt"
+ "net/url"
+)
+
+// GroupService handles Groups for the JIRA instance / API.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group
+type GroupService struct {
+ client *Client
+}
+
+// groupMembersResult is only a small wrapper around the Group* methods
+// to be able to parse the results
+type groupMembersResult struct {
+ StartAt int `json:"startAt"`
+ MaxResults int `json:"maxResults"`
+ Total int `json:"total"`
+ Members []GroupMember `json:"values"`
+}
+
+// Group represents a JIRA group
+type Group struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Type string `json:"type"`
+ Properties groupProperties `json:"properties"`
+ AdditionalProperties bool `json:"additionalProperties"`
+}
+
+type groupProperties struct {
+ Name groupPropertiesName `json:"name"`
+}
+
+type groupPropertiesName struct {
+ Type string `json:"type"`
+}
+
+// GroupMember reflects a single member of a group
+type GroupMember struct {
+ Self string `json:"self,omitempty"`
+ Name string `json:"name,omitempty"`
+ Key string `json:"key,omitempty"`
+ EmailAddress string `json:"emailAddress,omitempty"`
+ DisplayName string `json:"displayName,omitempty"`
+ Active bool `json:"active,omitempty"`
+ TimeZone string `json:"timeZone,omitempty"`
+}
+
+// GroupSearchOptions specifies the optional parameters for the Get Group methods
+type GroupSearchOptions struct {
+ StartAt int
+ MaxResults int
+ IncludeInactiveUsers bool
+}
+
+// Get returns a paginated list of users who are members of the specified group and its subgroups.
+// Users in the page are ordered by user names.
+// User of this resource is required to have sysadmin or admin permissions.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
+//
+// WARNING: This API only returns the first page of group members
+func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name))
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ group := new(groupMembersResult)
+ resp, err := s.client.Do(req, group)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return group.Members, resp, nil
+}
+
+// GetWithOptions returns a paginated list of members of the specified group and its subgroups.
+// Users in the page are ordered by user names.
+// User of this resource is required to have sysadmin or admin permissions.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
+func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) {
+ var apiEndpoint string
+ if options == nil {
+ apiEndpoint = fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name))
+ } else {
+ apiEndpoint = fmt.Sprintf(
+ "/rest/api/2/group/member?groupname=%s&startAt=%d&maxResults=%d&includeInactiveUsers=%t",
+ url.QueryEscape(name),
+ options.StartAt,
+ options.MaxResults,
+ options.IncludeInactiveUsers,
+ )
+ }
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ group := new(groupMembersResult)
+ resp, err := s.client.Do(req, group)
+ if err != nil {
+ return nil, resp, err
+ }
+ return group.Members, resp, nil
+}
+
+// Add adds user to group
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup
+func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s", groupname)
+ var user struct {
+ Name string `json:"name"`
+ }
+ user.Name = username
+ req, err := s.client.NewRequest("POST", apiEndpoint, &user)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ responseGroup := new(Group)
+ resp, err := s.client.Do(req, responseGroup)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return responseGroup, resp, nil
+}
+
+// Remove removes user from group
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup
+func (s *GroupService) Remove(groupname string, username string) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s&username=%s", groupname, username)
+ req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return resp, jerr
+ }
+
+ return resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/issue.go b/vendor/github.com/andygrunwald/go-jira/issue.go
new file mode 100644
index 00000000..93212b1d
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/issue.go
@@ -0,0 +1,1260 @@
+package jira
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/fatih/structs"
+ "github.com/google/go-querystring/query"
+ "github.com/trivago/tgo/tcontainer"
+)
+
+const (
+ // AssigneeAutomatic represents the value of the "Assignee: Automatic" of JIRA
+ AssigneeAutomatic = "-1"
+)
+
+// IssueService handles Issues for the JIRA instance / API.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue
+type IssueService struct {
+ client *Client
+}
+
+// UpdateQueryOptions specifies the optional parameters to the Edit issue
+type UpdateQueryOptions struct {
+ NotifyUsers bool `url:"notifyUsers,omitempty"`
+ OverrideScreenSecurity bool `url:"overrideScreenSecurity,omitempty"`
+ OverrideEditableFlag bool `url:"overrideEditableFlag,omitempty"`
+}
+
+// Issue represents a JIRA issue.
+type Issue struct {
+ Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ Key string `json:"key,omitempty" structs:"key,omitempty"`
+ Fields *IssueFields `json:"fields,omitempty" structs:"fields,omitempty"`
+ RenderedFields *IssueRenderedFields `json:"renderedFields,omitempty" structs:"renderedFields,omitempty"`
+ Changelog *Changelog `json:"changelog,omitempty" structs:"changelog,omitempty"`
+}
+
+// ChangelogItems reflects one single changelog item of a history item
+type ChangelogItems struct {
+ Field string `json:"field" structs:"field"`
+ FieldType string `json:"fieldtype" structs:"fieldtype"`
+ From interface{} `json:"from" structs:"from"`
+ FromString string `json:"fromString" structs:"fromString"`
+ To interface{} `json:"to" structs:"to"`
+ ToString string `json:"toString" structs:"toString"`
+}
+
+// ChangelogHistory reflects one single changelog history entry
+type ChangelogHistory struct {
+ Id string `json:"id" structs:"id"`
+ Author User `json:"author" structs:"author"`
+ Created string `json:"created" structs:"created"`
+ Items []ChangelogItems `json:"items" structs:"items"`
+}
+
+// Changelog reflects the change log of an issue
+type Changelog struct {
+ Histories []ChangelogHistory `json:"histories,omitempty"`
+}
+
+// Attachment represents a JIRA attachment
+type Attachment struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Filename string `json:"filename,omitempty" structs:"filename,omitempty"`
+ Author *User `json:"author,omitempty" structs:"author,omitempty"`
+ Created string `json:"created,omitempty" structs:"created,omitempty"`
+ Size int `json:"size,omitempty" structs:"size,omitempty"`
+ MimeType string `json:"mimeType,omitempty" structs:"mimeType,omitempty"`
+ Content string `json:"content,omitempty" structs:"content,omitempty"`
+ Thumbnail string `json:"thumbnail,omitempty" structs:"thumbnail,omitempty"`
+}
+
+// Epic represents the epic to which an issue is associated
+// Not that this struct does not process the returned "color" value
+type Epic struct {
+ ID int `json:"id" structs:"id"`
+ Key string `json:"key" structs:"key"`
+ Self string `json:"self" structs:"self"`
+ Name string `json:"name" structs:"name"`
+ Summary string `json:"summary" structs:"summary"`
+ Done bool `json:"done" structs:"done"`
+}
+
+// IssueFields represents single fields of a JIRA issue.
+// Every JIRA issue has several fields attached.
+type IssueFields struct {
+ // TODO Missing fields
+ // * "workratio": -1,
+ // * "lastViewed": null,
+ // * "environment": null,
+ Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
+ Type IssueType `json:"issuetype,omitempty" structs:"issuetype,omitempty"`
+ Project Project `json:"project,omitempty" structs:"project,omitempty"`
+ Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"`
+ Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"`
+ Resolutiondate Time `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"`
+ Created Time `json:"created,omitempty" structs:"created,omitempty"`
+ Duedate Date `json:"duedate,omitempty" structs:"duedate,omitempty"`
+ Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"`
+ Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"`
+ Updated Time `json:"updated,omitempty" structs:"updated,omitempty"`
+ Description string `json:"description,omitempty" structs:"description,omitempty"`
+ Summary string `json:"summary,omitempty" structs:"summary,omitempty"`
+ Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"`
+ Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"`
+ Components []*Component `json:"components,omitempty" structs:"components,omitempty"`
+ Status *Status `json:"status,omitempty" structs:"status,omitempty"`
+ Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"`
+ AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"`
+ TimeTracking *TimeTracking `json:"timetracking,omitempty" structs:"timetracking,omitempty"`
+ TimeSpent int `json:"timespent,omitempty" structs:"timespent,omitempty"`
+ TimeEstimate int `json:"timeestimate,omitempty" structs:"timeestimate,omitempty"`
+ TimeOriginalEstimate int `json:"timeoriginalestimate,omitempty" structs:"timeoriginalestimate,omitempty"`
+ Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"`
+ IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"`
+ Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"`
+ FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"`
+ AffectsVersions []*AffectsVersion `json:"versions,omitempty" structs:"versions,omitempty"`
+ Labels []string `json:"labels,omitempty" structs:"labels,omitempty"`
+ Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"`
+ Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"`
+ Epic *Epic `json:"epic,omitempty" structs:"epic,omitempty"`
+ Sprint *Sprint `json:"sprint,omitempty" structs:"sprint,omitempty"`
+ Parent *Parent `json:"parent,omitempty" structs:"parent,omitempty"`
+ AggregateTimeOriginalEstimate int `json:"aggregatetimeoriginalestimate,omitempty" structs:"aggregatetimeoriginalestimate,omitempty"`
+ AggregateTimeSpent int `json:"aggregatetimespent,omitempty" structs:"aggregatetimespent,omitempty"`
+ AggregateTimeEstimate int `json:"aggregatetimeestimate,omitempty" structs:"aggregatetimeestimate,omitempty"`
+ Unknowns tcontainer.MarshalMap
+}
+
+// MarshalJSON is a custom JSON marshal function for the IssueFields structs.
+// It handles JIRA custom fields and maps those from / to "Unknowns" key.
+func (i *IssueFields) MarshalJSON() ([]byte, error) {
+ m := structs.Map(i)
+ unknowns, okay := m["Unknowns"]
+ if okay {
+ // if unknowns present, shift all key value from unknown to a level up
+ for key, value := range unknowns.(tcontainer.MarshalMap) {
+ m[key] = value
+ }
+ delete(m, "Unknowns")
+ }
+ return json.Marshal(m)
+}
+
+// UnmarshalJSON is a custom JSON marshal function for the IssueFields structs.
+// It handles JIRA custom fields and maps those from / to "Unknowns" key.
+func (i *IssueFields) UnmarshalJSON(data []byte) error {
+
+ // Do the normal unmarshalling first
+ // Details for this way: http://choly.ca/post/go-json-marshalling/
+ type Alias IssueFields
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(i),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+
+ totalMap := tcontainer.NewMarshalMap()
+ err := json.Unmarshal(data, &totalMap)
+ if err != nil {
+ return err
+ }
+
+ t := reflect.TypeOf(*i)
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+ tagDetail := field.Tag.Get("json")
+ if tagDetail == "" {
+ // ignore if there are no tags
+ continue
+ }
+ options := strings.Split(tagDetail, ",")
+
+ if len(options) == 0 {
+ return fmt.Errorf("No tags options found for %s", field.Name)
+ }
+ // the first one is the json tag
+ key := options[0]
+ if _, okay := totalMap.Value(key); okay {
+ delete(totalMap, key)
+ }
+
+ }
+ i = (*IssueFields)(aux.Alias)
+ // all the tags found in the struct were removed. Whatever is left are unknowns to struct
+ i.Unknowns = totalMap
+ return nil
+
+}
+
+// IssueRenderedFields represents rendered fields of a JIRA issue.
+// Not all IssueFields are rendered.
+type IssueRenderedFields struct {
+ // TODO Missing fields
+ // * "aggregatetimespent": null,
+ // * "workratio": -1,
+ // * "lastViewed": null,
+ // * "aggregatetimeoriginalestimate": null,
+ // * "aggregatetimeestimate": null,
+ // * "environment": null,
+ Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"`
+ Created string `json:"created,omitempty" structs:"created,omitempty"`
+ Duedate string `json:"duedate,omitempty" structs:"duedate,omitempty"`
+ Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
+ Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"`
+ Description string `json:"description,omitempty" structs:"description,omitempty"`
+}
+
+// IssueType represents a type of a JIRA issue.
+// Typical types are "Request", "Bug", "Story", ...
+type IssueType struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Description string `json:"description,omitempty" structs:"description,omitempty"`
+ IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ Subtask bool `json:"subtask,omitempty" structs:"subtask,omitempty"`
+ AvatarID int `json:"avatarId,omitempty" structs:"avatarId,omitempty"`
+}
+
+// Watches represents a type of how many and which user are "observing" a JIRA issue to track the status / updates.
+type Watches struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ WatchCount int `json:"watchCount,omitempty" structs:"watchCount,omitempty"`
+ IsWatching bool `json:"isWatching,omitempty" structs:"isWatching,omitempty"`
+ Watchers []*Watcher `json:"watchers,omitempty" structs:"watchers,omitempty"`
+}
+
+// Watcher represents a simplified user that "observes" the issue
+type Watcher struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"`
+ Active bool `json:"active,omitempty" structs:"active,omitempty"`
+}
+
+// AvatarUrls represents different dimensions of avatars / images
+type AvatarUrls struct {
+ Four8X48 string `json:"48x48,omitempty" structs:"48x48,omitempty"`
+ Two4X24 string `json:"24x24,omitempty" structs:"24x24,omitempty"`
+ One6X16 string `json:"16x16,omitempty" structs:"16x16,omitempty"`
+ Three2X32 string `json:"32x32,omitempty" structs:"32x32,omitempty"`
+}
+
+// Component represents a "component" of a JIRA issue.
+// Components can be user defined in every JIRA instance.
+type Component struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+}
+
+// Status represents the current status of a JIRA issue.
+// Typical status are "Open", "In Progress", "Closed", ...
+// Status can be user defined in every JIRA instance.
+type Status struct {
+ Self string `json:"self" structs:"self"`
+ Description string `json:"description" structs:"description"`
+ IconURL string `json:"iconUrl" structs:"iconUrl"`
+ Name string `json:"name" structs:"name"`
+ ID string `json:"id" structs:"id"`
+ StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"`
+}
+
+// Progress represents the progress of a JIRA issue.
+type Progress struct {
+ Progress int `json:"progress" structs:"progress"`
+ Total int `json:"total" structs:"total"`
+ Percent int `json:"percent" structs:"percent"`
+}
+
+// Parent represents the parent of a JIRA issue, to be used with subtask issue types.
+type Parent struct {
+ ID string `json:"id,omitempty" structs:"id"`
+ Key string `json:"key,omitempty" structs:"key"`
+}
+
+// Time represents the Time definition of JIRA as a time.Time of go
+type Time time.Time
+
+func (t Time) Equal(u Time) bool {
+ return time.Time(t).Equal(time.Time(u))
+}
+
+// Date represents the Date definition of JIRA as a time.Time of go
+type Date time.Time
+
+// Wrapper struct for search result
+type transitionResult struct {
+ Transitions []Transition `json:"transitions" structs:"transitions"`
+}
+
+// Transition represents an issue transition in JIRA
+type Transition struct {
+ ID string `json:"id" structs:"id"`
+ Name string `json:"name" structs:"name"`
+ To Status `json:"to" structs:"status"`
+ Fields map[string]TransitionField `json:"fields" structs:"fields"`
+}
+
+// TransitionField represents the value of one Transition
+type TransitionField struct {
+ Required bool `json:"required" structs:"required"`
+}
+
+// CreateTransitionPayload is used for creating new issue transitions
+type CreateTransitionPayload struct {
+ Transition TransitionPayload `json:"transition" structs:"transition"`
+ Fields TransitionPayloadFields `json:"fields" structs:"fields"`
+}
+
+// TransitionPayload represents the request payload of Transition calls like DoTransition
+type TransitionPayload struct {
+ ID string `json:"id" structs:"id"`
+}
+
+// TransitionPayloadFields represents the fields that can be set when executing a transition
+type TransitionPayloadFields struct {
+ Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"`
+}
+
+// Option represents an option value in a SelectList or MultiSelect
+// custom issue field
+type Option struct {
+ Value string `json:"value" structs:"value"`
+}
+
+// UnmarshalJSON will transform the JIRA time into a time.Time
+// during the transformation of the JIRA JSON response
+func (t *Time) UnmarshalJSON(b []byte) error {
+ // Ignore null, like in the main JSON package.
+ if string(b) == "null" {
+ return nil
+ }
+ ti, err := time.Parse("\"2006-01-02T15:04:05.999-0700\"", string(b))
+ if err != nil {
+ return err
+ }
+ *t = Time(ti)
+ return nil
+}
+
+// MarshalJSON will transform the time.Time into a JIRA time
+// during the creation of a JIRA request
+func (t Time) MarshalJSON() ([]byte, error) {
+ return []byte(time.Time(t).Format("\"2006-01-02T15:04:05.999-0700\"")), nil
+}
+
+// UnmarshalJSON will transform the JIRA date into a time.Time
+// during the transformation of the JIRA JSON response
+func (t *Date) UnmarshalJSON(b []byte) error {
+ // Ignore null, like in the main JSON package.
+ if string(b) == "null" {
+ return nil
+ }
+ ti, err := time.Parse("\"2006-01-02\"", string(b))
+ if err != nil {
+ return err
+ }
+ *t = Date(ti)
+ return nil
+}
+
+// MarshalJSON will transform the Date object into a short
+// date string as JIRA expects during the creation of a
+// JIRA request
+func (t Date) MarshalJSON() ([]byte, error) {
+ time := time.Time(t)
+ return []byte(time.Format("\"2006-01-02\"")), nil
+}
+
+// Worklog represents the work log of a JIRA issue.
+// One Worklog contains zero or n WorklogRecords
+// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
+type Worklog struct {
+ StartAt int `json:"startAt" structs:"startAt"`
+ MaxResults int `json:"maxResults" structs:"maxResults"`
+ Total int `json:"total" structs:"total"`
+ Worklogs []WorklogRecord `json:"worklogs" structs:"worklogs"`
+}
+
+// WorklogRecord represents one entry of a Worklog
+type WorklogRecord struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ Author *User `json:"author,omitempty" structs:"author,omitempty"`
+ UpdateAuthor *User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"`
+ Comment string `json:"comment,omitempty" structs:"comment,omitempty"`
+ Created *Time `json:"created,omitempty" structs:"created,omitempty"`
+ Updated *Time `json:"updated,omitempty" structs:"updated,omitempty"`
+ Started *Time `json:"started,omitempty" structs:"started,omitempty"`
+ TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"`
+ TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ IssueID string `json:"issueId,omitempty" structs:"issueId,omitempty"`
+ Properties []EntityProperty `json:"properties,omitempty"`
+}
+
+type EntityProperty struct {
+ Key string `json:"key"`
+ Value interface{} `json:"value"`
+}
+
+// TimeTracking represents the timetracking fields of a JIRA issue.
+type TimeTracking struct {
+ OriginalEstimate string `json:"originalEstimate,omitempty" structs:"originalEstimate,omitempty"`
+ RemainingEstimate string `json:"remainingEstimate,omitempty" structs:"remainingEstimate,omitempty"`
+ TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"`
+ OriginalEstimateSeconds int `json:"originalEstimateSeconds,omitempty" structs:"originalEstimateSeconds,omitempty"`
+ RemainingEstimateSeconds int `json:"remainingEstimateSeconds,omitempty" structs:"remainingEstimateSeconds,omitempty"`
+ TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"`
+}
+
+// Subtasks represents all issues of a parent issue.
+type Subtasks struct {
+ ID string `json:"id" structs:"id"`
+ Key string `json:"key" structs:"key"`
+ Self string `json:"self" structs:"self"`
+ Fields IssueFields `json:"fields" structs:"fields"`
+}
+
+// IssueLink represents a link between two issues in JIRA.
+type IssueLink struct {
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ Type IssueLinkType `json:"type" structs:"type"`
+ OutwardIssue *Issue `json:"outwardIssue" structs:"outwardIssue"`
+ InwardIssue *Issue `json:"inwardIssue" structs:"inwardIssue"`
+ Comment *Comment `json:"comment,omitempty" structs:"comment,omitempty"`
+}
+
+// IssueLinkType represents a type of a link between to issues in JIRA.
+// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
+type IssueLinkType struct {
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ Name string `json:"name" structs:"name"`
+ Inward string `json:"inward" structs:"inward"`
+ Outward string `json:"outward" structs:"outward"`
+}
+
+// Comments represents a list of Comment.
+type Comments struct {
+ Comments []*Comment `json:"comments,omitempty" structs:"comments,omitempty"`
+}
+
+// Comment represents a comment by a person to an issue in JIRA.
+type Comment struct {
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ Author User `json:"author,omitempty" structs:"author,omitempty"`
+ Body string `json:"body,omitempty" structs:"body,omitempty"`
+ UpdateAuthor User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"`
+ Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
+ Created string `json:"created,omitempty" structs:"created,omitempty"`
+ Visibility CommentVisibility `json:"visibility,omitempty" structs:"visibility,omitempty"`
+}
+
+// FixVersion represents a software release in which an issue is fixed.
+type FixVersion struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ Description string `json:"description,omitempty" structs:"name,omitempty"`
+ Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"`
+ Released *bool `json:"released,omitempty" structs:"released,omitempty"`
+ ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"`
+ UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"`
+ ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number
+ StartDate string `json:"startDate,omitempty" structs:"startDate,omitempty"`
+}
+
+// AffectsVersion represents a software release which is affected by an issue.
+type AffectsVersion Version
+
+// CommentVisibility represents he visibility of a comment.
+// E.g. Type could be "role" and Value "Administrators"
+type CommentVisibility struct {
+ Type string `json:"type,omitempty" structs:"type,omitempty"`
+ Value string `json:"value,omitempty" structs:"value,omitempty"`
+}
+
+// SearchOptions specifies the optional parameters to various List methods that
+// support pagination.
+// Pagination is used for the JIRA REST APIs to conserve server resources and limit
+// response size for resources that return potentially large collection of items.
+// A request to a pages API will result in a values array wrapped in a JSON object with some paging metadata
+// Default Pagination options
+type SearchOptions struct {
+ // StartAt: The starting index of the returned projects. Base index: 0.
+ StartAt int `url:"startAt,omitempty"`
+ // MaxResults: The maximum number of projects to return per page. Default: 50.
+ MaxResults int `url:"maxResults,omitempty"`
+ // Expand: Expand specific sections in the returned issues
+ Expand string `url:"expand,omitempty"`
+ Fields []string
+ // ValidateQuery: The validateQuery param offers control over whether to validate and how strictly to treat the validation. Default: strict.
+ ValidateQuery string `url:"validateQuery,omitempty"`
+}
+
+// searchResult is only a small wrapper around the Search (with JQL) method
+// to be able to parse the results
+type searchResult struct {
+ Issues []Issue `json:"issues" structs:"issues"`
+ StartAt int `json:"startAt" structs:"startAt"`
+ MaxResults int `json:"maxResults" structs:"maxResults"`
+ Total int `json:"total" structs:"total"`
+}
+
+// GetQueryOptions specifies the optional parameters for the Get Issue methods
+type GetQueryOptions struct {
+ // Fields is the list of fields to return for the issue. By default, all fields are returned.
+ Fields string `url:"fields,omitempty"`
+ Expand string `url:"expand,omitempty"`
+ // Properties is the list of properties to return for the issue. By default no properties are returned.
+ Properties string `url:"properties,omitempty"`
+ // FieldsByKeys if true then fields in issues will be referenced by keys instead of ids
+ FieldsByKeys bool `url:"fieldsByKeys,omitempty"`
+ UpdateHistory bool `url:"updateHistory,omitempty"`
+ ProjectKeys string `url:"projectKeys,omitempty"`
+}
+
+// GetWorklogsQueryOptions specifies the optional parameters for the Get Worklogs method
+type GetWorklogsQueryOptions struct {
+ StartAt int64 `url:"startAt,omitempty"`
+ MaxResults int32 `url:"maxResults,omitempty"`
+ Expand string `url:"expand,omitempty"`
+}
+
+type AddWorklogQueryOptions struct {
+ NotifyUsers bool `url:"notifyUsers,omitempty"`
+ AdjustEstimate string `url:"adjustEstimate,omitempty"`
+ NewEstimate string `url:"newEstimate,omitempty"`
+ ReduceBy string `url:"reduceBy,omitempty"`
+ Expand string `url:"expand,omitempty"`
+ OverrideEditableFlag bool `url:"overrideEditableFlag,omitempty"`
+}
+
+// CustomFields represents custom fields of JIRA
+// This can heavily differ between JIRA instances
+type CustomFields map[string]string
+
+// Get returns a full representation of the issue for the given issue key.
+// JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
+// This can be an issue id, or an issue key.
+// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
+//
+// The given options will be appended to the query string
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue
+func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if options != nil {
+ q, err := query.Values(options)
+ if err != nil {
+ return nil, nil, err
+ }
+ req.URL.RawQuery = q.Encode()
+ }
+
+ issue := new(Issue)
+ resp, err := s.client.Do(req, issue)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return issue, resp, nil
+}
+
+// DownloadAttachment returns a Response of an attachment for a given attachmentID.
+// The attachment is in the Response.Body of the response.
+// This is an io.ReadCloser.
+// The caller should close the resp.Body.
+func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return resp, jerr
+ }
+
+ return resp, nil
+}
+
+// PostAttachment uploads r (io.Reader) as an attachment to a given issueID
+func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", issueID)
+
+ b := new(bytes.Buffer)
+ writer := multipart.NewWriter(b)
+
+ fw, err := writer.CreateFormFile("file", attachmentName)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if r != nil {
+ // Copy the file
+ if _, err = io.Copy(fw, r); err != nil {
+ return nil, nil, err
+ }
+ }
+ writer.Close()
+
+ req, err := s.client.NewMultiPartRequest("POST", apiEndpoint, b)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req.Header.Set("Content-Type", writer.FormDataContentType())
+
+ // PostAttachment response returns a JSON array (as multiple attachments can be posted)
+ attachment := new([]Attachment)
+ resp, err := s.client.Do(req, attachment)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return attachment, resp, nil
+}
+
+// DeleteAttachment deletes an attachment of a given attachmentID
+func (s *IssueService) DeleteAttachment(attachmentID string) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/attachment/%s", attachmentID)
+
+ req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return resp, jerr
+ }
+
+ return resp, nil
+}
+
+// GetWorklogs gets all the worklogs for an issue.
+// This method is especially important if you need to read all the worklogs, not just the first page.
+//
+// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-getIssueWorklog
+func (s *IssueService) GetWorklogs(issueID string, options ...func(*http.Request) error) (*Worklog, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID)
+
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ for _, option := range options {
+ err = option(req)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ v := new(Worklog)
+ resp, err := s.client.Do(req, v)
+ return v, resp, err
+}
+
+// Applies query options to http request.
+// This helper is meant to be used with all "QueryOptions" structs.
+func WithQueryOptions(options interface{}) func(*http.Request) error {
+ q, err := query.Values(options)
+ if err != nil {
+ return func(*http.Request) error {
+ return err
+ }
+ }
+
+ return func(r *http.Request) error {
+ r.URL.RawQuery = q.Encode()
+ return nil
+ }
+}
+
+// Create creates an issue or a sub-task from a JSON representation.
+// Creating a sub-task is similar to creating a regular issue, with two important differences:
+// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues
+func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) {
+ apiEndpoint := "rest/api/2/issue"
+ req, err := s.client.NewRequest("POST", apiEndpoint, issue)
+ if err != nil {
+ return nil, nil, err
+ }
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ // incase of error return the resp for further inspection
+ return nil, resp, err
+ }
+
+ responseIssue := new(Issue)
+ defer resp.Body.Close()
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, resp, fmt.Errorf("Could not read the returned data")
+ }
+ err = json.Unmarshal(data, responseIssue)
+ if err != nil {
+ return nil, resp, fmt.Errorf("Could not unmarshall the data into struct")
+ }
+ return responseIssue, resp, nil
+}
+
+// UpdateWithOptions updates an issue from a JSON representation,
+// while also specifiying query params. The issue is found by key.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue
+func (s *IssueService) UpdateWithOptions(issue *Issue, opts *UpdateQueryOptions) (*Issue, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", issue.Key)
+ url, err := addOptions(apiEndpoint, opts)
+ if err != nil {
+ return nil, nil, err
+ }
+ req, err := s.client.NewRequest("PUT", url, issue)
+ if err != nil {
+ return nil, nil, err
+ }
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ // This is just to follow the rest of the API's convention of returning an issue.
+ // Returning the same pointer here is pointless, so we return a copy instead.
+ ret := *issue
+ return &ret, resp, nil
+}
+
+// Update updates an issue from a JSON representation. The issue is found by key.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue
+func (s *IssueService) Update(issue *Issue) (*Issue, *Response, error) {
+ return s.UpdateWithOptions(issue, nil)
+}
+
+// UpdateIssue updates an issue from a JSON representation. The issue is found by key.
+//
+// https://docs.atlassian.com/jira/REST/7.4.0/#api/2/issue-editIssue
+func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", jiraID)
+ req, err := s.client.NewRequest("PUT", apiEndpoint, data)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ return resp, err
+ }
+
+ // This is just to follow the rest of the API's convention of returning an issue.
+ // Returning the same pointer here is pointless, so we return a copy instead.
+ return resp, nil
+}
+
+// AddComment adds a new comment to issueID.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment
+func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment", issueID)
+ req, err := s.client.NewRequest("POST", apiEndpoint, comment)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ responseComment := new(Comment)
+ resp, err := s.client.Do(req, responseComment)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return responseComment, resp, nil
+}
+
+// UpdateComment updates the body of a comment, identified by comment.ID, on the issueID.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-updateComment
+func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment, *Response, error) {
+ reqBody := struct {
+ Body string `json:"body"`
+ }{
+ Body: comment.Body,
+ }
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, comment.ID)
+ req, err := s.client.NewRequest("PUT", apiEndpoint, reqBody)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ responseComment := new(Comment)
+ resp, err := s.client.Do(req, responseComment)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return responseComment, resp, nil
+}
+
+// DeleteComment Deletes a comment from an issueID.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-issue-issueIdOrKey-comment-id-delete
+func (s *IssueService) DeleteComment(issueID, commentID string) error {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, commentID)
+ req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return jerr
+ }
+
+ return nil
+}
+
+// AddWorklogRecord adds a new worklog record to issueID.
+//
+// https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-issue-issueIdOrKey-worklog-post
+func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID)
+ req, err := s.client.NewRequest("POST", apiEndpoint, record)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ for _, option := range options {
+ err = option(req)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ responseRecord := new(WorklogRecord)
+ resp, err := s.client.Do(req, responseRecord)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return responseRecord, resp, nil
+}
+
+// AddLink adds a link between two issues.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink
+func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issueLink")
+ req, err := s.client.NewRequest("POST", apiEndpoint, issueLink)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+
+ return resp, err
+}
+
+// Search will search for tickets according to the jql
+//
+// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues
+func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) {
+ var u string
+ if options == nil {
+ u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql))
+ } else {
+ u = "rest/api/2/search?jql=" + url.QueryEscape(jql)
+ if options.StartAt != 0 {
+ u += fmt.Sprintf("&startAt=%d", options.StartAt)
+ }
+ if options.MaxResults != 0 {
+ u += fmt.Sprintf("&maxResults=%d", options.MaxResults)
+ }
+ if options.Expand != "" {
+ u += fmt.Sprintf("&expand=%s", options.Expand)
+ }
+ if strings.Join(options.Fields, ",") != "" {
+ u += fmt.Sprintf("&fields=%s", strings.Join(options.Fields, ","))
+ }
+ if options.ValidateQuery != "" {
+ u += fmt.Sprintf("&validateQuery=%s", options.ValidateQuery)
+ }
+ }
+
+ req, err := s.client.NewRequest("GET", u, nil)
+ if err != nil {
+ return []Issue{}, nil, err
+ }
+
+ v := new(searchResult)
+ resp, err := s.client.Do(req, v)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+ return v.Issues, resp, err
+}
+
+// SearchPages will get issues from all pages in a search
+//
+// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues
+func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Issue) error) error {
+ if options == nil {
+ options = &SearchOptions{
+ StartAt: 0,
+ MaxResults: 50,
+ }
+ }
+
+ if options.MaxResults == 0 {
+ options.MaxResults = 50
+ }
+
+ issues, resp, err := s.Search(jql, options)
+ if err != nil {
+ return err
+ }
+
+ for {
+ for _, issue := range issues {
+ err = f(issue)
+ if err != nil {
+ return err
+ }
+ }
+
+ if resp.StartAt+resp.MaxResults >= resp.Total {
+ return nil
+ }
+
+ options.StartAt += resp.MaxResults
+ issues, resp, err = s.Search(jql, options)
+ if err != nil {
+ return err
+ }
+ }
+}
+
+// GetCustomFields returns a map of customfield_* keys with string values
+func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ issue := new(map[string]interface{})
+ resp, err := s.client.Do(req, issue)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ m := *issue
+ f := m["fields"]
+ cf := make(CustomFields)
+ if f == nil {
+ return cf, resp, nil
+ }
+
+ if rec, ok := f.(map[string]interface{}); ok {
+ for key, val := range rec {
+ if strings.Contains(key, "customfield") {
+ if valMap, ok := val.(map[string]interface{}); ok {
+ if v, ok := valMap["value"]; ok {
+ val = v
+ }
+ }
+ cf[key] = fmt.Sprint(val)
+ }
+ }
+ }
+ return cf, resp, nil
+}
+
+// GetTransitions gets a list of the transitions possible for this issue by the current user,
+// along with fields that are required and their types.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions
+func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions?expand=transitions.fields", id)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ result := new(transitionResult)
+ resp, err := s.client.Do(req, result)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+ return result.Transitions, resp, err
+}
+
+// DoTransition performs a transition on an issue.
+// When performing the transition you can update or set other issue fields.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition
+func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) {
+ payload := CreateTransitionPayload{
+ Transition: TransitionPayload{
+ ID: transitionID,
+ },
+ }
+ return s.DoTransitionWithPayload(ticketID, payload)
+}
+
+// DoTransitionWithPayload performs a transition on an issue using any payload.
+// When performing the transition you can update or set other issue fields.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition
+func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID)
+
+ req, err := s.client.NewRequest("POST", apiEndpoint, payload)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+
+ return resp, err
+}
+
+// InitIssueWithMetaAndFields returns Issue with with values from fieldsConfig properly set.
+// * metaProject should contain metaInformation about the project where the issue should be created.
+// * metaIssuetype is the MetaInformation about the Issuetype that needs to be created.
+// * fieldsConfig is a key->value pair where key represents the name of the field as seen in the UI
+// And value is the string value for that particular key.
+// Note: This method doesn't verify that the fieldsConfig is complete with mandatory fields. The fieldsConfig is
+// supposed to be already verified with MetaIssueType.CheckCompleteAndAvailable. It will however return
+// error if the key is not found.
+// All values will be packed into Unknowns. This is much convenient. If the struct fields needs to be
+// configured as well, marshalling and unmarshalling will set the proper fields.
+func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIssueType, fieldsConfig map[string]string) (*Issue, error) {
+ issue := new(Issue)
+ issueFields := new(IssueFields)
+ issueFields.Unknowns = tcontainer.NewMarshalMap()
+
+ // map the field names the User presented to jira's internal key
+ allFields, _ := metaIssuetype.GetAllFields()
+ for key, value := range fieldsConfig {
+ jiraKey, found := allFields[key]
+ if !found {
+ return nil, fmt.Errorf("key %s is not found in the list of fields", key)
+ }
+
+ valueType, err := metaIssuetype.Fields.String(jiraKey + "/schema/type")
+ if err != nil {
+ return nil, err
+ }
+ switch valueType {
+ case "array":
+ elemType, err := metaIssuetype.Fields.String(jiraKey + "/schema/items")
+ if err != nil {
+ return nil, err
+ }
+ switch elemType {
+ case "component":
+ issueFields.Unknowns[jiraKey] = []Component{{Name: value}}
+ case "option":
+ issueFields.Unknowns[jiraKey] = []map[string]string{{"value": value}}
+ default:
+ issueFields.Unknowns[jiraKey] = []string{value}
+ }
+ case "string":
+ issueFields.Unknowns[jiraKey] = value
+ case "date":
+ issueFields.Unknowns[jiraKey] = value
+ case "datetime":
+ issueFields.Unknowns[jiraKey] = value
+ case "any":
+ // Treat any as string
+ issueFields.Unknowns[jiraKey] = value
+ case "project":
+ issueFields.Unknowns[jiraKey] = Project{
+ Name: metaProject.Name,
+ ID: metaProject.Id,
+ }
+ case "priority":
+ issueFields.Unknowns[jiraKey] = Priority{Name: value}
+ case "user":
+ issueFields.Unknowns[jiraKey] = User{
+ Name: value,
+ }
+ case "issuetype":
+ issueFields.Unknowns[jiraKey] = IssueType{
+ Name: value,
+ }
+ case "option":
+ issueFields.Unknowns[jiraKey] = Option{
+ Value: value,
+ }
+ default:
+ return nil, fmt.Errorf("Unknown issue type encountered: %s for %s", valueType, key)
+ }
+ }
+
+ issue.Fields = issueFields
+
+ return issue, nil
+}
+
+// Delete will delete a specified issue.
+func (s *IssueService) Delete(issueID string) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
+
+ // to enable deletion of subtasks; without this, the request will fail if the issue has subtasks
+ deletePayload := make(map[string]interface{})
+ deletePayload["deleteSubtasks"] = "true"
+ content, _ := json.Marshal(deletePayload)
+
+ req, err := s.client.NewRequest("DELETE", apiEndpoint, content)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ return resp, err
+}
+
+// GetWatchers wil return all the users watching/observing the given issue
+//
+// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getIssueWatchers
+func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) {
+ watchesAPIEndpoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID)
+
+ req, err := s.client.NewRequest("GET", watchesAPIEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ watches := new(Watches)
+ resp, err := s.client.Do(req, watches)
+ if err != nil {
+ return nil, nil, NewJiraError(resp, err)
+ }
+
+ result := []User{}
+ user := new(User)
+ for _, watcher := range watches.Watchers {
+ user, resp, err = s.client.User.Get(watcher.Name)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ result = append(result, *user)
+ }
+
+ return &result, resp, nil
+}
+
+// AddWatcher adds watcher to the given issue
+//
+// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-addWatcher
+func (s *IssueService) AddWatcher(issueID string, userName string) (*Response, error) {
+ apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID)
+
+ req, err := s.client.NewRequest("POST", apiEndPoint, userName)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+
+ return resp, err
+}
+
+// RemoveWatcher removes given user from given issue
+//
+// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-removeWatcher
+func (s *IssueService) RemoveWatcher(issueID string, userName string) (*Response, error) {
+ apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID)
+
+ req, err := s.client.NewRequest("DELETE", apiEndPoint, userName)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+
+ return resp, err
+}
+
+// UpdateAssignee updates the user assigned to work on the given issue
+//
+// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.2/#api/2/issue-assign
+func (s *IssueService) UpdateAssignee(issueID string, assignee *User) (*Response, error) {
+ apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/assignee", issueID)
+
+ req, err := s.client.NewRequest("PUT", apiEndPoint, assignee)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+
+ return resp, err
+}
+
+func (c ChangelogHistory) CreatedTime() (time.Time, error) {
+ var t time.Time
+ // Ignore null
+ if string(c.Created) == "null" {
+ return t, nil
+ }
+ t, err := time.Parse("2006-01-02T15:04:05.999-0700", c.Created)
+ return t, err
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/jira.go b/vendor/github.com/andygrunwald/go-jira/jira.go
new file mode 100644
index 00000000..4bd1c35c
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/jira.go
@@ -0,0 +1,460 @@
+package jira
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strings"
+ "time"
+
+ "github.com/google/go-querystring/query"
+ "github.com/pkg/errors"
+)
+
+// A Client manages communication with the JIRA API.
+type Client struct {
+ // HTTP client used to communicate with the API.
+ client *http.Client
+
+ // Base URL for API requests.
+ baseURL *url.URL
+
+ // Session storage if the user authentificate with a Session cookie
+ session *Session
+
+ // Services used for talking to different parts of the JIRA API.
+ Authentication *AuthenticationService
+ Issue *IssueService
+ Project *ProjectService
+ Board *BoardService
+ Sprint *SprintService
+ User *UserService
+ Group *GroupService
+ Version *VersionService
+ Priority *PriorityService
+ Field *FieldService
+ Component *ComponentService
+ Resolution *ResolutionService
+ StatusCategory *StatusCategoryService
+ Filter *FilterService
+ Role *RoleService
+ PermissionScheme *PermissionSchemeService
+}
+
+// NewClient returns a new JIRA API client.
+// If a nil httpClient is provided, http.DefaultClient will be used.
+// To use API methods which require authentication you can follow the preferred solution and
+// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library).
+// As an alternative you can use Session Cookie based authentication provided by this package as well.
+// See https://docs.atlassian.com/jira/REST/latest/#authentication
+// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash.
+func NewClient(httpClient *http.Client, baseURL string) (*Client, error) {
+ if httpClient == nil {
+ httpClient = http.DefaultClient
+ }
+
+ // ensure the baseURL contains a trailing slash so that all paths are preserved in later calls
+ if !strings.HasSuffix(baseURL, "/") {
+ baseURL += "/"
+ }
+
+ parsedBaseURL, err := url.Parse(baseURL)
+ if err != nil {
+ return nil, err
+ }
+
+ c := &Client{
+ client: httpClient,
+ baseURL: parsedBaseURL,
+ }
+ c.Authentication = &AuthenticationService{client: c}
+ c.Issue = &IssueService{client: c}
+ c.Project = &ProjectService{client: c}
+ c.Board = &BoardService{client: c}
+ c.Sprint = &SprintService{client: c}
+ c.User = &UserService{client: c}
+ c.Group = &GroupService{client: c}
+ c.Version = &VersionService{client: c}
+ c.Priority = &PriorityService{client: c}
+ c.Field = &FieldService{client: c}
+ c.Component = &ComponentService{client: c}
+ c.Resolution = &ResolutionService{client: c}
+ c.StatusCategory = &StatusCategoryService{client: c}
+ c.Filter = &FilterService{client: c}
+ c.Role = &RoleService{client: c}
+ c.PermissionScheme = &PermissionSchemeService{client: c}
+
+ return c, nil
+}
+
+// NewRawRequest creates an API request.
+// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
+// Allows using an optional native io.Reader for sourcing the request body.
+func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) {
+ rel, err := url.Parse(urlStr)
+ if err != nil {
+ return nil, err
+ }
+ // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
+ rel.Path = strings.TrimLeft(rel.Path, "/")
+
+ u := c.baseURL.ResolveReference(rel)
+
+ req, err := http.NewRequest(method, u.String(), body)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ // Set authentication information
+ if c.Authentication.authType == authTypeSession {
+ // Set session cookie if there is one
+ if c.session != nil {
+ for _, cookie := range c.session.Cookies {
+ req.AddCookie(cookie)
+ }
+ }
+ } else if c.Authentication.authType == authTypeBasic {
+ // Set basic auth information
+ if c.Authentication.username != "" {
+ req.SetBasicAuth(c.Authentication.username, c.Authentication.password)
+ }
+ }
+
+ return req, nil
+}
+
+// NewRequest creates an API request.
+// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
+// If specified, the value pointed to by body is JSON encoded and included as the request body.
+func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
+ rel, err := url.Parse(urlStr)
+ if err != nil {
+ return nil, err
+ }
+ // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
+ rel.Path = strings.TrimLeft(rel.Path, "/")
+
+ u := c.baseURL.ResolveReference(rel)
+
+ var buf io.ReadWriter
+ if body != nil {
+ buf = new(bytes.Buffer)
+ err = json.NewEncoder(buf).Encode(body)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ req, err := http.NewRequest(method, u.String(), buf)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ // Set authentication information
+ if c.Authentication.authType == authTypeSession {
+ // Set session cookie if there is one
+ if c.session != nil {
+ for _, cookie := range c.session.Cookies {
+ req.AddCookie(cookie)
+ }
+ }
+ } else if c.Authentication.authType == authTypeBasic {
+ // Set basic auth information
+ if c.Authentication.username != "" {
+ req.SetBasicAuth(c.Authentication.username, c.Authentication.password)
+ }
+ }
+
+ return req, nil
+}
+
+// addOptions adds the parameters in opt as URL query parameters to s. opt
+// must be a struct whose fields may contain "url" tags.
+func addOptions(s string, opt interface{}) (string, error) {
+ v := reflect.ValueOf(opt)
+ if v.Kind() == reflect.Ptr && v.IsNil() {
+ return s, nil
+ }
+
+ u, err := url.Parse(s)
+ if err != nil {
+ return s, err
+ }
+
+ qs, err := query.Values(opt)
+ if err != nil {
+ return s, err
+ }
+
+ u.RawQuery = qs.Encode()
+ return u.String(), nil
+}
+
+// NewMultiPartRequest creates an API request including a multi-part file.
+// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
+// If specified, the value pointed to by buf is a multipart form.
+func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
+ rel, err := url.Parse(urlStr)
+ if err != nil {
+ return nil, err
+ }
+ // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
+ rel.Path = strings.TrimLeft(rel.Path, "/")
+
+ u := c.baseURL.ResolveReference(rel)
+
+ req, err := http.NewRequest(method, u.String(), buf)
+ if err != nil {
+ return nil, err
+ }
+
+ // Set required headers
+ req.Header.Set("X-Atlassian-Token", "nocheck")
+
+ // Set authentication information
+ if c.Authentication.authType == authTypeSession {
+ // Set session cookie if there is one
+ if c.session != nil {
+ for _, cookie := range c.session.Cookies {
+ req.AddCookie(cookie)
+ }
+ }
+ } else if c.Authentication.authType == authTypeBasic {
+ // Set basic auth information
+ if c.Authentication.username != "" {
+ req.SetBasicAuth(c.Authentication.username, c.Authentication.password)
+ }
+ }
+
+ return req, nil
+}
+
+// Do sends an API request and returns the API response.
+// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
+func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
+ httpResp, err := c.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ err = CheckResponse(httpResp)
+ if err != nil {
+ // Even though there was an error, we still return the response
+ // in case the caller wants to inspect it further
+ return newResponse(httpResp, nil), err
+ }
+
+ if v != nil {
+ // Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
+ defer httpResp.Body.Close()
+ err = json.NewDecoder(httpResp.Body).Decode(v)
+ }
+
+ resp := newResponse(httpResp, v)
+ return resp, err
+}
+
+// CheckResponse checks the API response for errors, and returns them if present.
+// A response is considered an error if it has a status code outside the 200 range.
+// The caller is responsible to analyze the response body.
+// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes).
+func CheckResponse(r *http.Response) error {
+ if c := r.StatusCode; 200 <= c && c <= 299 {
+ return nil
+ }
+
+ err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode)
+ return err
+}
+
+// GetBaseURL will return you the Base URL.
+// This is the same URL as in the NewClient constructor
+func (c *Client) GetBaseURL() url.URL {
+ return *c.baseURL
+}
+
+// Response represents JIRA API response. It wraps http.Response returned from
+// API and provides information about paging.
+type Response struct {
+ *http.Response
+
+ StartAt int
+ MaxResults int
+ Total int
+}
+
+func newResponse(r *http.Response, v interface{}) *Response {
+ resp := &Response{Response: r}
+ resp.populatePageValues(v)
+ return resp
+}
+
+// Sets paging values if response json was parsed to searchResult type
+// (can be extended with other types if they also need paging info)
+func (r *Response) populatePageValues(v interface{}) {
+ switch value := v.(type) {
+ case *searchResult:
+ r.StartAt = value.StartAt
+ r.MaxResults = value.MaxResults
+ r.Total = value.Total
+ case *groupMembersResult:
+ r.StartAt = value.StartAt
+ r.MaxResults = value.MaxResults
+ r.Total = value.Total
+ }
+ return
+}
+
+// BasicAuthTransport is an http.RoundTripper that authenticates all requests
+// using HTTP Basic Authentication with the provided username and password.
+type BasicAuthTransport struct {
+ Username string
+ Password string
+
+ // Transport is the underlying HTTP transport to use when making requests.
+ // It will default to http.DefaultTransport if nil.
+ Transport http.RoundTripper
+}
+
+// RoundTrip implements the RoundTripper interface. We just add the
+// basic auth and return the RoundTripper for this transport type.
+func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ req2 := cloneRequest(req) // per RoundTripper contract
+
+ req2.SetBasicAuth(t.Username, t.Password)
+ return t.transport().RoundTrip(req2)
+}
+
+// Client returns an *http.Client that makes requests that are authenticated
+// using HTTP Basic Authentication. This is a nice little bit of sugar
+// so we can just get the client instead of creating the client in the calling code.
+// If it's necessary to send more information on client init, the calling code can
+// always skip this and set the transport itself.
+func (t *BasicAuthTransport) Client() *http.Client {
+ return &http.Client{Transport: t}
+}
+
+func (t *BasicAuthTransport) transport() http.RoundTripper {
+ if t.Transport != nil {
+ return t.Transport
+ }
+ return http.DefaultTransport
+}
+
+// CookieAuthTransport is an http.RoundTripper that authenticates all requests
+// using Jira's cookie-based authentication.
+//
+// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
+// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
+type CookieAuthTransport struct {
+ Username string
+ Password string
+ AuthURL string
+
+ // SessionObject is the authenticated cookie string.s
+ // It's passed in each call to prove the client is authenticated.
+ SessionObject []*http.Cookie
+
+ // Transport is the underlying HTTP transport to use when making requests.
+ // It will default to http.DefaultTransport if nil.
+ Transport http.RoundTripper
+}
+
+// RoundTrip adds the session object to the request.
+func (t *CookieAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ if t.SessionObject == nil {
+ err := t.setSessionObject()
+ if err != nil {
+ return nil, errors.Wrap(err, "cookieauth: no session object has been set")
+ }
+ }
+
+ req2 := cloneRequest(req) // per RoundTripper contract
+ for _, cookie := range t.SessionObject {
+ // Don't add an empty value cookie to the request
+ if cookie.Value != "" {
+ req2.AddCookie(cookie)
+ }
+ }
+
+ return t.transport().RoundTrip(req2)
+}
+
+// Client returns an *http.Client that makes requests that are authenticated
+// using cookie authentication
+func (t *CookieAuthTransport) Client() *http.Client {
+ return &http.Client{Transport: t}
+}
+
+// setSessionObject attempts to authenticate the user and set
+// the session object (e.g. cookie)
+func (t *CookieAuthTransport) setSessionObject() error {
+ req, err := t.buildAuthRequest()
+ if err != nil {
+ return err
+ }
+
+ var authClient = &http.Client{
+ Timeout: time.Second * 60,
+ }
+ resp, err := authClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ t.SessionObject = resp.Cookies()
+ return nil
+}
+
+// getAuthRequest assembles the request to get the authenticated cookie
+func (t *CookieAuthTransport) buildAuthRequest() (*http.Request, error) {
+ body := struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ }{
+ t.Username,
+ t.Password,
+ }
+
+ b := new(bytes.Buffer)
+ json.NewEncoder(b).Encode(body)
+
+ req, err := http.NewRequest("POST", t.AuthURL, b)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ return req, nil
+}
+
+func (t *CookieAuthTransport) transport() http.RoundTripper {
+ if t.Transport != nil {
+ return t.Transport
+ }
+ return http.DefaultTransport
+}
+
+// cloneRequest returns a clone of the provided *http.Request.
+// The clone is a shallow copy of the struct and its Header map.
+func cloneRequest(r *http.Request) *http.Request {
+ // shallow copy of the struct
+ r2 := new(http.Request)
+ *r2 = *r
+ // deep copy of the Header
+ r2.Header = make(http.Header, len(r.Header))
+ for k, s := range r.Header {
+ r2.Header[k] = append([]string(nil), s...)
+ }
+ return r2
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/metaissue.go b/vendor/github.com/andygrunwald/go-jira/metaissue.go
new file mode 100644
index 00000000..19813786
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/metaissue.go
@@ -0,0 +1,194 @@
+package jira
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/google/go-querystring/query"
+ "github.com/trivago/tgo/tcontainer"
+)
+
+// CreateMetaInfo contains information about fields and their attributed to create a ticket.
+type CreateMetaInfo struct {
+ Expand string `json:"expand,omitempty"`
+ Projects []*MetaProject `json:"projects,omitempty"`
+}
+
+// MetaProject is the meta information about a project returned from createmeta api
+type MetaProject struct {
+ Expand string `json:"expand,omitempty"`
+ Self string `json:"self,omitempty"`
+ Id string `json:"id,omitempty"`
+ Key string `json:"key,omitempty"`
+ Name string `json:"name,omitempty"`
+ // omitted avatarUrls
+ IssueTypes []*MetaIssueType `json:"issuetypes,omitempty"`
+}
+
+// MetaIssueType represents the different issue types a project has.
+//
+// Note: Fields is interface because this is an object which can
+// have arbitraty keys related to customfields. It is not possible to
+// expect these for a general way. This will be returning a map.
+// Further processing must be done depending on what is required.
+type MetaIssueType struct {
+ Self string `json:"self,omitempty"`
+ Id string `json:"id,omitempty"`
+ Description string `json:"description,omitempty"`
+ IconUrl string `json:"iconurl,omitempty"`
+ Name string `json:"name,omitempty"`
+ Subtasks bool `json:"subtask,omitempty"`
+ Expand string `json:"expand,omitempty"`
+ Fields tcontainer.MarshalMap `json:"fields,omitempty"`
+}
+
+// GetCreateMeta makes the api call to get the meta information required to create a ticket
+func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) {
+ return s.GetCreateMetaWithOptions(&GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"})
+}
+
+// GetCreateMetaWithOptions makes the api call to get the meta information without requiring to have a projectKey
+func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) {
+ apiEndpoint := "rest/api/2/issue/createmeta"
+
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ if options != nil {
+ q, err := query.Values(options)
+ if err != nil {
+ return nil, nil, err
+ }
+ req.URL.RawQuery = q.Encode()
+ }
+
+ meta := new(CreateMetaInfo)
+ resp, err := s.client.Do(req, meta)
+
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return meta, resp, nil
+}
+
+// GetProjectWithName returns a project with "name" from the meta information received. If not found, this returns nil.
+// The comparison of the name is case insensitive.
+func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject {
+ for _, m := range m.Projects {
+ if strings.ToLower(m.Name) == strings.ToLower(name) {
+ return m
+ }
+ }
+ return nil
+}
+
+// GetProjectWithKey returns a project with "name" from the meta information received. If not found, this returns nil.
+// The comparison of the name is case insensitive.
+func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject {
+ for _, m := range m.Projects {
+ if strings.ToLower(m.Key) == strings.ToLower(key) {
+ return m
+ }
+ }
+ return nil
+}
+
+// GetIssueTypeWithName returns an IssueType with name from a given MetaProject. If not found, this returns nil.
+// The comparison of the name is case insensitive
+func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueType {
+ for _, m := range p.IssueTypes {
+ if strings.ToLower(m.Name) == strings.ToLower(name) {
+ return m
+ }
+ }
+ return nil
+}
+
+// GetMandatoryFields returns a map of all the required fields from the MetaIssueTypes.
+// if a field returned by the api was:
+// "customfield_10806": {
+// "required": true,
+// "schema": {
+// "type": "any",
+// "custom": "com.pyxis.greenhopper.jira:gh-epic-link",
+// "customId": 10806
+// },
+// "name": "Epic Link",
+// "hasDefaultValue": false,
+// "operations": [
+// "set"
+// ]
+// }
+// the returned map would have "Epic Link" as the key and "customfield_10806" as value.
+// This choice has been made so that the it is easier to generate the create api request later.
+func (t *MetaIssueType) GetMandatoryFields() (map[string]string, error) {
+ ret := make(map[string]string)
+ for key := range t.Fields {
+ required, err := t.Fields.Bool(key + "/required")
+ if err != nil {
+ return nil, err
+ }
+ if required {
+ name, err := t.Fields.String(key + "/name")
+ if err != nil {
+ return nil, err
+ }
+ ret[name] = key
+ }
+ }
+ return ret, nil
+}
+
+// GetAllFields returns a map of all the fields for an IssueType. This includes all required and not required.
+// The key of the returned map is what you see in the form and the value is how it is representated in the jira schema.
+func (t *MetaIssueType) GetAllFields() (map[string]string, error) {
+ ret := make(map[string]string)
+ for key := range t.Fields {
+
+ name, err := t.Fields.String(key + "/name")
+ if err != nil {
+ return nil, err
+ }
+ ret[name] = key
+ }
+ return ret, nil
+}
+
+// CheckCompleteAndAvailable checks if the given fields satisfies the mandatory field required to create a issue for the given type
+// And also if the given fields are available.
+func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (bool, error) {
+ mandatory, err := t.GetMandatoryFields()
+ if err != nil {
+ return false, err
+ }
+ all, err := t.GetAllFields()
+ if err != nil {
+ return false, err
+ }
+
+ // check templateconfig against mandatory fields
+ for key := range mandatory {
+ if _, okay := config[key]; !okay {
+ var requiredFields []string
+ for name := range mandatory {
+ requiredFields = append(requiredFields, name)
+ }
+ return false, fmt.Errorf("Required field not found in provided jira.fields. Required are: %#v", requiredFields)
+ }
+ }
+
+ // check templateConfig against all fields to verify they are available
+ for key := range config {
+ if _, okay := all[key]; !okay {
+ var availableFields []string
+ for name := range all {
+ availableFields = append(availableFields, name)
+ }
+ return false, fmt.Errorf("Fields in jira.fields are not available in jira. Available are: %#v", availableFields)
+ }
+ }
+
+ return true, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/permissionscheme.go b/vendor/github.com/andygrunwald/go-jira/permissionscheme.go
new file mode 100644
index 00000000..6a48d65f
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/permissionscheme.go
@@ -0,0 +1,69 @@
+package jira
+
+import "fmt"
+
+// PermissionSchemeService handles permissionschemes for the JIRA instance / API.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Permissionscheme
+type PermissionSchemeService struct {
+ client *Client
+}
+type PermissionSchemes struct {
+ PermissionSchemes []PermissionScheme `json:"permissionSchemes" structs:"permissionSchemes"`
+}
+
+type Permission struct {
+ ID int `json:"id" structs:"id"`
+ Self string `json:"expand" structs:"expand"`
+ Holder Holder `json:"holder" structs:"holder"`
+ Name string `json:"permission" structs:"permission"`
+}
+
+type Holder struct {
+ Type string `json:"type" structs:"type"`
+ Parameter string `json:"parameter" structs:"parameter"`
+ Expand string `json:"expand" structs:"expand"`
+}
+
+// GetList returns a list of all permission schemes
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get
+func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) {
+ apiEndpoint := "/rest/api/3/permissionscheme"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ pss := new(PermissionSchemes)
+ resp, err := s.client.Do(req, &pss)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return pss, resp, nil
+}
+
+// Get returns a full representation of the permission scheme for the schemeID
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get
+func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/3/permissionscheme/%d", schemeID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ ps := new(PermissionScheme)
+ resp, err := s.client.Do(req, ps)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+ if ps.Self == "" {
+ return nil, resp, fmt.Errorf("No permissionscheme with ID %d found", schemeID)
+ }
+
+ return ps, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/priority.go b/vendor/github.com/andygrunwald/go-jira/priority.go
new file mode 100644
index 00000000..481f9592
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/priority.go
@@ -0,0 +1,37 @@
+package jira
+
+// PriorityService handles priorities for the JIRA instance / API.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority
+type PriorityService struct {
+ client *Client
+}
+
+// Priority represents a priority of a JIRA issue.
+// Typical types are "Normal", "Moderate", "Urgent", ...
+type Priority struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ StatusColor string `json:"statusColor,omitempty" structs:"statusColor,omitempty"`
+ Description string `json:"description,omitempty" structs:"description,omitempty"`
+}
+
+// GetList gets all priorities from JIRA
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get
+func (s *PriorityService) GetList() ([]Priority, *Response, error) {
+ apiEndpoint := "rest/api/2/priority"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ priorityList := []Priority{}
+ resp, err := s.client.Do(req, &priorityList)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return priorityList, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/project.go b/vendor/github.com/andygrunwald/go-jira/project.go
new file mode 100644
index 00000000..24184224
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/project.go
@@ -0,0 +1,161 @@
+package jira
+
+import (
+ "fmt"
+
+ "github.com/google/go-querystring/query"
+)
+
+// ProjectService handles projects for the JIRA instance / API.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project
+type ProjectService struct {
+ client *Client
+}
+
+// ProjectList represent a list of Projects
+type ProjectList []struct {
+ Expand string `json:"expand" structs:"expand"`
+ Self string `json:"self" structs:"self"`
+ ID string `json:"id" structs:"id"`
+ Key string `json:"key" structs:"key"`
+ Name string `json:"name" structs:"name"`
+ AvatarUrls AvatarUrls `json:"avatarUrls" structs:"avatarUrls"`
+ ProjectTypeKey string `json:"projectTypeKey" structs:"projectTypeKey"`
+ ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectsCategory,omitempty"`
+ IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"`
+}
+
+// ProjectCategory represents a single project category
+type ProjectCategory struct {
+ Self string `json:"self" structs:"self,omitempty"`
+ ID string `json:"id" structs:"id,omitempty"`
+ Name string `json:"name" structs:"name,omitempty"`
+ Description string `json:"description" structs:"description,omitempty"`
+}
+
+// Project represents a JIRA Project.
+type Project struct {
+ Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Key string `json:"key,omitempty" structs:"key,omitempty"`
+ Description string `json:"description,omitempty" structs:"description,omitempty"`
+ Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
+ Components []ProjectComponent `json:"components,omitempty" structs:"components,omitempty"`
+ IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"`
+ URL string `json:"url,omitempty" structs:"url,omitempty"`
+ Email string `json:"email,omitempty" structs:"email,omitempty"`
+ AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"`
+ Versions []Version `json:"versions,omitempty" structs:"versions,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ Roles map[string]string `json:"roles,omitempty" structs:"roles,omitempty"`
+ AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
+ ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectCategory,omitempty"`
+}
+
+// ProjectComponent represents a single component of a project
+type ProjectComponent struct {
+ Self string `json:"self" structs:"self,omitempty"`
+ ID string `json:"id" structs:"id,omitempty"`
+ Name string `json:"name" structs:"name,omitempty"`
+ Description string `json:"description" structs:"description,omitempty"`
+ Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
+ AssigneeType string `json:"assigneeType" structs:"assigneeType,omitempty"`
+ Assignee User `json:"assignee" structs:"assignee,omitempty"`
+ RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType,omitempty"`
+ RealAssignee User `json:"realAssignee" structs:"realAssignee,omitempty"`
+ IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid,omitempty"`
+ Project string `json:"project" structs:"project,omitempty"`
+ ProjectID int `json:"projectId" structs:"projectId,omitempty"`
+}
+
+// PermissionScheme represents the permission scheme for the project
+type PermissionScheme struct {
+ Expand string `json:"expand" structs:"expand,omitempty"`
+ Self string `json:"self" structs:"self,omitempty"`
+ ID int `json:"id" structs:"id,omitempty"`
+ Name string `json:"name" structs:"name,omitempty"`
+ Description string `json:"description" structs:"description,omitempty"`
+ Permissions []Permission `json:"permissions" structs:"permissions,omitempty"`
+}
+
+// GetList gets all projects form JIRA
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
+func (s *ProjectService) GetList() (*ProjectList, *Response, error) {
+ return s.ListWithOptions(&GetQueryOptions{})
+}
+
+// ListWithOptions gets all projects form JIRA with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get
+// a list of all projects and their supported issuetypes
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
+func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) {
+ apiEndpoint := "rest/api/2/project"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if options != nil {
+ q, err := query.Values(options)
+ if err != nil {
+ return nil, nil, err
+ }
+ req.URL.RawQuery = q.Encode()
+ }
+
+ projectList := new(ProjectList)
+ resp, err := s.client.Do(req, projectList)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return projectList, resp, nil
+}
+
+// Get returns a full representation of the project for the given issue key.
+// JIRA will attempt to identify the project by the projectIdOrKey path parameter.
+// This can be an project id, or an project key.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
+func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/project/%s", projectID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ project := new(Project)
+ resp, err := s.client.Do(req, project)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return project, resp, nil
+}
+
+// GetPermissionScheme returns a full representation of the permission scheme for the project
+// JIRA will attempt to identify the project by the projectIdOrKey path parameter.
+// This can be an project id, or an project key.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
+func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s/permissionscheme", projectID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ ps := new(PermissionScheme)
+ resp, err := s.client.Do(req, ps)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return ps, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/resolution.go b/vendor/github.com/andygrunwald/go-jira/resolution.go
new file mode 100644
index 00000000..36a651fb
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/resolution.go
@@ -0,0 +1,35 @@
+package jira
+
+// ResolutionService handles resolutions for the JIRA instance / API.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution
+type ResolutionService struct {
+ client *Client
+}
+
+// Resolution represents a resolution of a JIRA issue.
+// Typical types are "Fixed", "Suspended", "Won't Fix", ...
+type Resolution struct {
+ Self string `json:"self" structs:"self"`
+ ID string `json:"id" structs:"id"`
+ Description string `json:"description" structs:"description"`
+ Name string `json:"name" structs:"name"`
+}
+
+// GetList gets all resolutions from JIRA
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get
+func (s *ResolutionService) GetList() ([]Resolution, *Response, error) {
+ apiEndpoint := "rest/api/2/resolution"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ resolutionList := []Resolution{}
+ resp, err := s.client.Do(req, &resolutionList)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return resolutionList, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/role.go b/vendor/github.com/andygrunwald/go-jira/role.go
new file mode 100644
index 00000000..f2535e4a
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/role.go
@@ -0,0 +1,76 @@
+package jira
+
+import (
+ "fmt"
+)
+
+// RoleService handles roles for the JIRA instance / API.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-group-Role
+type RoleService struct {
+ client *Client
+}
+
+// Role represents a JIRA product role
+type Role struct {
+ Self string `json:"self" structs:"self"`
+ Name string `json:"name" structs:"name"`
+ ID int `json:"id" structs:"id"`
+ Description string `json:"description" structs:"description"`
+ Actors []*Actor `json:"actors" structs:"actors"`
+}
+
+// Actor represents a JIRA actor
+type Actor struct {
+ ID int `json:"id" structs:"id"`
+ DisplayName string `json:"displayName" structs:"displayName"`
+ Type string `json:"type" structs:"type"`
+ Name string `json:"name" structs:"name"`
+ AvatarURL string `json:"avatarUrl" structs:"avatarUrl"`
+ ActorUser *ActorUser `json:"actorUser" structs:"actoruser"`
+}
+
+// ActorUser contains the account id of the actor/user
+type ActorUser struct {
+ AccountID string `json:"accountId" structs:"accountId"`
+}
+
+// GetList returns a list of all available project roles
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get
+func (s *RoleService) GetList() (*[]Role, *Response, error) {
+ apiEndpoint := "rest/api/3/role"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ roles := new([]Role)
+ resp, err := s.client.Do(req, roles)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+ return roles, resp, err
+}
+
+// Get retreives a single Role from Jira
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get
+func (s *RoleService) Get(roleID int) (*Role, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/3/role/%d", roleID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ role := new(Role)
+ resp, err := s.client.Do(req, role)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+ if role.Self == "" {
+ return nil, resp, fmt.Errorf("No role with ID %d found", roleID)
+ }
+
+ return role, resp, err
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/sprint.go b/vendor/github.com/andygrunwald/go-jira/sprint.go
new file mode 100644
index 00000000..7e8e697d
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/sprint.go
@@ -0,0 +1,107 @@
+package jira
+
+import (
+ "fmt"
+
+ "github.com/google/go-querystring/query"
+)
+
+// SprintService handles sprints in JIRA Agile API.
+// See https://docs.atlassian.com/jira-software/REST/cloud/
+type SprintService struct {
+ client *Client
+}
+
+// IssuesWrapper represents a wrapper struct for moving issues to sprint
+type IssuesWrapper struct {
+ Issues []string `json:"issues"`
+}
+
+// IssuesInSprintResult represents a wrapper struct for search result
+type IssuesInSprintResult struct {
+ Issues []Issue `json:"issues"`
+}
+
+// MoveIssuesToSprint moves issues to a sprint, for a given sprint Id.
+// Issues can only be moved to open or active sprints.
+// The maximum number of issues that can be moved in one operation is 50.
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint
+func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
+
+ payload := IssuesWrapper{Issues: issueIDs}
+
+ req, err := s.client.NewRequest("POST", apiEndpoint, payload)
+
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+ return resp, err
+}
+
+// GetIssuesForSprint returns all issues in a sprint, for a given sprint Id.
+// This only includes issues that the user has permission to view.
+// By default, the returned issues are ordered by rank.
+//
+// JIRA API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint
+func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
+
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+
+ if err != nil {
+ return nil, nil, err
+ }
+
+ result := new(IssuesInSprintResult)
+ resp, err := s.client.Do(req, result)
+ if err != nil {
+ err = NewJiraError(resp, err)
+ }
+
+ return result.Issues, resp, err
+}
+
+// GetIssue returns a full representation of the issue for the given issue key.
+// JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
+// This can be an issue id, or an issue key.
+// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
+//
+// The given options will be appended to the query string
+//
+// JIRA API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue
+//
+// TODO: create agile service for holding all agile apis' implementation
+func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", issueID)
+
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if options != nil {
+ q, err := query.Values(options)
+ if err != nil {
+ return nil, nil, err
+ }
+ req.URL.RawQuery = q.Encode()
+ }
+
+ issue := new(Issue)
+ resp, err := s.client.Do(req, issue)
+
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ return issue, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/statuscategory.go b/vendor/github.com/andygrunwald/go-jira/statuscategory.go
new file mode 100644
index 00000000..05db4207
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/statuscategory.go
@@ -0,0 +1,44 @@
+package jira
+
+// StatusCategoryService handles status categories for the JIRA instance / API.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory
+type StatusCategoryService struct {
+ client *Client
+}
+
+// StatusCategory represents the category a status belongs to.
+// Those categories can be user defined in every JIRA instance.
+type StatusCategory struct {
+ Self string `json:"self" structs:"self"`
+ ID int `json:"id" structs:"id"`
+ Name string `json:"name" structs:"name"`
+ Key string `json:"key" structs:"key"`
+ ColorName string `json:"colorName" structs:"colorName"`
+}
+
+// These constants are the keys of the default JIRA status categories
+const (
+ StatusCategoryComplete = "done"
+ StatusCategoryInProgress = "indeterminate"
+ StatusCategoryToDo = "new"
+ StatusCategoryUndefined = "undefined"
+)
+
+// GetList gets all status categories from JIRA
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get
+func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) {
+ apiEndpoint := "rest/api/2/statuscategory"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ statusCategoryList := []StatusCategory{}
+ resp, err := s.client.Do(req, &statusCategoryList)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return statusCategoryList, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/user.go b/vendor/github.com/andygrunwald/go-jira/user.go
new file mode 100644
index 00000000..e14ebd83
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/user.go
@@ -0,0 +1,213 @@
+package jira
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+)
+
+// UserService handles users for the JIRA instance / API.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user
+type UserService struct {
+ client *Client
+}
+
+// User represents a JIRA user.
+type User struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ AccountID string `json:"accountId,omitempty" structs:"accountId,omitempty"`
+ // TODO: name & key are deprecated, see:
+ // https://developer.atlassian.com/cloud/jira/platform/api-changes-for-user-privacy-announcement/
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ Key string `json:"key,omitempty" structs:"key,omitempty"`
+ Password string `json:"-"`
+ EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"`
+ AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
+ DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"`
+ Active bool `json:"active,omitempty" structs:"active,omitempty"`
+ TimeZone string `json:"timeZone,omitempty" structs:"timeZone,omitempty"`
+ ApplicationKeys []string `json:"applicationKeys,omitempty" structs:"applicationKeys,omitempty"`
+}
+
+// UserGroup represents the group list
+type UserGroup struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+}
+
+type userSearchParam struct {
+ name string
+ value string
+}
+
+type userSearch []userSearchParam
+
+type userSearchF func(userSearch) userSearch
+
+// Get gets user info from JIRA
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser
+func (s *UserService) Get(username string) (*User, *Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ user := new(User)
+ resp, err := s.client.Do(req, user)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return user, resp, nil
+}
+
+// Create creates an user in JIRA.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser
+func (s *UserService) Create(user *User) (*User, *Response, error) {
+ apiEndpoint := "/rest/api/2/user"
+ req, err := s.client.NewRequest("POST", apiEndpoint, user)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ responseUser := new(User)
+ defer resp.Body.Close()
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ e := fmt.Errorf("Could not read the returned data")
+ return nil, resp, NewJiraError(resp, e)
+ }
+ err = json.Unmarshal(data, responseUser)
+ if err != nil {
+ e := fmt.Errorf("Could not unmarshall the data into struct")
+ return nil, resp, NewJiraError(resp, e)
+ }
+ return responseUser, resp, nil
+}
+
+// Delete deletes an user from JIRA.
+// Returns http.StatusNoContent on success.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-user-delete
+func (s *UserService) Delete(username string) (*Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username)
+ req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ return resp, NewJiraError(resp, err)
+ }
+ return resp, nil
+}
+
+// GetGroups returns the groups which the user belongs to
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUserGroups
+func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?username=%s", username)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ userGroups := new([]UserGroup)
+ resp, err := s.client.Do(req, userGroups)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return userGroups, resp, nil
+}
+
+// Get information about the current logged-in user
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-myself-get
+func (s *UserService) GetSelf() (*User, *Response, error) {
+ const apiEndpoint = "rest/api/2/myself"
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ var user User
+ resp, err := s.client.Do(req, &user)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return &user, resp, nil
+}
+
+// WithMaxResults sets the max results to return
+func WithMaxResults(maxResults int) userSearchF {
+ return func(s userSearch) userSearch {
+ s = append(s, userSearchParam{name: "maxResults", value: fmt.Sprintf("%d", maxResults)})
+ return s
+ }
+}
+
+// WithStartAt set the start pager
+func WithStartAt(startAt int) userSearchF {
+ return func(s userSearch) userSearch {
+ s = append(s, userSearchParam{name: "startAt", value: fmt.Sprintf("%d", startAt)})
+ return s
+ }
+}
+
+// WithActive sets the active users lookup
+func WithActive(active bool) userSearchF {
+ return func(s userSearch) userSearch {
+ s = append(s, userSearchParam{name: "includeActive", value: fmt.Sprintf("%t", active)})
+ return s
+ }
+}
+
+// WithInactive sets the inactive users lookup
+func WithInactive(inactive bool) userSearchF {
+ return func(s userSearch) userSearch {
+ s = append(s, userSearchParam{name: "includeInactive", value: fmt.Sprintf("%t", inactive)})
+ return s
+ }
+}
+
+// Find searches for user info from JIRA:
+// It can find users by email, username or name
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-findUsers
+func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) {
+ search := []userSearchParam{
+ {
+ name: "username",
+ value: property,
+ },
+ }
+ for _, f := range tweaks {
+ search = f(search)
+ }
+
+ var queryString = ""
+ for _, param := range search {
+ queryString += param.name + "=" + param.value + "&"
+ }
+
+ apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?%s", queryString[:len(queryString)-1])
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ users := []User{}
+ resp, err := s.client.Do(req, &users)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return users, resp, nil
+}
diff --git a/vendor/github.com/andygrunwald/go-jira/version.go b/vendor/github.com/andygrunwald/go-jira/version.go
new file mode 100644
index 00000000..87bcde7b
--- /dev/null
+++ b/vendor/github.com/andygrunwald/go-jira/version.go
@@ -0,0 +1,97 @@
+package jira
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+)
+
+// VersionService handles Versions for the JIRA instance / API.
+//
+// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/version
+type VersionService struct {
+ client *Client
+}
+
+// Version represents a single release version of a project
+type Version struct {
+ Self string `json:"self,omitempty" structs:"self,omitempty"`
+ ID string `json:"id,omitempty" structs:"id,omitempty"`
+ Name string `json:"name,omitempty" structs:"name,omitempty"`
+ Description string `json:"description,omitempty" structs:"name,omitempty"`
+ Archived bool `json:"archived,omitempty" structs:"archived,omitempty"`
+ Released bool `json:"released,omitempty" structs:"released,omitempty"`
+ ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"`
+ UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"`
+ ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number
+ StartDate string `json:"startDate,omitempty" structs:"startDate,omitempty"`
+}
+
+// Get gets version info from JIRA
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-get
+func (s *VersionService) Get(versionID int) (*Version, *Response, error) {
+ apiEndpoint := fmt.Sprintf("/rest/api/2/version/%v", versionID)
+ req, err := s.client.NewRequest("GET", apiEndpoint, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ version := new(Version)
+ resp, err := s.client.Do(req, version)
+ if err != nil {
+ return nil, resp, NewJiraError(resp, err)
+ }
+ return version, resp, nil
+}
+
+// Create creates a version in JIRA.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-post
+func (s *VersionService) Create(version *Version) (*Version, *Response, error) {
+ apiEndpoint := "/rest/api/2/version"
+ req, err := s.client.NewRequest("POST", apiEndpoint, version)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ responseVersion := new(Version)
+ defer resp.Body.Close()
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ e := fmt.Errorf("Could not read the returned data")
+ return nil, resp, NewJiraError(resp, e)
+ }
+ err = json.Unmarshal(data, responseVersion)
+ if err != nil {
+ e := fmt.Errorf("Could not unmarshall the data into struct")
+ return nil, resp, NewJiraError(resp, e)
+ }
+ return responseVersion, resp, nil
+}
+
+// Update updates a version from a JSON representation.
+//
+// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-put
+func (s *VersionService) Update(version *Version) (*Version, *Response, error) {
+ apiEndpoint := fmt.Sprintf("rest/api/2/version/%v", version.ID)
+ req, err := s.client.NewRequest("PUT", apiEndpoint, version)
+ if err != nil {
+ return nil, nil, err
+ }
+ resp, err := s.client.Do(req, nil)
+ if err != nil {
+ jerr := NewJiraError(resp, err)
+ return nil, resp, jerr
+ }
+
+ // This is just to follow the rest of the API's convention of returning a version.
+ // Returning the same pointer here is pointless, so we return a copy instead.
+ ret := *version
+ return &ret, resp, nil
+}
diff --git a/vendor/github.com/fatih/structs/LICENSE b/vendor/github.com/fatih/structs/LICENSE
new file mode 100644
index 00000000..34504e4b
--- /dev/null
+++ b/vendor/github.com/fatih/structs/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Fatih Arslan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/vendor/github.com/fatih/structs/field.go b/vendor/github.com/fatih/structs/field.go
new file mode 100644
index 00000000..e6978323
--- /dev/null
+++ b/vendor/github.com/fatih/structs/field.go
@@ -0,0 +1,141 @@
+package structs
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+)
+
+var (
+ errNotExported = errors.New("field is not exported")
+ errNotSettable = errors.New("field is not settable")
+)
+
+// Field represents a single struct field that encapsulates high level
+// functions around the field.
+type Field struct {
+ value reflect.Value
+ field reflect.StructField
+ defaultTag string
+}
+
+// Tag returns the value associated with key in the tag string. If there is no
+// such key in the tag, Tag returns the empty string.
+func (f *Field) Tag(key string) string {
+ return f.field.Tag.Get(key)
+}
+
+// Value returns the underlying value of the field. It panics if the field
+// is not exported.
+func (f *Field) Value() interface{} {
+ return f.value.Interface()
+}
+
+// IsEmbedded returns true if the given field is an anonymous field (embedded)
+func (f *Field) IsEmbedded() bool {
+ return f.field.Anonymous
+}
+
+// IsExported returns true if the given field is exported.
+func (f *Field) IsExported() bool {
+ return f.field.PkgPath == ""
+}
+
+// IsZero returns true if the given field is not initialized (has a zero value).
+// It panics if the field is not exported.
+func (f *Field) IsZero() bool {
+ zero := reflect.Zero(f.value.Type()).Interface()
+ current := f.Value()
+
+ return reflect.DeepEqual(current, zero)
+}
+
+// Name returns the name of the given field
+func (f *Field) Name() string {
+ return f.field.Name
+}
+
+// Kind returns the fields kind, such as "string", "map", "bool", etc ..
+func (f *Field) Kind() reflect.Kind {
+ return f.value.Kind()
+}
+
+// Set sets the field to given value v. It returns an error if the field is not
+// settable (not addressable or not exported) or if the given value's type
+// doesn't match the fields type.
+func (f *Field) Set(val interface{}) error {
+ // we can't set unexported fields, so be sure this field is exported
+ if !f.IsExported() {
+ return errNotExported
+ }
+
+ // do we get here? not sure...
+ if !f.value.CanSet() {
+ return errNotSettable
+ }
+
+ given := reflect.ValueOf(val)
+
+ if f.value.Kind() != given.Kind() {
+ return fmt.Errorf("wrong kind. got: %s want: %s", given.Kind(), f.value.Kind())
+ }
+
+ f.value.Set(given)
+ return nil
+}
+
+// Zero sets the field to its zero value. It returns an error if the field is not
+// settable (not addressable or not exported).
+func (f *Field) Zero() error {
+ zero := reflect.Zero(f.value.Type()).Interface()
+ return f.Set(zero)
+}
+
+// Fields returns a slice of Fields. This is particular handy to get the fields
+// of a nested struct . A struct tag with the content of "-" ignores the
+// checking of that particular field. Example:
+//
+// // Field is ignored by this package.
+// Field *http.Request `structs:"-"`
+//
+// It panics if field is not exported or if field's kind is not struct
+func (f *Field) Fields() []*Field {
+ return getFields(f.value, f.defaultTag)
+}
+
+// Field returns the field from a nested struct. It panics if the nested struct
+// is not exported or if the field was not found.
+func (f *Field) Field(name string) *Field {
+ field, ok := f.FieldOk(name)
+ if !ok {
+ panic("field not found")
+ }
+
+ return field
+}
+
+// FieldOk returns the field from a nested struct. The boolean returns whether
+// the field was found (true) or not (false).
+func (f *Field) FieldOk(name string) (*Field, bool) {
+ value := &f.value
+ // value must be settable so we need to make sure it holds the address of the
+ // variable and not a copy, so we can pass the pointer to strctVal instead of a
+ // copy (which is not assigned to any variable, hence not settable).
+ // see "https://blog.golang.org/laws-of-reflection#TOC_8."
+ if f.value.Kind() != reflect.Ptr {
+ a := f.value.Addr()
+ value = &a
+ }
+ v := strctVal(value.Interface())
+ t := v.Type()
+
+ field, ok := t.FieldByName(name)
+ if !ok {
+ return nil, false
+ }
+
+ return &Field{
+ field: field,
+ value: v.FieldByName(name),
+ }, true
+}
diff --git a/vendor/github.com/fatih/structs/structs.go b/vendor/github.com/fatih/structs/structs.go
new file mode 100644
index 00000000..3a877065
--- /dev/null
+++ b/vendor/github.com/fatih/structs/structs.go
@@ -0,0 +1,584 @@
+// Package structs contains various utilities functions to work with structs.
+package structs
+
+import (
+ "fmt"
+
+ "reflect"
+)
+
+var (
+ // DefaultTagName is the default tag name for struct fields which provides
+ // a more granular to tweak certain structs. Lookup the necessary functions
+ // for more info.
+ DefaultTagName = "structs" // struct's field default tag name
+)
+
+// Struct encapsulates a struct type to provide several high level functions
+// around the struct.
+type Struct struct {
+ raw interface{}
+ value reflect.Value
+ TagName string
+}
+
+// New returns a new *Struct with the struct s. It panics if the s's kind is
+// not struct.
+func New(s interface{}) *Struct {
+ return &Struct{
+ raw: s,
+ value: strctVal(s),
+ TagName: DefaultTagName,
+ }
+}
+
+// Map converts the given struct to a map[string]interface{}, where the keys
+// of the map are the field names and the values of the map the associated
+// values of the fields. The default key string is the struct field name but
+// can be changed in the struct field's tag value. The "structs" key in the
+// struct's field tag value is the key name. Example:
+//
+// // Field appears in map as key "myName".
+// Name string `structs:"myName"`
+//
+// A tag value with the content of "-" ignores that particular field. Example:
+//
+// // Field is ignored by this package.
+// Field bool `structs:"-"`
+//
+// A tag value with the content of "string" uses the stringer to get the value. Example:
+//
+// // The value will be output of Animal's String() func.
+// // Map will panic if Animal does not implement String().
+// Field *Animal `structs:"field,string"`
+//
+// A tag value with the option of "flatten" used in a struct field is to flatten its fields
+// in the output map. Example:
+//
+// // The FieldStruct's fields will be flattened into the output map.
+// FieldStruct time.Time `structs:",flatten"`
+//
+// A tag value with the option of "omitnested" stops iterating further if the type
+// is a struct. Example:
+//
+// // Field is not processed further by this package.
+// Field time.Time `structs:"myName,omitnested"`
+// Field *http.Request `structs:",omitnested"`
+//
+// A tag value with the option of "omitempty" ignores that particular field if
+// the field value is empty. Example:
+//
+// // Field appears in map as key "myName", but the field is
+// // skipped if empty.
+// Field string `structs:"myName,omitempty"`
+//
+// // Field appears in map as key "Field" (the default), but
+// // the field is skipped if empty.
+// Field string `structs:",omitempty"`
+//
+// Note that only exported fields of a struct can be accessed, non exported
+// fields will be neglected.
+func (s *Struct) Map() map[string]interface{} {
+ out := make(map[string]interface{})
+ s.FillMap(out)
+ return out
+}
+
+// FillMap is the same as Map. Instead of returning the output, it fills the
+// given map.
+func (s *Struct) FillMap(out map[string]interface{}) {
+ if out == nil {
+ return
+ }
+
+ fields := s.structFields()
+
+ for _, field := range fields {
+ name := field.Name
+ val := s.value.FieldByName(name)
+ isSubStruct := false
+ var finalVal interface{}
+
+ tagName, tagOpts := parseTag(field.Tag.Get(s.TagName))
+ if tagName != "" {
+ name = tagName
+ }
+
+ // if the value is a zero value and the field is marked as omitempty do
+ // not include
+ if tagOpts.Has("omitempty") {
+ zero := reflect.Zero(val.Type()).Interface()
+ current := val.Interface()
+
+ if reflect.DeepEqual(current, zero) {
+ continue
+ }
+ }
+
+ if !tagOpts.Has("omitnested") {
+ finalVal = s.nested(val)
+
+ v := reflect.ValueOf(val.Interface())
+ if v.Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+
+ switch v.Kind() {
+ case reflect.Map, reflect.Struct:
+ isSubStruct = true
+ }
+ } else {
+ finalVal = val.Interface()
+ }
+
+ if tagOpts.Has("string") {
+ s, ok := val.Interface().(fmt.Stringer)
+ if ok {
+ out[name] = s.String()
+ }
+ continue
+ }
+
+ if isSubStruct && (tagOpts.Has("flatten")) {
+ for k := range finalVal.(map[string]interface{}) {
+ out[k] = finalVal.(map[string]interface{})[k]
+ }
+ } else {
+ out[name] = finalVal
+ }
+ }
+}
+
+// Values converts the given s struct's field values to a []interface{}. A
+// struct tag with the content of "-" ignores the that particular field.
+// Example:
+//
+// // Field is ignored by this package.
+// Field int `structs:"-"`
+//
+// A value with the option of "omitnested" stops iterating further if the type
+// is a struct. Example:
+//
+// // Fields is not processed further by this package.
+// Field time.Time `structs:",omitnested"`
+// Field *http.Request `structs:",omitnested"`
+//
+// A tag value with the option of "omitempty" ignores that particular field and
+// is not added to the values if the field value is empty. Example:
+//
+// // Field is skipped if empty
+// Field string `structs:",omitempty"`
+//
+// Note that only exported fields of a struct can be accessed, non exported
+// fields will be neglected.
+func (s *Struct) Values() []interface{} {
+ fields := s.structFields()
+
+ var t []interface{}
+
+ for _, field := range fields {
+ val := s.value.FieldByName(field.Name)
+
+ _, tagOpts := parseTag(field.Tag.Get(s.TagName))
+
+ // if the value is a zero value and the field is marked as omitempty do
+ // not include
+ if tagOpts.Has("omitempty") {
+ zero := reflect.Zero(val.Type()).Interface()
+ current := val.Interface()
+
+ if reflect.DeepEqual(current, zero) {
+ continue
+ }
+ }
+
+ if tagOpts.Has("string") {
+ s, ok := val.Interface().(fmt.Stringer)
+ if ok {
+ t = append(t, s.String())
+ }
+ continue
+ }
+
+ if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
+ // look out for embedded structs, and convert them to a
+ // []interface{} to be added to the final values slice
+ t = append(t, Values(val.Interface())...)
+ } else {
+ t = append(t, val.Interface())
+ }
+ }
+
+ return t
+}
+
+// Fields returns a slice of Fields. A struct tag with the content of "-"
+// ignores the checking of that particular field. Example:
+//
+// // Field is ignored by this package.
+// Field bool `structs:"-"`
+//
+// It panics if s's kind is not struct.
+func (s *Struct) Fields() []*Field {
+ return getFields(s.value, s.TagName)
+}
+
+// Names returns a slice of field names. A struct tag with the content of "-"
+// ignores the checking of that particular field. Example:
+//
+// // Field is ignored by this package.
+// Field bool `structs:"-"`
+//
+// It panics if s's kind is not struct.
+func (s *Struct) Names() []string {
+ fields := getFields(s.value, s.TagName)
+
+ names := make([]string, len(fields))
+
+ for i, field := range fields {
+ names[i] = field.Name()
+ }
+
+ return names
+}
+
+func getFields(v reflect.Value, tagName string) []*Field {
+ if v.Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+
+ t := v.Type()
+
+ var fields []*Field
+
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+
+ if tag := field.Tag.Get(tagName); tag == "-" {
+ continue
+ }
+
+ f := &Field{
+ field: field,
+ value: v.FieldByName(field.Name),
+ }
+
+ fields = append(fields, f)
+
+ }
+
+ return fields
+}
+
+// Field returns a new Field struct that provides several high level functions
+// around a single struct field entity. It panics if the field is not found.
+func (s *Struct) Field(name string) *Field {
+ f, ok := s.FieldOk(name)
+ if !ok {
+ panic("field not found")
+ }
+
+ return f
+}
+
+// FieldOk returns a new Field struct that provides several high level functions
+// around a single struct field entity. The boolean returns true if the field
+// was found.
+func (s *Struct) FieldOk(name string) (*Field, bool) {
+ t := s.value.Type()
+
+ field, ok := t.FieldByName(name)
+ if !ok {
+ return nil, false
+ }
+
+ return &Field{
+ field: field,
+ value: s.value.FieldByName(name),
+ defaultTag: s.TagName,
+ }, true
+}
+
+// IsZero returns true if all fields in a struct is a zero value (not
+// initialized) A struct tag with the content of "-" ignores the checking of
+// that particular field. Example:
+//
+// // Field is ignored by this package.
+// Field bool `structs:"-"`
+//
+// A value with the option of "omitnested" stops iterating further if the type
+// is a struct. Example:
+//
+// // Field is not processed further by this package.
+// Field time.Time `structs:"myName,omitnested"`
+// Field *http.Request `structs:",omitnested"`
+//
+// Note that only exported fields of a struct can be accessed, non exported
+// fields will be neglected. It panics if s's kind is not struct.
+func (s *Struct) IsZero() bool {
+ fields := s.structFields()
+
+ for _, field := range fields {
+ val := s.value.FieldByName(field.Name)
+
+ _, tagOpts := parseTag(field.Tag.Get(s.TagName))
+
+ if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
+ ok := IsZero(val.Interface())
+ if !ok {
+ return false
+ }
+
+ continue
+ }
+
+ // zero value of the given field, such as "" for string, 0 for int
+ zero := reflect.Zero(val.Type()).Interface()
+
+ // current value of the given field
+ current := val.Interface()
+
+ if !reflect.DeepEqual(current, zero) {
+ return false
+ }
+ }
+
+ return true
+}
+
+// HasZero returns true if a field in a struct is not initialized (zero value).
+// A struct tag with the content of "-" ignores the checking of that particular
+// field. Example:
+//
+// // Field is ignored by this package.
+// Field bool `structs:"-"`
+//
+// A value with the option of "omitnested" stops iterating further if the type
+// is a struct. Example:
+//
+// // Field is not processed further by this package.
+// Field time.Time `structs:"myName,omitnested"`
+// Field *http.Request `structs:",omitnested"`
+//
+// Note that only exported fields of a struct can be accessed, non exported
+// fields will be neglected. It panics if s's kind is not struct.
+func (s *Struct) HasZero() bool {
+ fields := s.structFields()
+
+ for _, field := range fields {
+ val := s.value.FieldByName(field.Name)
+
+ _, tagOpts := parseTag(field.Tag.Get(s.TagName))
+
+ if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
+ ok := HasZero(val.Interface())
+ if ok {
+ return true
+ }
+
+ continue
+ }
+
+ // zero value of the given field, such as "" for string, 0 for int
+ zero := reflect.Zero(val.Type()).Interface()
+
+ // current value of the given field
+ current := val.Interface()
+
+ if reflect.DeepEqual(current, zero) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// Name returns the structs's type name within its package. For more info refer
+// to Name() function.
+func (s *Struct) Name() string {
+ return s.value.Type().Name()
+}
+
+// structFields returns the exported struct fields for a given s struct. This
+// is a convenient helper method to avoid duplicate code in some of the
+// functions.
+func (s *Struct) structFields() []reflect.StructField {
+ t := s.value.Type()
+
+ var f []reflect.StructField
+
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+ // we can't access the value of unexported fields
+ if field.PkgPath != "" {
+ continue
+ }
+
+ // don't check if it's omitted
+ if tag := field.Tag.Get(s.TagName); tag == "-" {
+ continue
+ }
+
+ f = append(f, field)
+ }
+
+ return f
+}
+
+func strctVal(s interface{}) reflect.Value {
+ v := reflect.ValueOf(s)
+
+ // if pointer get the underlying element≤
+ for v.Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+
+ if v.Kind() != reflect.Struct {
+ panic("not struct")
+ }
+
+ return v
+}
+
+// Map converts the given struct to a map[string]interface{}. For more info
+// refer to Struct types Map() method. It panics if s's kind is not struct.
+func Map(s interface{}) map[string]interface{} {
+ return New(s).Map()
+}
+
+// FillMap is the same as Map. Instead of returning the output, it fills the
+// given map.
+func FillMap(s interface{}, out map[string]interface{}) {
+ New(s).FillMap(out)
+}
+
+// Values converts the given struct to a []interface{}. For more info refer to
+// Struct types Values() method. It panics if s's kind is not struct.
+func Values(s interface{}) []interface{} {
+ return New(s).Values()
+}
+
+// Fields returns a slice of *Field. For more info refer to Struct types
+// Fields() method. It panics if s's kind is not struct.
+func Fields(s interface{}) []*Field {
+ return New(s).Fields()
+}
+
+// Names returns a slice of field names. For more info refer to Struct types
+// Names() method. It panics if s's kind is not struct.
+func Names(s interface{}) []string {
+ return New(s).Names()
+}
+
+// IsZero returns true if all fields is equal to a zero value. For more info
+// refer to Struct types IsZero() method. It panics if s's kind is not struct.
+func IsZero(s interface{}) bool {
+ return New(s).IsZero()
+}
+
+// HasZero returns true if any field is equal to a zero value. For more info
+// refer to Struct types HasZero() method. It panics if s's kind is not struct.
+func HasZero(s interface{}) bool {
+ return New(s).HasZero()
+}
+
+// IsStruct returns true if the given variable is a struct or a pointer to
+// struct.
+func IsStruct(s interface{}) bool {
+ v := reflect.ValueOf(s)
+ if v.Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+
+ // uninitialized zero value of a struct
+ if v.Kind() == reflect.Invalid {
+ return false
+ }
+
+ return v.Kind() == reflect.Struct
+}
+
+// Name returns the structs's type name within its package. It returns an
+// empty string for unnamed types. It panics if s's kind is not struct.
+func Name(s interface{}) string {
+ return New(s).Name()
+}
+
+// nested retrieves recursively all types for the given value and returns the
+// nested value.
+func (s *Struct) nested(val reflect.Value) interface{} {
+ var finalVal interface{}
+
+ v := reflect.ValueOf(val.Interface())
+ if v.Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+
+ switch v.Kind() {
+ case reflect.Struct:
+ n := New(val.Interface())
+ n.TagName = s.TagName
+ m := n.Map()
+
+ // do not add the converted value if there are no exported fields, ie:
+ // time.Time
+ if len(m) == 0 {
+ finalVal = val.Interface()
+ } else {
+ finalVal = m
+ }
+ case reflect.Map:
+ // get the element type of the map
+ mapElem := val.Type()
+ switch val.Type().Kind() {
+ case reflect.Ptr, reflect.Array, reflect.Map,
+ reflect.Slice, reflect.Chan:
+ mapElem = val.Type().Elem()
+ if mapElem.Kind() == reflect.Ptr {
+ mapElem = mapElem.Elem()
+ }
+ }
+
+ // only iterate over struct types, ie: map[string]StructType,
+ // map[string][]StructType,
+ if mapElem.Kind() == reflect.Struct ||
+ (mapElem.Kind() == reflect.Slice &&
+ mapElem.Elem().Kind() == reflect.Struct) {
+ m := make(map[string]interface{}, val.Len())
+ for _, k := range val.MapKeys() {
+ m[k.String()] = s.nested(val.MapIndex(k))
+ }
+ finalVal = m
+ break
+ }
+
+ // TODO(arslan): should this be optional?
+ finalVal = val.Interface()
+ case reflect.Slice, reflect.Array:
+ if val.Type().Kind() == reflect.Interface {
+ finalVal = val.Interface()
+ break
+ }
+
+ // TODO(arslan): should this be optional?
+ // do not iterate of non struct types, just pass the value. Ie: []int,
+ // []string, co... We only iterate further if it's a struct.
+ // i.e []foo or []*foo
+ if val.Type().Elem().Kind() != reflect.Struct &&
+ !(val.Type().Elem().Kind() == reflect.Ptr &&
+ val.Type().Elem().Elem().Kind() == reflect.Struct) {
+ finalVal = val.Interface()
+ break
+ }
+
+ slices := make([]interface{}, val.Len())
+ for x := 0; x < val.Len(); x++ {
+ slices[x] = s.nested(val.Index(x))
+ }
+ finalVal = slices
+ default:
+ finalVal = val.Interface()
+ }
+
+ return finalVal
+}
diff --git a/vendor/github.com/fatih/structs/tags.go b/vendor/github.com/fatih/structs/tags.go
new file mode 100644
index 00000000..136a31eb
--- /dev/null
+++ b/vendor/github.com/fatih/structs/tags.go
@@ -0,0 +1,32 @@
+package structs
+
+import "strings"
+
+// tagOptions contains a slice of tag options
+type tagOptions []string
+
+// Has returns true if the given option is available in tagOptions
+func (t tagOptions) Has(opt string) bool {
+ for _, tagOpt := range t {
+ if tagOpt == opt {
+ return true
+ }
+ }
+
+ return false
+}
+
+// parseTag splits a struct field's tag into its name and a list of options
+// which comes after a name. A tag is in the form of: "name,option1,option2".
+// The name can be neglectected.
+func parseTag(tag string) (string, tagOptions) {
+ // tag is one of followings:
+ // ""
+ // "name"
+ // "name,opt"
+ // "name,opt,opt2"
+ // ",opt"
+
+ res := strings.Split(tag, ",")
+ return res[0], res[1:]
+}
diff --git a/vendor/github.com/google/go-querystring/LICENSE b/vendor/github.com/google/go-querystring/LICENSE
new file mode 100644
index 00000000..ae121a1e
--- /dev/null
+++ b/vendor/github.com/google/go-querystring/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2013 Google. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/google/go-querystring/query/encode.go b/vendor/github.com/google/go-querystring/query/encode.go
new file mode 100644
index 00000000..37080b19
--- /dev/null
+++ b/vendor/github.com/google/go-querystring/query/encode.go
@@ -0,0 +1,320 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package query implements encoding of structs into URL query parameters.
+//
+// As a simple example:
+//
+// type Options struct {
+// Query string `url:"q"`
+// ShowAll bool `url:"all"`
+// Page int `url:"page"`
+// }
+//
+// opt := Options{ "foo", true, 2 }
+// v, _ := query.Values(opt)
+// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2"
+//
+// The exact mapping between Go values and url.Values is described in the
+// documentation for the Values() function.
+package query
+
+import (
+ "bytes"
+ "fmt"
+ "net/url"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var timeType = reflect.TypeOf(time.Time{})
+
+var encoderType = reflect.TypeOf(new(Encoder)).Elem()
+
+// Encoder is an interface implemented by any type that wishes to encode
+// itself into URL values in a non-standard way.
+type Encoder interface {
+ EncodeValues(key string, v *url.Values) error
+}
+
+// Values returns the url.Values encoding of v.
+//
+// Values expects to be passed a struct, and traverses it recursively using the
+// following encoding rules.
+//
+// Each exported struct field is encoded as a URL parameter unless
+//
+// - the field's tag is "-", or
+// - the field is empty and its tag specifies the "omitempty" option
+//
+// The empty values are false, 0, any nil pointer or interface value, any array
+// slice, map, or string of length zero, and any time.Time that returns true
+// for IsZero().
+//
+// The URL parameter name defaults to the struct field name but can be
+// specified in the struct field's tag value. The "url" key in the struct
+// field's tag value is the key name, followed by an optional comma and
+// options. For example:
+//
+// // Field is ignored by this package.
+// Field int `url:"-"`
+//
+// // Field appears as URL parameter "myName".
+// Field int `url:"myName"`
+//
+// // Field appears as URL parameter "myName" and the field is omitted if
+// // its value is empty
+// Field int `url:"myName,omitempty"`
+//
+// // Field appears as URL parameter "Field" (the default), but the field
+// // is skipped if empty. Note the leading comma.
+// Field int `url:",omitempty"`
+//
+// For encoding individual field values, the following type-dependent rules
+// apply:
+//
+// Boolean values default to encoding as the strings "true" or "false".
+// Including the "int" option signals that the field should be encoded as the
+// strings "1" or "0".
+//
+// time.Time values default to encoding as RFC3339 timestamps. Including the
+// "unix" option signals that the field should be encoded as a Unix time (see
+// time.Unix())
+//
+// Slice and Array values default to encoding as multiple URL values of the
+// same name. Including the "comma" option signals that the field should be
+// encoded as a single comma-delimited value. Including the "space" option
+// similarly encodes the value as a single space-delimited string. Including
+// the "semicolon" option will encode the value as a semicolon-delimited string.
+// Including the "brackets" option signals that the multiple URL values should
+// have "[]" appended to the value name. "numbered" will append a number to
+// the end of each incidence of the value name, example:
+// name0=value0&name1=value1, etc.
+//
+// Anonymous struct fields are usually encoded as if their inner exported
+// fields were fields in the outer struct, subject to the standard Go
+// visibility rules. An anonymous struct field with a name given in its URL
+// tag is treated as having that name, rather than being anonymous.
+//
+// Non-nil pointer values are encoded as the value pointed to.
+//
+// Nested structs are encoded including parent fields in value names for
+// scoping. e.g:
+//
+// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO"
+//
+// All other values are encoded using their default string representation.
+//
+// Multiple fields that encode to the same URL parameter name will be included
+// as multiple URL values of the same name.
+func Values(v interface{}) (url.Values, error) {
+ values := make(url.Values)
+ val := reflect.ValueOf(v)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return values, nil
+ }
+ val = val.Elem()
+ }
+
+ if v == nil {
+ return values, nil
+ }
+
+ if val.Kind() != reflect.Struct {
+ return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
+ }
+
+ err := reflectValue(values, val, "")
+ return values, err
+}
+
+// reflectValue populates the values parameter from the struct fields in val.
+// Embedded structs are followed recursively (using the rules defined in the
+// Values function documentation) breadth-first.
+func reflectValue(values url.Values, val reflect.Value, scope string) error {
+ var embedded []reflect.Value
+
+ typ := val.Type()
+ for i := 0; i < typ.NumField(); i++ {
+ sf := typ.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous { // unexported
+ continue
+ }
+
+ sv := val.Field(i)
+ tag := sf.Tag.Get("url")
+ if tag == "-" {
+ continue
+ }
+ name, opts := parseTag(tag)
+ if name == "" {
+ if sf.Anonymous && sv.Kind() == reflect.Struct {
+ // save embedded struct for later processing
+ embedded = append(embedded, sv)
+ continue
+ }
+
+ name = sf.Name
+ }
+
+ if scope != "" {
+ name = scope + "[" + name + "]"
+ }
+
+ if opts.Contains("omitempty") && isEmptyValue(sv) {
+ continue
+ }
+
+ if sv.Type().Implements(encoderType) {
+ if !reflect.Indirect(sv).IsValid() {
+ sv = reflect.New(sv.Type().Elem())
+ }
+
+ m := sv.Interface().(Encoder)
+ if err := m.EncodeValues(name, &values); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
+ var del byte
+ if opts.Contains("comma") {
+ del = ','
+ } else if opts.Contains("space") {
+ del = ' '
+ } else if opts.Contains("semicolon") {
+ del = ';'
+ } else if opts.Contains("brackets") {
+ name = name + "[]"
+ }
+
+ if del != 0 {
+ s := new(bytes.Buffer)
+ first := true
+ for i := 0; i < sv.Len(); i++ {
+ if first {
+ first = false
+ } else {
+ s.WriteByte(del)
+ }
+ s.WriteString(valueString(sv.Index(i), opts))
+ }
+ values.Add(name, s.String())
+ } else {
+ for i := 0; i < sv.Len(); i++ {
+ k := name
+ if opts.Contains("numbered") {
+ k = fmt.Sprintf("%s%d", name, i)
+ }
+ values.Add(k, valueString(sv.Index(i), opts))
+ }
+ }
+ continue
+ }
+
+ for sv.Kind() == reflect.Ptr {
+ if sv.IsNil() {
+ break
+ }
+ sv = sv.Elem()
+ }
+
+ if sv.Type() == timeType {
+ values.Add(name, valueString(sv, opts))
+ continue
+ }
+
+ if sv.Kind() == reflect.Struct {
+ reflectValue(values, sv, name)
+ continue
+ }
+
+ values.Add(name, valueString(sv, opts))
+ }
+
+ for _, f := range embedded {
+ if err := reflectValue(values, f, scope); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// valueString returns the string representation of a value.
+func valueString(v reflect.Value, opts tagOptions) string {
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return ""
+ }
+ v = v.Elem()
+ }
+
+ if v.Kind() == reflect.Bool && opts.Contains("int") {
+ if v.Bool() {
+ return "1"
+ }
+ return "0"
+ }
+
+ if v.Type() == timeType {
+ t := v.Interface().(time.Time)
+ if opts.Contains("unix") {
+ return strconv.FormatInt(t.Unix(), 10)
+ }
+ return t.Format(time.RFC3339)
+ }
+
+ return fmt.Sprint(v.Interface())
+}
+
+// isEmptyValue checks if a value should be considered empty for the purposes
+// of omitting fields with the "omitempty" option.
+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()
+ }
+
+ if v.Type() == timeType {
+ return v.Interface().(time.Time).IsZero()
+ }
+
+ return false
+}
+
+// tagOptions is the string following a comma in a struct field's "url" tag, or
+// the empty string. It does not include the leading comma.
+type tagOptions []string
+
+// parseTag splits a struct field's url tag into its name and comma-separated
+// options.
+func parseTag(tag string) (string, tagOptions) {
+ s := strings.Split(tag, ",")
+ return s[0], s[1:]
+}
+
+// Contains checks whether the tagOptions contains the specified option.
+func (o tagOptions) Contains(option string) bool {
+ for _, s := range o {
+ if s == option {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE
new file mode 100644
index 00000000..835ba3e7
--- /dev/null
+++ b/vendor/github.com/pkg/errors/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2015, Dave Cheney
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go
new file mode 100644
index 00000000..7421f326
--- /dev/null
+++ b/vendor/github.com/pkg/errors/errors.go
@@ -0,0 +1,282 @@
+// Package errors provides simple error handling primitives.
+//
+// The traditional error handling idiom in Go is roughly akin to
+//
+// if err != nil {
+// return err
+// }
+//
+// which when applied recursively up the call stack results in error reports
+// without context or debugging information. The errors package allows
+// programmers to add context to the failure path in their code in a way
+// that does not destroy the original value of the error.
+//
+// Adding context to an error
+//
+// The errors.Wrap function returns a new error that adds context to the
+// original error by recording a stack trace at the point Wrap is called,
+// together with the supplied message. For example
+//
+// _, err := ioutil.ReadAll(r)
+// if err != nil {
+// return errors.Wrap(err, "read failed")
+// }
+//
+// If additional control is required, the errors.WithStack and
+// errors.WithMessage functions destructure errors.Wrap into its component
+// operations: annotating an error with a stack trace and with a message,
+// respectively.
+//
+// Retrieving the cause of an error
+//
+// Using errors.Wrap constructs a stack of errors, adding context to the
+// preceding error. Depending on the nature of the error it may be necessary
+// to reverse the operation of errors.Wrap to retrieve the original error
+// for inspection. Any error value which implements this interface
+//
+// type causer interface {
+// Cause() error
+// }
+//
+// can be inspected by errors.Cause. errors.Cause will recursively retrieve
+// the topmost error that does not implement causer, which is assumed to be
+// the original cause. For example:
+//
+// switch err := errors.Cause(err).(type) {
+// case *MyError:
+// // handle specifically
+// default:
+// // unknown error
+// }
+//
+// Although the causer interface is not exported by this package, it is
+// considered a part of its stable public interface.
+//
+// Formatted printing of errors
+//
+// All error values returned from this package implement fmt.Formatter and can
+// be formatted by the fmt package. The following verbs are supported:
+//
+// %s print the error. If the error has a Cause it will be
+// printed recursively.
+// %v see %s
+// %+v extended format. Each Frame of the error's StackTrace will
+// be printed in detail.
+//
+// Retrieving the stack trace of an error or wrapper
+//
+// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
+// invoked. This information can be retrieved with the following interface:
+//
+// type stackTracer interface {
+// StackTrace() errors.StackTrace
+// }
+//
+// The returned errors.StackTrace type is defined as
+//
+// type StackTrace []Frame
+//
+// The Frame type represents a call site in the stack trace. Frame supports
+// the fmt.Formatter interface that can be used for printing information about
+// the stack trace of this error. For example:
+//
+// if err, ok := err.(stackTracer); ok {
+// for _, f := range err.StackTrace() {
+// fmt.Printf("%+s:%d", f)
+// }
+// }
+//
+// Although the stackTracer interface is not exported by this package, it is
+// considered a part of its stable public interface.
+//
+// See the documentation for Frame.Format for more details.
+package errors
+
+import (
+ "fmt"
+ "io"
+)
+
+// New returns an error with the supplied message.
+// New also records the stack trace at the point it was called.
+func New(message string) error {
+ return &fundamental{
+ msg: message,
+ stack: callers(),
+ }
+}
+
+// Errorf formats according to a format specifier and returns the string
+// as a value that satisfies error.
+// Errorf also records the stack trace at the point it was called.
+func Errorf(format string, args ...interface{}) error {
+ return &fundamental{
+ msg: fmt.Sprintf(format, args...),
+ stack: callers(),
+ }
+}
+
+// fundamental is an error that has a message and a stack, but no caller.
+type fundamental struct {
+ msg string
+ *stack
+}
+
+func (f *fundamental) Error() string { return f.msg }
+
+func (f *fundamental) Format(s fmt.State, verb rune) {
+ switch verb {
+ case 'v':
+ if s.Flag('+') {
+ io.WriteString(s, f.msg)
+ f.stack.Format(s, verb)
+ return
+ }
+ fallthrough
+ case 's':
+ io.WriteString(s, f.msg)
+ case 'q':
+ fmt.Fprintf(s, "%q", f.msg)
+ }
+}
+
+// WithStack annotates err with a stack trace at the point WithStack was called.
+// If err is nil, WithStack returns nil.
+func WithStack(err error) error {
+ if err == nil {
+ return nil
+ }
+ return &withStack{
+ err,
+ callers(),
+ }
+}
+
+type withStack struct {
+ error
+ *stack
+}
+
+func (w *withStack) Cause() error { return w.error }
+
+func (w *withStack) Format(s fmt.State, verb rune) {
+ switch verb {
+ case 'v':
+ if s.Flag('+') {
+ fmt.Fprintf(s, "%+v", w.Cause())
+ w.stack.Format(s, verb)
+ return
+ }
+ fallthrough
+ case 's':
+ io.WriteString(s, w.Error())
+ case 'q':
+ fmt.Fprintf(s, "%q", w.Error())
+ }
+}
+
+// Wrap returns an error annotating err with a stack trace
+// at the point Wrap is called, and the supplied message.
+// If err is nil, Wrap returns nil.
+func Wrap(err error, message string) error {
+ if err == nil {
+ return nil
+ }
+ err = &withMessage{
+ cause: err,
+ msg: message,
+ }
+ return &withStack{
+ err,
+ callers(),
+ }
+}
+
+// Wrapf returns an error annotating err with a stack trace
+// at the point Wrapf is called, and the format specifier.
+// If err is nil, Wrapf returns nil.
+func Wrapf(err error, format string, args ...interface{}) error {
+ if err == nil {
+ return nil
+ }
+ err = &withMessage{
+ cause: err,
+ msg: fmt.Sprintf(format, args...),
+ }
+ return &withStack{
+ err,
+ callers(),
+ }
+}
+
+// WithMessage annotates err with a new message.
+// If err is nil, WithMessage returns nil.
+func WithMessage(err error, message string) error {
+ if err == nil {
+ return nil
+ }
+ return &withMessage{
+ cause: err,
+ msg: message,
+ }
+}
+
+// WithMessagef annotates err with the format specifier.
+// If err is nil, WithMessagef returns nil.
+func WithMessagef(err error, format string, args ...interface{}) error {
+ if err == nil {
+ return nil
+ }
+ return &withMessage{
+ cause: err,
+ msg: fmt.Sprintf(format, args...),
+ }
+}
+
+type withMessage struct {
+ cause error
+ msg string
+}
+
+func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
+func (w *withMessage) Cause() error { return w.cause }
+
+func (w *withMessage) Format(s fmt.State, verb rune) {
+ switch verb {
+ case 'v':
+ if s.Flag('+') {
+ fmt.Fprintf(s, "%+v\n", w.Cause())
+ io.WriteString(s, w.msg)
+ return
+ }
+ fallthrough
+ case 's', 'q':
+ io.WriteString(s, w.Error())
+ }
+}
+
+// Cause returns the underlying cause of the error, if possible.
+// An error value has a cause if it implements the following
+// interface:
+//
+// type causer interface {
+// Cause() error
+// }
+//
+// If the error does not implement Cause, the original error will
+// be returned. If the error is nil, nil will be returned without further
+// investigation.
+func Cause(err error) error {
+ type causer interface {
+ Cause() error
+ }
+
+ for err != nil {
+ cause, ok := err.(causer)
+ if !ok {
+ break
+ }
+ err = cause.Cause()
+ }
+ return err
+}
diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go
new file mode 100644
index 00000000..2874a048
--- /dev/null
+++ b/vendor/github.com/pkg/errors/stack.go
@@ -0,0 +1,147 @@
+package errors
+
+import (
+ "fmt"
+ "io"
+ "path"
+ "runtime"
+ "strings"
+)
+
+// Frame represents a program counter inside a stack frame.
+type Frame uintptr
+
+// pc returns the program counter for this frame;
+// multiple frames may have the same PC value.
+func (f Frame) pc() uintptr { return uintptr(f) - 1 }
+
+// file returns the full path to the file that contains the
+// function for this Frame's pc.
+func (f Frame) file() string {
+ fn := runtime.FuncForPC(f.pc())
+ if fn == nil {
+ return "unknown"
+ }
+ file, _ := fn.FileLine(f.pc())
+ return file
+}
+
+// line returns the line number of source code of the
+// function for this Frame's pc.
+func (f Frame) line() int {
+ fn := runtime.FuncForPC(f.pc())
+ if fn == nil {
+ return 0
+ }
+ _, line := fn.FileLine(f.pc())
+ return line
+}
+
+// Format formats the frame according to the fmt.Formatter interface.
+//
+// %s source file
+// %d source line
+// %n function name
+// %v equivalent to %s:%d
+//
+// Format accepts flags that alter the printing of some verbs, as follows:
+//
+// %+s function name and path of source file relative to the compile time
+// GOPATH separated by \n\t (\n\t)
+// %+v equivalent to %+s:%d
+func (f Frame) Format(s fmt.State, verb rune) {
+ switch verb {
+ case 's':
+ switch {
+ case s.Flag('+'):
+ pc := f.pc()
+ fn := runtime.FuncForPC(pc)
+ if fn == nil {
+ io.WriteString(s, "unknown")
+ } else {
+ file, _ := fn.FileLine(pc)
+ fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
+ }
+ default:
+ io.WriteString(s, path.Base(f.file()))
+ }
+ case 'd':
+ fmt.Fprintf(s, "%d", f.line())
+ case 'n':
+ name := runtime.FuncForPC(f.pc()).Name()
+ io.WriteString(s, funcname(name))
+ case 'v':
+ f.Format(s, 's')
+ io.WriteString(s, ":")
+ f.Format(s, 'd')
+ }
+}
+
+// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
+type StackTrace []Frame
+
+// Format formats the stack of Frames according to the fmt.Formatter interface.
+//
+// %s lists source files for each Frame in the stack
+// %v lists the source file and line number for each Frame in the stack
+//
+// Format accepts flags that alter the printing of some verbs, as follows:
+//
+// %+v Prints filename, function, and line number for each Frame in the stack.
+func (st StackTrace) Format(s fmt.State, verb rune) {
+ switch verb {
+ case 'v':
+ switch {
+ case s.Flag('+'):
+ for _, f := range st {
+ fmt.Fprintf(s, "\n%+v", f)
+ }
+ case s.Flag('#'):
+ fmt.Fprintf(s, "%#v", []Frame(st))
+ default:
+ fmt.Fprintf(s, "%v", []Frame(st))
+ }
+ case 's':
+ fmt.Fprintf(s, "%s", []Frame(st))
+ }
+}
+
+// stack represents a stack of program counters.
+type stack []uintptr
+
+func (s *stack) Format(st fmt.State, verb rune) {
+ switch verb {
+ case 'v':
+ switch {
+ case st.Flag('+'):
+ for _, pc := range *s {
+ f := Frame(pc)
+ fmt.Fprintf(st, "\n%+v", f)
+ }
+ }
+ }
+}
+
+func (s *stack) StackTrace() StackTrace {
+ f := make([]Frame, len(*s))
+ for i := 0; i < len(f); i++ {
+ f[i] = Frame((*s)[i])
+ }
+ return f
+}
+
+func callers() *stack {
+ const depth = 32
+ var pcs [depth]uintptr
+ n := runtime.Callers(3, pcs[:])
+ var st stack = pcs[0:n]
+ return &st
+}
+
+// funcname removes the path prefix component of a function's name reported by func.Name().
+func funcname(name string) string {
+ i := strings.LastIndex(name, "/")
+ name = name[i+1:]
+ i = strings.Index(name, ".")
+ return name[i+1:]
+}
diff --git a/vendor/github.com/trivago/tgo/LICENSE b/vendor/github.com/trivago/tgo/LICENSE
new file mode 100644
index 00000000..8f71f43f
--- /dev/null
+++ b/vendor/github.com/trivago/tgo/LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/vendor/github.com/trivago/tgo/tcontainer/arrays.go b/vendor/github.com/trivago/tgo/tcontainer/arrays.go
new file mode 100644
index 00000000..ede9aed3
--- /dev/null
+++ b/vendor/github.com/trivago/tgo/tcontainer/arrays.go
@@ -0,0 +1,113 @@
+// Copyright 2015-2018 trivago N.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tcontainer
+
+import "sort"
+
+// Int64Slice is a typedef to allow sortable int64 slices
+type Int64Slice []int64
+
+func (s Int64Slice) Len() int {
+ return len(s)
+}
+
+func (s Int64Slice) Less(i, j int) bool {
+ return s[i] < s[j]
+}
+
+func (s Int64Slice) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+
+// Sort is a shortcut for sort.Sort(s)
+func (s Int64Slice) Sort() {
+ sort.Sort(s)
+}
+
+// IsSorted is a shortcut for sort.IsSorted(s)
+func (s Int64Slice) IsSorted() bool {
+ return sort.IsSorted(s)
+}
+
+// Set sets all values in this slice to the given value
+func (s Int64Slice) Set(v int64) {
+ for i := range s {
+ s[i] = v
+ }
+}
+
+// Uint64Slice is a typedef to allow sortable uint64 slices
+type Uint64Slice []uint64
+
+func (s Uint64Slice) Len() int {
+ return len(s)
+}
+
+func (s Uint64Slice) Less(i, j int) bool {
+ return s[i] < s[j]
+}
+
+func (s Uint64Slice) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+
+// Sort is a shortcut for sort.Sort(s)
+func (s Uint64Slice) Sort() {
+ sort.Sort(s)
+}
+
+// IsSorted is a shortcut for sort.IsSorted(s)
+func (s Uint64Slice) IsSorted() bool {
+ return sort.IsSorted(s)
+}
+
+// Set sets all values in this slice to the given value
+func (s Uint64Slice) Set(v uint64) {
+ for i := range s {
+ s[i] = v
+ }
+}
+
+// Float32Slice is a typedef to allow sortable float32 slices
+type Float32Slice []float32
+
+func (s Float32Slice) Len() int {
+ return len(s)
+}
+
+func (s Float32Slice) Less(i, j int) bool {
+ return s[i] < s[j]
+}
+
+func (s Float32Slice) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+
+// Sort is a shortcut for sort.Sort(s)
+func (s Float32Slice) Sort() {
+ sort.Sort(s)
+}
+
+// IsSorted is a shortcut for sort.IsSorted(s)
+func (s Float32Slice) IsSorted() bool {
+ return sort.IsSorted(s)
+}
+
+// Set sets all values in this slice to the given value
+func (s Float32Slice) Set(v float32) {
+ for i := range s {
+ s[i] = v
+ }
+}
diff --git a/vendor/github.com/trivago/tgo/tcontainer/bytepool.go b/vendor/github.com/trivago/tgo/tcontainer/bytepool.go
new file mode 100644
index 00000000..c1290b76
--- /dev/null
+++ b/vendor/github.com/trivago/tgo/tcontainer/bytepool.go
@@ -0,0 +1,157 @@
+// Copyright 2015-2018 trivago N.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tcontainer
+
+import (
+ "reflect"
+ "runtime"
+ "sync/atomic"
+ "unsafe"
+)
+
+const (
+ tiny = 64
+ small = 512
+ medium = 1024
+ large = 1024 * 10
+ huge = 1024 * 100
+
+ tinyCount = 16384 // 1 MB
+ smallCount = 2048 // 1 MB
+ mediumCount = 1024 // 1 MB
+ largeCount = 102 // ~1 MB
+ hugeCount = 10 // ~1 MB
+)
+
+type byteSlab struct {
+ buffer []byte
+ bufferSize uintptr
+ stride uintptr
+ basePtr *uintptr
+ nextPtr *uintptr
+}
+
+// BytePool is a fragmentation friendly way to allocated byte slices.
+type BytePool struct {
+ tinySlab byteSlab
+ smallSlab byteSlab
+ mediumSlab byteSlab
+ largeSlab byteSlab
+ hugeSlab byteSlab
+}
+
+func newByteSlab(size, count int) byteSlab {
+ bufferSize := count * size
+ buffer := make([]byte, bufferSize)
+ basePtr := (*reflect.SliceHeader)(unsafe.Pointer(&buffer)).Data
+ nextPtr := basePtr + uintptr(bufferSize)
+
+ return byteSlab{
+ buffer: buffer,
+ bufferSize: uintptr(bufferSize),
+ stride: uintptr(size),
+ basePtr: &basePtr,
+ nextPtr: &nextPtr,
+ }
+}
+
+func (slab *byteSlab) getSlice(size int) (chunk []byte) {
+ chunkHeader := (*reflect.SliceHeader)(unsafe.Pointer(&chunk))
+ chunkHeader.Len = size
+ chunkHeader.Cap = int(slab.stride)
+
+ for {
+ // WARNING: The following two lines are order sensitive
+ basePtr := atomic.LoadUintptr(slab.basePtr)
+ nextPtr := atomic.AddUintptr(slab.nextPtr, -slab.stride)
+ lastPtr := basePtr + slab.bufferSize
+
+ switch {
+ case nextPtr < basePtr || nextPtr >= lastPtr:
+ // out of range either means alloc while realloc or race between
+ // base and next during realloc. In the latter case we lose a chunk.
+ runtime.Gosched()
+
+ case nextPtr == basePtr:
+ // Last item: realloc
+ slab.buffer = make([]byte, slab.bufferSize)
+ dataPtr := (*reflect.SliceHeader)(unsafe.Pointer(&slab.buffer)).Data
+
+ // WARNING: The following two lines are order sensitive
+ atomic.StoreUintptr(slab.nextPtr, dataPtr+slab.bufferSize)
+ atomic.StoreUintptr(slab.basePtr, dataPtr)
+ fallthrough
+
+ default:
+ chunkHeader.Data = nextPtr
+ return
+ }
+ }
+}
+
+// NewBytePool creates a new BytePool with each slab using 1 MB of storage.
+// The pool contains 5 slabs of different sizes: 64B, 512B, 1KB, 10KB and 100KB.
+// Allocations above 100KB will be allocated directly.
+func NewBytePool() BytePool {
+ return BytePool{
+ tinySlab: newByteSlab(tiny, tinyCount),
+ smallSlab: newByteSlab(small, smallCount),
+ mediumSlab: newByteSlab(medium, mediumCount),
+ largeSlab: newByteSlab(large, largeCount),
+ hugeSlab: newByteSlab(huge, hugeCount),
+ }
+}
+
+// NewBytePoolWithSize creates a new BytePool with each slab size using n MB of
+// storage. See NewBytePool() for slab size details.
+func NewBytePoolWithSize(n int) BytePool {
+ if n <= 0 {
+ n = 1
+ }
+ return BytePool{
+ tinySlab: newByteSlab(tiny, tinyCount*n),
+ smallSlab: newByteSlab(small, smallCount*n),
+ mediumSlab: newByteSlab(medium, mediumCount*n),
+ largeSlab: newByteSlab(large, largeCount*n),
+ hugeSlab: newByteSlab(huge, hugeCount*n),
+ }
+}
+
+// Get returns a slice allocated to a normalized size.
+// Sizes are organized in evenly sized buckets so that fragmentation is kept low.
+func (b *BytePool) Get(size int) []byte {
+ switch {
+ case size == 0:
+ return []byte{}
+
+ case size <= tiny:
+ return b.tinySlab.getSlice(size)
+
+ case size <= small:
+ return b.smallSlab.getSlice(size)
+
+ case size <= medium:
+ return b.mediumSlab.getSlice(size)
+
+ case size <= large:
+ return b.largeSlab.getSlice(size)
+
+ case size <= huge:
+ return b.hugeSlab.getSlice(size)
+
+ default:
+ return make([]byte, size)
+ }
+}
diff --git a/vendor/github.com/trivago/tgo/tcontainer/marshalmap.go b/vendor/github.com/trivago/tgo/tcontainer/marshalmap.go
new file mode 100644
index 00000000..7564f3b5
--- /dev/null
+++ b/vendor/github.com/trivago/tgo/tcontainer/marshalmap.go
@@ -0,0 +1,565 @@
+// Copyright 2015-2018 trivago N.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tcontainer
+
+import (
+ "fmt"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/trivago/tgo/treflect"
+)
+
+// MarshalMap is a wrapper type to attach converter methods to maps normally
+// returned by marshalling methods, i.e. key/value parsers.
+// All methods that do a conversion will return an error if the value stored
+// behind key is not of the expected type or if the key is not existing in the
+// map.
+type MarshalMap map[string]interface{}
+
+const (
+ // MarshalMapSeparator defines the rune used for path separation
+ MarshalMapSeparator = '/'
+ // MarshalMapArrayBegin defines the rune starting array index notation
+ MarshalMapArrayBegin = '['
+ // MarshalMapArrayEnd defines the rune ending array index notation
+ MarshalMapArrayEnd = ']'
+)
+
+// NewMarshalMap creates a new marshal map (string -> interface{})
+func NewMarshalMap() MarshalMap {
+ return make(map[string]interface{})
+}
+
+// TryConvertToMarshalMap converts collections to MarshalMap if possible.
+// This is a deep conversion, i.e. each element in the collection will be
+// traversed. You can pass a formatKey function that will be applied to all
+// string keys that are detected.
+func TryConvertToMarshalMap(value interface{}, formatKey func(string) string) interface{} {
+ valueMeta := reflect.ValueOf(value)
+ switch valueMeta.Kind() {
+ default:
+ return value
+
+ case reflect.Array, reflect.Slice:
+ arrayLen := valueMeta.Len()
+ converted := make([]interface{}, arrayLen)
+ for i := 0; i < arrayLen; i++ {
+ converted[i] = TryConvertToMarshalMap(valueMeta.Index(i).Interface(), formatKey)
+ }
+ return converted
+
+ case reflect.Map:
+ converted := NewMarshalMap()
+ keys := valueMeta.MapKeys()
+
+ for _, keyMeta := range keys {
+ strKey, isString := keyMeta.Interface().(string)
+ if !isString {
+ continue
+ }
+ if formatKey != nil {
+ strKey = formatKey(strKey)
+ }
+ val := valueMeta.MapIndex(keyMeta).Interface()
+ converted[strKey] = TryConvertToMarshalMap(val, formatKey)
+ }
+ return converted // ### return, converted MarshalMap ###
+ }
+}
+
+// ConvertToMarshalMap tries to convert a compatible map type to a marshal map.
+// Compatible types are map[interface{}]interface{}, map[string]interface{} and of
+// course MarshalMap. The same rules as for ConvertValueToMarshalMap apply.
+func ConvertToMarshalMap(value interface{}, formatKey func(string) string) (MarshalMap, error) {
+ converted := TryConvertToMarshalMap(value, formatKey)
+ if result, isMap := converted.(MarshalMap); isMap {
+ return result, nil
+ }
+ return nil, fmt.Errorf("Root value cannot be converted to MarshalMap")
+}
+
+// Clone creates a copy of the given MarshalMap.
+func (mmap MarshalMap) Clone() MarshalMap {
+ clone := cloneMap(reflect.ValueOf(mmap))
+ return clone.Interface().(MarshalMap)
+}
+
+func cloneMap(mapValue reflect.Value) reflect.Value {
+ clone := reflect.MakeMap(mapValue.Type())
+ keys := mapValue.MapKeys()
+
+ for _, k := range keys {
+ v := mapValue.MapIndex(k)
+ switch k.Kind() {
+ default:
+ clone.SetMapIndex(k, v)
+
+ case reflect.Array, reflect.Slice:
+ if v.Type().Elem().Kind() == reflect.Map {
+ sliceCopy := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
+ for i := 0; i < v.Len(); i++ {
+ element := v.Index(i)
+ sliceCopy.Index(i).Set(cloneMap(element))
+ }
+ } else {
+ sliceCopy := reflect.MakeSlice(v.Type(), 0, v.Len())
+ reflect.Copy(sliceCopy, v)
+ clone.SetMapIndex(k, sliceCopy)
+ }
+
+ case reflect.Map:
+ vClone := cloneMap(v)
+ clone.SetMapIndex(k, vClone)
+ }
+ }
+
+ return clone
+}
+
+// Bool returns a value at key that is expected to be a boolean
+func (mmap MarshalMap) Bool(key string) (bool, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return false, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ boolValue, isBool := val.(bool)
+ if !isBool {
+ return false, fmt.Errorf(`"%s" is expected to be a boolean`, key)
+ }
+ return boolValue, nil
+}
+
+// Uint returns a value at key that is expected to be an uint64 or compatible
+// integer value.
+func (mmap MarshalMap) Uint(key string) (uint64, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return 0, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ if intVal, isNumber := treflect.Uint64(val); isNumber {
+ return intVal, nil
+ }
+
+ return 0, fmt.Errorf(`"%s" is expected to be an unsigned number type`, key)
+}
+
+// Int returns a value at key that is expected to be an int64 or compatible
+// integer value.
+func (mmap MarshalMap) Int(key string) (int64, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return 0, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ if intVal, isNumber := treflect.Int64(val); isNumber {
+ return intVal, nil
+ }
+
+ return 0, fmt.Errorf(`"%s" is expected to be a signed number type`, key)
+}
+
+// Float returns a value at key that is expected to be a float64 or compatible
+// float value.
+func (mmap MarshalMap) Float(key string) (float64, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return 0, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ if floatVal, isNumber := treflect.Float64(val); isNumber {
+ return floatVal, nil
+ }
+
+ return 0, fmt.Errorf(`"%s" is expected to be a signed number type`, key)
+}
+
+// Duration returns a value at key that is expected to be a string
+func (mmap MarshalMap) Duration(key string) (time.Duration, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return time.Duration(0), fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ switch val.(type) {
+ case time.Duration:
+ return val.(time.Duration), nil
+ case string:
+ return time.ParseDuration(val.(string))
+ }
+
+ return time.Duration(0), fmt.Errorf(`"%s" is expected to be a duration or string`, key)
+}
+
+// String returns a value at key that is expected to be a string
+func (mmap MarshalMap) String(key string) (string, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return "", fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ strValue, isString := val.(string)
+ if !isString {
+ return "", fmt.Errorf(`"%s" is expected to be a string`, key)
+ }
+ return strValue, nil
+}
+
+// Bytes returns a value at key that is expected to be a []byte
+func (mmap MarshalMap) Bytes(key string) ([]byte, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return []byte{}, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ bytesValue, isBytes := val.([]byte)
+ if !isBytes {
+ return []byte{}, fmt.Errorf(`"%s" is expected to be a []byte`, key)
+ }
+ return bytesValue, nil
+}
+
+// Slice is an alias for Array
+func (mmap MarshalMap) Slice(key string) ([]interface{}, error) {
+ return mmap.Array(key)
+}
+
+// Array returns a value at key that is expected to be a []interface{}
+func (mmap MarshalMap) Array(key string) ([]interface{}, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return nil, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ arrayValue, isArray := val.([]interface{})
+ if !isArray {
+ return nil, fmt.Errorf(`"%s" is expected to be an array`, key)
+ }
+ return arrayValue, nil
+}
+
+// Map returns a value at key that is expected to be a
+// map[interface{}]interface{}.
+func (mmap MarshalMap) Map(key string) (map[interface{}]interface{}, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return nil, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ mapValue, isMap := val.(map[interface{}]interface{})
+ if !isMap {
+ return nil, fmt.Errorf(`"%s" is expected to be a map`, key)
+ }
+ return mapValue, nil
+}
+
+func castToStringArray(key string, value interface{}) ([]string, error) {
+ switch value.(type) {
+ case string:
+ return []string{value.(string)}, nil
+
+ case []interface{}:
+ arrayVal := value.([]interface{})
+ stringArray := make([]string, 0, len(arrayVal))
+
+ for _, val := range arrayVal {
+ strValue, isString := val.(string)
+ if !isString {
+ return nil, fmt.Errorf(`"%s" does not contain string keys`, key)
+ }
+ stringArray = append(stringArray, strValue)
+ }
+ return stringArray, nil
+
+ case []string:
+ return value.([]string), nil
+
+ default:
+ return nil, fmt.Errorf(`"%s" is not a valid string array type`, key)
+ }
+}
+
+// StringSlice is an alias for StringArray
+func (mmap MarshalMap) StringSlice(key string) ([]string, error) {
+ return mmap.StringArray(key)
+}
+
+// StringArray returns a value at key that is expected to be a []string
+// This function supports conversion (by copy) from
+// * []interface{}
+func (mmap MarshalMap) StringArray(key string) ([]string, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return nil, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ return castToStringArray(key, val)
+}
+
+func castToInt64Array(key string, value interface{}) ([]int64, error) {
+ switch value.(type) {
+ case int:
+ return []int64{value.(int64)}, nil
+
+ case []interface{}:
+ arrayVal := value.([]interface{})
+ intArray := make([]int64, 0, len(arrayVal))
+
+ for _, val := range arrayVal {
+ intValue, isInt := val.(int64)
+ if !isInt {
+ return nil, fmt.Errorf(`"%s" does not contain int keys`, key)
+ }
+ intArray = append(intArray, intValue)
+ }
+ return intArray, nil
+
+ case []int64:
+ return value.([]int64), nil
+
+ default:
+ return nil, fmt.Errorf(`"%s" is not a valid string array type`, key)
+ }
+}
+
+// Int64Slice is an alias for Int64Array
+func (mmap MarshalMap) Int64Slice(key string) ([]int64, error) {
+ return mmap.Int64Array(key)
+}
+
+// Int64Array returns a value at key that is expected to be a []int64
+// This function supports conversion (by copy) from
+// * []interface{}
+func (mmap MarshalMap) Int64Array(key string) ([]int64, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return nil, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ return castToInt64Array(key, val)
+}
+
+// StringMap returns a value at key that is expected to be a map[string]string.
+// This function supports conversion (by copy) from
+// * map[interface{}]interface{}
+// * map[string]interface{}
+func (mmap MarshalMap) StringMap(key string) (map[string]string, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return nil, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ switch val.(type) {
+ case map[string]string:
+ return val.(map[string]string), nil
+
+ default:
+ valueMeta := reflect.ValueOf(val)
+ if valueMeta.Kind() != reflect.Map {
+ return nil, fmt.Errorf(`"%s" is expected to be a map[string]string but is %T`, key, val)
+ }
+
+ result := make(map[string]string)
+ for _, keyMeta := range valueMeta.MapKeys() {
+ strKey, isString := keyMeta.Interface().(string)
+ if !isString {
+ return nil, fmt.Errorf(`"%s" is expected to be a map[string]string. Key is not a string`, key)
+ }
+
+ value := valueMeta.MapIndex(keyMeta)
+ strValue, isString := value.Interface().(string)
+ if !isString {
+ return nil, fmt.Errorf(`"%s" is expected to be a map[string]string. Value is not a string`, key)
+ }
+
+ result[strKey] = strValue
+ }
+
+ return result, nil
+ }
+}
+
+// StringSliceMap is an alias for StringArrayMap
+func (mmap MarshalMap) StringSliceMap(key string) (map[string][]string, error) {
+ return mmap.StringArrayMap(key)
+}
+
+// StringArrayMap returns a value at key that is expected to be a
+// map[string][]string. This function supports conversion (by copy) from
+// * map[interface{}][]interface{}
+// * map[interface{}]interface{}
+// * map[string]interface{}
+func (mmap MarshalMap) StringArrayMap(key string) (map[string][]string, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return nil, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ switch val.(type) {
+ case map[string][]string:
+ return val.(map[string][]string), nil
+
+ default:
+ valueMeta := reflect.ValueOf(val)
+ if valueMeta.Kind() != reflect.Map {
+ return nil, fmt.Errorf(`"%s" is expected to be a map[string][]string but is %T`, key, val)
+ }
+
+ result := make(map[string][]string)
+ for _, keyMeta := range valueMeta.MapKeys() {
+ strKey, isString := keyMeta.Interface().(string)
+ if !isString {
+ return nil, fmt.Errorf(`"%s" is expected to be a map[string][]string. Key is not a string`, key)
+ }
+
+ value := valueMeta.MapIndex(keyMeta)
+ arrayValue, err := castToStringArray(strKey, value.Interface())
+ if err != nil {
+ return nil, fmt.Errorf(`"%s" is expected to be a map[string][]string. Value is not a []string`, key)
+ }
+
+ result[strKey] = arrayValue
+ }
+
+ return result, nil
+ }
+}
+
+// MarshalMap returns a value at key that is expected to be another MarshalMap
+// This function supports conversion (by copy) from
+// * map[interface{}]interface{}
+func (mmap MarshalMap) MarshalMap(key string) (MarshalMap, error) {
+ val, exists := mmap.Value(key)
+ if !exists {
+ return nil, fmt.Errorf(`"%s" is not set`, key)
+ }
+
+ return ConvertToMarshalMap(val, nil)
+}
+
+// Value returns a value from a given value path.
+// Fields can be accessed by their name. Nested fields can be accessed by using
+// "/" as a separator. Arrays can be addressed using the standard array
+// notation "[]".
+// Examples:
+// "key" -> mmap["key"] single value
+// "key1/key2" -> mmap["key1"]["key2"] nested map
+// "key1[0]" -> mmap["key1"][0] nested array
+// "key1[0]key2" -> mmap["key1"][0]["key2"] nested array, nested map
+func (mmap MarshalMap) Value(key string) (val interface{}, exists bool) {
+ exists = mmap.resolvePath(key, mmap, func(p, k reflect.Value, v interface{}) {
+ val = v
+ })
+ return val, exists
+}
+
+// Delete a value from a given path.
+// The path must point to a map key. Deleting from arrays is not supported.
+func (mmap MarshalMap) Delete(key string) {
+ mmap.resolvePath(key, mmap, func(p, k reflect.Value, v interface{}) {
+ if v != nil {
+ p.SetMapIndex(k, reflect.Value{})
+ }
+ })
+}
+
+// Set a value for a given path.
+// The path must point to a map key. Setting array elements is not supported.
+func (mmap MarshalMap) Set(key string, val interface{}) {
+ mmap.resolvePath(key, mmap, func(p, k reflect.Value, v interface{}) {
+ p.SetMapIndex(k, reflect.ValueOf(val))
+ })
+}
+
+func (mmap MarshalMap) resolvePathKey(key string) (int, int) {
+ keyEnd := len(key)
+ nextKeyStart := keyEnd
+ pathIdx := strings.IndexRune(key, MarshalMapSeparator)
+ arrayIdx := strings.IndexRune(key, MarshalMapArrayBegin)
+
+ if pathIdx > -1 && pathIdx < keyEnd {
+ keyEnd = pathIdx
+ nextKeyStart = pathIdx + 1 // don't include slash
+ }
+ if arrayIdx > -1 && arrayIdx < keyEnd {
+ keyEnd = arrayIdx
+ nextKeyStart = arrayIdx // include bracket because of multidimensional arrays
+ }
+
+ // a -> key: "a", remain: "" -- value
+ // a/b/c -> key: "a", remain: "b/c" -- nested map
+ // a[1]b/c -> key: "a", remain: "[1]b/c" -- nested array
+
+ return keyEnd, nextKeyStart
+}
+
+func (mmap MarshalMap) resolvePath(k string, v interface{}, action func(p, k reflect.Value, v interface{})) bool {
+ if len(k) == 0 {
+ action(reflect.Value{}, reflect.ValueOf(k), v) // ### return, found requested value ###
+ return true
+ }
+
+ vValue := reflect.ValueOf(v)
+ switch vValue.Kind() {
+ case reflect.Array, reflect.Slice:
+ startIdx := strings.IndexRune(k, MarshalMapArrayBegin) // Must be first char, otherwise malformed
+ endIdx := strings.IndexRune(k, MarshalMapArrayEnd) // Must be > startIdx, otherwise malformed
+
+ if startIdx == -1 || endIdx == -1 {
+ return false
+ }
+
+ if startIdx == 0 && endIdx > startIdx {
+ index, err := strconv.Atoi(k[startIdx+1 : endIdx])
+
+ // [1] -> index: "1", remain: "" -- value
+ // [1]a/b -> index: "1", remain: "a/b" -- nested map
+ // [1][2] -> index: "1", remain: "[2]" -- nested array
+
+ if err == nil && index < vValue.Len() {
+ item := vValue.Index(index).Interface()
+ key := k[endIdx+1:]
+ return mmap.resolvePath(key, item, action) // ### return, nested array ###
+ }
+ }
+
+ case reflect.Map:
+ kValue := reflect.ValueOf(k)
+ if storedValue := vValue.MapIndex(kValue); storedValue.IsValid() {
+ action(vValue, kValue, storedValue.Interface())
+ return true
+ }
+
+ keyEnd, nextKeyStart := mmap.resolvePathKey(k)
+ if keyEnd == len(k) {
+ action(vValue, kValue, nil) // call action to support setting non-existing keys
+ return false // ### return, key not found ###
+ }
+
+ nextKey := k[:keyEnd]
+ nkValue := reflect.ValueOf(nextKey)
+
+ if storedValue := vValue.MapIndex(nkValue); storedValue.IsValid() {
+ remain := k[nextKeyStart:]
+ return mmap.resolvePath(remain, storedValue.Interface(), action) // ### return, nested map ###
+ }
+ }
+
+ return false
+}
diff --git a/vendor/github.com/trivago/tgo/tcontainer/trie.go b/vendor/github.com/trivago/tgo/tcontainer/trie.go
new file mode 100644
index 00000000..f7217093
--- /dev/null
+++ b/vendor/github.com/trivago/tgo/tcontainer/trie.go
@@ -0,0 +1,227 @@
+// Copyright 2015-2018 trivago N.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tcontainer
+
+// TrieNode represents a single node inside a trie.
+// Each node can contain a payload which can be retrieved after a successfull
+// match. In addition to that PathLen will contain the length of the match.
+type TrieNode struct {
+ suffix []byte
+ children []*TrieNode
+ longestPath int
+ PathLen int
+ Payload interface{}
+}
+
+// NewTrie creates a new root TrieNode
+func NewTrie(data []byte, payload interface{}) *TrieNode {
+ return &TrieNode{
+ suffix: data,
+ children: []*TrieNode{},
+ longestPath: len(data),
+ PathLen: len(data),
+ Payload: payload,
+ }
+}
+
+func (node *TrieNode) addNewChild(data []byte, payload interface{}, pathLen int) {
+ if node.longestPath < pathLen {
+ node.longestPath = pathLen
+ }
+
+ idx := len(node.children)
+ node.children = append(node.children, nil)
+
+ for idx > 0 {
+ nextIdx := idx - 1
+ if node.children[nextIdx].longestPath > pathLen {
+ break
+ }
+ node.children[idx] = node.children[nextIdx]
+ idx = nextIdx
+ }
+
+ node.children[idx] = &TrieNode{
+ suffix: data,
+ children: []*TrieNode{},
+ longestPath: pathLen,
+ PathLen: pathLen,
+ Payload: payload,
+ }
+}
+
+func (node *TrieNode) replace(oldChild *TrieNode, newChild *TrieNode) {
+ for i, child := range node.children {
+ if child == oldChild {
+ node.children[i] = newChild
+ return // ### return, replaced ###
+ }
+ }
+}
+
+// ForEach applies a function to each node in the tree including and below the
+// passed node.
+func (node *TrieNode) ForEach(callback func(*TrieNode)) {
+ callback(node)
+ for _, child := range node.children {
+ child.ForEach(callback)
+ }
+}
+
+// Add adds a new data path to the trie.
+// The TrieNode returned is the (new) root node so you should always reassign
+// the root with the return value of Add.
+func (node *TrieNode) Add(data []byte, payload interface{}) *TrieNode {
+ return node.addPath(data, payload, len(data), nil)
+}
+
+func (node *TrieNode) addPath(data []byte, payload interface{}, pathLen int, parent *TrieNode) *TrieNode {
+ dataLen := len(data)
+ suffixLen := len(node.suffix)
+ testLen := suffixLen
+ if dataLen < suffixLen {
+ testLen = dataLen
+ }
+
+ var splitIdx int
+ for splitIdx = 0; splitIdx < testLen; splitIdx++ {
+ if data[splitIdx] != node.suffix[splitIdx] {
+ break // ### break, split found ###
+ }
+ }
+
+ if splitIdx == suffixLen {
+ // Continue down or stop here (full suffix match)
+
+ if splitIdx == dataLen {
+ node.Payload = payload // may overwrite
+ return node // ### return, path already stored ###
+ }
+
+ data = data[splitIdx:]
+ if suffixLen > 0 {
+ for _, child := range node.children {
+ if child.suffix[0] == data[0] {
+ child.addPath(data, payload, pathLen, node)
+ return node // ### return, continue on path ###
+ }
+ }
+ }
+
+ node.addNewChild(data, payload, pathLen)
+ return node // ### return, new leaf ###
+ }
+
+ if splitIdx == dataLen {
+ // Make current node a subpath of new data node (full data match)
+ // This case implies that dataLen < suffixLen as splitIdx == suffixLen
+ // did not match.
+
+ node.suffix = node.suffix[splitIdx:]
+
+ newParent := NewTrie(data, payload)
+ newParent.PathLen = pathLen
+ newParent.longestPath = node.longestPath
+ newParent.children = []*TrieNode{node}
+
+ if parent != nil {
+ parent.replace(node, newParent)
+ }
+ return newParent // ### return, rotation ###
+ }
+
+ // New parent required with both nodes as children (partial match)
+
+ node.suffix = node.suffix[splitIdx:]
+
+ newParent := NewTrie(data[:splitIdx], nil)
+ newParent.PathLen = 0
+ newParent.longestPath = node.longestPath
+ newParent.children = []*TrieNode{node}
+ newParent.addNewChild(data[splitIdx:], payload, pathLen)
+
+ if parent != nil {
+ parent.replace(node, newParent)
+ }
+ return newParent // ### return, new parent ###
+}
+
+// Match compares the trie to the given data stream.
+// Match returns true if data can be completely matched to the trie.
+func (node *TrieNode) Match(data []byte) *TrieNode {
+ dataLen := len(data)
+ suffixLen := len(node.suffix)
+ if dataLen < suffixLen {
+ return nil // ### return, cannot be fully matched ###
+ }
+
+ for i := 0; i < suffixLen; i++ {
+ if data[i] != node.suffix[i] {
+ return nil // ### return, no match ###
+ }
+ }
+
+ if dataLen == suffixLen {
+ if node.PathLen > 0 {
+ return node // ### return, full match ###
+ }
+ return nil // ### return, invalid match ###
+ }
+
+ data = data[suffixLen:]
+ numChildren := len(node.children)
+ for i := 0; i < numChildren; i++ {
+ matchedNode := node.children[i].Match(data)
+ if matchedNode != nil {
+ return matchedNode // ### return, match found ###
+ }
+ }
+
+ return nil // ### return, no valid path ###
+}
+
+// MatchStart compares the trie to the beginning of the given data stream.
+// MatchStart returns true if the beginning of data can be matched to the trie.
+func (node *TrieNode) MatchStart(data []byte) *TrieNode {
+ dataLen := len(data)
+ suffixLen := len(node.suffix)
+ if dataLen < suffixLen {
+ return nil // ### return, cannot be fully matched ###
+ }
+
+ for i := 0; i < suffixLen; i++ {
+ if data[i] != node.suffix[i] {
+ return nil // ### return, no match ###
+ }
+ }
+
+ // Match longest path first
+
+ data = data[suffixLen:]
+ numChildren := len(node.children)
+ for i := 0; i < numChildren; i++ {
+ matchedNode := node.children[i].MatchStart(data)
+ if matchedNode != nil {
+ return matchedNode // ### return, match found ###
+ }
+ }
+
+ // May be only a part of data but we have a valid match
+
+ if node.PathLen > 0 {
+ return node // ### return, full match ###
+ }
+ return nil // ### return, no valid path ###
+}
diff --git a/vendor/github.com/trivago/tgo/treflect/clone.go b/vendor/github.com/trivago/tgo/treflect/clone.go
new file mode 100644
index 00000000..b8871262
--- /dev/null
+++ b/vendor/github.com/trivago/tgo/treflect/clone.go
@@ -0,0 +1,80 @@
+// Copyright 2015-2018 trivago N.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package treflect
+
+import (
+ "reflect"
+)
+
+// Clone does a deep copy of the given value.
+// Please note that field have to be public in order to be copied.
+// Private fields will be ignored.
+func Clone(v interface{}) interface{} {
+ value := reflect.ValueOf(v)
+ copy := clone(value)
+ return copy.Interface()
+}
+
+func clone(v reflect.Value) reflect.Value {
+ switch v.Kind() {
+ case reflect.Struct:
+ copy := reflect.New(v.Type())
+
+ for i := 0; i < v.Type().NumField(); i++ {
+ field := v.Field(i)
+ targetField := copy.Elem().Field(i)
+ if !targetField.CanSet() {
+ continue // ignore private fields
+ }
+ fieldCopy := clone(field)
+ targetField.Set(fieldCopy)
+ }
+ return copy.Elem()
+
+ case reflect.Chan:
+ copy := reflect.MakeChan(v.Type(), v.Len())
+ return copy
+
+ case reflect.Map:
+ copy := reflect.MakeMap(v.Type())
+ keys := v.MapKeys()
+ for _, k := range keys {
+ fieldCopy := clone(v.MapIndex(k))
+ copy.SetMapIndex(k, fieldCopy)
+ }
+ return copy
+
+ case reflect.Slice:
+ copy := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
+ for i := 0; i < v.Len(); i++ {
+ elementCopy := clone(v.Index(i))
+ copy.Index(i).Set(elementCopy)
+ }
+ return copy
+
+ case reflect.Array:
+ copy := reflect.New(v.Type()).Elem()
+ for i := 0; i < v.Len(); i++ {
+ elementCopy := clone(v.Index(i))
+ copy.Index(i).Set(elementCopy)
+ }
+ return copy
+
+ default:
+ copy := reflect.New(v.Type())
+ copy.Elem().Set(v)
+ return copy.Elem()
+ }
+}
diff --git a/vendor/github.com/trivago/tgo/treflect/reflection.go b/vendor/github.com/trivago/tgo/treflect/reflection.go
new file mode 100644
index 00000000..48a11f47
--- /dev/null
+++ b/vendor/github.com/trivago/tgo/treflect/reflection.go
@@ -0,0 +1,371 @@
+// Copyright 2015-2018 trivago N.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package treflect
+
+import (
+ "fmt"
+ "reflect"
+ "unsafe"
+)
+
+// GetMissingMethods checks if a given object implements all methods of a
+// given interface. It returns the interface coverage [0..1] as well as an array
+// of error messages. If the interface is correctly implemented the coverage is
+// 1 and the error message array is empty.
+func GetMissingMethods(objType reflect.Type, ifaceType reflect.Type) (float32, []string) {
+ missing := []string{}
+ if objType.Implements(ifaceType) {
+ return 1.0, missing
+ }
+
+ methodCount := ifaceType.NumMethod()
+ for mIdx := 0; mIdx < methodCount; mIdx++ {
+ ifaceMethod := ifaceType.Method(mIdx)
+ objMethod, exists := objType.MethodByName(ifaceMethod.Name)
+ signatureMismatch := false
+
+ switch {
+ case !exists:
+ missing = append(missing, fmt.Sprintf("Missing: \"%s\" %v", ifaceMethod.Name, ifaceMethod.Type))
+ continue // ### continue, error found ###
+
+ case ifaceMethod.Type.NumOut() != objMethod.Type.NumOut():
+ signatureMismatch = true
+
+ case ifaceMethod.Type.NumIn()+1 != objMethod.Type.NumIn():
+ signatureMismatch = true
+
+ default:
+ for oIdx := 0; !signatureMismatch && oIdx < ifaceMethod.Type.NumOut(); oIdx++ {
+ signatureMismatch = ifaceMethod.Type.Out(oIdx) != objMethod.Type.Out(oIdx)
+ }
+ for iIdx := 0; !signatureMismatch && iIdx < ifaceMethod.Type.NumIn(); iIdx++ {
+ signatureMismatch = ifaceMethod.Type.In(iIdx) != objMethod.Type.In(iIdx+1)
+ }
+ }
+
+ if signatureMismatch {
+ missing = append(missing, fmt.Sprintf("Invalid: \"%s\" %v is not %v", ifaceMethod.Name, objMethod.Type, ifaceMethod.Type))
+ }
+ }
+
+ return float32(methodCount-len(missing)) / float32(methodCount), missing
+}
+
+// Int64 converts any signed number type to an int64.
+// The second parameter is returned as false if a non-number type was given.
+func Int64(v interface{}) (int64, bool) {
+
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Int:
+ return int64(v.(int)), true
+ case reflect.Int8:
+ return int64(v.(int8)), true
+ case reflect.Int16:
+ return int64(v.(int16)), true
+ case reflect.Int32:
+ return int64(v.(int32)), true
+ case reflect.Int64:
+ return v.(int64), true
+ case reflect.Float32:
+ return int64(v.(float32)), true
+ case reflect.Float64:
+ return int64(v.(float64)), true
+ }
+
+ return 0, false
+}
+
+// Uint64 converts any unsigned number type to an uint64.
+// The second parameter is returned as false if a non-number type was given.
+func Uint64(v interface{}) (uint64, bool) {
+
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Uint:
+ return uint64(v.(uint)), true
+ case reflect.Uint8:
+ return uint64(v.(uint8)), true
+ case reflect.Uint16:
+ return uint64(v.(uint16)), true
+ case reflect.Uint32:
+ return uint64(v.(uint32)), true
+ case reflect.Uint64:
+ return v.(uint64), true
+ }
+
+ return 0, false
+}
+
+// Float32 converts any number type to an float32.
+// The second parameter is returned as false if a non-number type was given.
+func Float32(v interface{}) (float32, bool) {
+
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Int:
+ return float32(v.(int)), true
+ case reflect.Uint:
+ return float32(v.(uint)), true
+ case reflect.Int8:
+ return float32(v.(int8)), true
+ case reflect.Uint8:
+ return float32(v.(uint8)), true
+ case reflect.Int16:
+ return float32(v.(int16)), true
+ case reflect.Uint16:
+ return float32(v.(uint16)), true
+ case reflect.Int32:
+ return float32(v.(int32)), true
+ case reflect.Uint32:
+ return float32(v.(uint32)), true
+ case reflect.Int64:
+ return float32(v.(int64)), true
+ case reflect.Uint64:
+ return float32(v.(uint64)), true
+ case reflect.Float32:
+ return v.(float32), true
+ case reflect.Float64:
+ return float32(v.(float64)), true
+ }
+
+ return 0, false
+}
+
+// Float64 converts any number type to an float64.
+// The second parameter is returned as false if a non-number type was given.
+func Float64(v interface{}) (float64, bool) {
+
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Int:
+ return float64(v.(int)), true
+ case reflect.Uint:
+ return float64(v.(uint)), true
+ case reflect.Int8:
+ return float64(v.(int8)), true
+ case reflect.Uint8:
+ return float64(v.(uint8)), true
+ case reflect.Int16:
+ return float64(v.(int16)), true
+ case reflect.Uint16:
+ return float64(v.(uint16)), true
+ case reflect.Int32:
+ return float64(v.(int32)), true
+ case reflect.Uint32:
+ return float64(v.(uint32)), true
+ case reflect.Int64:
+ return float64(v.(int64)), true
+ case reflect.Uint64:
+ return float64(v.(uint64)), true
+ case reflect.Float32:
+ return float64(v.(float32)), true
+ case reflect.Float64:
+ return v.(float64), true
+ }
+
+ return 0, false
+}
+
+// RemovePtrFromType will return the type of t and strips away any pointer(s)
+// in front of the actual type.
+func RemovePtrFromType(t interface{}) reflect.Type {
+ var v reflect.Type
+ if rt, isType := t.(reflect.Type); isType {
+ v = rt
+ } else {
+ v = reflect.TypeOf(t)
+ }
+ for v.Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+ return v
+}
+
+// RemovePtrFromValue will return the value of t and strips away any pointer(s)
+// in front of the actual type.
+func RemovePtrFromValue(t interface{}) reflect.Value {
+ var v reflect.Value
+ if rv, isValue := t.(reflect.Value); isValue {
+ v = rv
+ } else {
+ v = reflect.ValueOf(t)
+ }
+ for v.Type().Kind() == reflect.Ptr {
+ v = v.Elem()
+ }
+ return v
+}
+
+// UnsafeCopy will copy data from src to dst while ignoring type information.
+// Both types need to be of the same size and dst and src have to be pointers.
+// UnsafeCopy will panic if these requirements are not met.
+func UnsafeCopy(dst, src interface{}) {
+ dstValue := reflect.ValueOf(dst)
+ srcValue := reflect.ValueOf(src)
+ UnsafeCopyValue(dstValue, srcValue)
+}
+
+// UnsafeCopyValue will copy data from src to dst while ignoring type
+// information. Both types need to be of the same size or this function will
+// panic. Also both types must support dereferencing via reflect.Elem()
+func UnsafeCopyValue(dstValue reflect.Value, srcValue reflect.Value) {
+ dstType := dstValue.Elem().Type()
+ srcType := srcValue.Type()
+
+ var srcPtr uintptr
+ if srcValue.Kind() != reflect.Ptr {
+ // If we don't get a pointer to our source data we need to forcefully
+ // retrieve it by accessing the interface pointer. This is ok as we
+ // only read from it.
+ iface := srcValue.Interface()
+ srcPtr = reflect.ValueOf(&iface).Elem().InterfaceData()[1] // Pointer to data
+ } else {
+ srcType = srcValue.Elem().Type()
+ srcPtr = srcValue.Pointer()
+ }
+
+ if dstType.Size() != srcType.Size() {
+ panic("Type size mismatch between " + dstType.String() + " and " + srcType.String())
+ }
+
+ dstAsSlice := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
+ Data: dstValue.Pointer(),
+ Len: int(dstType.Size()),
+ Cap: int(dstType.Size()),
+ }))
+
+ srcAsSlice := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
+ Data: srcPtr,
+ Len: int(srcType.Size()),
+ Cap: int(srcType.Size()),
+ }))
+
+ copy(dstAsSlice, srcAsSlice)
+}
+
+// SetMemberByName sets member name of the given pointer-to-struct to the data
+// passed to this function. The member may be private, too.
+func SetMemberByName(ptrToStruct interface{}, name string, data interface{}) {
+ structVal := reflect.Indirect(reflect.ValueOf(ptrToStruct))
+ member := structVal.FieldByName(name)
+
+ SetValue(member, data)
+}
+
+// SetMemberByIndex sets member idx of the given pointer-to-struct to the data
+// passed to this function. The member may be private, too.
+func SetMemberByIndex(ptrToStruct interface{}, idx int, data interface{}) {
+ structVal := reflect.Indirect(reflect.ValueOf(ptrToStruct))
+ member := structVal.Field(idx)
+
+ SetValue(member, data)
+}
+
+// SetValue sets an addressable value to the data passed to this function.
+// In contrast to golangs reflect package this will also work with private
+// variables. Please note that this function may not support all types, yet.
+func SetValue(member reflect.Value, data interface{}) {
+ if member.CanSet() {
+ member.Set(reflect.ValueOf(data).Convert(member.Type()))
+ return // ### return, easy way ###
+ }
+
+ if !member.CanAddr() {
+ panic("SetValue requires addressable member type")
+ }
+
+ ptrToMember := unsafe.Pointer(member.UnsafeAddr())
+ dataValue := reflect.ValueOf(data)
+
+ switch member.Kind() {
+ case reflect.Bool:
+ *(*bool)(ptrToMember) = dataValue.Bool()
+
+ case reflect.Uint:
+ *(*uint)(ptrToMember) = uint(dataValue.Uint())
+
+ case reflect.Uint8:
+ *(*uint8)(ptrToMember) = uint8(dataValue.Uint())
+
+ case reflect.Uint16:
+ *(*uint16)(ptrToMember) = uint16(dataValue.Uint())
+
+ case reflect.Uint32:
+ *(*uint32)(ptrToMember) = uint32(dataValue.Uint())
+
+ case reflect.Uint64:
+ *(*uint64)(ptrToMember) = dataValue.Uint()
+
+ case reflect.Int:
+ *(*int)(ptrToMember) = int(dataValue.Int())
+
+ case reflect.Int8:
+ *(*int8)(ptrToMember) = int8(dataValue.Int())
+
+ case reflect.Int16:
+ *(*int16)(ptrToMember) = int16(dataValue.Int())
+
+ case reflect.Int32:
+ *(*int32)(ptrToMember) = int32(dataValue.Int())
+
+ case reflect.Int64:
+ *(*int64)(ptrToMember) = dataValue.Int()
+
+ case reflect.Float32:
+ *(*float32)(ptrToMember) = float32(dataValue.Float())
+
+ case reflect.Float64:
+ *(*float64)(ptrToMember) = dataValue.Float()
+
+ case reflect.Complex64:
+ *(*complex64)(ptrToMember) = complex64(dataValue.Complex())
+
+ case reflect.Complex128:
+ *(*complex128)(ptrToMember) = dataValue.Complex()
+
+ case reflect.String:
+ *(*string)(ptrToMember) = dataValue.String()
+
+ case reflect.Map, reflect.Chan:
+ // Exploit the fact that "map" is actually "*runtime.hmap" and force
+ // overwrite that pointer in the passed struct.
+ // Same foes for "chan" which is actually "*runtime.hchan".
+
+ // Note: Assigning a map or channel to another variable does NOT copy
+ // the contents so copying the pointer follows go's standard behavior.
+ dataAsPtr := unsafe.Pointer(dataValue.Pointer())
+ *(**uintptr)(ptrToMember) = (*uintptr)(dataAsPtr)
+
+ case reflect.Interface:
+ // Interfaces are basically two pointers, see runtime.iface.
+ // We want to modify exactly that data, which is returned by
+ // the InterfaceData() method.
+
+ if dataValue.Kind() != reflect.Interface {
+ // A type reference was passed. In order to overwrite the memory
+ // Representation of an interface we need to generate it first.
+ // Reflect does not allow us to do that unless we use the
+ // InterfaceData method which exposes the internal representation
+ // of an interface.
+ interfaceData := reflect.ValueOf(&data).Elem().InterfaceData()
+ dataValue = reflect.ValueOf(interfaceData)
+ }
+ fallthrough
+
+ default:
+ // Complex types are assigned memcpy style.
+ // Note: This should not break the garbage collector although we cannot
+ // be 100% sure on this.
+ UnsafeCopyValue(member.Addr(), dataValue)
+ }
+}
diff --git a/vendor/github.com/trivago/tgo/treflect/typeregistry.go b/vendor/github.com/trivago/tgo/treflect/typeregistry.go
new file mode 100644
index 00000000..d3e3d7ef
--- /dev/null
+++ b/vendor/github.com/trivago/tgo/treflect/typeregistry.go
@@ -0,0 +1,97 @@
+// Copyright 2015-2018 trivago N.V.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package treflect
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+)
+
+// TypeRegistry is a name to type registry used to create objects by name.
+type TypeRegistry struct {
+ namedType map[string]reflect.Type
+}
+
+// NewTypeRegistry creates a new TypeRegistry. Note that there is a global type
+// registry available in the main tgo package (tgo.TypeRegistry).
+func NewTypeRegistry() TypeRegistry {
+ return TypeRegistry{
+ namedType: make(map[string]reflect.Type),
+ }
+}
+
+// Register a plugin to the TypeRegistry by passing an uninitialized object.
+func (registry TypeRegistry) Register(typeInstance interface{}) {
+ registry.RegisterWithDepth(typeInstance, 1)
+}
+
+// RegisterWithDepth to register a plugin to the TypeRegistry by passing an uninitialized object.
+func (registry TypeRegistry) RegisterWithDepth(typeInstance interface{}, depth int) {
+ structType := reflect.TypeOf(typeInstance)
+ packageName := structType.PkgPath()
+ typeName := structType.Name()
+
+ pathTokens := strings.Split(packageName, "/")
+ maxDepth := 3
+ if len(pathTokens) < maxDepth {
+ maxDepth = len(pathTokens)
+ }
+
+ for n := depth; n <= maxDepth; n++ {
+ shortTypeName := strings.Join(pathTokens[len(pathTokens)-n:], ".") + "." + typeName
+ registry.namedType[shortTypeName] = structType
+ }
+}
+
+// New creates an uninitialized object by class name.
+// The class name has to be "package.class" or "package/subpackage.class".
+// The gollum package is omitted from the package path.
+func (registry TypeRegistry) New(typeName string) (interface{}, error) {
+ structType, exists := registry.namedType[typeName]
+ if exists {
+ return reflect.New(structType).Interface(), nil
+ }
+ return nil, fmt.Errorf("Unknown class: %s", typeName)
+}
+
+// GetTypeOf returns only the type asscociated with the given name.
+// If the name is not registered, nil is returned.
+// The type returned will be a pointer type.
+func (registry TypeRegistry) GetTypeOf(typeName string) reflect.Type {
+ if structType, exists := registry.namedType[typeName]; exists {
+ return reflect.PtrTo(structType)
+ }
+ return nil
+}
+
+// IsTypeRegistered returns true if a type is registered to this registry.
+// Note that GetTypeOf can do the same thing by checking for nil but also
+// returns the type, so in many cases you will want to call this function.
+func (registry TypeRegistry) IsTypeRegistered(typeName string) bool {
+ _, exists := registry.namedType[typeName]
+ return exists
+}
+
+// GetRegistered returns the names of all registered types for a given package
+func (registry TypeRegistry) GetRegistered(packageName string) []string {
+ var result []string
+ for key := range registry.namedType {
+ if strings.HasPrefix(key, packageName) {
+ result = append(result, key)
+ }
+ }
+ return result
+}