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 }