mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Compile them only once at startup. I didn't measure if this makes a difference, but it's easy to do, and now that we potentially need to check them more often, it might be worth it.
279 lines
8.9 KiB
Go
279 lines
8.9 KiB
Go
package hosting_service
|
|
|
|
import (
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, gitea, 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(url.QueryEscape(from)), nil
|
|
}
|
|
return gitService.getPullRequestURLIntoTargetBranch(url.QueryEscape(from), url.QueryEscape(to)), nil
|
|
}
|
|
|
|
func (self *HostingServiceMgr) GetCommitURL(commitHash string) (string, error) {
|
|
gitService, err := self.getService()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pullRequestURL := gitService.getCommitURL(commitHash)
|
|
|
|
return pullRequestURL, nil
|
|
}
|
|
|
|
// e.g. 'jesseduffield/lazygit'
|
|
func (self *HostingServiceMgr) GetRepoName() (string, error) {
|
|
gitService, err := self.getService()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
repoName := gitService.repoName
|
|
|
|
return repoName, nil
|
|
}
|
|
|
|
// ServiceInfo holds the resolved hosting service for a remote URL. Owner
|
|
// comes from the "owner" named regex capture, which only exists for
|
|
// owner/repo-shaped providers (github, gitlab, bitbucket, gitea, codeberg);
|
|
// it's empty for azuredevops and bitbucketServer, whose URLs are organised
|
|
// differently. Repository is populated for every provider, but RepoName may
|
|
// have more than two segments (e.g. "org/project/repo" for azuredevops).
|
|
type ServiceInfo struct {
|
|
Provider string // e.g. "github"
|
|
WebDomain string // e.g. "github.com", or "git.acme.com" for an on-prem instance
|
|
Owner string // e.g. "jesseduffield"
|
|
Repository string // e.g. "lazygit"
|
|
RepoName string // e.g. "jesseduffield/lazygit"
|
|
}
|
|
|
|
// GetServiceInfo identifies which hosting service the configured remote URL
|
|
// belongs to and returns enough information to talk to its web/API host.
|
|
func (self *HostingServiceMgr) GetServiceInfo() (ServiceInfo, error) {
|
|
serviceDomain, err := self.getServiceDomain(self.remoteURL)
|
|
if err != nil {
|
|
return ServiceInfo{}, err
|
|
}
|
|
|
|
matches, err := serviceDomain.serviceDefinition.parseRemoteUrl(self.remoteURL)
|
|
if err != nil {
|
|
return ServiceInfo{}, err
|
|
}
|
|
|
|
return ServiceInfo{
|
|
Provider: serviceDomain.serviceDefinition.provider,
|
|
WebDomain: serviceDomain.webDomain,
|
|
Owner: matches["owner"],
|
|
Repository: matches["repo"],
|
|
RepoName: utils.ResolvePlaceholderString(serviceDomain.serviceDefinition.repoNameTemplate, matches),
|
|
}, nil
|
|
}
|
|
|
|
func (self *HostingServiceMgr) getService() (*Service, error) {
|
|
serviceDomain, err := self.getServiceDomain(self.remoteURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repoURL, err := serviceDomain.serviceDefinition.getRepoURLFromRemoteURL(self.remoteURL, serviceDomain.webDomain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repoName, err := serviceDomain.serviceDefinition.getRepoNameFromRemoteURL(self.remoteURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Service{
|
|
repoURL: repoURL,
|
|
repoName: repoName,
|
|
ServiceDefinition: serviceDomain.serviceDefinition,
|
|
}, nil
|
|
}
|
|
|
|
func (self *HostingServiceMgr) getServiceDomain(repoURL string) (*ServiceDomain, error) {
|
|
candidateServiceDomains := self.getCandidateServiceDomains()
|
|
|
|
for _, serviceDomain := range candidateServiceDomains {
|
|
if strings.Contains(repoURL, serviceDomain.gitDomain) {
|
|
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
|
|
}
|
|
|
|
serviceDomains := slices.Clone(defaultServiceDomains)
|
|
|
|
for gitDomain, typeAndDomain := range self.configServiceDomains {
|
|
provider, webDomain, success := strings.Cut(typeAndDomain, ":")
|
|
|
|
// we allow for one ':' for specifying the TCP port
|
|
if !success || strings.Count(webDomain, ":") > 1 {
|
|
self.log.Errorf("Unexpected format for git service: '%s'. Expected something like 'github.com:github.com'", typeAndDomain)
|
|
continue
|
|
}
|
|
|
|
serviceDefinition, ok := serviceDefinitionByProvider[provider]
|
|
if !ok {
|
|
providerNames := lo.Map(serviceDefinitions, func(serviceDefinition ServiceDefinition, _ int) string {
|
|
return 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
|
|
}
|
|
|
|
type ServiceDefinition struct {
|
|
provider string
|
|
pullRequestURLIntoDefaultBranch string
|
|
pullRequestURLIntoTargetBranch string
|
|
commitURL string
|
|
urlRegexps []*regexp.Regexp
|
|
|
|
// can expect 'webdomain' to be passed in. Otherwise, you get to pick what we match in the regex
|
|
repoURLTemplate string
|
|
repoNameTemplate string
|
|
}
|
|
|
|
func (self ServiceDefinition) getRepoURLFromRemoteURL(url string, webDomain string) (string, error) {
|
|
matches, err := self.parseRemoteUrl(url)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
matches["webDomain"] = webDomain
|
|
return utils.ResolvePlaceholderString(self.repoURLTemplate, matches), nil
|
|
}
|
|
|
|
func (self ServiceDefinition) getRepoNameFromRemoteURL(url string) (string, error) {
|
|
matches, err := self.parseRemoteUrl(url)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return utils.ResolvePlaceholderString(self.repoNameTemplate, matches), nil
|
|
}
|
|
|
|
func (self ServiceDefinition) parseRemoteUrl(url string) (map[string]string, error) {
|
|
for _, re := range self.urlRegexps {
|
|
matches := utils.FindNamedMatches(re, url)
|
|
if matches != nil {
|
|
return matches, nil
|
|
}
|
|
}
|
|
|
|
return nil, errors.New("Failed to parse repo information from url")
|
|
}
|
|
|
|
// RepoInformation holds the owner and repository name parsed from a remote URL.
|
|
type RepoInformation struct {
|
|
Owner string
|
|
Repository string
|
|
}
|
|
|
|
// GetRepoInfoFromURL parses a remote URL (SSH or HTTPS) and extracts the
|
|
// owner and repository name using the default URL regex patterns.
|
|
func GetRepoInfoFromURL(url string) (RepoInformation, error) {
|
|
for _, re := range defaultUrlRegexps {
|
|
matches := utils.FindNamedMatches(re, url)
|
|
if matches != nil {
|
|
return RepoInformation{
|
|
Owner: matches["owner"],
|
|
Repository: matches["repo"],
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return RepoInformation{}, errors.New("Failed to parse repo information from url")
|
|
}
|
|
|
|
type Service struct {
|
|
repoURL string
|
|
// e.g. 'jesseduffield/lazygit'
|
|
repoName 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(commitHash string) string {
|
|
return self.resolveUrl(self.commitURL, map[string]string{"CommitHash": commitHash})
|
|
}
|
|
|
|
func (self *Service) resolveUrl(templateString string, args map[string]string) string {
|
|
return self.repoURL + utils.ResolvePlaceholderString(templateString, args)
|
|
}
|