diff --git a/pkg/commands/hosting_service/definitions.go b/pkg/commands/hosting_service/definitions.go new file mode 100644 index 000000000..c70062a67 --- /dev/null +++ b/pkg/commands/hosting_service/definitions.go @@ -0,0 +1,54 @@ +package hosting_service + +// if you want to make a custom regex for a given service feel free to test it out +// at regoio.herokuapp.com +var defaultUrlRegexStrings = []string{ + `^(?:https?|ssh)://.*/(?P.*)/(?P.*?)(?:\.git)?$`, + `^git@.*:(?P.*)/(?P.*?)(?:\.git)?$`, +} + +// we've got less type safety using go templates but this lends itself better to +// users adding custom service definitions in their config +var githubServiceDef = ServiceDefinition{ + provider: "github", + pullRequestURLIntoDefaultBranch: "/compare/{{.From}}?expand=1", + pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1", + commitURL: "/commit/{{.CommitSha}}", + regexStrings: defaultUrlRegexStrings, +} + +var bitbucketServiceDef = ServiceDefinition{ + provider: "bitbucket", + pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1", + pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1", + commitURL: "/commits/{{.CommitSha}}", + regexStrings: defaultUrlRegexStrings, +} + +var gitLabServiceDef = ServiceDefinition{ + provider: "gitlab", + pullRequestURLIntoDefaultBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}", + pullRequestURLIntoTargetBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}", + commitURL: "/commit/{{.CommitSha}}", + regexStrings: defaultUrlRegexStrings, +} + +var serviceDefinitions = []ServiceDefinition{githubServiceDef, bitbucketServiceDef, gitLabServiceDef} + +var defaultServiceDomains = []ServiceDomain{ + { + serviceDefinition: githubServiceDef, + gitDomain: "github.com", + webDomain: "github.com", + }, + { + serviceDefinition: bitbucketServiceDef, + gitDomain: "bitbucket.org", + webDomain: "bitbucket.org", + }, + { + serviceDefinition: gitLabServiceDef, + gitDomain: "gitlab.com", + webDomain: "gitlab.com", + }, +} diff --git a/pkg/commands/hosting_service/hosting_service.go b/pkg/commands/hosting_service/hosting_service.go new file mode 100644 index 000000000..1762902a4 --- /dev/null +++ b/pkg/commands/hosting_service/hosting_service.go @@ -0,0 +1,201 @@ +package hosting_service + +import ( + "fmt" + "regexp" + "strings" + + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/sirupsen/logrus" +) + +// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc. +// Different git hosting services have different URL formats for when you want to open a PR or view a commit, +// and this package's responsibility is to determine which service you're using based on the remote URL, +// and then which URL you need for whatever use case you have. + +type HostingServiceMgr struct { + log logrus.FieldLogger + tr *i18n.TranslationSet + remoteURL string // e.g. https://github.com/jesseduffield/lazygit + + // see https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls + configServiceDomains map[string]string +} + +// NewHostingServiceMgr creates new instance of PullRequest +func NewHostingServiceMgr(log logrus.FieldLogger, tr *i18n.TranslationSet, remoteURL string, configServiceDomains map[string]string) *HostingServiceMgr { + return &HostingServiceMgr{ + log: log, + tr: tr, + remoteURL: remoteURL, + configServiceDomains: configServiceDomains, + } +} + +func (self *HostingServiceMgr) GetPullRequestURL(from string, to string) (string, error) { + gitService, err := self.getService() + if err != nil { + return "", err + } + + if to == "" { + return gitService.getPullRequestURLIntoDefaultBranch(from), nil + } else { + return gitService.getPullRequestURLIntoTargetBranch(from, to), nil + } +} + +func (self *HostingServiceMgr) GetCommitURL(commitSha string) (string, error) { + gitService, err := self.getService() + if err != nil { + return "", err + } + + pullRequestURL := gitService.getCommitURL(commitSha) + + return pullRequestURL, nil +} + +func (self *HostingServiceMgr) getService() (*Service, error) { + serviceDomain, err := self.getServiceDomain(self.remoteURL) + if err != nil { + return nil, err + } + + root, err := serviceDomain.getRootFromRemoteURL(self.remoteURL) + if err != nil { + return nil, err + } + + return &Service{ + root: root, + ServiceDefinition: serviceDomain.serviceDefinition, + }, nil +} + +func (self *HostingServiceMgr) getServiceDomain(repoURL string) (*ServiceDomain, error) { + candidateServiceDomains := self.getCandidateServiceDomains() + + for _, serviceDomain := range candidateServiceDomains { + // I feel like it makes more sense to see if the repo url contains the service domain's git domain, + // but I don't want to break anything by changing that right now. + if strings.Contains(repoURL, serviceDomain.serviceDefinition.provider) { + return &serviceDomain, nil + } + } + + return nil, errors.New(self.tr.UnsupportedGitService) +} + +func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain { + serviceDefinitionByProvider := map[string]ServiceDefinition{} + for _, serviceDefinition := range serviceDefinitions { + serviceDefinitionByProvider[serviceDefinition.provider] = serviceDefinition + } + + var serviceDomains = make([]ServiceDomain, len(defaultServiceDomains)) + copy(serviceDomains, defaultServiceDomains) + + if len(self.configServiceDomains) > 0 { + for gitDomain, typeAndDomain := range self.configServiceDomains { + splitData := strings.Split(typeAndDomain, ":") + if len(splitData) != 2 { + self.log.Errorf("Unexpected format for git service: '%s'. Expected something like 'github.com:github.com'", typeAndDomain) + continue + } + + provider := splitData[0] + webDomain := splitData[1] + + serviceDefinition, ok := serviceDefinitionByProvider[provider] + if !ok { + providerNames := []string{} + for _, serviceDefinition := range serviceDefinitions { + providerNames = append(providerNames, serviceDefinition.provider) + } + self.log.Errorf("Unknown git service type: '%s'. Expected one of %s", provider, strings.Join(providerNames, ", ")) + continue + } + + serviceDomains = append(serviceDomains, ServiceDomain{ + gitDomain: gitDomain, + webDomain: webDomain, + serviceDefinition: serviceDefinition, + }) + } + } + + return serviceDomains +} + +// a service domains pairs a service definition with the actual domain it's being served from. +// Sometimes the git service is hosted in a custom domains so although it'll use say +// the github service definition, it'll actually be served from e.g. my-custom-github.com +type ServiceDomain struct { + gitDomain string // the one that appears in the git remote url + webDomain string // the one that appears in the web url + 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 +} + +func (self ServiceDefinition) getRepoInfoFromURL(url string) (*RepoInformation, 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 + } + } + + return nil, errors.New("Failed to parse repo information from url") +} + +type Service struct { + root string + ServiceDefinition +} + +func (self *Service) getPullRequestURLIntoDefaultBranch(from string) string { + return self.resolveUrl(self.pullRequestURLIntoDefaultBranch, map[string]string{"From": from}) +} + +func (self *Service) getPullRequestURLIntoTargetBranch(from string, to string) string { + return self.resolveUrl(self.pullRequestURLIntoTargetBranch, map[string]string{"From": from, "To": to}) +} + +func (self *Service) getCommitURL(commitSha string) string { + return self.resolveUrl(self.commitURL, map[string]string{"CommitSha": commitSha}) +} + +func (self *Service) resolveUrl(templateString string, args map[string]string) string { + return self.root + utils.ResolvePlaceholderString(templateString, args) +} diff --git a/pkg/commands/hosting_service/hosting_service_test.go b/pkg/commands/hosting_service/hosting_service_test.go new file mode 100644 index 000000000..ab7a2b402 --- /dev/null +++ b/pkg/commands/hosting_service/hosting_service_test.go @@ -0,0 +1,233 @@ +package hosting_service + +import ( + "testing" + + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/test" + "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 { + 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 + from string + to string + remoteUrl string + configServiceDomains map[string]string + test func(url string, err error) + expectedLoggedErrors []string + } + + scenarios := []scenario{ + { + testName: "Opens a link to new pull request on bitbucket", + from: "feature/profile-page", + remoteUrl: "git@bitbucket.org:johndoe/social_network.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url) + }, + }, + { + testName: "Opens a link to new pull request on bitbucket with http remote url", + from: "feature/events", + remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url) + }, + }, + { + testName: "Opens a link to new pull request on github", + from: "feature/sum-operation", + remoteUrl: "git@github.com:peter/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url) + }, + }, + { + testName: "Opens a link to new pull request on bitbucket with specific target branch", + from: "feature/profile-page/avatar", + to: "feature/profile-page", + remoteUrl: "git@bitbucket.org:johndoe/social_network.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url) + }, + }, + { + testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch", + from: "feature/remote-events", + to: "feature/events", + remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url) + }, + }, + { + testName: "Opens a link to new pull request on github with specific target branch", + from: "feature/sum-operation", + to: "feature/operations", + remoteUrl: "git@github.com:peter/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url) + }, + }, + { + testName: "Opens a link to new pull request on gitlab", + from: "feature/ui", + remoteUrl: "git@gitlab.com:peter/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url) + }, + }, + { + testName: "Opens a link to new pull request on gitlab in nested groups", + from: "feature/ui", + remoteUrl: "git@gitlab.com:peter/public/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url) + }, + }, + { + testName: "Opens a link to new pull request on gitlab with specific target branch", + from: "feature/commit-ui", + to: "epic/ui", + remoteUrl: "git@gitlab.com:peter/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url) + }, + }, + { + testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups", + from: "feature/commit-ui", + to: "epic/ui", + remoteUrl: "git@gitlab.com:peter/public/calculator.git", + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url) + }, + }, + { + testName: "Throws an error if git service is unsupported", + from: "feature/divide-operation", + remoteUrl: "git@something.com:peter/calculator.git", + test: func(url string, err error) { + assert.EqualError(t, err, "Unsupported git service") + }, + }, + { + testName: "Does not log error when config service domains are valid", + from: "feature/profile-page", + remoteUrl: "git@bitbucket.org:johndoe/social_network.git", + configServiceDomains: map[string]string{ + // valid configuration for a custom service URL + "git.work.com": "gitlab:code.work.com", + }, + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url) + }, + expectedLoggedErrors: nil, + }, + { + testName: "Logs error when config service domain is malformed", + from: "feature/profile-page", + remoteUrl: "git@bitbucket.org:johndoe/social_network.git", + configServiceDomains: map[string]string{ + "noservice.work.com": "noservice.work.com", + }, + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url) + }, + expectedLoggedErrors: []string{"Unexpected format for git service: 'noservice.work.com'. Expected something like 'github.com:github.com'"}, + }, + { + testName: "Logs error when config service domain uses unknown provider", + from: "feature/profile-page", + remoteUrl: "git@bitbucket.org:johndoe/social_network.git", + configServiceDomains: map[string]string{ + "invalid.work.com": "noservice:invalid.work.com", + }, + test: func(url string, err error) { + assert.NoError(t, err) + assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url) + }, + expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab"}, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + tr := i18n.EnglishTranslationSet() + log := &test.FakeFieldLogger{} + hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains) + s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to)) + log.AssertErrors(t, s.expectedLoggedErrors) + }) + } +} diff --git a/pkg/commands/pull_request.go b/pkg/commands/pull_request.go deleted file mode 100644 index 2f2a9cd1d..000000000 --- a/pkg/commands/pull_request.go +++ /dev/null @@ -1,281 +0,0 @@ -package commands - -import ( - "fmt" - "regexp" - "strings" - - "github.com/go-errors/errors" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -// if you want to make a custom regex for a given service feel free to test it out -// at regoio.herokuapp.com -var defaultUrlRegexStrings = []string{ - `^(?:https?|ssh)://.*/(?P.*)/(?P.*?)(?:\.git)?$`, - `^git@.*:(?P.*)/(?P.*?)(?:\.git)?$`, -} - -type ServiceDefinition struct { - provider string - pullRequestURLIntoDefaultBranch string - pullRequestURLIntoTargetBranch string - commitURL string - regexStrings []string -} - -func (self ServiceDefinition) getRepoInfoFromURL(url string) (*RepoInformation, 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 - } - } - - return nil, errors.New("Failed to parse repo information from url") -} - -// a service domains pairs a service definition with the actual domain it's being served from. -// Sometimes the git service is hosted in a custom domains so although it'll use say -// the github service definition, it'll actually be served from e.g. my-custom-github.com -type ServiceDomain struct { - gitDomain string // the one that appears in the git remote url - webDomain string // the one that appears in the web url - serviceDefinition ServiceDefinition -} - -func (self ServiceDomain) getRootFromRepoURL(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 -} - -// we've got less type safety using go templates but this lends itself better to -// users adding custom service definitions in their config -var GithubServiceDef = ServiceDefinition{ - provider: "github", - pullRequestURLIntoDefaultBranch: "/compare/{{.From}}?expand=1", - pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1", - commitURL: "/commit/{{.CommitSha}}", - regexStrings: defaultUrlRegexStrings, -} - -var BitbucketServiceDef = ServiceDefinition{ - provider: "bitbucket", - pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1", - pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1", - commitURL: "/commits/{{.CommitSha}}", - regexStrings: defaultUrlRegexStrings, -} - -var GitLabServiceDef = ServiceDefinition{ - provider: "gitlab", - pullRequestURLIntoDefaultBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}", - pullRequestURLIntoTargetBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}", - commitURL: "/commit/{{.CommitSha}}", - regexStrings: defaultUrlRegexStrings, -} - -var serviceDefinitions = []ServiceDefinition{GithubServiceDef, BitbucketServiceDef, GitLabServiceDef} -var defaultServiceDomains = []ServiceDomain{ - { - serviceDefinition: GithubServiceDef, - gitDomain: "github.com", - webDomain: "github.com", - }, - { - serviceDefinition: BitbucketServiceDef, - gitDomain: "bitbucket.org", - webDomain: "bitbucket.org", - }, - { - serviceDefinition: GitLabServiceDef, - gitDomain: "gitlab.com", - webDomain: "gitlab.com", - }, -} - -type Service struct { - root string - ServiceDefinition -} - -func (self *Service) getPullRequestURLIntoDefaultBranch(from string) string { - return self.resolveUrl(self.pullRequestURLIntoDefaultBranch, map[string]string{"From": from}) -} - -func (self *Service) getPullRequestURLIntoTargetBranch(from string, to string) string { - return self.resolveUrl(self.pullRequestURLIntoTargetBranch, map[string]string{"From": from, "To": to}) -} - -func (self *Service) getCommitURL(commitSha string) string { - return self.resolveUrl(self.commitURL, map[string]string{"CommitSha": commitSha}) -} - -func (self *Service) resolveUrl(templateString string, args map[string]string) string { - return self.root + utils.ResolvePlaceholderString(templateString, args) -} - -// PullRequest opens a link in browser to create new pull request -// with selected branch -type PullRequest struct { - GitCommand *GitCommand -} - -// RepoInformation holds some basic information about the repo -type RepoInformation struct { - Owner string - Repository string -} - -// NewPullRequest creates new instance of PullRequest -func NewPullRequest(gitCommand *GitCommand) *PullRequest { - return &PullRequest{ - GitCommand: gitCommand, - } -} - -func (pr *PullRequest) getService() (*Service, error) { - serviceDomain, err := pr.getServiceDomain() - if err != nil { - return nil, err - } - - repoURL := pr.GitCommand.GetRemoteURL() - - root, err := serviceDomain.getRootFromRepoURL(repoURL) - if err != nil { - return nil, err - } - - return &Service{ - root: root, - ServiceDefinition: serviceDomain.serviceDefinition, - }, nil -} - -func (pr *PullRequest) getServiceDomain() (*ServiceDomain, error) { - candidateServiceDomains := pr.getCandidateServiceDomains() - - repoURL := pr.GitCommand.GetRemoteURL() - - for _, serviceDomain := range candidateServiceDomains { - // I feel like it makes more sense to see if the repo url contains the service domain's git domain, - // but I don't want to break anything by changing that right now. - if strings.Contains(repoURL, serviceDomain.serviceDefinition.provider) { - return &serviceDomain, nil - } - } - - return nil, errors.New(pr.GitCommand.Tr.UnsupportedGitService) -} - -func (pr *PullRequest) getCandidateServiceDomains() []ServiceDomain { - serviceDefinitionByProvider := map[string]ServiceDefinition{} - for _, serviceDefinition := range serviceDefinitions { - serviceDefinitionByProvider[serviceDefinition.provider] = serviceDefinition - } - - var serviceDomains = make([]ServiceDomain, len(defaultServiceDomains)) - copy(serviceDomains, defaultServiceDomains) - - // see https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls - configServices := pr.GitCommand.Config.GetUserConfig().Services - if len(configServices) > 0 { - for gitDomain, typeAndDomain := range configServices { - splitData := strings.Split(typeAndDomain, ":") - if len(splitData) != 2 { - pr.GitCommand.Log.Errorf("Unexpected format for git service: '%s'. Expected something like 'github.com:github.com'", typeAndDomain) - continue - } - - provider := splitData[0] - webDomain := splitData[1] - - serviceDefinition, ok := serviceDefinitionByProvider[provider] - if !ok { - providerNames := []string{} - for _, serviceDefinition := range serviceDefinitions { - providerNames = append(providerNames, serviceDefinition.provider) - } - pr.GitCommand.Log.Errorf("Unknown git service type: '%s'. Expected one of %s", provider, strings.Join(providerNames, ", ")) - continue - } - - serviceDomains = append(serviceDomains, ServiceDomain{ - gitDomain: gitDomain, - webDomain: webDomain, - serviceDefinition: serviceDefinition, - }) - } - } - - return serviceDomains -} - -// CreatePullRequest opens link to new pull request in browser -func (pr *PullRequest) CreatePullRequest(from string, to string) (string, error) { - pullRequestURL, err := pr.getPullRequestURL(from, to) - if err != nil { - return "", err - } - - return pullRequestURL, pr.GitCommand.OSCommand.OpenLink(pullRequestURL) -} - -// CopyURL copies the pull request URL to the clipboard -func (pr *PullRequest) CopyURL(from string, to string) (string, error) { - pullRequestURL, err := pr.getPullRequestURL(from, to) - if err != nil { - return "", err - } - - return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL) -} - -func (pr *PullRequest) getPullRequestURL(from string, to string) (string, error) { - branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(from) - - if !branchExistsOnRemote { - return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote) - } - - gitService, err := pr.getService() - if err != nil { - return "", err - } - - if to == "" { - return gitService.getPullRequestURLIntoDefaultBranch(from), nil - } else { - return gitService.getPullRequestURLIntoTargetBranch(from, to), nil - } -} - -func (pr *PullRequest) getCommitURL(commitSha string) (string, error) { - gitService, err := pr.getService() - if err != nil { - return "", err - } - - pullRequestURL := gitService.getCommitURL(commitSha) - - return pullRequestURL, nil -} - -func (pr *PullRequest) OpenCommitInBrowser(commitSha string) (string, error) { - url, err := pr.getCommitURL(commitSha) - if err != nil { - return "", err - } - - return url, pr.GitCommand.OSCommand.OpenLink(url) -} diff --git a/pkg/commands/pull_request_default_test.go b/pkg/commands/pull_request_default_test.go deleted file mode 100644 index 9a7d775ee..000000000 --- a/pkg/commands/pull_request_default_test.go +++ /dev/null @@ -1,256 +0,0 @@ -//go:build !windows -// +build !windows - -package commands - -import ( - "os/exec" - "strings" - "testing" - - "github.com/jesseduffield/lazygit/pkg/commands/git_config" - "github.com/jesseduffield/lazygit/pkg/secureexec" - "github.com/stretchr/testify/assert" -) - -// TestCreatePullRequest is a function. -func TestCreatePullRequest(t *testing.T) { - type scenario struct { - testName string - from string - to string - remoteUrl string - command func(string, ...string) *exec.Cmd - test func(url string, err error) - } - - scenarios := []scenario{ - { - testName: "Opens a link to new pull request on bitbucket", - from: "feature/profile-page", - remoteUrl: "git@bitbucket.org:johndoe/social_network.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url) - }, - }, - { - testName: "Opens a link to new pull request on bitbucket with http remote url", - from: "feature/events", - remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url) - }, - }, - { - testName: "Opens a link to new pull request on github", - from: "feature/sum-operation", - remoteUrl: "git@github.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@github.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url) - }, - }, - { - testName: "Opens a link to new pull request on bitbucket with specific target branch", - from: "feature/profile-page/avatar", - to: "feature/profile-page", - remoteUrl: "git@bitbucket.org:johndoe/social_network.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url) - }, - }, - { - testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch", - from: "feature/remote-events", - to: "feature/events", - remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url) - }, - }, - { - testName: "Opens a link to new pull request on github with specific target branch", - from: "feature/sum-operation", - to: "feature/operations", - remoteUrl: "git@github.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@github.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url) - }, - }, - { - testName: "Opens a link to new pull request on gitlab", - from: "feature/ui", - remoteUrl: "git@gitlab.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url) - }, - }, - { - testName: "Opens a link to new pull request on gitlab in nested groups", - from: "feature/ui", - remoteUrl: "git@gitlab.com:peter/public/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url) - }, - }, - { - testName: "Opens a link to new pull request on gitlab with specific target branch", - from: "feature/commit-ui", - to: "epic/ui", - remoteUrl: "git@gitlab.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url) - }, - }, - { - testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups", - from: "feature/commit-ui", - to: "epic/ui", - remoteUrl: "git@gitlab.com:peter/public/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "bash") - assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url) - }, - }, - { - testName: "Throws an error if git service is unsupported", - from: "feature/divide-operation", - remoteUrl: "git@something.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.Error(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCommand := NewDummyGitCommand() - gitCommand.OSCommand.Command = s.command - gitCommand.OSCommand.Platform.OS = "darwin" - gitCommand.OSCommand.Platform.Shell = "bash" - gitCommand.OSCommand.Platform.ShellArg = "-c" - gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}" - gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{ - // valid configuration for a custom service URL - "git.work.com": "gitlab:code.work.com", - // invalid configurations for a custom service URL - "invalid.work.com": "noservice:invalid.work.com", - "noservice.work.com": "noservice.work.com", - } - gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl}) - dummyPullRequest := NewPullRequest(gitCommand) - s.test(dummyPullRequest.CreatePullRequest(s.from, s.to)) - }) - } -} diff --git a/pkg/commands/pull_request_test.go b/pkg/commands/pull_request_test.go deleted file mode 100644 index 844a15998..000000000 --- a/pkg/commands/pull_request_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package commands - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -// TestGetRepoInfoFromURL is a function. -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 { - t.Run(s.testName, func(t *testing.T) { - result, err := s.serviceDefinition.getRepoInfoFromURL(s.repoURL) - assert.NoError(t, err) - s.test(result) - }) - } -} diff --git a/pkg/commands/pull_request_windows_test.go b/pkg/commands/pull_request_windows_test.go deleted file mode 100644 index 73e91f157..000000000 --- a/pkg/commands/pull_request_windows_test.go +++ /dev/null @@ -1,256 +0,0 @@ -//go:build windows -// +build windows - -package commands - -import ( - "os/exec" - "strings" - "testing" - - "github.com/jesseduffield/lazygit/pkg/commands/git_config" - "github.com/jesseduffield/lazygit/pkg/secureexec" - "github.com/stretchr/testify/assert" -) - -// TestCreatePullRequestOnWindows is a function. -func TestCreatePullRequestOnWindows(t *testing.T) { - type scenario struct { - testName string - from string - to string - remoteUrl string - command func(string, ...string) *exec.Cmd - test func(url string, err error) - } - - scenarios := []scenario{ - { - testName: "Opens a link to new pull request on bitbucket", - from: "feature/profile-page", - remoteUrl: "git@bitbucket.org:johndoe/social_network.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page^&t=1"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url) - }, - }, - { - testName: "Opens a link to new pull request on bitbucket with http remote url", - from: "feature/events", - remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events^&t=1"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url) - }, - }, - { - testName: "Opens a link to new pull request on github", - from: "feature/sum-operation", - remoteUrl: "git@github.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@github.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url) - }, - }, - { - testName: "Opens a link to new pull request on bitbucket with specific target branch", - from: "feature/profile-page/avatar", - to: "feature/profile-page", - remoteUrl: "git@bitbucket.org:johndoe/social_network.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar^&dest=feature/profile-page^&t=1"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url) - }, - }, - { - testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch", - from: "feature/remote-events", - to: "feature/events", - remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events^&dest=feature/events^&t=1"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url) - }, - }, - { - testName: "Opens a link to new pull request on github with specific target branch", - from: "feature/sum-operation", - to: "feature/operations", - remoteUrl: "git@github.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@github.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url) - }, - }, - { - testName: "Opens a link to new pull request on gitlab", - from: "feature/ui", - remoteUrl: "git@gitlab.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url) - }, - }, - { - testName: "Opens a link to new pull request on gitlab in nested groups", - from: "feature/ui", - remoteUrl: "git@gitlab.com:peter/public/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url) - }, - }, - { - testName: "Opens a link to new pull request on gitlab with specific target branch", - from: "feature/commit-ui", - to: "epic/ui", - remoteUrl: "git@gitlab.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url) - }, - }, - { - testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups", - from: "feature/commit-ui", - to: "epic/ui", - remoteUrl: "git@gitlab.com:peter/public/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - // Handle git remote url call - if strings.HasPrefix(cmd, "git") { - return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git") - } - - assert.Equal(t, cmd, "cmd") - assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"}) - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.NoError(t, err) - assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url) - }, - }, - { - testName: "Throws an error if git service is unsupported", - from: "feature/divide-operation", - remoteUrl: "git@something.com:peter/calculator.git", - command: func(cmd string, args ...string) *exec.Cmd { - return secureexec.Command("echo") - }, - test: func(url string, err error) { - assert.Error(t, err) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - gitCommand := NewDummyGitCommand() - gitCommand.OSCommand.Command = s.command - gitCommand.OSCommand.Platform.OS = "windows" - gitCommand.OSCommand.Platform.Shell = "cmd" - gitCommand.OSCommand.Platform.ShellArg = "/c" - gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = `start "" {{link}}` - gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{ - // valid configuration for a custom service URL - "git.work.com": "gitlab:code.work.com", - // invalid configurations for a custom service URL - "invalid.work.com": "noservice:invalid.work.com", - "noservice.work.com": "noservice.work.com", - } - gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl}) - dummyPullRequest := NewPullRequest(gitCommand) - s.test(dummyPullRequest.Create(s.from, s.to)) - }) - } -} diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index 487084e97..dee321979 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -1,6 +1,7 @@ package gui import ( + "errors" "fmt" "strings" @@ -104,13 +105,24 @@ func (gui *Gui) handleCreatePullRequestMenu() error { } func (gui *Gui) handleCopyPullRequestURLPress() error { - pullRequest := commands.NewPullRequest(gui.GitCommand) + hostingServiceMgr := gui.getHostingServiceMgr() branch := gui.getSelectedBranch() - url, err := pullRequest.CopyURL(branch.Name, "") + + branchExistsOnRemote := gui.GitCommand.CheckRemoteBranchExists(branch.Name) + + if !branchExistsOnRemote { + return gui.surfaceError(errors.New(gui.Tr.NoBranchOnRemote)) + } + + url, err := hostingServiceMgr.GetPullRequestURL(branch.Name, "") if err != nil { return gui.surfaceError(err) } + if err := gui.GitCommand.OSCommand.CopyToClipboard(url); err != nil { + return gui.surfaceError(err) + } + gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Copying to clipboard: '%s'", url), "Copy URL", false)) gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard) diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 3beec3d28..a606ce952 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -804,11 +804,17 @@ func (gui *Gui) handleOpenCommitInBrowser() error { return nil } - pullRequest := commands.NewPullRequest(gui.GitCommand) - url, err := pullRequest.OpenCommitInBrowser(commit.Sha) + hostingServiceMgr := gui.getHostingServiceMgr() + + url, err := hostingServiceMgr.GetCommitURL(commit.Sha) if err != nil { return gui.surfaceError(err) } + + if err := gui.GitCommand.OSCommand.OpenLink(url); err != nil { + return gui.surfaceError(err) + } + gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf(gui.Tr.OpeningCommitInBrowser, url), gui.Tr.CreatePullRequest, false)) return nil diff --git a/pkg/gui/pull_request_menu_panel.go b/pkg/gui/pull_request_menu_panel.go index dd59f99b6..cabaa6a96 100644 --- a/pkg/gui/pull_request_menu_panel.go +++ b/pkg/gui/pull_request_menu_panel.go @@ -3,7 +3,7 @@ package gui import ( "fmt" - "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/hosting_service" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" ) @@ -56,12 +56,23 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB } func (gui *Gui) createPullRequest(from string, to string) error { - pullRequest := commands.NewPullRequest(gui.GitCommand) - url, err := pullRequest.CreatePullRequest(from, to) + hostingServiceMgr := gui.getHostingServiceMgr() + url, err := hostingServiceMgr.GetPullRequestURL(from, to) if err != nil { return gui.surfaceError(err) } + + if err := gui.GitCommand.OSCommand.OpenLink(url); err != nil { + return gui.surfaceError(err) + } + gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf(gui.Tr.CreatingPullRequestAtUrl, url), gui.Tr.CreatePullRequest, false)) return nil } + +func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr { + remoteUrl := gui.GitCommand.GetRemoteURL() + configServices := gui.Config.GetUserConfig().Services + return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices) +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index aa2af0a3a..d3ed9af15 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -559,7 +559,8 @@ Thanks for using lazygit! Seriously you rock. Three things to share with you: Or even just star the repo to share the love! ` -func englishTranslationSet() TranslationSet { +// exporting this so we can use it in tests +func EnglishTranslationSet() TranslationSet { return TranslationSet{ NotEnoughSpace: "Not enough space to render panels", DiffTitle: "Diff", diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index 8f6452c54..f6c57da8d 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -33,7 +33,7 @@ func NewTranslationSetFromConfig(log *logrus.Entry, configLanguage string) (*Tra func NewTranslationSet(log *logrus.Entry, language string) *TranslationSet { log.Info("language: " + language) - baseSet := englishTranslationSet() + baseSet := EnglishTranslationSet() for languageCode, translationSet := range GetTranslationSets() { if strings.HasPrefix(language, languageCode) { @@ -48,7 +48,7 @@ func GetTranslationSets() map[string]TranslationSet { return map[string]TranslationSet{ "pl": polishTranslationSet(), "nl": dutchTranslationSet(), - "en": englishTranslationSet(), + "en": EnglishTranslationSet(), "zh": chineseTranslationSet(), } } diff --git a/pkg/test/log.go b/pkg/test/log.go new file mode 100644 index 000000000..3b166bb5d --- /dev/null +++ b/pkg/test/log.go @@ -0,0 +1,41 @@ +package test + +import ( + "fmt" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +var ( + _ logrus.FieldLogger = &FakeFieldLogger{} +) + +// for now we're just tracking calls to the Error and Errorf methods +type FakeFieldLogger struct { + loggedErrors []string + *logrus.Entry +} + +func (self *FakeFieldLogger) Error(args ...interface{}) { + if len(args) != 1 { + panic("Expected exactly one argument to FakeFieldLogger.Error") + } + + switch arg := args[0].(type) { + case error: + self.loggedErrors = append(self.loggedErrors, arg.Error()) + case string: + self.loggedErrors = append(self.loggedErrors, arg) + } +} + +func (self *FakeFieldLogger) Errorf(format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + self.loggedErrors = append(self.loggedErrors, msg) +} + +func (self *FakeFieldLogger) AssertErrors(t *testing.T, expectedErrors []string) { + assert.EqualValues(t, expectedErrors, self.loggedErrors) +}