code.gitea.io/gitea@v1.22.3/services/migrations/gitea_downloader.go (about) 1 // Copyright 2020 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 "strings" 14 "time" 15 16 "code.gitea.io/gitea/modules/log" 17 base "code.gitea.io/gitea/modules/migration" 18 "code.gitea.io/gitea/modules/structs" 19 20 gitea_sdk "code.gitea.io/sdk/gitea" 21 ) 22 23 var ( 24 _ base.Downloader = &GiteaDownloader{} 25 _ base.DownloaderFactory = &GiteaDownloaderFactory{} 26 ) 27 28 func init() { 29 RegisterDownloaderFactory(&GiteaDownloaderFactory{}) 30 } 31 32 // GiteaDownloaderFactory defines a gitea downloader factory 33 type GiteaDownloaderFactory struct{} 34 35 // New returns a Downloader related to this factory according MigrateOptions 36 func (f *GiteaDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { 37 u, err := url.Parse(opts.CloneAddr) 38 if err != nil { 39 return nil, err 40 } 41 42 baseURL := u.Scheme + "://" + u.Host 43 repoNameSpace := strings.TrimPrefix(u.Path, "/") 44 repoNameSpace = strings.TrimSuffix(repoNameSpace, ".git") 45 46 path := strings.Split(repoNameSpace, "/") 47 if len(path) < 2 { 48 return nil, fmt.Errorf("invalid path: %s", repoNameSpace) 49 } 50 51 repoPath := strings.Join(path[len(path)-2:], "/") 52 if len(path) > 2 { 53 subPath := strings.Join(path[:len(path)-2], "/") 54 baseURL += "/" + subPath 55 } 56 57 log.Trace("Create gitea downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace) 58 59 return NewGiteaDownloader(ctx, baseURL, repoPath, opts.AuthUsername, opts.AuthPassword, opts.AuthToken) 60 } 61 62 // GitServiceType returns the type of git service 63 func (f *GiteaDownloaderFactory) GitServiceType() structs.GitServiceType { 64 return structs.GiteaService 65 } 66 67 // GiteaDownloader implements a Downloader interface to get repository information's 68 type GiteaDownloader struct { 69 base.NullDownloader 70 ctx context.Context 71 client *gitea_sdk.Client 72 baseURL string 73 repoOwner string 74 repoName string 75 pagination bool 76 maxPerPage int 77 } 78 79 // NewGiteaDownloader creates a gitea Downloader via gitea API 80 // 81 // Use either a username/password or personal token. token is preferred 82 // Note: Public access only allows very basic access 83 func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, password, token string) (*GiteaDownloader, error) { 84 giteaClient, err := gitea_sdk.NewClient( 85 baseURL, 86 gitea_sdk.SetToken(token), 87 gitea_sdk.SetBasicAuth(username, password), 88 gitea_sdk.SetContext(ctx), 89 gitea_sdk.SetHTTPClient(NewMigrationHTTPClient()), 90 ) 91 if err != nil { 92 log.Error(fmt.Sprintf("Failed to create NewGiteaDownloader for: %s. Error: %v", baseURL, err)) 93 return nil, err 94 } 95 96 path := strings.Split(repoPath, "/") 97 98 paginationSupport := true 99 if err = giteaClient.CheckServerVersionConstraint(">=1.12"); err != nil { 100 paginationSupport = false 101 } 102 103 // set small maxPerPage since we can only guess 104 // (default would be 50 but this can differ) 105 maxPerPage := 10 106 // gitea instances >=1.13 can tell us what maximum they have 107 apiConf, _, err := giteaClient.GetGlobalAPISettings() 108 if err != nil { 109 log.Info("Unable to get global API settings. Ignoring these.") 110 log.Debug("giteaClient.GetGlobalAPISettings. Error: %v", err) 111 } 112 if apiConf != nil { 113 maxPerPage = apiConf.MaxResponseItems 114 } 115 116 return &GiteaDownloader{ 117 ctx: ctx, 118 client: giteaClient, 119 baseURL: baseURL, 120 repoOwner: path[0], 121 repoName: path[1], 122 pagination: paginationSupport, 123 maxPerPage: maxPerPage, 124 }, nil 125 } 126 127 // SetContext set context 128 func (g *GiteaDownloader) SetContext(ctx context.Context) { 129 g.ctx = ctx 130 } 131 132 // String implements Stringer 133 func (g *GiteaDownloader) String() string { 134 return fmt.Sprintf("migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) 135 } 136 137 func (g *GiteaDownloader) LogString() string { 138 if g == nil { 139 return "<GiteaDownloader nil>" 140 } 141 return fmt.Sprintf("<GiteaDownloader %s %s/%s>", g.baseURL, g.repoOwner, g.repoName) 142 } 143 144 // GetRepoInfo returns a repository information 145 func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) { 146 if g == nil { 147 return nil, errors.New("error: GiteaDownloader is nil") 148 } 149 150 repo, _, err := g.client.GetRepo(g.repoOwner, g.repoName) 151 if err != nil { 152 return nil, err 153 } 154 155 return &base.Repository{ 156 Name: repo.Name, 157 Owner: repo.Owner.UserName, 158 IsPrivate: repo.Private, 159 Description: repo.Description, 160 CloneURL: repo.CloneURL, 161 OriginalURL: repo.HTMLURL, 162 DefaultBranch: repo.DefaultBranch, 163 }, nil 164 } 165 166 // GetTopics return gitea topics 167 func (g *GiteaDownloader) GetTopics() ([]string, error) { 168 topics, _, err := g.client.ListRepoTopics(g.repoOwner, g.repoName, gitea_sdk.ListRepoTopicsOptions{}) 169 return topics, err 170 } 171 172 // GetMilestones returns milestones 173 func (g *GiteaDownloader) GetMilestones() ([]*base.Milestone, error) { 174 milestones := make([]*base.Milestone, 0, g.maxPerPage) 175 176 for i := 1; ; i++ { 177 // make sure gitea can shutdown gracefully 178 select { 179 case <-g.ctx.Done(): 180 return nil, nil 181 default: 182 } 183 184 ms, _, err := g.client.ListRepoMilestones(g.repoOwner, g.repoName, gitea_sdk.ListMilestoneOption{ 185 ListOptions: gitea_sdk.ListOptions{ 186 PageSize: g.maxPerPage, 187 Page: i, 188 }, 189 State: gitea_sdk.StateAll, 190 }) 191 if err != nil { 192 return nil, err 193 } 194 195 for i := range ms { 196 // old gitea instances dont have this information 197 createdAT := time.Time{} 198 var updatedAT *time.Time 199 if ms[i].Closed != nil { 200 createdAT = *ms[i].Closed 201 updatedAT = ms[i].Closed 202 } 203 204 // new gitea instances (>=1.13) do 205 if !ms[i].Created.IsZero() { 206 createdAT = ms[i].Created 207 } 208 if ms[i].Updated != nil && !ms[i].Updated.IsZero() { 209 updatedAT = ms[i].Updated 210 } 211 212 milestones = append(milestones, &base.Milestone{ 213 Title: ms[i].Title, 214 Description: ms[i].Description, 215 Deadline: ms[i].Deadline, 216 Created: createdAT, 217 Updated: updatedAT, 218 Closed: ms[i].Closed, 219 State: string(ms[i].State), 220 }) 221 } 222 if !g.pagination || len(ms) < g.maxPerPage { 223 break 224 } 225 } 226 return milestones, nil 227 } 228 229 func (g *GiteaDownloader) convertGiteaLabel(label *gitea_sdk.Label) *base.Label { 230 return &base.Label{ 231 Name: label.Name, 232 Color: label.Color, 233 Description: label.Description, 234 } 235 } 236 237 // GetLabels returns labels 238 func (g *GiteaDownloader) GetLabels() ([]*base.Label, error) { 239 labels := make([]*base.Label, 0, g.maxPerPage) 240 241 for i := 1; ; i++ { 242 // make sure gitea can shutdown gracefully 243 select { 244 case <-g.ctx.Done(): 245 return nil, nil 246 default: 247 } 248 249 ls, _, err := g.client.ListRepoLabels(g.repoOwner, g.repoName, gitea_sdk.ListLabelsOptions{ListOptions: gitea_sdk.ListOptions{ 250 PageSize: g.maxPerPage, 251 Page: i, 252 }}) 253 if err != nil { 254 return nil, err 255 } 256 257 for i := range ls { 258 labels = append(labels, g.convertGiteaLabel(ls[i])) 259 } 260 if !g.pagination || len(ls) < g.maxPerPage { 261 break 262 } 263 } 264 return labels, nil 265 } 266 267 func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Release { 268 r := &base.Release{ 269 TagName: rel.TagName, 270 TargetCommitish: rel.Target, 271 Name: rel.Title, 272 Body: rel.Note, 273 Draft: rel.IsDraft, 274 Prerelease: rel.IsPrerelease, 275 PublisherID: rel.Publisher.ID, 276 PublisherName: rel.Publisher.UserName, 277 PublisherEmail: rel.Publisher.Email, 278 Published: rel.PublishedAt, 279 Created: rel.CreatedAt, 280 } 281 282 httpClient := NewMigrationHTTPClient() 283 284 for _, asset := range rel.Attachments { 285 assetID := asset.ID // Don't optimize this, for closure we need a local variable 286 assetDownloadURL := asset.DownloadURL 287 size := int(asset.Size) 288 dlCount := int(asset.DownloadCount) 289 r.Assets = append(r.Assets, &base.ReleaseAsset{ 290 ID: asset.ID, 291 Name: asset.Name, 292 Size: &size, 293 DownloadCount: &dlCount, 294 Created: asset.Created, 295 DownloadURL: &asset.DownloadURL, 296 DownloadFunc: func() (io.ReadCloser, error) { 297 asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, assetID) 298 if err != nil { 299 return nil, err 300 } 301 302 if !hasBaseURL(assetDownloadURL, g.baseURL) { 303 WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, assetDownloadURL) 304 return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil 305 } 306 307 // FIXME: for a private download? 308 req, err := http.NewRequest("GET", assetDownloadURL, nil) 309 if err != nil { 310 return nil, err 311 } 312 resp, err := httpClient.Do(req) 313 if err != nil { 314 return nil, err 315 } 316 317 // resp.Body is closed by the uploader 318 return resp.Body, nil 319 }, 320 }) 321 } 322 return r 323 } 324 325 // GetReleases returns releases 326 func (g *GiteaDownloader) GetReleases() ([]*base.Release, error) { 327 releases := make([]*base.Release, 0, g.maxPerPage) 328 329 for i := 1; ; i++ { 330 // make sure gitea can shutdown gracefully 331 select { 332 case <-g.ctx.Done(): 333 return nil, nil 334 default: 335 } 336 337 rl, _, err := g.client.ListReleases(g.repoOwner, g.repoName, gitea_sdk.ListReleasesOptions{ListOptions: gitea_sdk.ListOptions{ 338 PageSize: g.maxPerPage, 339 Page: i, 340 }}) 341 if err != nil { 342 return nil, err 343 } 344 345 for i := range rl { 346 releases = append(releases, g.convertGiteaRelease(rl[i])) 347 } 348 if !g.pagination || len(rl) < g.maxPerPage { 349 break 350 } 351 } 352 return releases, nil 353 } 354 355 func (g *GiteaDownloader) getIssueReactions(index int64) ([]*base.Reaction, error) { 356 var reactions []*base.Reaction 357 if err := g.client.CheckServerVersionConstraint(">=1.11"); err != nil { 358 log.Info("GiteaDownloader: instance to old, skip getIssueReactions") 359 return reactions, nil 360 } 361 rl, _, err := g.client.GetIssueReactions(g.repoOwner, g.repoName, index) 362 if err != nil { 363 return nil, err 364 } 365 366 for _, reaction := range rl { 367 reactions = append(reactions, &base.Reaction{ 368 UserID: reaction.User.ID, 369 UserName: reaction.User.UserName, 370 Content: reaction.Reaction, 371 }) 372 } 373 return reactions, nil 374 } 375 376 func (g *GiteaDownloader) getCommentReactions(commentID int64) ([]*base.Reaction, error) { 377 var reactions []*base.Reaction 378 if err := g.client.CheckServerVersionConstraint(">=1.11"); err != nil { 379 log.Info("GiteaDownloader: instance to old, skip getCommentReactions") 380 return reactions, nil 381 } 382 rl, _, err := g.client.GetIssueCommentReactions(g.repoOwner, g.repoName, commentID) 383 if err != nil { 384 return nil, err 385 } 386 387 for i := range rl { 388 reactions = append(reactions, &base.Reaction{ 389 UserID: rl[i].User.ID, 390 UserName: rl[i].User.UserName, 391 Content: rl[i].Reaction, 392 }) 393 } 394 return reactions, nil 395 } 396 397 // GetIssues returns issues according start and limit 398 func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { 399 if perPage > g.maxPerPage { 400 perPage = g.maxPerPage 401 } 402 allIssues := make([]*base.Issue, 0, perPage) 403 404 issues, _, err := g.client.ListRepoIssues(g.repoOwner, g.repoName, gitea_sdk.ListIssueOption{ 405 ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: perPage}, 406 State: gitea_sdk.StateAll, 407 Type: gitea_sdk.IssueTypeIssue, 408 }) 409 if err != nil { 410 return nil, false, fmt.Errorf("error while listing issues: %w", err) 411 } 412 for _, issue := range issues { 413 labels := make([]*base.Label, 0, len(issue.Labels)) 414 for i := range issue.Labels { 415 labels = append(labels, g.convertGiteaLabel(issue.Labels[i])) 416 } 417 418 var milestone string 419 if issue.Milestone != nil { 420 milestone = issue.Milestone.Title 421 } 422 423 reactions, err := g.getIssueReactions(issue.Index) 424 if err != nil { 425 WarnAndNotice("Unable to load reactions during migrating issue #%d in %s. Error: %v", issue.Index, g, err) 426 } 427 428 var assignees []string 429 for i := range issue.Assignees { 430 assignees = append(assignees, issue.Assignees[i].UserName) 431 } 432 433 allIssues = append(allIssues, &base.Issue{ 434 Title: issue.Title, 435 Number: issue.Index, 436 PosterID: issue.Poster.ID, 437 PosterName: issue.Poster.UserName, 438 PosterEmail: issue.Poster.Email, 439 Content: issue.Body, 440 Milestone: milestone, 441 State: string(issue.State), 442 Created: issue.Created, 443 Updated: issue.Updated, 444 Closed: issue.Closed, 445 Reactions: reactions, 446 Labels: labels, 447 Assignees: assignees, 448 IsLocked: issue.IsLocked, 449 ForeignIndex: issue.Index, 450 }) 451 } 452 453 isEnd := len(issues) < perPage 454 if !g.pagination { 455 isEnd = len(issues) == 0 456 } 457 return allIssues, isEnd, nil 458 } 459 460 // GetComments returns comments according issueNumber 461 func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { 462 allComments := make([]*base.Comment, 0, g.maxPerPage) 463 464 for i := 1; ; i++ { 465 // make sure gitea can shutdown gracefully 466 select { 467 case <-g.ctx.Done(): 468 return nil, false, nil 469 default: 470 } 471 472 comments, _, err := g.client.ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex(), gitea_sdk.ListIssueCommentOptions{ListOptions: gitea_sdk.ListOptions{ 473 PageSize: g.maxPerPage, 474 Page: i, 475 }}) 476 if err != nil { 477 return nil, false, fmt.Errorf("error while listing comments for issue #%d. Error: %w", commentable.GetForeignIndex(), err) 478 } 479 480 for _, comment := range comments { 481 reactions, err := g.getCommentReactions(comment.ID) 482 if err != nil { 483 WarnAndNotice("Unable to load comment reactions during migrating issue #%d for comment %d in %s. Error: %v", commentable.GetForeignIndex(), comment.ID, g, err) 484 } 485 486 allComments = append(allComments, &base.Comment{ 487 IssueIndex: commentable.GetLocalIndex(), 488 Index: comment.ID, 489 PosterID: comment.Poster.ID, 490 PosterName: comment.Poster.UserName, 491 PosterEmail: comment.Poster.Email, 492 Content: comment.Body, 493 Created: comment.Created, 494 Updated: comment.Updated, 495 Reactions: reactions, 496 }) 497 } 498 499 if !g.pagination || len(comments) < g.maxPerPage { 500 break 501 } 502 } 503 return allComments, true, nil 504 } 505 506 // GetPullRequests returns pull requests according page and perPage 507 func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { 508 if perPage > g.maxPerPage { 509 perPage = g.maxPerPage 510 } 511 allPRs := make([]*base.PullRequest, 0, perPage) 512 513 prs, _, err := g.client.ListRepoPullRequests(g.repoOwner, g.repoName, gitea_sdk.ListPullRequestsOptions{ 514 ListOptions: gitea_sdk.ListOptions{ 515 Page: page, 516 PageSize: perPage, 517 }, 518 State: gitea_sdk.StateAll, 519 }) 520 if err != nil { 521 return nil, false, fmt.Errorf("error while listing pull requests (page: %d, pagesize: %d). Error: %w", page, perPage, err) 522 } 523 for _, pr := range prs { 524 var milestone string 525 if pr.Milestone != nil { 526 milestone = pr.Milestone.Title 527 } 528 529 labels := make([]*base.Label, 0, len(pr.Labels)) 530 for i := range pr.Labels { 531 labels = append(labels, g.convertGiteaLabel(pr.Labels[i])) 532 } 533 534 var ( 535 headUserName string 536 headRepoName string 537 headCloneURL string 538 headRef string 539 headSHA string 540 ) 541 if pr.Head != nil { 542 if pr.Head.Repository != nil { 543 headUserName = pr.Head.Repository.Owner.UserName 544 headRepoName = pr.Head.Repository.Name 545 headCloneURL = pr.Head.Repository.CloneURL 546 } 547 headSHA = pr.Head.Sha 548 headRef = pr.Head.Ref 549 } 550 551 var mergeCommitSHA string 552 if pr.MergedCommitID != nil { 553 mergeCommitSHA = *pr.MergedCommitID 554 } 555 556 reactions, err := g.getIssueReactions(pr.Index) 557 if err != nil { 558 WarnAndNotice("Unable to load reactions during migrating pull #%d in %s. Error: %v", pr.Index, g, err) 559 } 560 561 var assignees []string 562 for i := range pr.Assignees { 563 assignees = append(assignees, pr.Assignees[i].UserName) 564 } 565 566 createdAt := time.Time{} 567 if pr.Created != nil { 568 createdAt = *pr.Created 569 } 570 updatedAt := time.Time{} 571 if pr.Created != nil { 572 updatedAt = *pr.Updated 573 } 574 575 closedAt := pr.Closed 576 if pr.Merged != nil && closedAt == nil { 577 closedAt = pr.Merged 578 } 579 580 allPRs = append(allPRs, &base.PullRequest{ 581 Title: pr.Title, 582 Number: pr.Index, 583 PosterID: pr.Poster.ID, 584 PosterName: pr.Poster.UserName, 585 PosterEmail: pr.Poster.Email, 586 Content: pr.Body, 587 State: string(pr.State), 588 Created: createdAt, 589 Updated: updatedAt, 590 Closed: closedAt, 591 Labels: labels, 592 Milestone: milestone, 593 Reactions: reactions, 594 Assignees: assignees, 595 Merged: pr.HasMerged, 596 MergedTime: pr.Merged, 597 MergeCommitSHA: mergeCommitSHA, 598 IsLocked: pr.IsLocked, 599 PatchURL: pr.PatchURL, 600 Head: base.PullRequestBranch{ 601 Ref: headRef, 602 SHA: headSHA, 603 RepoName: headRepoName, 604 OwnerName: headUserName, 605 CloneURL: headCloneURL, 606 }, 607 Base: base.PullRequestBranch{ 608 Ref: pr.Base.Ref, 609 SHA: pr.Base.Sha, 610 RepoName: g.repoName, 611 OwnerName: g.repoOwner, 612 }, 613 ForeignIndex: pr.Index, 614 }) 615 // SECURITY: Ensure that the PR is safe 616 _ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g) 617 } 618 619 isEnd := len(prs) < perPage 620 if !g.pagination { 621 isEnd = len(prs) == 0 622 } 623 return allPRs, isEnd, nil 624 } 625 626 // GetReviews returns pull requests review 627 func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { 628 if err := g.client.CheckServerVersionConstraint(">=1.12"); err != nil { 629 log.Info("GiteaDownloader: instance to old, skip GetReviews") 630 return nil, nil 631 } 632 633 allReviews := make([]*base.Review, 0, g.maxPerPage) 634 635 for i := 1; ; i++ { 636 // make sure gitea can shutdown gracefully 637 select { 638 case <-g.ctx.Done(): 639 return nil, nil 640 default: 641 } 642 643 prl, _, err := g.client.ListPullReviews(g.repoOwner, g.repoName, reviewable.GetForeignIndex(), gitea_sdk.ListPullReviewsOptions{ListOptions: gitea_sdk.ListOptions{ 644 Page: i, 645 PageSize: g.maxPerPage, 646 }}) 647 if err != nil { 648 return nil, err 649 } 650 651 for _, pr := range prl { 652 if pr.Reviewer == nil { 653 // Presumably this is a team review which we cannot migrate at present but we have to skip this review as otherwise the review will be mapped on to an incorrect user. 654 // TODO: handle team reviews 655 continue 656 } 657 658 rcl, _, err := g.client.ListPullReviewComments(g.repoOwner, g.repoName, reviewable.GetForeignIndex(), pr.ID) 659 if err != nil { 660 return nil, err 661 } 662 var reviewComments []*base.ReviewComment 663 for i := range rcl { 664 line := int(rcl[i].LineNum) 665 if rcl[i].OldLineNum > 0 { 666 line = int(rcl[i].OldLineNum) * -1 667 } 668 669 reviewComments = append(reviewComments, &base.ReviewComment{ 670 ID: rcl[i].ID, 671 Content: rcl[i].Body, 672 TreePath: rcl[i].Path, 673 DiffHunk: rcl[i].DiffHunk, 674 Line: line, 675 CommitID: rcl[i].CommitID, 676 PosterID: rcl[i].Reviewer.ID, 677 CreatedAt: rcl[i].Created, 678 UpdatedAt: rcl[i].Updated, 679 }) 680 } 681 682 review := &base.Review{ 683 ID: pr.ID, 684 IssueIndex: reviewable.GetLocalIndex(), 685 ReviewerID: pr.Reviewer.ID, 686 ReviewerName: pr.Reviewer.UserName, 687 Official: pr.Official, 688 CommitID: pr.CommitID, 689 Content: pr.Body, 690 CreatedAt: pr.Submitted, 691 State: string(pr.State), 692 Comments: reviewComments, 693 } 694 695 allReviews = append(allReviews, review) 696 } 697 698 if len(prl) < g.maxPerPage { 699 break 700 } 701 } 702 return allReviews, nil 703 }