diff --git a/cmd/add.go b/cmd/add.go index 53c6e39..9d9d2ec 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -16,9 +16,14 @@ import ( func GrepAddCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "grep", - Short: "Generates the tickets reading grep's output from stdin", - Long: "Generates tickets for the campaign reading from the standard input the output grep. The grep command must be run with the -n flag", + Use: "grep", + Short: "Generates the tickets reading grep's output from stdin", + Long: `Generates tickets for the campaign reading from the standard input the output grep. The grep command must be run with the -n flag. The generated ticket will contain three fields: + + - filename: the filename yield by grep + - lineNo: the line number yield by grep + - text: the trimmed line that grep captured for the expression +`, Example: ` grep -nriIF --include \*.go cobra.Command | campaigner add grep`, Args: cobra.NoArgs, Run: grepAddCmdF, @@ -104,7 +109,7 @@ func parseGrepLine(line string) (*model.Ticket, error) { Data: map[string]interface{}{ "filename": filename, "lineNo": lineNo, - "text": text, + "text": strings.TrimSpace(text), }, }, nil } diff --git a/cmd/init.go b/cmd/init.go index d3778d8..0982dd5 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,6 +1,9 @@ package cmd import ( + "bufio" + "fmt" + "os" "strings" "git.ctrlz.es/mgdelacroix/campaigner/campaign" @@ -14,44 +17,78 @@ func InitCmd() *cobra.Command { Use: "init", Short: "Creates a new campaign in the current directory", Example: ` campaigner init \ + --jira-username johndoe \ + --jira-token secret \ + --github-token TOKEN \ --url http://my-jira-instance.com \ --epic ASD-27 \ --issue-type Story \ + --repository johndoe/awesomeproject \ + -l 'Area/API' -l 'Tech/Go' \ --summary 'Refactor {{.function}} to inject the configuration service' \ --template ./refactor-config.tmpl`, - Args: cobra.NoArgs, - Run: initCmdF, + Args: cobra.NoArgs, + Run: initCmdF, } - cmd.Flags().StringP("url", "u", "", "The jira server URL") - _ = cmd.MarkFlagRequired("url") - cmd.Flags().StringP("epic", "e", "", "The epic id to associate this campaign with") - _ = cmd.MarkFlagRequired("epic") - cmd.Flags().StringP("summary", "s", "", "The summary of the tickets") - _ = cmd.MarkFlagRequired("summary") - cmd.Flags().StringP("template", "t", "", "The template path for the description of the tickets") - _ = cmd.MarkFlagRequired("template") - cmd.Flags().StringP("issue-type", "i", "Story", "The issue type to create the tickets as") + cmd.Flags().String("jira-username", "", "the jira username") + cmd.Flags().String("jira-token", "", "the jira token or password") + cmd.Flags().String("github-token", "", "the github token") + cmd.Flags().StringP("url", "u", "", "the jira server URL") + cmd.Flags().StringP("epic", "e", "", "the epic id to associate this campaign with") + cmd.Flags().StringP("repository", "r", "", "the github repository") + cmd.Flags().StringSliceP("label", "l", []string{}, "the labels to add to the Github issues") + cmd.Flags().StringP("summary", "s", "", "the summary of the tickets") + cmd.Flags().StringP("template", "t", "", "the template path for the description of the tickets") + cmd.Flags().StringP("issue-type", "i", "Story", "the issue type to create the tickets as") return cmd } func initCmdF(cmd *cobra.Command, _ []string) { - url, _ := cmd.Flags().GetString("url") - epic, _ := cmd.Flags().GetString("epic") - summary, _ := cmd.Flags().GetString("summary") - template, _ := cmd.Flags().GetString("template") + getStringFlagOrAskIfEmpty := func(name string, question string) string { + val, _ := cmd.Flags().GetString(name) + if val == "" { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("%s ", question) + answer, err := reader.ReadString('\n') + if err != nil { + ErrorAndExit(cmd, err) + } + val = strings.TrimSpace(answer) + } + return val + } + + jiraUsername := getStringFlagOrAskIfEmpty("jira-username", "JIRA username:") + jiraToken := getStringFlagOrAskIfEmpty("jira-token", "JIRA password or token:") + githubToken := getStringFlagOrAskIfEmpty("github-token", "GitHub token:") + url := getStringFlagOrAskIfEmpty("url", "JIRA server URL:") + epic := getStringFlagOrAskIfEmpty("epic", "JIRA epic:") + repo := getStringFlagOrAskIfEmpty("repository", "GitHub repository:") + summary := getStringFlagOrAskIfEmpty("summary", "Ticket summary template:") + template := getStringFlagOrAskIfEmpty("template", "Ticket description template path:") issueType, _ := cmd.Flags().GetString("issue-type") + labels, _ := cmd.Flags().GetStringSlice("label") project := strings.Split(epic, "-")[0] cmp := &model.Campaign{ - Url: url, - Project: project, - Epic: epic, - IssueType: issueType, - Summary: summary, - Template: template, + Jira: model.ConfigJira{ + Url: url, + Username: jiraUsername, + Token: jiraToken, + Project: project, + Epic: epic, + IssueType: issueType, + }, + Github: model.ConfigGithub{ + Token: githubToken, + Repo: repo, + Labels: labels, + }, + Summary: summary, + Template: template, } if err := campaign.Save(cmp); err != nil { ErrorAndExit(cmd, err) diff --git a/cmd/publish.go b/cmd/publish.go index 790e48d..4eba5db 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -4,7 +4,6 @@ import ( "fmt" "git.ctrlz.es/mgdelacroix/campaigner/campaign" - "git.ctrlz.es/mgdelacroix/campaigner/config" "git.ctrlz.es/mgdelacroix/campaigner/github" "git.ctrlz.es/mgdelacroix/campaigner/jira" @@ -64,17 +63,12 @@ func jiraPublishCmdF(cmd *cobra.Command, _ []string) error { return fmt.Errorf("One of --all or --batch flags is required") } - cfg, err := config.ReadConfig() - if err != nil { - ErrorAndExit(cmd, err) - } - cmp, err := campaign.Read() if err != nil { ErrorAndExit(cmd, err) } - jiraClient, err := jira.NewClient(cmp.Url, cfg.JiraUsername, cfg.JiraToken) + jiraClient, err := jira.NewClient(cmp.Jira.Url, cmp.Jira.Username, cmp.Jira.Token) if err != nil { ErrorAndExit(cmd, err) } @@ -104,17 +98,12 @@ func githubPublishCmdF(cmd *cobra.Command, _ []string) error { return fmt.Errorf("One of --all or --batch flags is required") } - cfg, err := config.ReadConfig() - if err != nil { - ErrorAndExit(cmd, err) - } - cmp, err := campaign.Read() if err != nil { ErrorAndExit(cmd, err) } - githubClient := github.NewClient("my/repo", cfg.GithubToken) + githubClient := github.NewClient(cmp.Github.Repo, cmp.Github.Token) if all { count, err := githubClient.PublishAll(cmp, dryRun) diff --git a/cmd/root.go b/cmd/root.go index 6e3e41f..cef6b4e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,9 +17,7 @@ func RootCmd() *cobra.Command { AddCmd(), FilterCmd(), InitCmd(), - StandaloneCmd(), StatusCmd(), - TokenCmd(), PublishCmd(), SyncCmd(), ) diff --git a/cmd/standalone.go b/cmd/standalone.go deleted file mode 100644 index 07993d1..0000000 --- a/cmd/standalone.go +++ /dev/null @@ -1,163 +0,0 @@ -package cmd - -import ( - "fmt" - "strings" - - "git.ctrlz.es/mgdelacroix/campaigner/config" - "git.ctrlz.es/mgdelacroix/campaigner/jira" - "git.ctrlz.es/mgdelacroix/campaigner/model" - - "github.com/spf13/cobra" -) - -func StandaloneCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "standalone", - Short: "Standalone fire-and-forget commands", - } - - cmd.AddCommand( - CreateJiraTicketStandaloneCmd(), - GetJiraTicketStandaloneCmd(), - ) - - return cmd -} - -func CreateJiraTicketStandaloneCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "create-jira-ticket", - Short: "Creates a jira ticket from a template", - Args: cobra.NoArgs, - RunE: createJiraTicketStandaloneCmdF, - } - - cmd.Flags().String("url", "", "The jira server URL") - _ = cmd.MarkFlagRequired("url") - cmd.Flags().String("epic", "", "The jira epic id to associate the ticket with") - _ = cmd.MarkFlagRequired("epic") - cmd.Flags().String("summary", "", "The summary of the ticket") - _ = cmd.MarkFlagRequired("summary") - cmd.Flags().String("template", "", "The template to render the description of the ticket") - _ = cmd.MarkFlagRequired("template") - cmd.Flags().String("username", "", "The jira username") - cmd.Flags().String("token", "", "The jira token") - cmd.Flags().StringSliceP("vars", "v", []string{}, "The variables to use in the template") - cmd.Flags().Bool("dry-run", false, "Print the ticket information instead of creating it") - - return cmd -} - -func GetJiraTicketStandaloneCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "get-jira-ticket", - Short: "Gets the ticket from jira", - Args: cobra.ExactArgs(1), - Run: getJiraTicketStandaloneCmdF, - } - - cmd.Flags().String("url", "", "The jira server URL") - _ = cmd.MarkFlagRequired("url") - cmd.Flags().String("username", "", "The jira username") - cmd.Flags().String("token", "", "The jira token") - - return cmd -} - -func getVarMap(vars []string) (map[string]interface{}, error) { - varMap := map[string]interface{}{} - for _, v := range vars { - parts := strings.Split(v, "=") - if len(parts) < 2 { - return nil, fmt.Errorf("cannot parse var %s", v) - } - varMap[parts[0]] = strings.Join(parts[1:], "=") - } - return varMap, nil -} - -func createJiraTicketStandaloneCmdF(cmd *cobra.Command, _ []string) error { - url, _ := cmd.Flags().GetString("url") - epic, _ := cmd.Flags().GetString("epic") - username, _ := cmd.Flags().GetString("username") - token, _ := cmd.Flags().GetString("token") - summary, _ := cmd.Flags().GetString("summary") - template, _ := cmd.Flags().GetString("template") - vars, _ := cmd.Flags().GetStringSlice("vars") - dryRun, _ := cmd.Flags().GetBool("dry-run") - - project := strings.Split(epic, "-")[0] - - if username == "" || token == "" { - cfg, err := config.ReadConfig() - if err != nil { - ErrorAndExit(cmd, err) - } - - if username == "" { - username = cfg.JiraUsername - } - if token == "" { - token = cfg.JiraToken - } - } - - varMap, err := getVarMap(vars) - if err != nil { - return fmt.Errorf("error processing vars: %w", err) - } - - jiraClient, err := jira.NewClient(url, username, token) - if err != nil { - ErrorAndExit(cmd, err) - } - - campaign := &model.Campaign{ - Epic: epic, - Project: project, - Summary: summary, - Template: template, - } - ticket := &model.Ticket{Data: varMap} - - issue, err := jiraClient.PublishTicket(ticket, campaign, dryRun) - if err != nil { - ErrorAndExit(cmd, err) - } - - cmd.Printf("Ticket %s successfully created in JIRA", issue.Key) - return nil -} - -func getJiraTicketStandaloneCmdF(cmd *cobra.Command, args []string) { - url, _ := cmd.Flags().GetString("url") - username, _ := cmd.Flags().GetString("username") - token, _ := cmd.Flags().GetString("token") - - if username == "" || token == "" { - cfg, err := config.ReadConfig() - if err != nil { - ErrorAndExit(cmd, err) - } - - if username == "" { - username = cfg.JiraUsername - } - if token == "" { - token = cfg.JiraToken - } - } - - jiraClient, err := jira.NewClient(url, username, token) - if err != nil { - ErrorAndExit(cmd, err) - } - - issue, err := jiraClient.GetIssue(args[0]) - if err != nil { - ErrorAndExit(cmd, err) - } - - fmt.Printf("Summary: %s\nKey: %s\nStatus: %s\nAsignee: %s\n", issue.Fields.Summary, issue.Key, issue.Fields.Status.Name, issue.Fields.Assignee.DisplayName) -} diff --git a/cmd/token.go b/cmd/token.go deleted file mode 100644 index 6d511d2..0000000 --- a/cmd/token.go +++ /dev/null @@ -1,79 +0,0 @@ -package cmd - -import ( - "git.ctrlz.es/mgdelacroix/campaigner/config" - - "github.com/spf13/cobra" -) - -func TokenSetJiraCmd() *cobra.Command { - return &cobra.Command{ - Use: "jira USERNAME TOKEN", - Short: "Sets the value of the jira token", - Args: cobra.ExactArgs(2), - RunE: tokenSetJiraCmdF, - } -} - -func TokenSetGithubCmd() *cobra.Command { - return &cobra.Command{ - Use: "github TOKEN", - Short: "Sets the value of the github token", - Args: cobra.ExactArgs(1), - RunE: tokenSetGithubCmdF, - } -} - -func TokenSetCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "set", - Short: "Sets the value of the platform tokens", - } - - cmd.AddCommand( - TokenSetJiraCmd(), - TokenSetGithubCmd(), - ) - - return cmd -} - -func TokenCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "token", - Short: "Subcommands related to tokens", - } - - cmd.AddCommand( - TokenSetCmd(), - ) - - return cmd -} - -func tokenSetJiraCmdF(cmd *cobra.Command, args []string) error { - cfg, err := config.ReadConfig() - if err != nil { - ErrorAndExit(cmd, err) - } - - cfg.JiraUsername = args[0] - cfg.JiraToken = args[1] - if err := config.SaveConfig(cfg); err != nil { - ErrorAndExit(cmd, err) - } - return nil -} - -func tokenSetGithubCmdF(cmd *cobra.Command, args []string) error { - cfg, err := config.ReadConfig() - if err != nil { - ErrorAndExit(cmd, err) - } - - cfg.GithubToken = args[0] - if err := config.SaveConfig(cfg); err != nil { - ErrorAndExit(cmd, err) - } - return nil -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 50abc14..0000000 --- a/config/config.go +++ /dev/null @@ -1,59 +0,0 @@ -package config - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "os/user" - - "git.ctrlz.es/mgdelacroix/campaigner/model" -) - -func getConfigPath() (string, error) { - user, err := user.Current() - if err != nil { - return "", err - } - return user.HomeDir + "/.campaigner", nil -} - -func ReadConfig() (*model.Config, error) { - configPath, err := getConfigPath() - if err != nil { - return nil, err - } - - if _, err := os.Stat(configPath); err != nil { - return &model.Config{}, nil - } - - fileContents, err := ioutil.ReadFile(configPath) - if err != nil { - return nil, fmt.Errorf("there was a problem reading the config file: %w", err) - } - - var config model.Config - if err := json.Unmarshal(fileContents, &config); err != nil { - return nil, fmt.Errorf("there was a problem parsing the config file: %w", err) - } - - return &config, nil -} - -func SaveConfig(config *model.Config) error { - configPath, err := getConfigPath() - if err != nil { - return err - } - - marshaledConfig, err := json.MarshalIndent(config, "", " ") - if err != nil { - return err - } - - if err := ioutil.WriteFile(configPath, marshaledConfig, 0600); err != nil { - return fmt.Errorf("cannot save the config: %w", err) - } - return nil -} diff --git a/github/github.go b/github/github.go index 8e177bc..2df449b 100644 --- a/github/github.go +++ b/github/github.go @@ -2,10 +2,13 @@ package github import ( "context" + "encoding/json" + "fmt" "git.ctrlz.es/mgdelacroix/campaigner/campaign" "git.ctrlz.es/mgdelacroix/campaigner/model" + "github.com/StevenACoffman/j2m" "github.com/google/go-github/v29/github" "golang.org/x/oauth2" ) @@ -28,7 +31,28 @@ func NewClient(repo, token string) *GithubClient { } func (c *GithubClient) PublishTicket(ticket *model.Ticket, cmp *model.Campaign, dryRun bool) (*github.Issue, error) { - return nil, nil + mdDescription := j2m.JiraToMD(ticket.Description) + issueRequest := &github.IssueRequest{ + Title: &ticket.Summary, + Body: &mdDescription, + Labels: &cmp.Github.Labels, + } + + if dryRun { + b, _ := json.MarshalIndent(issueRequest, "", " ") + fmt.Println(string(b)) + return &github.Issue{ + Title: issueRequest.Title, + Body: issueRequest.Body, + }, nil + } + + owner, repo := cmp.RepoComponents() + newIssue, _, err := c.Issues.Create(context.Background(), owner, repo, issueRequest) + if err != nil { + return nil, err + } + return newIssue, nil } func (c *GithubClient) PublishNextTicket(cmp *model.Campaign, dryRun bool) (bool, error) { @@ -47,9 +71,6 @@ func (c *GithubClient) PublishNextTicket(cmp *model.Campaign, dryRun bool) (bool } ticket.GithubLink = *issue.ID - // move this to a publish service that can do both github and - // jira, as we need to update a jira issue field with the github - // link if err := campaign.Save(cmp); err != nil { return false, err } @@ -72,7 +93,7 @@ func (c *GithubClient) PublishAll(cmp *model.Campaign, dryRun bool) (int, error) } func (c *GithubClient) PublishBatch(cmp *model.Campaign, batch int, dryRun bool) error { - for i := 0; i <= batch; i++ { + for i := 1; i <= batch; i++ { next, err := c.PublishNextTicket(cmp, dryRun) if err != nil { return err diff --git a/go.mod b/go.mod index b7d93e7..8a3f219 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.ctrlz.es/mgdelacroix/campaigner go 1.13 require ( - github.com/fatih/color v1.9.0 // indirect + github.com/StevenACoffman/j2m v0.0.0-20190826163711-7d8d00c99217 github.com/fatih/structs v1.1.0 // indirect github.com/google/go-github/v29 v29.0.3 github.com/spf13/cobra v0.0.6 diff --git a/go.sum b/go.sum index e05bb5e..5907686 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/StevenACoffman/j2m v0.0.0-20190826163711-7d8d00c99217 h1:y2QXqPIec+mEDDToinjdFymzQZmLNRIKtuOkdomqyA4= +github.com/StevenACoffman/j2m v0.0.0-20190826163711-7d8d00c99217/go.mod h1:y1vzL6Jab7oLzLLE2CtItTyEI6hKQnNMmqDrO+2a7Pk= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -17,8 +19,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -38,7 +38,6 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github/v29 v29.0.3 h1:IktKCTwU//aFHnpA+2SLIi7Oo9uhAzgsdZNbcAqhgdc= github.com/google/go-github/v29 v29.0.3/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= @@ -60,11 +59,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -130,9 +124,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/jira/jira.go b/jira/jira.go index 9b0b4df..a7ab70b 100644 --- a/jira/jira.go +++ b/jira/jira.go @@ -56,22 +56,22 @@ func (c *JiraClient) GetIssueFromTicket(ticket *model.Ticket, cmp *model.Campaig data := map[string]string{ "Description": description, "Summary": summary, - "Project": cmp.Project, - "Issue Type": cmp.IssueType, - "Epic Link": cmp.Epic, + "Project": cmp.Jira.Project, + "Issue Type": cmp.Jira.IssueType, + "Epic Link": cmp.Jira.Epic, } - createMetaInfo, _, err := c.Issue.GetCreateMeta(cmp.Project) + createMetaInfo, _, err := c.Issue.GetCreateMeta(cmp.Jira.Project) if err != nil { return nil, err } - project := createMetaInfo.GetProjectWithKey(cmp.Project) + project := createMetaInfo.GetProjectWithKey(cmp.Jira.Project) if project == nil { - return nil, fmt.Errorf("Error retrieving project with key %s", cmp.Project) + return nil, fmt.Errorf("Error retrieving project with key %s", cmp.Jira.Project) } - issueType := project.GetIssueTypeWithName(cmp.IssueType) + issueType := project.GetIssueTypeWithName(cmp.Jira.IssueType) if issueType == nil { return nil, fmt.Errorf("Error retrieving issue type with name Story") } @@ -134,6 +134,7 @@ func (c *JiraClient) PublishNextTicket(cmp *model.Campaign, dryRun bool) (bool, ticket.JiraLink = issue.Key ticket.Summary = issue.Fields.Summary + ticket.Description = issue.Fields.Description if err := campaign.Save(cmp); err != nil { return false, err } @@ -156,7 +157,7 @@ func (c *JiraClient) PublishAll(cmp *model.Campaign, dryRun bool) (int, error) { } func (c *JiraClient) PublishBatch(cmp *model.Campaign, batch int, dryRun bool) error { - for i := 0; i <= batch; i++ { + for i := 1; i <= batch; i++ { next, err := c.PublishNextTicket(cmp, dryRun) if err != nil { return err diff --git a/model/campaign.go b/model/campaign.go index 5813025..abc9afc 100644 --- a/model/campaign.go +++ b/model/campaign.go @@ -1,19 +1,35 @@ package model import ( + "bytes" "fmt" "io" + "strings" + "text/template" ) +type ConfigJira struct { + Url string `json:"url"` + Username string `json:"username"` + Token string `json:"token"` + Project string `json:"project"` + Epic string `json:"epic"` + IssueType string `json:"issue_type"` +} + +type ConfigGithub struct { + Token string `json:"token"` + Repo string `json:"repo"` + Labels []string `json:"labels"` +} + // ToDo: add key-value extra params as a map to allow for customfield_whatever = team type Campaign struct { - Url string `json:"url"` - Project string `json:"project"` - Epic string `json:"epic"` - IssueType string `json:"issue_type"` - Summary string `json:"summary"` - Template string `json:"template"` - Tickets []*Ticket `json:"tickets,omitempty"` + Jira ConfigJira `json:"jira"` + Github ConfigGithub `json:"github"` + Summary string `json:"summary"` + Template string `json:"template"` + Tickets []*Ticket `json:"tickets,omitempty"` } func (c *Campaign) NextJiraUnpublishedTicket() *Ticket { @@ -27,7 +43,7 @@ func (c *Campaign) NextJiraUnpublishedTicket() *Ticket { func (c *Campaign) NextGithubUnpublishedTicket() *Ticket { for _, ticket := range c.Tickets { - if ticket.JiraLink != "" && ticket.GithubLink != 0 { + if ticket.JiraLink != "" && ticket.GithubLink == 0 { return ticket } } @@ -35,14 +51,50 @@ func (c *Campaign) NextGithubUnpublishedTicket() *Ticket { } func (c *Campaign) PrintStatus(w io.Writer) { - fmt.Fprintf(w, "Url: %s\n", c.Url) - fmt.Fprintf(w, "Project: %s\n", c.Project) - fmt.Fprintf(w, "Epic: %s\n", c.Epic) - fmt.Fprintf(w, "Issue Type: %s\n", c.IssueType) + fmt.Fprintf(w, "JIRA URL: %s\n", c.Jira.Url) + fmt.Fprintf(w, "JIRA Project: %s\n", c.Jira.Project) + fmt.Fprintf(w, "JIRA Epic: %s\n", c.Jira.Epic) + fmt.Fprintf(w, "JIRA Issue Type: %s\n", c.Jira.IssueType) + fmt.Fprintf(w, "GitHub Repo: %s\n", c.Github.Repo) + fmt.Fprintf(w, "GitHub Labels: %s\n", c.Github.Labels) fmt.Fprintf(w, "Summary: %s\n", c.Summary) fmt.Fprintf(w, "Template: %s\n", c.Template) + fmt.Fprintln(w, "") for _, ticket := range c.Tickets { ticket.PrintStatus(w) } } + +func (c *Campaign) FillTicket(t *Ticket) error { + summaryTmpl, err := template.New("").Parse(c.Summary) + if err != nil { + return err + } + + var summaryBytes bytes.Buffer + if err := summaryTmpl.Execute(&summaryBytes, t.Data); err != nil { + return err + } + t.Summary = summaryBytes.String() + + descriptionTemplate, err := template.ParseFiles(c.Template) + if err != nil { + return err + } + + var descriptionBytes bytes.Buffer + if err := descriptionTemplate.Execute(&descriptionBytes, t.Data); err != nil { + return err + } + t.Description = descriptionBytes.String() + return nil +} + +func (c *Campaign) RepoComponents() (string, string) { + parts := strings.Split(c.Github.Repo, "/") + if len(parts) == 2 { + return parts[0], parts[1] + } + return "", "" +} diff --git a/model/config.go b/model/config.go deleted file mode 100644 index 19b8b87..0000000 --- a/model/config.go +++ /dev/null @@ -1,7 +0,0 @@ -package model - -type Config struct { - GithubToken string `json:"github_token"` - JiraUsername string `json:"jira_username"` - JiraToken string `json:"jira_token"` -} diff --git a/model/ticket.go b/model/ticket.go index 9c53386..556534e 100644 --- a/model/ticket.go +++ b/model/ticket.go @@ -6,10 +6,13 @@ import ( ) type Ticket struct { - GithubLink int64 `json:"githubLink,omitempty"` - JiraLink string `json:"jiraLink,omitempty"` - Summary string `json:"summary,omitempty"` - Data map[string]interface{} `json:"data,omitempty"` + GithubLink int64 `json:"github_link,omitempty"` + GithubStatus string `json:"github_status,omitempty"` + JiraLink string `json:"jira_link,omitempty"` + JiraStatus string `json:"jira_status,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` } func RemoveDuplicateTickets(tickets []*Ticket, fileOnly bool) []*Ticket { @@ -33,5 +36,7 @@ func RemoveDuplicateTickets(tickets []*Ticket, fileOnly bool) []*Ticket { } func (t *Ticket) PrintStatus(w io.Writer) { - fmt.Fprintf(w, " [%s] %s\n", t.JiraLink, t.Summary) + if t.Summary != "" { + fmt.Fprintf(w, "[%s] %s\n", t.JiraLink, t.Summary) + } } diff --git a/vendor/github.com/StevenACoffman/j2m/.gitignore b/vendor/github.com/StevenACoffman/j2m/.gitignore new file mode 100644 index 0000000..f1c181e --- /dev/null +++ b/vendor/github.com/StevenACoffman/j2m/.gitignore @@ -0,0 +1,12 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out diff --git a/vendor/github.com/StevenACoffman/j2m/LICENSE b/vendor/github.com/StevenACoffman/j2m/LICENSE new file mode 100644 index 0000000..432bcf1 --- /dev/null +++ b/vendor/github.com/StevenACoffman/j2m/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Steve Coffman + +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/StevenACoffman/j2m/Makefile b/vendor/github.com/StevenACoffman/j2m/Makefile new file mode 100644 index 0000000..b0cc99d --- /dev/null +++ b/vendor/github.com/StevenACoffman/j2m/Makefile @@ -0,0 +1,15 @@ +.DEFAULT_GOAL := easy +.PHONY: install clean all easy + +bin/j2m: + go build -o bin/j2m cmd/j2m.go + +all: bin/j2m + +install: bin/j2m + cp bin/* ~/bin + +clean: + rm -f bin/* + +easy: all \ No newline at end of file diff --git a/vendor/github.com/StevenACoffman/j2m/README.md b/vendor/github.com/StevenACoffman/j2m/README.md new file mode 100644 index 0000000..b1b71eb --- /dev/null +++ b/vendor/github.com/StevenACoffman/j2m/README.md @@ -0,0 +1,61 @@ +[![GoDoc](https://godoc.org/github.com/StevenACoffman/j2m?status.svg)](https://godoc.org/github.com/StevenACoffman/j2m) +[![GoReportcard](https://goreportcard.com/badge/github.com/StevenACoffman/j2m?status.svg)](https://goreportcard.com/report/github.com/StevenACoffman/j2m) + +# jira-to-md + +## JIRA to MarkDown text format converter +Golang tool to convert from JIRA Markdown text formatting to GitHub Flavored MarkDown. + +## Credits +This fun toy was heavily inspired by the J2M project by Fokke Zandbergen (http://j2m.fokkezb.nl/). Major credit to Fokke, kylefarris (and other contributors) for establishing the RexExp patterns for this to work. The maintained JavaScript fork I based this on is [here](https://github.com/kylefarris/J2M) + +## Supported Conversions + +* Headers (H1-H6) +* Bold +* Italic +* Bold + Italic +* Un-ordered lists +* Ordered lists +* Programming Language-specific code blocks (with help from herbert-venancio) +* Inline preformatted text spans +* Un-named links +* Named links +* Monospaced Text +* ~~Citations~~ (currently kinda buggy) +* Strikethroughs +* Inserts +* Superscripts +* Subscripts +* Single-paragraph blockquotes +* Tables +* Panels + + +## How to Use + +### Markdown String + +``` +**Some bold things** +*Some italic stuff* +## H2 + +``` + +### Atlassian Wiki MarkUp Syntax (JIRA) + +We'll refer to this as the `jira` variable in the examples below. + +``` +*Some bold things** +_Some italic stuff_ +h2. H2 +[http://google.com] +``` + +### Examples + +``` +cat j2m.jira | j2m +``` diff --git a/vendor/github.com/StevenACoffman/j2m/go.mod b/vendor/github.com/StevenACoffman/j2m/go.mod new file mode 100644 index 0000000..56efaa5 --- /dev/null +++ b/vendor/github.com/StevenACoffman/j2m/go.mod @@ -0,0 +1,3 @@ +module github.com/StevenACoffman/j2m + +go 1.12 diff --git a/vendor/github.com/StevenACoffman/j2m/j2m.go b/vendor/github.com/StevenACoffman/j2m/j2m.go new file mode 100644 index 0000000..9a3bbbb --- /dev/null +++ b/vendor/github.com/StevenACoffman/j2m/j2m.go @@ -0,0 +1,153 @@ +package j2m + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +type jiration struct { + re *regexp.Regexp + repl interface{} +} + +// JiraToMD takes a string in Jira Markdown, and outputs Github Markdown +func JiraToMD(str string) string { + jirations := []jiration{ + { // UnOrdered Lists + re: regexp.MustCompile(`(?m)^[ \t]*(\*+)\s+`), + repl: func(groups []string) string { + _, stars := groups[0], groups[1] + return strings.Repeat(" ", len(stars)-1) + "* " + }, + }, + { //Ordered Lists + re: regexp.MustCompile(`(?m)^[ \t]*(#+)\s+`), + repl: func(groups []string) string { + _, nums := groups[0], groups[1] + return strings.Repeat(" ", len(nums)-1) + "1. " + }, + }, + { //Headers 1-6 + re: regexp.MustCompile(`(?m)^h([0-6])\.(.*)$`), + repl: func(groups []string) string { + _, level, content := groups[0], groups[1], groups[2] + i, _ := strconv.Atoi(level) + return strings.Repeat("#", i) + content + }, + }, + { // Bold + re: regexp.MustCompile(`\*(\S.*)\*`), + repl: "**$1**", + }, + { // Italic + re: regexp.MustCompile(`\_(\S.*)\_`), + repl: "*$1*", + }, + { // Monospaced text + re: regexp.MustCompile(`\{\{([^}]+)\}\}`), + repl: "`$1`", + }, + { // Citations (buggy) + re: regexp.MustCompile(`\?\?((?:.[^?]|[^?].)+)\?\?`), + repl: "$1", + }, + { // Inserts + re: regexp.MustCompile(`\+([^+]*)\+`), + repl: "$1", + }, + { // Superscript + re: regexp.MustCompile(`\^([^^]*)\^`), + repl: "$1", + }, + { // Subscript + re: regexp.MustCompile(`~([^~]*)~`), + repl: "$1", + }, + { // Strikethrough + re: regexp.MustCompile(`(\s+)-(\S+.*?\S)-(\s+)`), + repl: "$1~~$2~~$3", + }, + { // Code Block + re: regexp.MustCompile(`\{code(:([a-z]+))?([:|]?(title|borderStyle|borderColor|borderWidth|bgColor|titleBGColor)=.+?)*\}`), + repl: "```$2", + }, + { // Code Block End + re: regexp.MustCompile(`{code}`), + repl: "```", + }, + { // Pre-formatted text + re: regexp.MustCompile(`{noformat}`), + repl: "```", + }, + { // Un-named Links + re: regexp.MustCompile(`(?U)\[([^|]+)\]`), + repl: "<$1>", + }, + { // Images + re: regexp.MustCompile(`!(.+)!`), + repl: "![]($1)", + }, + { // Named Links + re: regexp.MustCompile(`\[(.+?)\|(.+)\]`), + repl: "[$1]($2)", + }, + { // Single Paragraph Blockquote + re: regexp.MustCompile(`(?m)^bq\.\s+`), + repl: "> ", + }, + { // Remove color: unsupported in md + re: regexp.MustCompile(`(?m)\{color:[^}]+\}(.*)\{color\}`), + repl: "$1", + }, + { // panel into table + re: regexp.MustCompile(`(?m)\{panel:title=([^}]*)\}\n?(.*?)\n?\{panel\}`), + repl: "\n| $1 |\n| --- |\n| $2 |", + }, + { //table header + re: regexp.MustCompile(`(?m)^[ \t]*((?:\|\|.*?)+\|\|)[ \t]*$`), + repl: func(groups []string) string { + _, headers := groups[0], groups[1] + reBarred := regexp.MustCompile(`\|\|`) + + singleBarred := reBarred.ReplaceAllString(headers, "|") + fillerRe := regexp.MustCompile(`\|[^|]+`) + return "\n" + singleBarred + "\n" + fillerRe.ReplaceAllString(singleBarred, "| --- ") + }, + }, + { // remove leading-space of table headers and rows + re: regexp.MustCompile(`(?m)^[ \t]*\|`), + repl: "|", + }, + } + for _, jiration := range jirations { + switch v := jiration.repl.(type) { + case string: + str = jiration.re.ReplaceAllString(str, v) + case func([]string) string: + str = replaceAllStringSubmatchFunc(jiration.re, str, v) + default: + fmt.Printf("I don't know about type %T!\n", v) + } + } + return str +} + +// https://gist.github.com/elliotchance/d419395aa776d632d897 +func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { + result := "" + lastIndex := 0 + + for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { + groups := []string{} + for i := 0; i < len(v); i += 2 { + groups = append(groups, str[v[i]:v[i+1]]) + } + + result += str[lastIndex:v[0]] + repl(groups) + lastIndex = v[1] + } + + return result + str[lastIndex:] +} diff --git a/vendor/github.com/StevenACoffman/j2m/j2m.jira b/vendor/github.com/StevenACoffman/j2m/j2m.jira new file mode 100644 index 0000000..ac41c89 --- /dev/null +++ b/vendor/github.com/StevenACoffman/j2m/j2m.jira @@ -0,0 +1,65 @@ +h1. Biggest heading + +h2. Bigger heading + +h1. Biggest heading +h2. Bigger heading +h3. Big heading +h4. Normal heading +h5. Small heading +h6. Smallest heading + +*strong* +_emphasis_ +{{monospaced}} +-deleted- ++inserted+ +^superscript^ +~subscript~ + +{code:javascript} +var hello = 'world'; +{code} + +!http://google.com/image! +[!http://google.com/image!|http://google.com/link] + +[http://google.com] +[Google|http://google.com] + +GitHub Flavor +-deleted- + +{code} + preformatted piece of text + so _no_ further _formatting_ is done here +{code} + +_*Should be bold AND italic*_ + +* First li +* Second li +** Indented li +*** Three columns in li +* Back to first level li + +# First li +# Second li +## Indented li +### Three columns in li +# Back to first level li + +* Here's _italic_ inside li +* here's *bold* inside li +* Here's _*bold + italic*_ inside li +** Here they are in one line indented: _italic_ *bold* + +bq. Here's a long single-paragraph block quote. It should look pretty and stuff. + +{panel:title=A title} +Panel text +{panel} + +||Heading 1||Heading 2|| +|Col A1|Col A2| +|Col B1|Col B2| \ No newline at end of file diff --git a/vendor/github.com/StevenACoffman/j2m/j2m.md b/vendor/github.com/StevenACoffman/j2m/j2m.md new file mode 100644 index 0000000..25e5501 --- /dev/null +++ b/vendor/github.com/StevenACoffman/j2m/j2m.md @@ -0,0 +1,68 @@ +# Biggest heading + +## Bigger heading + +# Biggest heading +## Bigger heading +### Big heading +#### Normal heading +##### Small heading +###### Smallest heading + +**strong** +*emphasis* +`monospaced` +~~deleted~~ +inserted +superscript +subscript + +```javascript +var hello = 'world'; +``` + +![](http://google.com/image) +[![](http://google.com/image)](http://google.com/link) + + +[Google](http://google.com) + +GitHub Flavor +~~deleted~~ + +``` + preformatted piece of text + so *no_ further _formatting* is done here +``` + +***Should be bold AND italic*** + +* First li +* Second li + * Indented li + * Three columns in li +* Back to first level li + +1. First li +1. Second li + 1. Indented li + 1. Three columns in li +1. Back to first level li + +* Here's *italic* inside li +* here's **bold** inside li +* Here's ***bold + italic*** inside li + * Here they are in one line indented: *italic* **bold** + +> Here's a long single-paragraph block quote. It should look pretty and stuff. + + +| A title | +| --- | +| Panel text | + + +|Heading 1|Heading 2| +| --- | --- | +|Col A1|Col A2| +|Col B1|Col B2| \ No newline at end of file diff --git a/vendor/modules.txt b/vendor/modules.txt index 48cecf3..9349fb7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,5 @@ +# github.com/StevenACoffman/j2m v0.0.0-20190826163711-7d8d00c99217 +github.com/StevenACoffman/j2m # github.com/fatih/structs v1.1.0 github.com/fatih/structs # github.com/golang/protobuf v1.3.2