├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go └── pipeline.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ivan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jiraf 2 | 3 | Jiraf finds an issue in Jira by key and generates a git branch name from its summary. 4 | 5 | ![gif](https://raw.githubusercontent.com/ivaaaan/gifs/master/jiraf.gif) 6 | 7 | ## Installation 8 | 9 | ### Git 10 | 11 | ```bash 12 | cd /tmp 13 | git clone https://github.com/ivaaaan/jiraf.git 14 | cd jiraf 15 | go install 16 | ``` 17 | 18 | ## Configuration 19 | 20 | Configuration file stored in `~/.config/jiraf/config.yml` in the YAML format. 21 | 22 | ```yaml 23 | url: 24 | username: 25 | password: 26 | format: 27 | pipeline: 28 | : [...] 29 | ... 30 | ``` 31 | 32 | ### Example 33 | 34 | ```yaml 35 | url: 36 | username: 37 | password: 38 | format: "%s_%s" 39 | pipeline: 40 | replace: [' ', '-'] 41 | replace_regexp: ['[^a-zA-Z0-9-]+', ''] 42 | to_lower: 43 | ``` 44 | 45 | This configuration will generate a branch name like this: `ISSUE-1_summary-of-your-issue` 46 | 47 | ## Pipeline 48 | 49 | Pipeline is a set of functions with arguments. Each of the function will be called on a result from a previous function. 50 | 51 | Available functions: 52 | 53 | | Name | Arguments | Description | 54 | |------------------|---------------|-------------------------------------------------------| 55 | | `to_lower` | | Convert a string to lowercase | 56 | | `replace` | `old, new` | Replace all `old` with `new` | 57 | | `replace_regexp` | `regexp, new` | Replace all characters that match `regexp` with `new` | 58 | 59 | ## Usage 60 | 61 | ### Git subcommand 62 | 63 | One way to integrate jiraf with git is to create a simple script like this: 64 | 65 | ```bash 66 | #!/bin/bash 67 | 68 | git checkout -b $(jiraf $1) 69 | ``` 70 | 71 | And put this script into `~/bin/git-cb` file (replace "cb" with a preferred subcommand name). Now you can just run `git cb ISSUE-1`, which will create a new branch with a generated name by jiraf. 72 | 73 | ### Alias 74 | 75 | Another one is an alias: 76 | 77 | ```bash 78 | alias gcb='() { git checkout -b $(jiraf $1) }' 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ivaaaan/jiraf 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/andygrunwald/go-jira v1.13.0 // indirect 7 | gopkg.in/yaml.v2 v2.4.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andygrunwald/go-jira v1.13.0 h1:vvIImGgX32bHfoiyUwkNo+/YrPnRczNarvhLOncP6dE= 2 | github.com/andygrunwald/go-jira v1.13.0/go.mod h1:jYi4kFDbRPZTJdJOVJO4mpMMIwdB+rcZwSO58DzPd2I= 3 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 4 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 5 | github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= 6 | github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 7 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 8 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= 9 | github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 10 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 11 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 12 | github.com/trivago/tgo v1.0.1 h1:bxatjJIXNIpV18bucU4Uk/LaoxvxuOlp/oowRHyncLQ= 13 | github.com/trivago/tgo v1.0.1/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= 14 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 15 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 16 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 18 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 22 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/andygrunwald/go-jira" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | var help = `Jiraf finds an issue in Jira by key and generates a git branch name from its summary. 13 | 14 | Usage: 15 | jiraf 16 | 17 | Options: 18 | --help, Print this message 19 | ` 20 | 21 | type Config struct { 22 | URL string `yaml:"url"` 23 | Username string `yaml:"username"` 24 | Password string `yaml:"password"` 25 | BranchNameRegExp string `yaml:"replace_regexp"` 26 | Format string `yaml:"format"` 27 | Pipeline map[string][]string `yaml:"pipeline"` 28 | } 29 | 30 | func defaultConfigPath() (string, error) { 31 | home, err := os.UserHomeDir() 32 | if err != nil { 33 | return "", nil 34 | } 35 | 36 | return home + "/.config/jiraf/config.yml", nil 37 | } 38 | 39 | func getConfig(path string) (*Config, error) { 40 | f, err := os.Open(path) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | defer f.Close() 46 | 47 | decoder := yaml.NewDecoder(f) 48 | 49 | var config Config 50 | if err := decoder.Decode(&config); err != nil { 51 | return nil, err 52 | } 53 | 54 | return &config, nil 55 | } 56 | 57 | func main() { 58 | configPath, err := defaultConfigPath() 59 | if err != nil { 60 | fmt.Fprintf(os.Stderr, "error getting a default config path: %v", err) 61 | os.Exit(1) 62 | } 63 | 64 | config, err := getConfig(configPath) 65 | if err != nil { 66 | fmt.Fprintf(os.Stderr, "error parsing config: %v", err) 67 | os.Exit(1) 68 | } 69 | 70 | tp := jira.BasicAuthTransport{ 71 | Username: config.Username, 72 | Password: config.Password, 73 | } 74 | 75 | jiraClient, err := jira.NewClient(tp.Client(), config.URL) 76 | if err != nil { 77 | fmt.Fprintf(os.Stderr, "error initializing a Jira client: %v", err) 78 | os.Exit(1) 79 | } 80 | 81 | if len(os.Args) < 2 { 82 | fmt.Fprint(os.Stderr, "expected single argument, a ticket ID (e.g., PROJECT-123)") 83 | os.Exit(1) 84 | } 85 | 86 | if os.Args[1] == "--help" { 87 | fmt.Fprint(os.Stdout, help) 88 | os.Exit(0) 89 | } 90 | 91 | issue, _, err := jiraClient.Issue.Get(os.Args[1], &jira.GetQueryOptions{}) 92 | if err != nil { 93 | fmt.Fprintf(os.Stderr, "error getting a Jira issue: %v", err) 94 | os.Exit(1) 95 | } 96 | 97 | branchName := issue.Fields.Summary 98 | 99 | var errors []string 100 | for pipeName, args := range config.Pipeline { 101 | if pipeFunc, ok := PipelineMap[pipeName]; ok { 102 | branchName, err = pipeFunc(branchName, args...) 103 | if err != nil { 104 | errors = append(errors, fmt.Sprintf("%s function returned an error: %v", pipeName, err)) 105 | } 106 | } 107 | } 108 | 109 | if len(errors) > 0 { 110 | fmt.Fprintf(os.Stderr, "one or more functions returned errors: %s", strings.Join(errors, ",")) 111 | os.Exit(1) 112 | } 113 | 114 | fmt.Fprintf(os.Stdout, config.Format, issue.Key, branchName) 115 | } 116 | -------------------------------------------------------------------------------- /pipeline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // Pipe is an interface for plugins that can be used to generate a branch name from summary 10 | type Pipe func(s string, args ...string) (string, error) 11 | 12 | func ToLower(s string, args ...string) (string, error) { 13 | return strings.ToLower(s), nil 14 | } 15 | 16 | func Replace(s string, args ...string) (string, error) { 17 | if len(args) < 2 { 18 | return s, errors.New("invalid function usage: replace ") 19 | } 20 | 21 | return strings.ReplaceAll(s, args[0], args[1]), nil 22 | } 23 | 24 | func ReplaceRegexp(s string, args ...string) (string, error) { 25 | if len(args) < 2 { 26 | return s, errors.New("invalid function usage: replace_regexp ") 27 | } 28 | 29 | reg, err := regexp.Compile(args[0]) 30 | if err != nil { 31 | return s, err 32 | } 33 | 34 | return reg.ReplaceAllString(s, args[1]), nil 35 | } 36 | 37 | var PipelineMap map[string]Pipe = map[string]Pipe{ 38 | "to_lower": ToLower, 39 | "replace_regexp": ReplaceRegexp, 40 | "replace": Replace, 41 | } 42 | --------------------------------------------------------------------------------