-
Notifications
You must be signed in to change notification settings - Fork 234
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #366 from CircleCI-Public/git-inference
Add command to open current project
- Loading branch information
Showing
9 changed files
with
280 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
|
||
"github.com/CircleCI-Public/circleci-cli/git" | ||
"github.com/pkg/browser" | ||
"github.com/pkg/errors" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func projectUrl(remote *git.Remote) string { | ||
return fmt.Sprintf("https://app.circleci.com/%s/%s/%s/pipelines", | ||
url.PathEscape(strings.ToLower(string(remote.VcsType))), | ||
url.PathEscape(remote.Organization), | ||
url.PathEscape(remote.Project)) | ||
} | ||
|
||
var errorMessage = ` | ||
Unable detect which URL should be opened. This command is intended to be run from | ||
a git repository with a remote named 'origin' that is hosted on Github or Bitbucket | ||
Error` | ||
|
||
func openProjectInBrowser() error { | ||
|
||
remote, err := git.InferProjectFromGitRemotes() | ||
|
||
if err != nil { | ||
return errors.Wrap(err, errorMessage) | ||
} | ||
|
||
return browser.OpenURL(projectUrl(remote)) | ||
} | ||
|
||
func newOpenCommand() *cobra.Command { | ||
|
||
openCommand := &cobra.Command{ | ||
Use: "open", | ||
Short: "Open the current project in the browser.", | ||
RunE: func(_ *cobra.Command, _ []string) error { | ||
return openProjectInBrowser() | ||
}, | ||
} | ||
return openCommand | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package cmd | ||
|
||
import ( | ||
"github.com/CircleCI-Public/circleci-cli/git" | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("open", func() { | ||
It("can build urls", func() { | ||
Expect(projectUrl(&git.Remote{ | ||
Project: "foo", | ||
Organization: "bar", | ||
VcsType: git.GitHub, | ||
})).To(Equal("https://app.circleci.com/github/bar/foo/pipelines")) | ||
}) | ||
|
||
It("escapes garbage", func() { | ||
Expect(projectUrl(&git.Remote{ | ||
Project: "/one/two", | ||
Organization: "%^&*()[]", | ||
VcsType: git.Bitbucket, | ||
})).To(Equal("https://app.circleci.com/bitbucket/%25%5E&%2A%28%29%5B%5D/%2Fone%2Ftwo/pipelines")) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package git | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os/exec" | ||
"regexp" | ||
"strings" | ||
) | ||
|
||
type VcsType string | ||
|
||
const ( | ||
GitHub VcsType = "GITHUB" | ||
Bitbucket VcsType = "BITBUCKET" | ||
) | ||
|
||
type Remote struct { | ||
VcsType VcsType | ||
Organization string | ||
Project string | ||
} | ||
|
||
// Parse the output of `git remote` to infer what VCS provider is being used by | ||
// in the current working directory. The assumption is that the 'origin' remote | ||
// will be a Bitbucket or GitHub project. This matching is a best effort approach, | ||
// and pull requests are welcome to make it more robust. | ||
func InferProjectFromGitRemotes() (*Remote, error) { | ||
|
||
remoteUrl, err := getRemoteUrl("origin") | ||
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return findRemote(remoteUrl) | ||
} | ||
|
||
func findRemote(url string) (*Remote, error) { | ||
vcsType, slug, err := findProviderAndSlug(url) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
matches := strings.Split(slug, "/") | ||
|
||
if len(matches) != 2 { | ||
return nil, fmt.Errorf("Splitting '%s' into organization and project failed", slug) | ||
} | ||
|
||
return &Remote{ | ||
VcsType: vcsType, | ||
Organization: matches[0], | ||
Project: strings.TrimSuffix(matches[1], ".git"), | ||
}, nil | ||
} | ||
|
||
func findProviderAndSlug(url string) (VcsType, string, error) { | ||
|
||
var vcsParsers = map[VcsType][]*regexp.Regexp{ | ||
GitHub: { | ||
regexp.MustCompile(`^git@github\.com:(.*)`), | ||
regexp.MustCompile(`https://(?:.*@)?github\.com/(.*)`), | ||
}, | ||
Bitbucket: { | ||
regexp.MustCompile(`^git@bitbucket\.org:(.*)`), | ||
regexp.MustCompile(`https://(?:.*@)?bitbucket\.org/(.*)`), | ||
}, | ||
} | ||
|
||
for provider, regexes := range vcsParsers { | ||
for _, regex := range regexes { | ||
if matches := regex.FindStringSubmatch(url); matches != nil { | ||
return provider, matches[1], nil | ||
} | ||
} | ||
} | ||
|
||
return "", "", fmt.Errorf("Unknown git remote: %s", url) | ||
} | ||
|
||
func getRemoteUrl(remoteName string) (string, error) { | ||
|
||
// Ensure that git is on the path | ||
if _, err := exec.LookPath("git"); err != nil { | ||
return "", errors.New("Could not find 'git' on the path; this command requires git to be installed.") | ||
} | ||
|
||
// Ensure that we are in a git repository | ||
if output, err := exec.Command("git", "status").CombinedOutput(); err != nil { | ||
if strings.Contains(string(output), "not a git repository") { | ||
return "", errors.New("This command must be run from inside a git repository") | ||
} | ||
// If `git status` fails for any other reason, let's optimisticly continue | ||
// execution and allow the call to `git remote` to fail. | ||
} | ||
|
||
out, err := exec.Command("git", "remote", "get-url", remoteName).CombinedOutput() | ||
if err != nil { | ||
return "", fmt.Errorf("Error finding the %s git remote: %s", | ||
remoteName, | ||
strings.TrimSpace(string(out))) | ||
} | ||
return string(out), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package git_test | ||
|
||
import ( | ||
"testing" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
func TestGit(t *testing.T) { | ||
RegisterFailHandler(Fail) | ||
RunSpecs(t, "Git Suite") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package git | ||
|
||
import ( | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("Dealing with git", func() { | ||
|
||
Context("remotes", func() { | ||
|
||
Describe("integration tests", func() { | ||
|
||
It("fails gracefully when the remote can't be found", func() { | ||
// This test will fail if the current working directory has git remote | ||
// named 'peristeronic'. | ||
_, err := getRemoteUrl("peristeronic") | ||
Expect(err).To(MatchError("Error finding the peristeronic git remote: fatal: No such remote 'peristeronic'")) | ||
}) | ||
|
||
It("can read git output", func() { | ||
Expect(getRemoteUrl("origin")).To(MatchRegexp(`github`)) | ||
}) | ||
|
||
}) | ||
|
||
It("should parse these", func() { | ||
|
||
cases := map[string]*Remote{ | ||
"[email protected]:foobar/foo-service.git": &Remote{ | ||
VcsType: GitHub, | ||
Organization: "foobar", | ||
Project: "foo-service", | ||
}, | ||
|
||
"[email protected]:example/makefile_sh.git": &Remote{ | ||
VcsType: Bitbucket, | ||
Organization: "example", | ||
Project: "makefile_sh", | ||
}, | ||
|
||
"https://github.com/apple/pear.git": &Remote{ | ||
VcsType: GitHub, | ||
Organization: "apple", | ||
Project: "pear", | ||
}, | ||
|
||
"[email protected]:example/makefile_sh": &Remote{ | ||
VcsType: Bitbucket, | ||
Organization: "example", | ||
Project: "makefile_sh", | ||
}, | ||
|
||
"https://[email protected]/kiwi/fruit.git": &Remote{ | ||
VcsType: Bitbucket, | ||
Organization: "kiwi", | ||
Project: "fruit", | ||
}, | ||
|
||
"https://[email protected]/kiwi/fruit": &Remote{ | ||
VcsType: Bitbucket, | ||
Organization: "kiwi", | ||
Project: "fruit", | ||
}, | ||
} | ||
|
||
for url, remote := range cases { | ||
Expect(findRemote(url)).To(Equal(remote)) | ||
} | ||
|
||
}) | ||
|
||
It("should not parse these", func() { | ||
|
||
cases := map[string]string{ | ||
"asd/asd/asd": "Unknown git remote: asd/asd/asd", | ||
"[email protected]:foo/bar/baz": "Splitting 'foo/bar/baz' into organization and project failed", | ||
} | ||
for url, message := range cases { | ||
_, err := findRemote(url) | ||
Expect(err).To(MatchError(message)) | ||
} | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters