code.gitea.io/gitea@v1.22.3/services/migrations/gitlab.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package migrations 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 "path" 14 "regexp" 15 "strings" 16 "time" 17 18 issues_model "code.gitea.io/gitea/models/issues" 19 "code.gitea.io/gitea/modules/container" 20 "code.gitea.io/gitea/modules/log" 21 base "code.gitea.io/gitea/modules/migration" 22 "code.gitea.io/gitea/modules/structs" 23 24 "github.com/xanzy/go-gitlab" 25 ) 26 27 var ( 28 _ base.Downloader = &GitlabDownloader{} 29 _ base.DownloaderFactory = &GitlabDownloaderFactory{} 30 ) 31 32 func init() { 33 RegisterDownloaderFactory(&GitlabDownloaderFactory{}) 34 } 35 36 // GitlabDownloaderFactory defines a gitlab downloader factory 37 type GitlabDownloaderFactory struct{} 38 39 // New returns a Downloader related to this factory according MigrateOptions 40 func (f *GitlabDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { 41 u, err := url.Parse(opts.CloneAddr) 42 if err != nil { 43 return nil, err 44 } 45 46 baseURL := u.Scheme + "://" + u.Host 47 repoNameSpace := strings.TrimPrefix(u.Path, "/") 48 repoNameSpace = strings.TrimSuffix(repoNameSpace, ".git") 49 50 log.Trace("Create gitlab downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace) 51 52 return NewGitlabDownloader(ctx, baseURL, repoNameSpace, opts.AuthUsername, opts.AuthPassword, opts.AuthToken) 53 } 54 55 // GitServiceType returns the type of git service 56 func (f *GitlabDownloaderFactory) GitServiceType() structs.GitServiceType { 57 return structs.GitlabService 58 } 59 60 type gitlabIIDResolver struct { 61 maxIssueIID int64 62 frozen bool 63 } 64 65 func (r *gitlabIIDResolver) recordIssueIID(issueIID int) { 66 if r.frozen { 67 panic("cannot record issue IID after pull request IID generation has started") 68 } 69 r.maxIssueIID = max(r.maxIssueIID, int64(issueIID)) 70 } 71 72 func (r *gitlabIIDResolver) generatePullRequestNumber(mrIID int) int64 { 73 r.frozen = true 74 return r.maxIssueIID + int64(mrIID) 75 } 76 77 // GitlabDownloader implements a Downloader interface to get repository information 78 // from gitlab via go-gitlab 79 // - issueCount is incremented in GetIssues() to ensure PR and Issue numbers do not overlap, 80 // because Gitlab has individual Issue and Pull Request numbers. 81 type GitlabDownloader struct { 82 base.NullDownloader 83 ctx context.Context 84 client *gitlab.Client 85 baseURL string 86 repoID int 87 repoName string 88 iidResolver gitlabIIDResolver 89 maxPerPage int 90 } 91 92 // NewGitlabDownloader creates a gitlab Downloader via gitlab API 93 // 94 // Use either a username/password, personal token entered into the username field, or anonymous/public access 95 // Note: Public access only allows very basic access 96 func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, password, token string) (*GitlabDownloader, error) { 97 gitlabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient())) 98 // Only use basic auth if token is blank and password is NOT 99 // Basic auth will fail with empty strings, but empty token will allow anonymous public API usage 100 if token == "" && password != "" { 101 gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient())) 102 } 103 104 if err != nil { 105 log.Trace("Error logging into gitlab: %v", err) 106 return nil, err 107 } 108 109 // split namespace and subdirectory 110 pathParts := strings.Split(strings.Trim(repoPath, "/"), "/") 111 var resp *gitlab.Response 112 u, _ := url.Parse(baseURL) 113 for len(pathParts) >= 2 { 114 _, resp, err = gitlabClient.Version.GetVersion() 115 if err == nil || resp != nil && resp.StatusCode == http.StatusUnauthorized { 116 err = nil // if no authentication given, this still should work 117 break 118 } 119 120 u.Path = path.Join(u.Path, pathParts[0]) 121 baseURL = u.String() 122 pathParts = pathParts[1:] 123 _ = gitlab.WithBaseURL(baseURL)(gitlabClient) 124 repoPath = strings.Join(pathParts, "/") 125 } 126 if err != nil { 127 log.Trace("Error could not get gitlab version: %v", err) 128 return nil, err 129 } 130 131 log.Trace("gitlab downloader: use BaseURL: '%s' and RepoPath: '%s'", baseURL, repoPath) 132 133 // Grab and store project/repo ID here, due to issues using the URL escaped path 134 gr, _, err := gitlabClient.Projects.GetProject(repoPath, nil, nil, gitlab.WithContext(ctx)) 135 if err != nil { 136 log.Trace("Error retrieving project: %v", err) 137 return nil, err 138 } 139 140 if gr == nil { 141 log.Trace("Error getting project, project is nil") 142 return nil, errors.New("Error getting project, project is nil") 143 } 144 145 return &GitlabDownloader{ 146 ctx: ctx, 147 client: gitlabClient, 148 baseURL: baseURL, 149 repoID: gr.ID, 150 repoName: gr.Name, 151 maxPerPage: 100, 152 }, nil 153 } 154 155 // String implements Stringer 156 func (g *GitlabDownloader) String() string { 157 return fmt.Sprintf("migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName) 158 } 159 160 func (g *GitlabDownloader) LogString() string { 161 if g == nil { 162 return "<GitlabDownloader nil>" 163 } 164 return fmt.Sprintf("<GitlabDownloader %s [%d]/%s>", g.baseURL, g.repoID, g.repoName) 165 } 166 167 // SetContext set context 168 func (g *GitlabDownloader) SetContext(ctx context.Context) { 169 g.ctx = ctx 170 } 171 172 // GetRepoInfo returns a repository information 173 func (g *GitlabDownloader) GetRepoInfo() (*base.Repository, error) { 174 gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx)) 175 if err != nil { 176 return nil, err 177 } 178 179 var private bool 180 switch gr.Visibility { 181 case gitlab.InternalVisibility: 182 private = true 183 case gitlab.PrivateVisibility: 184 private = true 185 } 186 187 var owner string 188 if gr.Owner == nil { 189 log.Trace("gr.Owner is nil, trying to get owner from Namespace") 190 if gr.Namespace != nil && gr.Namespace.Kind == "user" { 191 owner = gr.Namespace.Path 192 } 193 } else { 194 owner = gr.Owner.Username 195 } 196 197 // convert gitlab repo to stand Repo 198 return &base.Repository{ 199 Owner: owner, 200 Name: gr.Name, 201 IsPrivate: private, 202 Description: gr.Description, 203 OriginalURL: gr.WebURL, 204 CloneURL: gr.HTTPURLToRepo, 205 DefaultBranch: gr.DefaultBranch, 206 }, nil 207 } 208 209 // GetTopics return gitlab topics 210 func (g *GitlabDownloader) GetTopics() ([]string, error) { 211 gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx)) 212 if err != nil { 213 return nil, err 214 } 215 return gr.TagList, err 216 } 217 218 // GetMilestones returns milestones 219 func (g *GitlabDownloader) GetMilestones() ([]*base.Milestone, error) { 220 perPage := g.maxPerPage 221 state := "all" 222 milestones := make([]*base.Milestone, 0, perPage) 223 for i := 1; ; i++ { 224 ms, _, err := g.client.Milestones.ListMilestones(g.repoID, &gitlab.ListMilestonesOptions{ 225 State: &state, 226 ListOptions: gitlab.ListOptions{ 227 Page: i, 228 PerPage: perPage, 229 }, 230 }, nil, gitlab.WithContext(g.ctx)) 231 if err != nil { 232 return nil, err 233 } 234 235 for _, m := range ms { 236 var desc string 237 if m.Description != "" { 238 desc = m.Description 239 } 240 state := "open" 241 var closedAt *time.Time 242 if m.State != "" { 243 state = m.State 244 if state == "closed" { 245 closedAt = m.UpdatedAt 246 } 247 } 248 249 var deadline *time.Time 250 if m.DueDate != nil { 251 deadlineParsed, err := time.Parse("2006-01-02", m.DueDate.String()) 252 if err != nil { 253 log.Trace("Error parsing Milestone DueDate time") 254 deadline = nil 255 } else { 256 deadline = &deadlineParsed 257 } 258 } 259 260 milestones = append(milestones, &base.Milestone{ 261 Title: m.Title, 262 Description: desc, 263 Deadline: deadline, 264 State: state, 265 Created: *m.CreatedAt, 266 Updated: m.UpdatedAt, 267 Closed: closedAt, 268 }) 269 } 270 if len(ms) < perPage { 271 break 272 } 273 } 274 return milestones, nil 275 } 276 277 func (g *GitlabDownloader) normalizeColor(val string) string { 278 val = strings.TrimLeft(val, "#") 279 val = strings.ToLower(val) 280 if len(val) == 3 { 281 c := []rune(val) 282 val = fmt.Sprintf("%c%c%c%c%c%c", c[0], c[0], c[1], c[1], c[2], c[2]) 283 } 284 if len(val) != 6 { 285 return "" 286 } 287 return val 288 } 289 290 // GetLabels returns labels 291 func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) { 292 perPage := g.maxPerPage 293 labels := make([]*base.Label, 0, perPage) 294 for i := 1; ; i++ { 295 ls, _, err := g.client.Labels.ListLabels(g.repoID, &gitlab.ListLabelsOptions{ListOptions: gitlab.ListOptions{ 296 Page: i, 297 PerPage: perPage, 298 }}, nil, gitlab.WithContext(g.ctx)) 299 if err != nil { 300 return nil, err 301 } 302 for _, label := range ls { 303 baseLabel := &base.Label{ 304 Name: label.Name, 305 Color: g.normalizeColor(label.Color), 306 Description: label.Description, 307 } 308 labels = append(labels, baseLabel) 309 } 310 if len(ls) < perPage { 311 break 312 } 313 } 314 return labels, nil 315 } 316 317 func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Release { 318 var zero int 319 r := &base.Release{ 320 TagName: rel.TagName, 321 TargetCommitish: rel.Commit.ID, 322 Name: rel.Name, 323 Body: rel.Description, 324 Created: *rel.CreatedAt, 325 PublisherID: int64(rel.Author.ID), 326 PublisherName: rel.Author.Username, 327 } 328 329 httpClient := NewMigrationHTTPClient() 330 331 for k, asset := range rel.Assets.Links { 332 assetID := asset.ID // Don't optimize this, for closure we need a local variable 333 r.Assets = append(r.Assets, &base.ReleaseAsset{ 334 ID: int64(asset.ID), 335 Name: asset.Name, 336 ContentType: &rel.Assets.Sources[k].Format, 337 Size: &zero, 338 DownloadCount: &zero, 339 DownloadFunc: func() (io.ReadCloser, error) { 340 link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(g.ctx)) 341 if err != nil { 342 return nil, err 343 } 344 345 if !hasBaseURL(link.URL, g.baseURL) { 346 WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, link.URL) 347 return io.NopCloser(strings.NewReader(link.URL)), nil 348 } 349 350 req, err := http.NewRequest("GET", link.URL, nil) 351 if err != nil { 352 return nil, err 353 } 354 req = req.WithContext(g.ctx) 355 resp, err := httpClient.Do(req) 356 if err != nil { 357 return nil, err 358 } 359 360 // resp.Body is closed by the uploader 361 return resp.Body, nil 362 }, 363 }) 364 } 365 return r 366 } 367 368 // GetReleases returns releases 369 func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) { 370 perPage := g.maxPerPage 371 releases := make([]*base.Release, 0, perPage) 372 for i := 1; ; i++ { 373 ls, _, err := g.client.Releases.ListReleases(g.repoID, &gitlab.ListReleasesOptions{ 374 ListOptions: gitlab.ListOptions{ 375 Page: i, 376 PerPage: perPage, 377 }, 378 }, nil, gitlab.WithContext(g.ctx)) 379 if err != nil { 380 return nil, err 381 } 382 383 for _, release := range ls { 384 releases = append(releases, g.convertGitlabRelease(release)) 385 } 386 if len(ls) < perPage { 387 break 388 } 389 } 390 return releases, nil 391 } 392 393 type gitlabIssueContext struct { 394 IsMergeRequest bool 395 } 396 397 // GetIssues returns issues according start and limit 398 // 399 // Note: issue label description and colors are not supported by the go-gitlab library at this time 400 func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { 401 state := "all" 402 sort := "asc" 403 404 if perPage > g.maxPerPage { 405 perPage = g.maxPerPage 406 } 407 408 opt := &gitlab.ListProjectIssuesOptions{ 409 State: &state, 410 Sort: &sort, 411 ListOptions: gitlab.ListOptions{ 412 PerPage: perPage, 413 Page: page, 414 }, 415 } 416 417 allIssues := make([]*base.Issue, 0, perPage) 418 419 issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil, gitlab.WithContext(g.ctx)) 420 if err != nil { 421 return nil, false, fmt.Errorf("error while listing issues: %w", err) 422 } 423 for _, issue := range issues { 424 labels := make([]*base.Label, 0, len(issue.Labels)) 425 for _, l := range issue.Labels { 426 labels = append(labels, &base.Label{ 427 Name: l, 428 }) 429 } 430 431 var milestone string 432 if issue.Milestone != nil { 433 milestone = issue.Milestone.Title 434 } 435 436 var reactions []*gitlab.AwardEmoji 437 awardPage := 1 438 for { 439 awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx)) 440 if err != nil { 441 return nil, false, fmt.Errorf("error while listing issue awards: %w", err) 442 } 443 444 reactions = append(reactions, awards...) 445 446 if len(awards) < perPage { 447 break 448 } 449 450 awardPage++ 451 } 452 453 allIssues = append(allIssues, &base.Issue{ 454 Title: issue.Title, 455 Number: int64(issue.IID), 456 PosterID: int64(issue.Author.ID), 457 PosterName: issue.Author.Username, 458 Content: issue.Description, 459 Milestone: milestone, 460 State: issue.State, 461 Created: *issue.CreatedAt, 462 Labels: labels, 463 Reactions: g.awardsToReactions(reactions), 464 Closed: issue.ClosedAt, 465 IsLocked: issue.DiscussionLocked, 466 Updated: *issue.UpdatedAt, 467 ForeignIndex: int64(issue.IID), 468 Context: gitlabIssueContext{IsMergeRequest: false}, 469 }) 470 471 // record the issue IID, to be used in GetPullRequests() 472 g.iidResolver.recordIssueIID(issue.IID) 473 } 474 475 return allIssues, len(issues) < perPage, nil 476 } 477 478 // GetComments returns comments according issueNumber 479 // TODO: figure out how to transfer comment reactions 480 func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { 481 context, ok := commentable.GetContext().(gitlabIssueContext) 482 if !ok { 483 return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext()) 484 } 485 486 allComments := make([]*base.Comment, 0, g.maxPerPage) 487 488 page := 1 489 490 for { 491 var comments []*gitlab.Discussion 492 var resp *gitlab.Response 493 var err error 494 if !context.IsMergeRequest { 495 comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListIssueDiscussionsOptions{ 496 Page: page, 497 PerPage: g.maxPerPage, 498 }, nil, gitlab.WithContext(g.ctx)) 499 } else { 500 comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListMergeRequestDiscussionsOptions{ 501 Page: page, 502 PerPage: g.maxPerPage, 503 }, nil, gitlab.WithContext(g.ctx)) 504 } 505 506 if err != nil { 507 return nil, false, fmt.Errorf("error while listing comments: %v %w", g.repoID, err) 508 } 509 for _, comment := range comments { 510 for _, note := range comment.Notes { 511 allComments = append(allComments, g.convertNoteToComment(commentable.GetLocalIndex(), note)) 512 } 513 } 514 if resp.NextPage == 0 { 515 break 516 } 517 page = resp.NextPage 518 } 519 520 page = 1 521 for { 522 var stateEvents []*gitlab.StateEvent 523 var resp *gitlab.Response 524 var err error 525 if context.IsMergeRequest { 526 stateEvents, resp, err = g.client.ResourceStateEvents.ListMergeStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{ 527 ListOptions: gitlab.ListOptions{ 528 Page: page, 529 PerPage: g.maxPerPage, 530 }, 531 }, nil, gitlab.WithContext(g.ctx)) 532 } else { 533 stateEvents, resp, err = g.client.ResourceStateEvents.ListIssueStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{ 534 ListOptions: gitlab.ListOptions{ 535 Page: page, 536 PerPage: g.maxPerPage, 537 }, 538 }, nil, gitlab.WithContext(g.ctx)) 539 } 540 if err != nil { 541 return nil, false, fmt.Errorf("error while listing state events: %v %w", g.repoID, err) 542 } 543 544 for _, stateEvent := range stateEvents { 545 comment := &base.Comment{ 546 IssueIndex: commentable.GetLocalIndex(), 547 Index: int64(stateEvent.ID), 548 PosterID: int64(stateEvent.User.ID), 549 PosterName: stateEvent.User.Username, 550 Content: "", 551 Created: *stateEvent.CreatedAt, 552 } 553 switch stateEvent.State { 554 case gitlab.ClosedEventType: 555 comment.CommentType = issues_model.CommentTypeClose.String() 556 case gitlab.MergedEventType: 557 comment.CommentType = issues_model.CommentTypeMergePull.String() 558 case gitlab.ReopenedEventType: 559 comment.CommentType = issues_model.CommentTypeReopen.String() 560 default: 561 // Ignore other event types 562 continue 563 } 564 allComments = append(allComments, comment) 565 } 566 567 if resp.NextPage == 0 { 568 break 569 } 570 page = resp.NextPage 571 } 572 573 return allComments, true, nil 574 } 575 576 var targetBranchChangeRegexp = regexp.MustCompile("^changed target branch from `(.*?)` to `(.*?)`$") 577 578 func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.Note) *base.Comment { 579 comment := &base.Comment{ 580 IssueIndex: localIndex, 581 Index: int64(note.ID), 582 PosterID: int64(note.Author.ID), 583 PosterName: note.Author.Username, 584 PosterEmail: note.Author.Email, 585 Content: note.Body, 586 Created: *note.CreatedAt, 587 Meta: map[string]any{}, 588 } 589 590 // Try to find the underlying event of system notes. 591 if note.System { 592 if match := targetBranchChangeRegexp.FindStringSubmatch(note.Body); match != nil { 593 comment.CommentType = issues_model.CommentTypeChangeTargetBranch.String() 594 comment.Meta["OldRef"] = match[1] 595 comment.Meta["NewRef"] = match[2] 596 } else if strings.HasPrefix(note.Body, "enabled an automatic merge") { 597 comment.CommentType = issues_model.CommentTypePRScheduledToAutoMerge.String() 598 } else if note.Body == "canceled the automatic merge" { 599 comment.CommentType = issues_model.CommentTypePRUnScheduledToAutoMerge.String() 600 } 601 } 602 603 return comment 604 } 605 606 // GetPullRequests returns pull requests according page and perPage 607 func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { 608 if perPage > g.maxPerPage { 609 perPage = g.maxPerPage 610 } 611 612 view := "simple" 613 opt := &gitlab.ListProjectMergeRequestsOptions{ 614 ListOptions: gitlab.ListOptions{ 615 PerPage: perPage, 616 Page: page, 617 }, 618 View: &view, 619 } 620 621 allPRs := make([]*base.PullRequest, 0, perPage) 622 623 prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(g.ctx)) 624 if err != nil { 625 return nil, false, fmt.Errorf("error while listing merge requests: %w", err) 626 } 627 for _, simplePR := range prs { 628 // Load merge request again by itself, as not all fields are populated in the ListProjectMergeRequests endpoint. 629 // See https://gitlab.com/gitlab-org/gitlab/-/issues/29620 630 pr, _, err := g.client.MergeRequests.GetMergeRequest(g.repoID, simplePR.IID, nil) 631 if err != nil { 632 return nil, false, fmt.Errorf("error while loading merge request: %w", err) 633 } 634 635 labels := make([]*base.Label, 0, len(pr.Labels)) 636 for _, l := range pr.Labels { 637 labels = append(labels, &base.Label{ 638 Name: l, 639 }) 640 } 641 642 var merged bool 643 if pr.State == "merged" { 644 merged = true 645 pr.State = "closed" 646 } 647 648 mergeTime := pr.MergedAt 649 if merged && pr.MergedAt == nil { 650 mergeTime = pr.UpdatedAt 651 } 652 653 closeTime := pr.ClosedAt 654 if merged && pr.ClosedAt == nil { 655 closeTime = pr.UpdatedAt 656 } 657 658 mergeCommitSHA := pr.MergeCommitSHA 659 if mergeCommitSHA == "" { 660 mergeCommitSHA = pr.SquashCommitSHA 661 } 662 663 var locked bool 664 if pr.State == "locked" { 665 locked = true 666 } 667 668 var milestone string 669 if pr.Milestone != nil { 670 milestone = pr.Milestone.Title 671 } 672 673 var reactions []*gitlab.AwardEmoji 674 awardPage := 1 675 for { 676 awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx)) 677 if err != nil { 678 return nil, false, fmt.Errorf("error while listing merge requests awards: %w", err) 679 } 680 681 reactions = append(reactions, awards...) 682 683 if len(awards) < perPage { 684 break 685 } 686 687 awardPage++ 688 } 689 690 // Generate new PR Numbers by the known Issue Numbers, because they share the same number space in Gitea, but they are independent in Gitlab 691 newPRNumber := g.iidResolver.generatePullRequestNumber(pr.IID) 692 693 allPRs = append(allPRs, &base.PullRequest{ 694 Title: pr.Title, 695 Number: newPRNumber, 696 PosterName: pr.Author.Username, 697 PosterID: int64(pr.Author.ID), 698 Content: pr.Description, 699 Milestone: milestone, 700 State: pr.State, 701 Created: *pr.CreatedAt, 702 Closed: closeTime, 703 Labels: labels, 704 Merged: merged, 705 MergeCommitSHA: mergeCommitSHA, 706 MergedTime: mergeTime, 707 IsLocked: locked, 708 Reactions: g.awardsToReactions(reactions), 709 Head: base.PullRequestBranch{ 710 Ref: pr.SourceBranch, 711 SHA: pr.SHA, 712 RepoName: g.repoName, 713 OwnerName: pr.Author.Username, 714 CloneURL: pr.WebURL, 715 }, 716 Base: base.PullRequestBranch{ 717 Ref: pr.TargetBranch, 718 SHA: pr.DiffRefs.BaseSha, 719 RepoName: g.repoName, 720 OwnerName: pr.Author.Username, 721 }, 722 PatchURL: pr.WebURL + ".patch", 723 ForeignIndex: int64(pr.IID), 724 Context: gitlabIssueContext{IsMergeRequest: true}, 725 }) 726 727 // SECURITY: Ensure that the PR is safe 728 _ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g) 729 } 730 731 return allPRs, len(prs) < perPage, nil 732 } 733 734 // GetReviews returns pull requests review 735 func (g *GitlabDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { 736 approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(reviewable.GetForeignIndex()), gitlab.WithContext(g.ctx)) 737 if err != nil { 738 if resp != nil && resp.StatusCode == http.StatusNotFound { 739 log.Error(fmt.Sprintf("GitlabDownloader: while migrating a error occurred: '%s'", err.Error())) 740 return []*base.Review{}, nil 741 } 742 return nil, err 743 } 744 745 var createdAt time.Time 746 if approvals.CreatedAt != nil { 747 createdAt = *approvals.CreatedAt 748 } else if approvals.UpdatedAt != nil { 749 createdAt = *approvals.UpdatedAt 750 } else { 751 createdAt = time.Now() 752 } 753 754 reviews := make([]*base.Review, 0, len(approvals.ApprovedBy)) 755 for _, user := range approvals.ApprovedBy { 756 reviews = append(reviews, &base.Review{ 757 IssueIndex: reviewable.GetLocalIndex(), 758 ReviewerID: int64(user.User.ID), 759 ReviewerName: user.User.Username, 760 CreatedAt: createdAt, 761 // All we get are approvals 762 State: base.ReviewStateApproved, 763 }) 764 } 765 766 return reviews, nil 767 } 768 769 func (g *GitlabDownloader) awardsToReactions(awards []*gitlab.AwardEmoji) []*base.Reaction { 770 result := make([]*base.Reaction, 0, len(awards)) 771 uniqCheck := make(container.Set[string]) 772 for _, award := range awards { 773 uid := fmt.Sprintf("%s%d", award.Name, award.User.ID) 774 if uniqCheck.Add(uid) { 775 result = append(result, &base.Reaction{ 776 UserID: int64(award.User.ID), 777 UserName: award.User.Username, 778 Content: award.Name, 779 }) 780 } 781 } 782 return result 783 }