github.com/buildkite/cli
Advanced tools
+10
-0
@@ -7,2 +7,12 @@ # Changelog | ||
| ## [v0.5.0](https://github.com/buildkite/cli/tree/v0.5.0) (2019-04-18) | ||
| [Full Changelog](https://github.com/buildkite/cli/compare/v0.4.1...v0.5.0) | ||
| ### Added | ||
| - Fix bug where file backend is default over keychain [#51](https://github.com/buildkite/cli/pull/51) (@lox) | ||
| - Handle wildcards in branch patterns [#49](https://github.com/buildkite/cli/pull/49) (@lox) | ||
| ### Fixed | ||
| - Fix spelling in error messages [#50](https://github.com/buildkite/cli/pull/50) (@jsleeio) | ||
| ## [v0.4.1](https://github.com/buildkite/cli/tree/v0.4.1) (2019-03-13) | ||
@@ -9,0 +19,0 @@ [Full Changelog](https://github.com/buildkite/cli/compare/v0.4.0...v0.4.1) |
+0
-9
@@ -94,11 +94,2 @@ package main | ||
| // infer keyring backend types if we get certain config | ||
| if keyringFileDir != "" { | ||
| keyringBackend = `file` | ||
| } else if keyringKeychain != "" { | ||
| keyringBackend = `keychain` | ||
| } else if keyringPassDir != "" { | ||
| keyringBackend = `pass` | ||
| } | ||
| var allowedBackends []keyring.BackendType | ||
@@ -105,0 +96,0 @@ if keyringBackend != `` { |
+2
-2
@@ -43,3 +43,3 @@ package cli | ||
| if err != nil { | ||
| return nil, fmt.Errorf("Error retriving github oauth credentials: %v", err) | ||
| return nil, fmt.Errorf("Error retrieving github oauth credentials: %v", err) | ||
| } | ||
@@ -61,3 +61,3 @@ | ||
| if err != nil { | ||
| return nil, NewExitError(fmt.Errorf("Error retriving buildkite graphql credentials: %v", err), 1) | ||
| return nil, NewExitError(fmt.Errorf("Error retrieving buildkite graphql credentials: %v", err), 1) | ||
| } | ||
@@ -64,0 +64,0 @@ |
+123
-0
@@ -104,2 +104,26 @@ package local | ||
| }, | ||
| { | ||
| `{ | ||
| "command": "echo foo", | ||
| "branches": "!foo" | ||
| }`, | ||
| step{ | ||
| Command: &commandStep{ | ||
| Commands: []string{`echo foo`}, | ||
| }, | ||
| Branches: []string{`!foo`}, | ||
| }, | ||
| }, | ||
| { | ||
| `{ | ||
| "command": "echo foo", | ||
| "branches": "master stable/*" | ||
| }`, | ||
| step{ | ||
| Command: &commandStep{ | ||
| Commands: []string{`echo foo`}, | ||
| }, | ||
| Branches: []string{`master`, `stable/*`}, | ||
| }, | ||
| }, | ||
| } { | ||
@@ -288,1 +312,100 @@ t.Run("", func(t *testing.T) { | ||
| } | ||
| func TestMatchingBranches(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| Pattern string | ||
| Branch string | ||
| }{ | ||
| // empty branch patterns match all | ||
| {"", "foo"}, | ||
| {"", ""}, | ||
| {"", " "}, | ||
| // single name branch matches | ||
| {"foo bar", "foo"}, | ||
| {"foo bar", "bar"}, | ||
| // with whitespace | ||
| {" master ", "master"}, | ||
| {"master ", "master"}, | ||
| {" master", "master"}, | ||
| // ones with a slash | ||
| {"feature/authentication", "feature/authentication"}, | ||
| // not-checking | ||
| {"!foo", "master"}, | ||
| {"!release/production !release/test", "master"}, | ||
| // prefix wildcards | ||
| {"*-do-the-thing", "can-you-do-the-thing"}, | ||
| {"!*-do-the-thing", "can-you-do-the-thing-please"}, | ||
| // wildcards | ||
| {"release/*", "release/foo"}, | ||
| {"release/*", "release/bar/bing"}, | ||
| {"release-*", "release-thingy"}, | ||
| {"release-* release/*", "release-thingy"}, | ||
| {"release-* release/*", "release/thingy"}, | ||
| {"this-*-thing-is-the-*", "this-ruby-thing-is-the-worst"}, | ||
| {"this-*-thing-is-the-*", "this-regex-thing-is-the-best"}, | ||
| {"this-*-thing-is-the-*", "this-*-thing-is-the-*"}, | ||
| {"this-*-thing-is-the-*", "this--thing-is-the-best-"}, | ||
| } { | ||
| t.Run("", func(t *testing.T) { | ||
| branches, err := ParseBranchPattern(tc.Pattern) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| s := step{ | ||
| Branches: branches, | ||
| } | ||
| if !s.MatchBranch(tc.Branch) { | ||
| t.Errorf("Expected pattern %q to match branch %q", tc.Pattern, tc.Branch) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestNotMatchingBranches(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| Pattern string | ||
| Branch string | ||
| }{ | ||
| // branch names | ||
| {"foo bar", "bang"}, | ||
| // not-matchers | ||
| {"!foo bar", "foo"}, | ||
| {"!release/*", "release/foo"}, | ||
| {"!release/*", "release/bar"}, | ||
| {"!refs/tags/*", "refs/tags/production"}, | ||
| {"!release/production !release/test", "release/production"}, | ||
| {"!release/production !release/test", "release/test"}, | ||
| // ones with a slash | ||
| {"feature/authentication", "update/deployment"}, | ||
| // wildcards | ||
| {"release-*", "release/thingy"}, | ||
| {"release-*", "master"}, | ||
| {"*-do-the-thing", "this-is-not-the-thing"}, | ||
| } { | ||
| t.Run("", func(t *testing.T) { | ||
| branches, err := ParseBranchPattern(tc.Pattern) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| s := step{ | ||
| Branches: branches, | ||
| } | ||
| if s.MatchBranch(tc.Branch) { | ||
| t.Errorf("Expected pattern %q to NOT match branch %q", tc.Pattern, tc.Branch) | ||
| } | ||
| }) | ||
| } | ||
| } |
+70
-11
@@ -7,2 +7,3 @@ package local | ||
| "log" | ||
| "regexp" | ||
| "sort" | ||
@@ -46,13 +47,7 @@ "strings" | ||
| // Handle various types of branch vs branches | ||
| if branches != nil { | ||
| switch b := branches.(type) { | ||
| case []interface{}: | ||
| for _, bi := range b { | ||
| s.Branches = append(s.Branches, strings.Split(bi.(string), ",")...) | ||
| } | ||
| case string: | ||
| s.Branches = append(s.Branches, strings.Split(b, ",")...) | ||
| default: | ||
| log.Printf("Branches is unhandled type %T", branches) | ||
| var err error | ||
| s.Branches, err = ParseBranchPattern(branches) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,2 +71,43 @@ } | ||
| func ParseBranchPattern(branches interface{}) ([]string, error) { | ||
| var result []string | ||
| switch b := branches.(type) { | ||
| case []interface{}: | ||
| for _, bi := range b { | ||
| result = append(result, strings.Fields(bi.(string))...) | ||
| } | ||
| case string: | ||
| result = append(result, strings.Fields(b)...) | ||
| default: | ||
| return nil, fmt.Errorf("Branches is unhandled type %T", branches) | ||
| } | ||
| return result, nil | ||
| } | ||
| func MatchBranchPattern(branch string, pattern string) bool { | ||
| expected := true | ||
| // Handle negation at the start | ||
| for strings.HasPrefix(pattern, `!`) { | ||
| expected = !expected | ||
| pattern = strings.TrimPrefix(pattern, `!`) | ||
| } | ||
| // Compile a regex for the rest | ||
| re, err := regexp.Compile(`^` + strings.Replace(pattern, `*`, `.*?`, -1) + `$`) | ||
| if err != nil { | ||
| log.Printf("Failed to compile regex: %v", err) | ||
| return false | ||
| } | ||
| // Test it against the branch | ||
| if re.MatchString(branch) == expected { | ||
| return true | ||
| } | ||
| return false | ||
| } | ||
| func (s step) MatchBranch(branch string) bool { | ||
@@ -81,7 +117,30 @@ if len(s.Branches) == 0 { | ||
| } | ||
| // Apply a heuristic, if we have multiple negatives it's AND-ed | ||
| // otherwise it's OR-d. Gross, but we know what the user meant. | ||
| var negationCount int | ||
| for _, b := range s.Branches { | ||
| if b == branch { | ||
| if strings.HasPrefix(b, `!`) { | ||
| negationCount += 1 | ||
| } | ||
| } | ||
| if negationCount > 1 { | ||
| // Has multiple negatives, so the patterns are AND-ed | ||
| for _, pattern := range s.Branches { | ||
| if !MatchBranchPattern(branch, pattern) { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
| // Has zero of one negatives, so the patterns are OR-ed | ||
| for _, pattern := range s.Branches { | ||
| if MatchBranchPattern(branch, pattern) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
@@ -88,0 +147,0 @@ } |
+1
-1
@@ -5,3 +5,3 @@ package cli | ||
| // Version is the version of the CLI tool | ||
| Version = "0.4.1" | ||
| Version = "0.5.0" | ||
| ) | ||
@@ -8,0 +8,0 @@ |