code.gitea.io/gitea@v1.21.7/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 414 labels := make([]*base.Label, 0, len(issue.Labels)) 415 for i := range issue.Labels { 416 labels = append(labels, g.convertGiteaLabel(issue.Labels[i])) 417 } 418 419 var milestone string 420 if issue.Milestone != nil { 421 milestone = issue.Milestone.Title 422 } 423 424 reactions, err := g.getIssueReactions(issue.Index) 425 if err != nil { 426 WarnAndNotice("Unable to load reactions during migrating issue #%d in %s. Error: %v", issue.Index, g, err) 427 } 428 429 var assignees []string 430 for i := range issue.Assignees { 431 assignees = append(assignees, issue.Assignees[i].UserName) 432 } 433 434 allIssues = append(allIssues, &base.Issue{ 435 Title: issue.Title, 436 Number: issue.Index, 437 PosterID: issue.Poster.ID, 438 PosterName: issue.Poster.UserName, 439 PosterEmail: issue.Poster.Email, 440 Content: issue.Body, 441 Milestone: milestone, 442 State: string(issue.State), 443 Created: issue.Created, 444 Updated: issue.Updated, 445 Closed: issue.Closed, 446 Reactions: reactions, 447 Labels: labels, 448 Assignees: assignees, 449 IsLocked: issue.IsLocked, 450 ForeignIndex: issue.Index, 451 }) 452 } 453 454 isEnd := len(issues) < perPage 455 if !g.pagination { 456 isEnd = len(issues) == 0 457 } 458 return allIssues, isEnd, nil 459 } 460 461 // GetComments returns comments according issueNumber 462 func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { 463 allComments := make([]*base.Comment, 0, g.maxPerPage) 464 465 for i := 1; ; i++ { 466 // make sure gitea can shutdown gracefully 467 select { 468 case <-g.ctx.Done(): 469 return nil, false, nil 470 default: 471 } 472 473 comments, _, err := g.client.ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex(), gitea_sdk.ListIssueCommentOptions{ListOptions: gitea_sdk.ListOptions{ 474 PageSize: g.maxPerPage, 475 Page: i, 476 }}) 477 if err != nil { 478 return nil, false, fmt.Errorf("error while listing comments for issue #%d. Error: %w", commentable.GetForeignIndex(), err) 479 } 480 481 for _, comment := range comments { 482 reactions, err := g.getCommentReactions(comment.ID) 483 if err != nil { 484 WarnAndNotice("Unable to load comment reactions during migrating issue #%d for comment %d in %s. Error: %v", commentable.GetForeignIndex(), comment.ID, g, err) 485 } 486 487 allComments = append(allComments, &base.Comment{ 488 IssueIndex: commentable.GetLocalIndex(), 489 Index: comment.ID, 490 PosterID: comment.Poster.ID, 491 PosterName: comment.Poster.UserName, 492 PosterEmail: comment.Poster.Email, 493 Content: comment.Body, 494 Created: comment.Created, 495 Updated: comment.Updated, 496 Reactions: reactions, 497 }) 498 } 499 500 if !g.pagination || len(comments) < g.maxPerPage { 501 break 502 } 503 } 504 return allComments, true, nil 505 } 506 507 // GetPullRequests returns pull requests according page and perPage 508 func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { 509 if perPage > g.maxPerPage { 510 perPage = g.maxPerPage 511 } 512 allPRs := make([]*base.PullRequest, 0, perPage) 513 514 prs, _, err := g.client.ListRepoPullRequests(g.repoOwner, g.repoName, gitea_sdk.ListPullRequestsOptions{ 515 ListOptions: gitea_sdk.ListOptions{ 516 Page: page, 517 PageSize: perPage, 518 }, 519 State: gitea_sdk.StateAll, 520 }) 521 if err != nil { 522 return nil, false, fmt.Errorf("error while listing pull requests (page: %d, pagesize: %d). Error: %w", page, perPage, err) 523 } 524 for _, pr := range prs { 525 var milestone string 526 if pr.Milestone != nil { 527 milestone = pr.Milestone.Title 528 } 529 530 labels := make([]*base.Label, 0, len(pr.Labels)) 531 for i := range pr.Labels { 532 labels = append(labels, g.convertGiteaLabel(pr.Labels[i])) 533 } 534 535 var ( 536 headUserName string 537 headRepoName string 538 headCloneURL string 539 headRef string 540 headSHA string 541 ) 542 if pr.Head != nil { 543 if pr.Head.Repository != nil { 544 headUserName = pr.Head.Repository.Owner.UserName 545 headRepoName = pr.Head.Repository.Name 546 headCloneURL = pr.Head.Repository.CloneURL 547 } 548 headSHA = pr.Head.Sha 549 headRef = pr.Head.Ref 550 } 551 552 var mergeCommitSHA string 553 if pr.MergedCommitID != nil { 554 mergeCommitSHA = *pr.MergedCommitID 555 } 556 557 reactions, err := g.getIssueReactions(pr.Index) 558 if err != nil { 559 WarnAndNotice("Unable to load reactions during migrating pull #%d in %s. Error: %v", pr.Index, g, err) 560 } 561 562 var assignees []string 563 for i := range pr.Assignees { 564 assignees = append(assignees, pr.Assignees[i].UserName) 565 } 566 567 createdAt := time.Time{} 568 if pr.Created != nil { 569 createdAt = *pr.Created 570 } 571 updatedAt := time.Time{} 572 if pr.Created != nil { 573 updatedAt = *pr.Updated 574 } 575 576 closedAt := pr.Closed 577 if pr.Merged != nil && closedAt == nil { 578 closedAt = pr.Merged 579 } 580 581 allPRs = append(allPRs, &base.PullRequest{ 582 Title: pr.Title, 583 Number: pr.Index, 584 PosterID: pr.Poster.ID, 585 PosterName: pr.Poster.UserName, 586 PosterEmail: pr.Poster.Email, 587 Content: pr.Body, 588 State: string(pr.State), 589 Created: createdAt, 590 Updated: updatedAt, 591 Closed: closedAt, 592 Labels: labels, 593 Milestone: milestone, 594 Reactions: reactions, 595 Assignees: assignees, 596 Merged: pr.HasMerged, 597 MergedTime: pr.Merged, 598 MergeCommitSHA: mergeCommitSHA, 599 IsLocked: pr.IsLocked, 600 PatchURL: pr.PatchURL, 601 Head: base.PullRequestBranch{ 602 Ref: headRef, 603 SHA: headSHA, 604 RepoName: headRepoName, 605 OwnerName: headUserName, 606 CloneURL: headCloneURL, 607 }, 608 Base: base.PullRequestBranch{ 609 Ref: pr.Base.Ref, 610 SHA: pr.Base.Sha, 611 RepoName: g.repoName, 612 OwnerName: g.repoOwner, 613 }, 614 ForeignIndex: pr.Index, 615 }) 616 // SECURITY: Ensure that the PR is safe 617 _ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g) 618 } 619 620 isEnd := len(prs) < perPage 621 if !g.pagination { 622 isEnd = len(prs) == 0 623 } 624 return allPRs, isEnd, nil 625 } 626 627 // GetReviews returns pull requests review 628 func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { 629 if err := g.client.CheckServerVersionConstraint(">=1.12"); err != nil { 630 log.Info("GiteaDownloader: instance to old, skip GetReviews") 631 return nil, nil 632 } 633 634 allReviews := make([]*base.Review, 0, g.maxPerPage) 635 636 for i := 1; ; i++ { 637 // make sure gitea can shutdown gracefully 638 select { 639 case <-g.ctx.Done(): 640 return nil, nil 641 default: 642 } 643 644 prl, _, err := g.client.ListPullReviews(g.repoOwner, g.repoName, reviewable.GetForeignIndex(), gitea_sdk.ListPullReviewsOptions{ListOptions: gitea_sdk.ListOptions{ 645 Page: i, 646 PageSize: g.maxPerPage, 647 }}) 648 if err != nil { 649 return nil, err 650 } 651 652 for _, pr := range prl { 653 if pr.Reviewer == nil { 654 // 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. 655 // TODO: handle team reviews 656 continue 657 } 658 659 rcl, _, err := g.client.ListPullReviewComments(g.repoOwner, g.repoName, reviewable.GetForeignIndex(), pr.ID) 660 if err != nil { 661 return nil, err 662 } 663 var reviewComments []*base.ReviewComment 664 for i := range rcl { 665 line := int(rcl[i].LineNum) 666 if rcl[i].OldLineNum > 0 { 667 line = int(rcl[i].OldLineNum) * -1 668 } 669 670 reviewComments = append(reviewComments, &base.ReviewComment{ 671 ID: rcl[i].ID, 672 Content: rcl[i].Body, 673 TreePath: rcl[i].Path, 674 DiffHunk: rcl[i].DiffHunk, 675 Line: line, 676 CommitID: rcl[i].CommitID, 677 PosterID: rcl[i].Reviewer.ID, 678 CreatedAt: rcl[i].Created, 679 UpdatedAt: rcl[i].Updated, 680 }) 681 } 682 683 review := &base.Review{ 684 ID: pr.ID, 685 IssueIndex: reviewable.GetLocalIndex(), 686 ReviewerID: pr.Reviewer.ID, 687 ReviewerName: pr.Reviewer.UserName, 688 Official: pr.Official, 689 CommitID: pr.CommitID, 690 Content: pr.Body, 691 CreatedAt: pr.Submitted, 692 State: string(pr.State), 693 Comments: reviewComments, 694 } 695 696 allReviews = append(allReviews, review) 697 } 698 699 if len(prl) < g.maxPerPage { 700 break 701 } 702 } 703 return allReviews, nil 704 }