diff --git a/pkg/commands/hosting_service/definitions.go b/pkg/commands/hosting_service/definitions.go index c70062a67..3e7f144da 100644 --- a/pkg/commands/hosting_service/definitions.go +++ b/pkg/commands/hosting_service/definitions.go @@ -6,6 +6,7 @@ var defaultUrlRegexStrings = []string{ `^(?:https?|ssh)://.*/(?P.*)/(?P.*?)(?:\.git)?$`, `^git@.*:(?P.*)/(?P.*?)(?:\.git)?$`, } +var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}" // we've got less type safety using go templates but this lends itself better to // users adding custom service definitions in their config @@ -15,6 +16,7 @@ var githubServiceDef = ServiceDefinition{ pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1", commitURL: "/commit/{{.CommitSha}}", regexStrings: defaultUrlRegexStrings, + repoURLTemplate: defaultRepoURLTemplate, } var bitbucketServiceDef = ServiceDefinition{ @@ -23,6 +25,7 @@ var bitbucketServiceDef = ServiceDefinition{ pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1", commitURL: "/commits/{{.CommitSha}}", regexStrings: defaultUrlRegexStrings, + repoURLTemplate: defaultRepoURLTemplate, } var gitLabServiceDef = ServiceDefinition{ @@ -31,9 +34,27 @@ var gitLabServiceDef = ServiceDefinition{ pullRequestURLIntoTargetBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}", commitURL: "/commit/{{.CommitSha}}", regexStrings: defaultUrlRegexStrings, + repoURLTemplate: defaultRepoURLTemplate, } -var serviceDefinitions = []ServiceDefinition{githubServiceDef, bitbucketServiceDef, gitLabServiceDef} +var azdoServiceDef = ServiceDefinition{ + provider: "azuredevops", + pullRequestURLIntoDefaultBranch: "/pullrequestcreate?sourceRef={{.From}}", + pullRequestURLIntoTargetBranch: "/pullrequestcreate?sourceRef={{.From}}&targetRef={{.To}}", + commitURL: "/commit/{{.CommitSha}}", + regexStrings: []string{ + `^git@ssh.dev.azure.com.*/(?P.*)/(?P.*)/(?P.*?)(?:\.git)?$`, + `^https://.*@dev.azure.com/(?P.*?)/(?P.*?)/_git/(?P.*?)(?:\.git)?$`, + }, + repoURLTemplate: "https://{{.webDomain}}/{{.org}}/{{.project}}/_git/{{.repo}}", +} + +var serviceDefinitions = []ServiceDefinition{ + githubServiceDef, + bitbucketServiceDef, + gitLabServiceDef, + azdoServiceDef, +} var defaultServiceDomains = []ServiceDomain{ { @@ -51,4 +72,9 @@ var defaultServiceDomains = []ServiceDomain{ gitDomain: "gitlab.com", webDomain: "gitlab.com", }, + { + serviceDefinition: azdoServiceDef, + gitDomain: "dev.azure.com", + webDomain: "dev.azure.com", + }, } diff --git a/pkg/commands/hosting_service/hosting_service.go b/pkg/commands/hosting_service/hosting_service.go index 01e07e9eb..4a0a49681 100644 --- a/pkg/commands/hosting_service/hosting_service.go +++ b/pkg/commands/hosting_service/hosting_service.go @@ -1,7 +1,6 @@ package hosting_service import ( - "fmt" "net/url" "regexp" "strings" @@ -66,13 +65,13 @@ func (self *HostingServiceMgr) getService() (*Service, error) { return nil, err } - root, err := serviceDomain.getRootFromRemoteURL(self.remoteURL) + repoURL, err := serviceDomain.serviceDefinition.getRepoURLFromRemoteURL(self.remoteURL, serviceDomain.webDomain) if err != nil { return nil, err } return &Service{ - root: root, + repoURL: repoURL, ServiceDefinition: serviceDomain.serviceDefinition, }, nil } @@ -139,47 +138,32 @@ type ServiceDomain struct { serviceDefinition ServiceDefinition } -func (self ServiceDomain) getRootFromRemoteURL(repoURL string) (string, error) { - // we may want to make this more specific to the service in future e.g. if - // some new service comes along which has a different root url structure. - repoInfo, err := self.serviceDefinition.getRepoInfoFromURL(repoURL) - if err != nil { - return "", err - } - return fmt.Sprintf("https://%s/%s/%s", self.webDomain, repoInfo.Owner, repoInfo.Repository), nil -} - -// RepoInformation holds some basic information about the repo -type RepoInformation struct { - Owner string - Repository string -} - type ServiceDefinition struct { provider string pullRequestURLIntoDefaultBranch string pullRequestURLIntoTargetBranch string commitURL string regexStrings []string + + // can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex + repoURLTemplate string } -func (self ServiceDefinition) getRepoInfoFromURL(url string) (*RepoInformation, error) { +func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) { for _, regexStr := range self.regexStrings { re := regexp.MustCompile(regexStr) - matches := utils.FindNamedMatches(re, url) - if matches != nil { - return &RepoInformation{ - Owner: matches["owner"], - Repository: matches["repo"], - }, nil + input := utils.FindNamedMatches(re, url) + if input != nil { + input["webDomain"] = webDomain + return utils.ResolvePlaceholderString(self.repoURLTemplate, input), nil } } - return nil, errors.New("Failed to parse repo information from url") + return "", errors.New("Failed to parse repo information from url") } type Service struct { - root string + repoURL string ServiceDefinition } @@ -196,5 +180,5 @@ func (self *Service) getCommitURL(commitSha string) string { } func (self *Service) resolveUrl(templateString string, args map[string]string) string { - return self.root + utils.ResolvePlaceholderString(templateString, args) + return self.repoURL + utils.ResolvePlaceholderString(templateString, args) } diff --git a/pkg/commands/hosting_service/hosting_service_test.go b/pkg/commands/hosting_service/hosting_service_test.go index 98c097a33..5bffa0165 100644 --- a/pkg/commands/hosting_service/hosting_service_test.go +++ b/pkg/commands/hosting_service/hosting_service_test.go @@ -8,63 +8,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetRepoInfoFromURL(t *testing.T) { - type scenario struct { - serviceDefinition ServiceDefinition - testName string - repoURL string - test func(*RepoInformation) - } - - scenarios := []scenario{ - { - githubServiceDef, - "Returns repository information for git remote url", - "git@github.com:petersmith/super_calculator", - func(repoInfo *RepoInformation) { - assert.EqualValues(t, repoInfo.Owner, "petersmith") - assert.EqualValues(t, repoInfo.Repository, "super_calculator") - }, - }, - { - githubServiceDef, - "Returns repository information for git remote url, trimming trailing '.git'", - "git@github.com:petersmith/super_calculator.git", - func(repoInfo *RepoInformation) { - assert.EqualValues(t, repoInfo.Owner, "petersmith") - assert.EqualValues(t, repoInfo.Repository, "super_calculator") - }, - }, - { - githubServiceDef, - "Returns repository information for ssh remote url", - "ssh://git@github.com/petersmith/super_calculator", - func(repoInfo *RepoInformation) { - assert.EqualValues(t, repoInfo.Owner, "petersmith") - assert.EqualValues(t, repoInfo.Repository, "super_calculator") - }, - }, - { - githubServiceDef, - "Returns repository information for http remote url", - "https://my_username@bitbucket.org/johndoe/social_network.git", - func(repoInfo *RepoInformation) { - assert.EqualValues(t, repoInfo.Owner, "johndoe") - assert.EqualValues(t, repoInfo.Repository, "social_network") - }, - }, - } - - for _, s := range scenarios { - s := s - t.Run(s.testName, func(t *testing.T) { - result, err := s.serviceDefinition.getRepoInfoFromURL(s.repoURL) - assert.NoError(t, err) - s.test(result) - }) - } -} - func TestGetPullRequestURL(t *testing.T) { type scenario struct { testName string @@ -172,6 +115,44 @@ func TestGetPullRequestURL(t *testing.T) { assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url) }, }, + { + testName: "Opens a link to new pull request on Azure DevOps (SSH)", + from: "feature/new", + remoteUrl: "git@ssh.dev.azure.com:v3/myorg/myproject/myrepo", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature/new", url) + }, + }, + { + testName: "Opens a link to new pull request on Azure DevOps (SSH) with specifc target", + from: "feature/new", + to: "dev", + remoteUrl: "git@ssh.dev.azure.com:v3/myorg/myproject/myrepo", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature/new&targetRef=dev", url) + }, + }, + { + testName: "Opens a link to new pull request on Azure DevOps (HTTP)", + from: "feature/new", + remoteUrl: "https://myorg@dev.azure.com/myorg/myproject/_git/myrepo", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature/new", url) + }, + }, + { + testName: "Opens a link to new pull request on Azure DevOps (HTTP) with specifc target", + from: "feature/new", + to: "dev", + remoteUrl: "https://myorg@dev.azure.com/myorg/myproject/_git/myrepo", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature/new&targetRef=dev", url) + }, + }, { testName: "Throws an error if git service is unsupported", from: "feature/divide-operation", @@ -218,7 +199,7 @@ func TestGetPullRequestURL(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url) }, - expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab"}, + expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops"}, }, { testName: "Escapes reserved URL characters in from branch name",