github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/gits/provider.go (about)

     1  package gits
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/olli-ai/jx/v2/pkg/auth"
    12  	"github.com/olli-ai/jx/v2/pkg/util"
    13  	"github.com/pkg/errors"
    14  	"gopkg.in/AlecAivazis/survey.v1"
    15  )
    16  
    17  type GitOrganisation struct {
    18  	Login string
    19  }
    20  
    21  type GitRepository struct {
    22  	ID               int64
    23  	Name             string
    24  	AllowMergeCommit bool
    25  	HTMLURL          string
    26  	CloneURL         string
    27  	SSHURL           string
    28  	Language         string
    29  	Fork             bool
    30  	Stars            int
    31  	URL              string
    32  	Scheme           string
    33  	Host             string
    34  	Organisation     string
    35  	Project          string
    36  	Private          bool
    37  	HasIssues        bool
    38  	OpenIssueCount   int
    39  	HasWiki          bool
    40  	HasProjects      bool
    41  	Archived         bool
    42  }
    43  
    44  type GitPullRequest struct {
    45  	URL                string
    46  	Author             *GitUser
    47  	Owner              string
    48  	Repo               string
    49  	Number             *int
    50  	Mergeable          *bool
    51  	Merged             *bool
    52  	HeadRef            *string
    53  	State              *string
    54  	StatusesURL        *string
    55  	IssueURL           *string
    56  	DiffURL            *string
    57  	MergeCommitSHA     *string
    58  	ClosedAt           *time.Time
    59  	MergedAt           *time.Time
    60  	LastCommitSha      string
    61  	Title              string
    62  	Body               string
    63  	Assignees          []*GitUser
    64  	RequestedReviewers []*GitUser
    65  	Labels             []*Label
    66  	UpdatedAt          *time.Time
    67  	HeadOwner          *string // HeadOwner is the string the PR is created from
    68  }
    69  
    70  // Label represents a label on an Issue
    71  type Label struct {
    72  	ID          *int64
    73  	URL         *string
    74  	Name        *string
    75  	Color       *string
    76  	Description *string
    77  	Default     *bool
    78  }
    79  
    80  type GitCommit struct {
    81  	SHA       string
    82  	Message   string
    83  	Author    *GitUser
    84  	URL       string
    85  	Branch    string
    86  	Committer *GitUser
    87  }
    88  
    89  type ListCommitsArguments struct {
    90  	SHA     string
    91  	Path    string
    92  	Author  string
    93  	Since   time.Time
    94  	Until   time.Time
    95  	Page    int
    96  	PerPage int
    97  }
    98  
    99  type GitIssue struct {
   100  	URL           string
   101  	Owner         string
   102  	Repo          string
   103  	Number        *int
   104  	Key           string
   105  	Title         string
   106  	Body          string
   107  	State         *string
   108  	Labels        []GitLabel
   109  	StatusesURL   *string
   110  	IssueURL      *string
   111  	CreatedAt     *time.Time
   112  	UpdatedAt     *time.Time
   113  	ClosedAt      *time.Time
   114  	IsPullRequest bool
   115  	User          *GitUser
   116  	ClosedBy      *GitUser
   117  	Assignees     []GitUser
   118  }
   119  
   120  type GitUser struct {
   121  	URL       string
   122  	Login     string
   123  	Name      string
   124  	Email     string
   125  	AvatarURL string
   126  }
   127  
   128  type GitRelease struct {
   129  	ID            int64
   130  	Name          string
   131  	TagName       string
   132  	Body          string
   133  	PreRelease    bool
   134  	URL           string
   135  	HTMLURL       string
   136  	DownloadCount int
   137  	Assets        *[]GitReleaseAsset
   138  }
   139  
   140  // GitReleaseAsset represents a release stored in Git
   141  type GitReleaseAsset struct {
   142  	ID                 int64
   143  	BrowserDownloadURL string
   144  	Name               string
   145  	ContentType        string
   146  }
   147  
   148  type GitLabel struct {
   149  	URL   string
   150  	Name  string
   151  	Color string
   152  }
   153  
   154  type GitRepoStatus struct {
   155  	ID      string
   156  	Context string
   157  	URL     string
   158  
   159  	// State is the current state of the repository. Possible values are:
   160  	// pending, success, error, or failure.
   161  	State string `json:"state,omitempty"`
   162  
   163  	// TargetURL is the URL of the page representing this status
   164  	TargetURL string `json:"target_url,omitempty"`
   165  
   166  	// Description is a short high level summary of the status.
   167  	Description string
   168  }
   169  
   170  type GitPullRequestArguments struct {
   171  	Title         string
   172  	Body          string
   173  	Head          string
   174  	Base          string
   175  	GitRepository *GitRepository
   176  	Labels        []string
   177  }
   178  
   179  func (a *GitPullRequestArguments) String() string {
   180  	return fmt.Sprintf("Title: %s; Body: %s; Head: %s; Base: %s; Labels: %s; Git Repo: %s", a.Title, a.Body, a.Head, a.Base, strings.Join(a.Labels, ", "), a.GitRepository.URL)
   181  }
   182  
   183  type GitWebHookArguments struct {
   184  	ID          int64
   185  	Owner       string
   186  	Repo        *GitRepository
   187  	URL         string
   188  	ExistingURL string
   189  	Secret      string
   190  	InsecureSSL bool
   191  }
   192  
   193  type GitFileContent struct {
   194  	Type        string
   195  	Encoding    string
   196  	Size        int
   197  	Name        string
   198  	Path        string
   199  	Content     string
   200  	Sha         string
   201  	Url         string
   202  	GitUrl      string
   203  	HtmlUrl     string
   204  	DownloadUrl string
   205  }
   206  
   207  // GitBranch is info on a git branch including the commit at the tip
   208  type GitBranch struct {
   209  	Name      string
   210  	Commit    *GitCommit
   211  	Protected bool
   212  }
   213  
   214  // PullRequestInfo describes a pull request that has been created
   215  type PullRequestInfo struct {
   216  	GitProvider          GitProvider
   217  	PullRequest          *GitPullRequest
   218  	PullRequestArguments *GitPullRequestArguments
   219  }
   220  
   221  // GitProject is a project for managing issues
   222  type GitProject struct {
   223  	Name        string
   224  	Description string
   225  	Number      int
   226  	State       string
   227  }
   228  
   229  // IsClosed returns true if the PullRequest has been closed
   230  func (pr *GitPullRequest) IsClosed() bool {
   231  	return pr.ClosedAt != nil
   232  }
   233  
   234  // NumberString returns the string representation of the Pull Request number or blank if its missing
   235  func (pr *GitPullRequest) NumberString() string {
   236  	n := pr.Number
   237  	if n == nil {
   238  		return ""
   239  	}
   240  	return "#" + strconv.Itoa(*n)
   241  }
   242  
   243  // ShortSha returns short SHA of the commit.
   244  func (c *GitCommit) ShortSha() string {
   245  	shortLen := 9
   246  	if len(c.SHA) < shortLen+1 {
   247  		return c.SHA
   248  	}
   249  	return c.SHA[:shortLen]
   250  }
   251  
   252  // Subject returns the subject line of the commit message.
   253  func (c *GitCommit) Subject() string {
   254  	lines := strings.Split(c.Message, "\n")
   255  	return lines[0]
   256  }
   257  
   258  // OneLine returns the commit in the Oneline format
   259  func (c *GitCommit) OneLine() string {
   260  	return fmt.Sprintf("%s %s", c.ShortSha(), c.Subject())
   261  }
   262  
   263  // CreateProvider creates a git provider for the given auth details
   264  func CreateProvider(server *auth.AuthServer, user *auth.UserAuth, git Gitter) (GitProvider, error) {
   265  	if server.Kind == "" {
   266  		server.Kind = SaasGitKind(server.URL)
   267  	}
   268  	if server.Kind == KindBitBucketCloud {
   269  		return NewBitbucketCloudProvider(server, user, git)
   270  	} else if server.Kind == KindBitBucketServer {
   271  		return NewBitbucketServerProvider(server, user, git)
   272  	} else if server.Kind == KindGitea {
   273  		return NewGiteaProvider(server, user, git)
   274  	} else if server.Kind == KindGitlab {
   275  		return NewGitlabProvider(server, user, git)
   276  	} else if server.Kind == KindGitFake {
   277  		return NewFakeProvider(), nil
   278  	} else {
   279  		return NewGitHubProvider(server, user, git)
   280  	}
   281  }
   282  
   283  // GetHost returns the Git Provider hostname, e.g github.com
   284  func GetHost(gitProvider GitProvider) (string, error) {
   285  	if gitProvider == nil {
   286  		return "", fmt.Errorf("no Git provider")
   287  	}
   288  
   289  	if gitProvider.ServerURL() == "" {
   290  		return "", fmt.Errorf("no Git provider server URL found")
   291  	}
   292  	url, err := url.Parse(gitProvider.ServerURL())
   293  	if err != nil {
   294  		return "", fmt.Errorf("error parsing ")
   295  	}
   296  	return url.Host, nil
   297  }
   298  
   299  func ProviderAccessTokenURL(kind string, url string, username string) string {
   300  	switch kind {
   301  	case KindBitBucketCloud:
   302  		// TODO pass in the username
   303  		return BitBucketCloudAccessTokenURL(url, username)
   304  	case KindBitBucketServer:
   305  		return BitBucketServerAccessTokenURL(url)
   306  	case KindGitea:
   307  		return GiteaAccessTokenURL(url)
   308  	case KindGitlab:
   309  		return GitlabAccessTokenURL(url)
   310  	default:
   311  		return GitHubAccessTokenURL(url)
   312  	}
   313  }
   314  
   315  // PickOwner allows to select a potential owner of a repository
   316  func PickOwner(orgLister OrganisationLister, userName string, handles util.IOFileHandles) (string, error) {
   317  	msg := "Who should be the owner of the repository?"
   318  	return pickOwner(orgLister, userName, msg, handles)
   319  }
   320  
   321  // PickOrganisation picks an organisations login if there is one available
   322  func PickOrganisation(orgLister OrganisationLister, userName string, handles util.IOFileHandles) (string, error) {
   323  	msg := "Which organisation do you want to use?"
   324  	return pickOwner(orgLister, userName, msg, handles)
   325  }
   326  
   327  func pickOwner(orgLister OrganisationLister, userName string, message string, handles util.IOFileHandles) (string, error) {
   328  	prompt := &survey.Select{
   329  		Message: message,
   330  		Options: GetOrganizations(orgLister, userName),
   331  		Default: userName,
   332  	}
   333  
   334  	orgName := ""
   335  	surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err)
   336  	err := survey.AskOne(prompt, &orgName, nil, surveyOpts)
   337  	if err != nil {
   338  		return "", err
   339  	}
   340  	if orgName == userName {
   341  		return "", nil
   342  	}
   343  	return orgName, nil
   344  }
   345  
   346  // GetOrganizations gets the organisation
   347  func GetOrganizations(orgLister OrganisationLister, userName string) []string {
   348  	var orgNames []string
   349  	// Always include the username as a pseudo organization
   350  	if userName != "" {
   351  		orgNames = append(orgNames, userName)
   352  	}
   353  
   354  	orgs, _ := orgLister.ListOrganisations()
   355  	for _, o := range orgs {
   356  		if name := o.Login; name != "" {
   357  			orgNames = append(orgNames, name)
   358  		}
   359  	}
   360  	sort.Strings(orgNames)
   361  	return orgNames
   362  }
   363  
   364  func PickRepositories(provider GitProvider, owner string, message string, selectAll bool, filter string, handles util.IOFileHandles) ([]*GitRepository, error) {
   365  	answer := []*GitRepository{}
   366  	repos, err := provider.ListRepositories(owner)
   367  	if err != nil {
   368  		return answer, err
   369  	}
   370  
   371  	repoMap := map[string]*GitRepository{}
   372  	allRepoNames := []string{}
   373  	for _, repo := range repos {
   374  		n := repo.Name
   375  		if n != "" && (filter == "" || strings.Contains(n, filter)) {
   376  			allRepoNames = append(allRepoNames, n)
   377  			repoMap[n] = repo
   378  		}
   379  	}
   380  	if len(allRepoNames) == 0 {
   381  		return answer, fmt.Errorf("No matching repositories could be found!")
   382  	}
   383  	sort.Strings(allRepoNames)
   384  
   385  	prompt := &survey.MultiSelect{
   386  		Message: message,
   387  		Options: allRepoNames,
   388  	}
   389  	if selectAll {
   390  		prompt.Default = allRepoNames
   391  	}
   392  	repoNames := []string{}
   393  	surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err)
   394  	err = survey.AskOne(prompt, &repoNames, nil, surveyOpts)
   395  
   396  	for _, n := range repoNames {
   397  		repo := repoMap[n]
   398  		if repo != nil {
   399  			answer = append(answer, repo)
   400  		}
   401  	}
   402  	return answer, err
   403  }
   404  
   405  // IsGitRepoStatusSuccess returns true if all the statuses are successful
   406  func IsGitRepoStatusSuccess(statuses ...*GitRepoStatus) bool {
   407  	for _, status := range statuses {
   408  		if !status.IsSuccess() {
   409  			return false
   410  		}
   411  	}
   412  	return true
   413  }
   414  
   415  // IsGitRepoStatusFailed returns true if any of the statuses have failed
   416  func IsGitRepoStatusFailed(statuses ...*GitRepoStatus) bool {
   417  	for _, status := range statuses {
   418  		if status.IsFailed() {
   419  			return true
   420  		}
   421  	}
   422  	return false
   423  }
   424  
   425  func (s *GitRepoStatus) IsSuccess() bool {
   426  	return s.State == "success"
   427  }
   428  
   429  func (s *GitRepoStatus) IsFailed() bool {
   430  	return s.State == "error" || s.State == "failure"
   431  }
   432  
   433  // PickOrCreateProvider picks an existing server and auth or creates a new one if required
   434  // then create a GitProvider for it
   435  func (i *GitRepository) PickOrCreateProvider(authConfigSvc auth.ConfigService, message string, batchMode bool, gitKind string, githubAppMode bool, git Gitter, handles util.IOFileHandles) (GitProvider, error) {
   436  	config := authConfigSvc.Config()
   437  	hostUrl := i.HostURLWithoutUser()
   438  	server := config.GetOrCreateServer(hostUrl)
   439  	if server.Kind == "" {
   440  		server.Kind = gitKind
   441  	}
   442  	var userAuth *auth.UserAuth
   443  	var err error
   444  	if githubAppMode && i.Organisation != "" {
   445  		for _, u := range server.Users {
   446  			if i.Organisation == u.GithubAppOwner {
   447  				userAuth = u
   448  				break
   449  			}
   450  		}
   451  	}
   452  	if userAuth == nil {
   453  		userAuth, err = config.PickServerUserAuth(server, message, batchMode, i.Organisation, handles)
   454  		if err != nil {
   455  			return nil, err
   456  		}
   457  	}
   458  	if userAuth.IsInvalid() {
   459  		userAuth, err = createUserForServer(batchMode, userAuth, authConfigSvc, server, git, handles)
   460  	}
   461  	return i.CreateProviderForUser(server, userAuth, gitKind, git)
   462  }
   463  
   464  func (i *GitRepository) CreateProviderForUser(server *auth.AuthServer, user *auth.UserAuth, gitKind string, git Gitter) (GitProvider, error) {
   465  	if i.Host == GitHubHost {
   466  		return NewGitHubProvider(server, user, git)
   467  	}
   468  	if gitKind != "" && server.Kind != gitKind {
   469  		server.Kind = gitKind
   470  	}
   471  	return CreateProvider(server, user, git)
   472  }
   473  
   474  func (i *GitRepository) CreateProvider(inCluster bool, authConfigSvc auth.ConfigService, gitKind string, ghOwner string, git Gitter, batchMode bool, handles util.IOFileHandles) (GitProvider, error) {
   475  	hostUrl := i.HostURLWithoutUser()
   476  	return CreateProviderForURL(inCluster, authConfigSvc, gitKind, hostUrl, ghOwner, git, batchMode, handles)
   477  }
   478  
   479  // ProviderURL returns the git provider URL
   480  func (i *GitRepository) ProviderURL() string {
   481  	scheme := i.Scheme
   482  	if !strings.HasPrefix(scheme, "http") {
   483  		scheme = "https"
   484  	}
   485  	return scheme + "://" + i.Host
   486  }
   487  
   488  // CreateProviderForURL creates the Git provider for the given git kind and host URL
   489  func CreateProviderForURL(inCluster bool, authConfigSvc auth.ConfigService, gitKind string, hostURL string, ghOwner string, git Gitter, batchMode bool,
   490  	handles util.IOFileHandles) (GitProvider, error) {
   491  	config := authConfigSvc.Config()
   492  	server := config.GetOrCreateServer(hostURL)
   493  	if gitKind != "" {
   494  		server.Kind = gitKind
   495  	}
   496  
   497  	var userAuth *auth.UserAuth
   498  	if ghOwner != "" {
   499  		for _, u := range server.Users {
   500  			if ghOwner == u.GithubAppOwner {
   501  				userAuth = u
   502  				break
   503  			}
   504  		}
   505  	} else {
   506  		userAuth = config.CurrentUser(server, inCluster)
   507  	}
   508  	if userAuth != nil && !userAuth.IsInvalid() {
   509  		return CreateProvider(server, userAuth, git)
   510  	}
   511  
   512  	if ghOwner == "" {
   513  		kind := server.Kind
   514  		if kind == "" {
   515  			kind = "GIT"
   516  		}
   517  		userAuthVar := auth.CreateAuthUserFromEnvironment(strings.ToUpper(kind))
   518  		if !userAuthVar.IsInvalid() {
   519  			return CreateProvider(server, &userAuthVar, git)
   520  		}
   521  
   522  		var err error
   523  		userAuth, err = createUserForServer(batchMode, &auth.UserAuth{}, authConfigSvc, server, git, handles)
   524  		if err != nil {
   525  			return nil, errors.Wrapf(err, "creating user for server %q", server.URL)
   526  		}
   527  	}
   528  	if userAuth != nil && !userAuth.IsInvalid() {
   529  		return CreateProvider(server, userAuth, git)
   530  	}
   531  	return nil, fmt.Errorf("no valid git user found for kind %s host %s %s", gitKind, hostURL, ghOwner)
   532  }
   533  
   534  func createUserForServer(batchMode bool, userAuth *auth.UserAuth, authConfigSvc auth.ConfigService, server *auth.AuthServer,
   535  	git Gitter, handles util.IOFileHandles) (*auth.UserAuth, error) {
   536  
   537  	f := func(username string) error {
   538  		git.PrintCreateRepositoryGenerateAccessToken(server, username, handles.Out)
   539  		return nil
   540  	}
   541  
   542  	defaultUserName := ""
   543  	err := authConfigSvc.Config().EditUserAuth(server.Label(), userAuth, defaultUserName, false, batchMode, f, handles)
   544  	if err != nil {
   545  		return userAuth, err
   546  	}
   547  
   548  	err = authConfigSvc.SaveUserAuth(server.URL, userAuth)
   549  	if err != nil {
   550  		return userAuth, fmt.Errorf("failed to store git auth configuration %s", err)
   551  	}
   552  	if userAuth.IsInvalid() {
   553  		return userAuth, fmt.Errorf("you did not properly define the user authentication")
   554  	}
   555  	return userAuth, nil
   556  }
   557  
   558  // ToGitLabels converts the list of label names into an array of GitLabels
   559  func ToGitLabels(names []string) []GitLabel {
   560  	answer := []GitLabel{}
   561  	for _, n := range names {
   562  		answer = append(answer, GitLabel{Name: n})
   563  	}
   564  	return answer
   565  }
   566  
   567  // IsRepoStatusUpToDate takes a provider, an owner, repo, sha, and GitRepoStatus, and checks if there's an existing commit
   568  // status for the owner/repo/sha/context (from the GitRepoStatus) with the GitRepoStatus's status, target URL, and description
   569  func IsRepoStatusUpToDate(provider GitProvider, owner string, repo string, sha string, commitStatus *GitRepoStatus) (bool, error) {
   570  	statuses, err := provider.ListCommitStatus(owner, repo, sha)
   571  	if err != nil {
   572  		return false, errors.Wrapf(err, "fetching commit statuses for %s/%s, sha %s", owner, repo, sha)
   573  	}
   574  	for _, existingStatus := range statuses {
   575  		if existingStatus != nil && existingStatus.Context == commitStatus.Context {
   576  			if existingStatus.State == commitStatus.State &&
   577  				existingStatus.TargetURL == commitStatus.TargetURL &&
   578  				existingStatus.Description == commitStatus.Description {
   579  				return true, nil
   580  			}
   581  		}
   582  	}
   583  	return false, nil
   584  }