diff --git a/README.md b/README.md index 69e5fbb..a4f94e3 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,39 @@ `jt` is a CLI tool for viewing and manipulating JIRA issues. +An example usage to transition an issue to a new status: +``` +jt "In Progress" TEAM-1234 +``` + +If you are in a git repository on a topic branch who's name matches `team-1234[-whatever]`, you can omit +the issue argument as it is implied. + ### Usage: -jt [command] +jt [new state] [issue number] + +**Note:** + +We case insensitively look for valid transition states in your issue's workflow. If you give `tRiAgE` +we will find `Triage`, if that is a valid transition for your issue's current status. -### Available Commands: +If no valid transition state matches *exactly*, we then try matching against +possible states that have had their whitespace removed. If you give "todo" we will find possible state `To Do`. + +If still no valid transition state is matched, we will then try partial match, so that +"done" will match possible state `Deployed / Done`. + +This will otherwise only transition an issue to a matching valid state according to your +JIRA board's workflow. + +### Other Available Commands: | command | what it does | |---|---| -| block | Transition an issue to Blocked status | | completion | generate the autocompletion script for the specified shell | -| done | Transition an issue to Deployed / Done status | | help | Help about any command | -| land | Transition an issue to Landed status | | onit | Self-assign and transition an issue to In Progress status | -| review | Transition an issue to Review status | | take | Assign an issue to you | -| todo | Transition an issue to To Do status | -| triage | Transition an issue to Triage status | -| wti | What The Issue? - View an issue | +| wti | What The Issue? - View an issue in Github Markdown | Shared Flags: | flag | what it does | @@ -41,7 +57,9 @@ Go developers with `$HOME/bin` in their `$PATH` can run `mage` if they have [mag Alternatively, `go run mage.go` will work even without `mage` installed, but it will still put the binary in `$HOME/bin`. ### Development and Limitations -Currently, the config does not allow overriding the workflow states. - Also, if a user doesn't have a config file, it should help them create one. +### Alternatives + +There is another [jira cli](https://github.com/go-jira/jira) that is quite sophisticated, featureful, +and maybe complicated, but I found custom workflow transitions either didn't work, or were cumbersome. \ No newline at end of file diff --git a/cmd/block.go b/cmd/block.go deleted file mode 100644 index 91234dd..0000000 --- a/cmd/block.go +++ /dev/null @@ -1,49 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/StevenACoffman/jt/pkg/atlassian" - "os" - - "github.com/spf13/cobra" -) - -// blockCmd represents the block command -var blockCmd = &cobra.Command{ - Use: "block", - Short: "Transition an issue to Blocked status", - Long: `Transition an issue to Blocked status`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Println("You failed to pass a jira issue argument") - os.Exit(exitFail) - } - issueKey := args[0] - issue, _, issueErr := jiraClient.Issue.Get(issueKey, nil) - if issueErr != nil { - fmt.Printf("Unable to get Issue %s: %+v", issueKey, issueErr) - os.Exit(exitFail) - } - - err := atlassian.MoveIssueToStatus(jiraClient, issue, issueKey, atlassian.BlockedStatusID) - if err != nil { - fmt.Println(err) - os.Exit(exitFail) - } - os.Exit(exitSuccess) - }, -} - -func init() { - rootCmd.AddCommand(blockCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // blockCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // blockCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/done.go b/cmd/done.go deleted file mode 100644 index ca7db51..0000000 --- a/cmd/done.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright © 2021 Steve Coffman - -*/ -package cmd - -import ( - "fmt" - "github.com/StevenACoffman/jt/pkg/atlassian" - "os" - - "github.com/spf13/cobra" -) - -// doneCmd represents the done command -var doneCmd = &cobra.Command{ - Use: "done", - Short: "Transition an issue to Deployed / Done status", - Long: `Transition an issue to Deployed / Done status`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Println("You failed to pass a jira issue argument") - os.Exit(exitFail) - } - issueKey := args[0] - issue, _, issueErr := jiraClient.Issue.Get(issueKey, nil) - if issueErr != nil { - fmt.Printf("Unable to get Issue %s: %+v", issueKey, issueErr) - os.Exit(exitFail) - } - - err := atlassian.MoveIssueToStatus(jiraClient, issue, issueKey, atlassian.DeployedDoneStatusID) - if err != nil { - fmt.Println(err) - os.Exit(exitFail) - } - os.Exit(exitSuccess) - }, -} - -func init() { - rootCmd.AddCommand(doneCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // doneCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // doneCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/land.go b/cmd/land.go deleted file mode 100644 index 5e9406d..0000000 --- a/cmd/land.go +++ /dev/null @@ -1,49 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/StevenACoffman/jt/pkg/atlassian" - "os" - - "github.com/spf13/cobra" -) - -// landCmd represents the land command -var landCmd = &cobra.Command{ - Use: "land", - Short: "Transition an issue to Landed status", - Long: `Transition an issue to Landed status`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Println("You failed to pass a jira issue argument") - os.Exit(exitFail) - } - issueKey := args[0] - issue, _, issueErr := jiraClient.Issue.Get(issueKey, nil) - if issueErr != nil { - fmt.Printf("Unable to get Issue %s: %+v", issueKey, issueErr) - os.Exit(exitFail) - } - - err := atlassian.MoveIssueToStatus(jiraClient, issue, issueKey, atlassian.LandedStatusID) - if err != nil { - fmt.Println(err) - os.Exit(exitFail) - } - os.Exit(exitSuccess) - }, -} - -func init() { - rootCmd.AddCommand(landCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // landCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // landCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/onit.go b/cmd/onit.go index 41c2661..a26fdc0 100644 --- a/cmd/onit.go +++ b/cmd/onit.go @@ -26,7 +26,7 @@ var onitCmd = &cobra.Command{ os.Exit(exitFail) } - err := atlassian.MoveIssueToStatus(jiraClient, issue, issueKey, atlassian.InProgressStatusID) + err := atlassian.MoveIssueToStatusByName(jiraClient, issue, issueKey, "In Progress") if err != nil { fmt.Println(err) os.Exit(exitFail) diff --git a/cmd/review.go b/cmd/review.go deleted file mode 100644 index 7e80dc5..0000000 --- a/cmd/review.go +++ /dev/null @@ -1,49 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/StevenACoffman/jt/pkg/atlassian" - "os" - - "github.com/spf13/cobra" -) - -// reviewCmd represents the review command -var reviewCmd = &cobra.Command{ - Use: "review", - Short: "Transition an issue to Review status", - Long: `Transition an issue to Review status`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Println("You failed to pass a jira issue argument") - os.Exit(exitFail) - } - issueKey := args[0] - issue, _, issueErr := jiraClient.Issue.Get(issueKey, nil) - if issueErr != nil { - fmt.Printf("Unable to get Issue %s: %+v", issueKey, issueErr) - os.Exit(exitFail) - } - - err := atlassian.MoveIssueToStatus(jiraClient, issue, issueKey, atlassian.InReviewStatusID) - if err != nil { - fmt.Println(err) - os.Exit(exitFail) - } - os.Exit(exitSuccess) - }, -} - -func init() { - rootCmd.AddCommand(reviewCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // reviewCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // reviewCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/root.go b/cmd/root.go index e524374..8ea49c6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,11 +27,34 @@ var rootCmd = &cobra.Command{ Use: "jt", Short: "jt - JIRA Issue Tool", Long: `jt is a CLI tool for viewing and manipulating JIRA issues.`, + Args: cobra.RangeArgs(1, 2), // Uncomment the following line if your bare application // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { - // fmt.Println("hi") - // }, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Println("You need to pass a desired jira status argument (and maybe a jira issue like TEAM-1234)") + os.Exit(exitFail) + } + var issueKey string + statusName := args[0] + if len(args) > 1 { + issueKey = args[1] + } else { + + } + issue, _, issueErr := jiraClient.Issue.Get(issueKey, nil) + if issueErr != nil { + fmt.Printf("Unable to get Issue %s: %+v", issueKey, issueErr) + os.Exit(exitFail) + } + + err := atlassian.MoveIssueToStatusByName(jiraClient, issue, issueKey, statusName) + if err != nil { + fmt.Println(err) + os.Exit(exitFail) + } + os.Exit(exitSuccess) + }, } // Execute adds all child commands to the root command and sets flags appropriately. @@ -51,10 +74,6 @@ func init() { // will be global for your application. rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/jira)") - - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cmd/todo.go b/cmd/todo.go deleted file mode 100644 index f63c85f..0000000 --- a/cmd/todo.go +++ /dev/null @@ -1,49 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/StevenACoffman/jt/pkg/atlassian" - "os" - - "github.com/spf13/cobra" -) - -// todoCmd represents the todo command -var todoCmd = &cobra.Command{ - Use: "todo", - Short: "Transition an issue to To Do status", - Long: `Transition an issue to To Do status`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Println("You failed to pass a jira issue argument") - os.Exit(exitFail) - } - issueKey := args[0] - issue, _, issueErr := jiraClient.Issue.Get(issueKey, nil) - if issueErr != nil { - fmt.Printf("Unable to get Issue %s: %+v", issueKey, issueErr) - os.Exit(exitFail) - } - - err := atlassian.MoveIssueToStatus(jiraClient, issue, issueKey, atlassian.ToDoStatusID) - if err != nil { - fmt.Println(err) - os.Exit(exitFail) - } - os.Exit(exitSuccess) - }, -} - -func init() { - rootCmd.AddCommand(todoCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // todoCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // todoCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/triage.go b/cmd/triage.go deleted file mode 100644 index 4d8628c..0000000 --- a/cmd/triage.go +++ /dev/null @@ -1,49 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/StevenACoffman/jt/pkg/atlassian" - "os" - - "github.com/spf13/cobra" -) - -// triageCmd represents the triage command -var triageCmd = &cobra.Command{ - Use: "triage", - Short: "Transition an issue to Triage status", - Long: `Transition an issue to Triage status`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Println("You failed to pass a jira issue argument") - os.Exit(exitFail) - } - issueKey := args[0] - issue, _, issueErr := jiraClient.Issue.Get(issueKey, nil) - if issueErr != nil { - fmt.Printf("Unable to get Issue %s: %+v", issueKey, issueErr) - os.Exit(exitFail) - } - - err := atlassian.MoveIssueToStatus(jiraClient, issue, issueKey, atlassian.TriageStatusID) - if err != nil { - fmt.Println(err) - os.Exit(exitFail) - } - os.Exit(exitSuccess) - }, -} - -func init() { - rootCmd.AddCommand(triageCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // triageCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // triageCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} diff --git a/cmd/wti.go b/cmd/wti.go index 6993323..dd2eeb8 100644 --- a/cmd/wti.go +++ b/cmd/wti.go @@ -12,6 +12,7 @@ var wtiCmd = &cobra.Command{ Use: "wti", Short: "What The Issue? - View an issue", Long: `What The Issue? Will View an issue.`, + Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { fmt.Println("You failed to pass a jira issue argument") diff --git a/pkg/atlassian/jira.go b/pkg/atlassian/jira.go index c2bddd8..c645924 100644 --- a/pkg/atlassian/jira.go +++ b/pkg/atlassian/jira.go @@ -7,6 +7,7 @@ import ( "regexp" "strconv" "strings" + "unicode" "github.com/StevenACoffman/jt/pkg/middleware" @@ -267,65 +268,6 @@ func JiraMarkupToGithubMarkdown(jiraClient *jira.Client, str string) string { resolved := jiraAccountResolver.JiraMarkupMentionToEmail(str) return JiraToMD(resolved) } -// ordered states -const TriageStatusID = "10111" -const ToDoStatusID = "10000" -const BlockedStatusID = "10107" -const InProgressStatusID = "10105" -const DeployedDoneStatusID = "10001" -const ReadyForQEStatusID = "10357" -const InReviewStatusID = "10108" -const LandedStatusID = "10149" -const WontDoStatusID = "10250" - -var statusIDToName = map[string]string{ - TriageStatusID : "Triage", - ToDoStatusID : "ToDo", - BlockedStatusID : "Blocked", - InProgressStatusID : "InProgress", - DeployedDoneStatusID : "DeployedDone", - ReadyForQEStatusID : "ReadyForQE", - InReviewStatusID : "InReview", - LandedStatusID : "Landed", - WontDoStatusID : "WontDo", -} - -var nameToID = map[string]string{ -"Triage":TriageStatusID, -"ToDo":ToDoStatusID, -"Blocked":BlockedStatusID, -"InProgress":InProgressStatusID, -"DeployedDone":DeployedDoneStatusID, -"ReadyForQE":ReadyForQEStatusID, -"InReview":InReviewStatusID, -"Landed":LandedStatusID, -"WontDo":WontDoStatusID, -} - -func StatusNameFromID(statusID string) string { - return statusIDToName[statusID] -} - -func MoveIssueToStatus(jiraClient *jira.Client, issue *jira.Issue, issueKey string, statusID string) error { - originalStatus := issue.Fields.Status.Name - if issue.Fields.Status.ID == statusID { - return fmt.Errorf("issue is Already in Status %s\n", statusIDToName[statusID]) - } - - err := transitionIssue(jiraClient, issueKey, statusID) - if err != nil { - return err - } - issue, _, err = jiraClient.Issue.Get(issueKey, nil) - if err != nil { - return err - } - fmt.Printf("Issue %s Status successfully changed from: %s and set to: %+v\n", - issueKey, originalStatus, issue.Fields.Status.Name) - - return nil -} - func AssignIssueToSelf(jiraClient *jira.Client, issue *jira.Issue, issueKey string) error { self, _, selfErr := jiraClient.User.GetSelf() @@ -407,21 +349,78 @@ func TrimJira(s string) string { return result } -func transitionIssue(jiraClient *jira.Client, issueKey string, statusID string) error { +func MoveIssueToStatusByName(jiraClient *jira.Client, issue *jira.Issue, issueKey string, statusName string) error { + originalStatus := issue.Fields.Status.Name + if issue.Fields.Status.Name == statusName || + CaseInsensitiveContains(issue.Fields.Status.Name, statusName){ + return fmt.Errorf("issue is Already in Status %s\n", issue.Fields.Status.Name) + } + + err := transitionIssueByStatusName(jiraClient, issueKey, statusName) + if err != nil { + return err + } + issue, _, err = jiraClient.Issue.Get(issueKey, nil) + if err != nil { + return err + } + fmt.Printf("Issue %s Status successfully changed from: %s and set to: %+v\n", + issueKey, originalStatus, issue.Fields.Status.Name) + + return nil +} + +func transitionIssueByStatusName(jiraClient *jira.Client, issueKey string, statusName string) error { var transitionID string possibleTransitions, _, err := jiraClient.Issue.GetTransitions(issueKey) if err != nil { return err } for _, v := range possibleTransitions { - if v.To.ID == statusID { + if strings.EqualFold(v.To.Name, statusName) { transitionID = v.ID break } } + // no exact match, so remove whitespace so that "ToDo" arg will match "TO DO" status + if transitionID == "" { + for _, v := range possibleTransitions { + if strings.EqualFold(RemoveWhiteSpace(v.To.Name), statusName) { + transitionID = v.ID + break + } + } + } + // still no match, so look for partial, so "Done" arg will match "Deployed / Done" if transitionID == "" { - return fmt.Errorf("there does not appear to be a valid transition to %s", statusIDToName[statusID]) + // substring match only if exact match fails + for _, v := range possibleTransitions { + if CaseInsensitiveContains(v.To.Name, statusName) { + transitionID = v.ID + break + } + } + } + + if transitionID == "" { + return fmt.Errorf("there does not appear to be a valid transition to %s", statusName) } _, err = jiraClient.Issue.DoTransition(issueKey, transitionID) return err } + +func RemoveWhiteSpace(str string) string { + var b strings.Builder + b.Grow(len(str)) + for _, ch := range str { + if !unicode.IsSpace(ch) { + b.WriteRune(ch) + } + } + return b.String() +} + +func CaseInsensitiveContains(s, substr string) bool { + s, substr = strings.ToUpper(s), strings.ToUpper(substr) + return strings.Contains(s, substr) +} \ No newline at end of file