Simplifies init process and adds github issue publishing

This commit is contained in:
Miguel de la Cruz 2020-04-27 11:42:29 +02:00
parent 21c18b3095
commit be543e2cc4
23 changed files with 581 additions and 390 deletions

View file

@ -18,7 +18,12 @@ 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",
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
}

View file

@ -1,6 +1,9 @@
package cmd
import (
"bufio"
"fmt"
"os"
"strings"
"git.ctrlz.es/mgdelacroix/campaigner/campaign"
@ -14,42 +17,76 @@ 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,
}
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{
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,
}

View file

@ -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)

View file

@ -17,9 +17,7 @@ func RootCmd() *cobra.Command {
AddCmd(),
FilterCmd(),
InitCmd(),
StandaloneCmd(),
StatusCmd(),
TokenCmd(),
PublishCmd(),
SyncCmd(),
)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

2
go.mod
View file

@ -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

13
go.sum
View file

@ -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=

View file

@ -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

View file

@ -1,16 +1,32 @@
package model
import (
"bytes"
"fmt"
"io"
"strings"
"text/template"
)
// ToDo: add key-value extra params as a map to allow for customfield_whatever = team
type Campaign struct {
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 {
Jira ConfigJira `json:"jira"`
Github ConfigGithub `json:"github"`
Summary string `json:"summary"`
Template string `json:"template"`
Tickets []*Ticket `json:"tickets,omitempty"`
@ -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 "", ""
}

View file

@ -1,7 +0,0 @@
package model
type Config struct {
GithubToken string `json:"github_token"`
JiraUsername string `json:"jira_username"`
JiraToken string `json:"jira_token"`
}

View file

@ -6,9 +6,12 @@ import (
)
type Ticket struct {
GithubLink int64 `json:"githubLink,omitempty"`
JiraLink string `json:"jiraLink,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"`
}
@ -33,5 +36,7 @@ func RemoveDuplicateTickets(tickets []*Ticket, fileOnly bool) []*Ticket {
}
func (t *Ticket) PrintStatus(w io.Writer) {
if t.Summary != "" {
fmt.Fprintf(w, "[%s] %s\n", t.JiraLink, t.Summary)
}
}

12
vendor/github.com/StevenACoffman/j2m/.gitignore generated vendored Normal file
View file

@ -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

21
vendor/github.com/StevenACoffman/j2m/LICENSE generated vendored Normal file
View file

@ -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.

15
vendor/github.com/StevenACoffman/j2m/Makefile generated vendored Normal file
View file

@ -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

61
vendor/github.com/StevenACoffman/j2m/README.md generated vendored Normal file
View file

@ -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
<http://google.com>
```
### 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
```

3
vendor/github.com/StevenACoffman/j2m/go.mod generated vendored Normal file
View file

@ -0,0 +1,3 @@
module github.com/StevenACoffman/j2m
go 1.12

153
vendor/github.com/StevenACoffman/j2m/j2m.go generated vendored Normal file
View file

@ -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: "<cite>$1</cite>",
},
{ // Inserts
re: regexp.MustCompile(`\+([^+]*)\+`),
repl: "<ins>$1</ins>",
},
{ // Superscript
re: regexp.MustCompile(`\^([^^]*)\^`),
repl: "<sup>$1</sup>",
},
{ // Subscript
re: regexp.MustCompile(`~([^~]*)~`),
repl: "<sub>$1</sub>",
},
{ // 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:]
}

65
vendor/github.com/StevenACoffman/j2m/j2m.jira generated vendored Normal file
View file

@ -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|

68
vendor/github.com/StevenACoffman/j2m/j2m.md generated vendored Normal file
View file

@ -0,0 +1,68 @@
# Biggest heading
## Bigger heading
# Biggest heading
## Bigger heading
### Big heading
#### Normal heading
##### Small heading
###### Smallest heading
**strong**
*emphasis*
`monospaced`
~~deleted~~
<ins>inserted</ins>
<sup>superscript</sup>
<sub>subscript</sub>
```javascript
var hello = 'world';
```
![](http://google.com/image)
[![](http://google.com/image)](http://google.com/link)
<http://google.com>
[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|

2
vendor/modules.txt vendored
View file

@ -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