code.gitea.io/gitea@v1.22.3/services/migrations/gitea_uploader.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // Copyright 2018 Jonas Franz. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package migrations 6 7 import ( 8 "context" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "strconv" 14 "strings" 15 "time" 16 17 "code.gitea.io/gitea/models" 18 "code.gitea.io/gitea/models/db" 19 issues_model "code.gitea.io/gitea/models/issues" 20 repo_model "code.gitea.io/gitea/models/repo" 21 user_model "code.gitea.io/gitea/models/user" 22 base_module "code.gitea.io/gitea/modules/base" 23 "code.gitea.io/gitea/modules/git" 24 "code.gitea.io/gitea/modules/gitrepo" 25 "code.gitea.io/gitea/modules/label" 26 "code.gitea.io/gitea/modules/log" 27 base "code.gitea.io/gitea/modules/migration" 28 repo_module "code.gitea.io/gitea/modules/repository" 29 "code.gitea.io/gitea/modules/setting" 30 "code.gitea.io/gitea/modules/storage" 31 "code.gitea.io/gitea/modules/structs" 32 "code.gitea.io/gitea/modules/timeutil" 33 "code.gitea.io/gitea/modules/uri" 34 "code.gitea.io/gitea/modules/util" 35 "code.gitea.io/gitea/services/pull" 36 repo_service "code.gitea.io/gitea/services/repository" 37 38 "github.com/google/uuid" 39 ) 40 41 var _ base.Uploader = &GiteaLocalUploader{} 42 43 // GiteaLocalUploader implements an Uploader to gitea sites 44 type GiteaLocalUploader struct { 45 ctx context.Context 46 doer *user_model.User 47 repoOwner string 48 repoName string 49 repo *repo_model.Repository 50 labels map[string]*issues_model.Label 51 milestones map[string]int64 52 issues map[int64]*issues_model.Issue 53 gitRepo *git.Repository 54 prHeadCache map[string]string 55 sameApp bool 56 userMap map[int64]int64 // external user id mapping to user id 57 prCache map[int64]*issues_model.PullRequest 58 gitServiceType structs.GitServiceType 59 } 60 61 // NewGiteaLocalUploader creates an gitea Uploader via gitea API v1 62 func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner, repoName string) *GiteaLocalUploader { 63 return &GiteaLocalUploader{ 64 ctx: ctx, 65 doer: doer, 66 repoOwner: repoOwner, 67 repoName: repoName, 68 labels: make(map[string]*issues_model.Label), 69 milestones: make(map[string]int64), 70 issues: make(map[int64]*issues_model.Issue), 71 prHeadCache: make(map[string]string), 72 userMap: make(map[int64]int64), 73 prCache: make(map[int64]*issues_model.PullRequest), 74 } 75 } 76 77 // MaxBatchInsertSize returns the table's max batch insert size 78 func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int { 79 switch tp { 80 case "issue": 81 return db.MaxBatchInsertSize(new(issues_model.Issue)) 82 case "comment": 83 return db.MaxBatchInsertSize(new(issues_model.Comment)) 84 case "milestone": 85 return db.MaxBatchInsertSize(new(issues_model.Milestone)) 86 case "label": 87 return db.MaxBatchInsertSize(new(issues_model.Label)) 88 case "release": 89 return db.MaxBatchInsertSize(new(repo_model.Release)) 90 case "pullrequest": 91 return db.MaxBatchInsertSize(new(issues_model.PullRequest)) 92 } 93 return 10 94 } 95 96 // CreateRepo creates a repository 97 func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error { 98 owner, err := user_model.GetUserByName(g.ctx, g.repoOwner) 99 if err != nil { 100 return err 101 } 102 103 var r *repo_model.Repository 104 if opts.MigrateToRepoID <= 0 { 105 r, err = repo_service.CreateRepositoryDirectly(g.ctx, g.doer, owner, repo_service.CreateRepoOptions{ 106 Name: g.repoName, 107 Description: repo.Description, 108 OriginalURL: repo.OriginalURL, 109 GitServiceType: opts.GitServiceType, 110 IsPrivate: opts.Private || setting.Repository.ForcePrivate, 111 IsMirror: opts.Mirror, 112 Status: repo_model.RepositoryBeingMigrated, 113 }) 114 } else { 115 r, err = repo_model.GetRepositoryByID(g.ctx, opts.MigrateToRepoID) 116 } 117 if err != nil { 118 return err 119 } 120 r.DefaultBranch = repo.DefaultBranch 121 r.Description = repo.Description 122 123 r, err = repo_service.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{ 124 RepoName: g.repoName, 125 Description: repo.Description, 126 OriginalURL: repo.OriginalURL, 127 GitServiceType: opts.GitServiceType, 128 Mirror: repo.IsMirror, 129 LFS: opts.LFS, 130 LFSEndpoint: opts.LFSEndpoint, 131 CloneAddr: repo.CloneURL, // SECURITY: we will assume that this has already been checked 132 Private: repo.IsPrivate, 133 Wiki: opts.Wiki, 134 Releases: opts.Releases, // if didn't get releases, then sync them from tags 135 MirrorInterval: opts.MirrorInterval, 136 }, NewMigrationHTTPTransport()) 137 138 g.sameApp = strings.HasPrefix(repo.OriginalURL, setting.AppURL) 139 g.repo = r 140 if err != nil { 141 return err 142 } 143 g.gitRepo, err = gitrepo.OpenRepository(g.ctx, g.repo) 144 if err != nil { 145 return err 146 } 147 148 // detect object format from git repository and update to database 149 objectFormat, err := g.gitRepo.GetObjectFormat() 150 if err != nil { 151 return err 152 } 153 g.repo.ObjectFormatName = objectFormat.Name() 154 return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "object_format_name") 155 } 156 157 // Close closes this uploader 158 func (g *GiteaLocalUploader) Close() { 159 if g.gitRepo != nil { 160 g.gitRepo.Close() 161 } 162 } 163 164 // CreateTopics creates topics 165 func (g *GiteaLocalUploader) CreateTopics(topics ...string) error { 166 // Ignore topics too long for the db 167 c := 0 168 for _, topic := range topics { 169 if len(topic) > 50 { 170 continue 171 } 172 173 topics[c] = topic 174 c++ 175 } 176 topics = topics[:c] 177 return repo_model.SaveTopics(g.ctx, g.repo.ID, topics...) 178 } 179 180 // CreateMilestones creates milestones 181 func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) error { 182 mss := make([]*issues_model.Milestone, 0, len(milestones)) 183 for _, milestone := range milestones { 184 var deadline timeutil.TimeStamp 185 if milestone.Deadline != nil { 186 deadline = timeutil.TimeStamp(milestone.Deadline.Unix()) 187 } 188 if deadline == 0 { 189 deadline = timeutil.TimeStamp(time.Date(9999, 1, 1, 0, 0, 0, 0, setting.DefaultUILocation).Unix()) 190 } 191 192 if milestone.Created.IsZero() { 193 if milestone.Updated != nil { 194 milestone.Created = *milestone.Updated 195 } else if milestone.Deadline != nil { 196 milestone.Created = *milestone.Deadline 197 } else { 198 milestone.Created = time.Now() 199 } 200 } 201 if milestone.Updated == nil || milestone.Updated.IsZero() { 202 milestone.Updated = &milestone.Created 203 } 204 205 ms := issues_model.Milestone{ 206 RepoID: g.repo.ID, 207 Name: milestone.Title, 208 Content: milestone.Description, 209 IsClosed: milestone.State == "closed", 210 CreatedUnix: timeutil.TimeStamp(milestone.Created.Unix()), 211 UpdatedUnix: timeutil.TimeStamp(milestone.Updated.Unix()), 212 DeadlineUnix: deadline, 213 } 214 if ms.IsClosed && milestone.Closed != nil { 215 ms.ClosedDateUnix = timeutil.TimeStamp(milestone.Closed.Unix()) 216 } 217 mss = append(mss, &ms) 218 } 219 220 err := issues_model.InsertMilestones(g.ctx, mss...) 221 if err != nil { 222 return err 223 } 224 225 for _, ms := range mss { 226 g.milestones[ms.Name] = ms.ID 227 } 228 return nil 229 } 230 231 // CreateLabels creates labels 232 func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { 233 lbs := make([]*issues_model.Label, 0, len(labels)) 234 for _, l := range labels { 235 if color, err := label.NormalizeColor(l.Color); err != nil { 236 log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", l.Color, l.Name, g.repoOwner, g.repoName) 237 l.Color = "#ffffff" 238 } else { 239 l.Color = color 240 } 241 242 lbs = append(lbs, &issues_model.Label{ 243 RepoID: g.repo.ID, 244 Name: l.Name, 245 Exclusive: l.Exclusive, 246 Description: l.Description, 247 Color: l.Color, 248 }) 249 } 250 251 err := issues_model.NewLabels(g.ctx, lbs...) 252 if err != nil { 253 return err 254 } 255 for _, lb := range lbs { 256 g.labels[lb.Name] = lb 257 } 258 return nil 259 } 260 261 // CreateReleases creates releases 262 func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { 263 rels := make([]*repo_model.Release, 0, len(releases)) 264 for _, release := range releases { 265 if release.Created.IsZero() { 266 if !release.Published.IsZero() { 267 release.Created = release.Published 268 } else { 269 release.Created = time.Now() 270 } 271 } 272 273 // SECURITY: The TagName must be a valid git ref 274 if release.TagName != "" && !git.IsValidRefPattern(release.TagName) { 275 release.TagName = "" 276 } 277 278 // SECURITY: The TargetCommitish must be a valid git ref 279 if release.TargetCommitish != "" && !git.IsValidRefPattern(release.TargetCommitish) { 280 release.TargetCommitish = "" 281 } 282 283 rel := repo_model.Release{ 284 RepoID: g.repo.ID, 285 TagName: release.TagName, 286 LowerTagName: strings.ToLower(release.TagName), 287 Target: release.TargetCommitish, 288 Title: release.Name, 289 Note: release.Body, 290 IsDraft: release.Draft, 291 IsPrerelease: release.Prerelease, 292 IsTag: false, 293 CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), 294 } 295 296 if err := g.remapUser(release, &rel); err != nil { 297 return err 298 } 299 300 // calc NumCommits if possible 301 if rel.TagName != "" { 302 commit, err := g.gitRepo.GetTagCommit(rel.TagName) 303 if !git.IsErrNotExist(err) { 304 if err != nil { 305 return fmt.Errorf("GetTagCommit[%v]: %w", rel.TagName, err) 306 } 307 rel.Sha1 = commit.ID.String() 308 rel.NumCommits, err = commit.CommitsCount() 309 if err != nil { 310 return fmt.Errorf("CommitsCount: %w", err) 311 } 312 } 313 } 314 315 for _, asset := range release.Assets { 316 if asset.Created.IsZero() { 317 if !asset.Updated.IsZero() { 318 asset.Created = asset.Updated 319 } else { 320 asset.Created = release.Created 321 } 322 } 323 attach := repo_model.Attachment{ 324 UUID: uuid.New().String(), 325 Name: asset.Name, 326 DownloadCount: int64(*asset.DownloadCount), 327 Size: int64(*asset.Size), 328 CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()), 329 } 330 331 // SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here 332 // ... we must assume that they are safe and simply download the attachment 333 err := func() error { 334 // asset.DownloadURL maybe a local file 335 var rc io.ReadCloser 336 var err error 337 if asset.DownloadFunc != nil { 338 rc, err = asset.DownloadFunc() 339 if err != nil { 340 return err 341 } 342 } else if asset.DownloadURL != nil { 343 rc, err = uri.Open(*asset.DownloadURL) 344 if err != nil { 345 return err 346 } 347 } 348 if rc == nil { 349 return nil 350 } 351 _, err = storage.Attachments.Save(attach.RelativePath(), rc, int64(*asset.Size)) 352 rc.Close() 353 return err 354 }() 355 if err != nil { 356 return err 357 } 358 359 rel.Attachments = append(rel.Attachments, &attach) 360 } 361 362 rels = append(rels, &rel) 363 } 364 365 return repo_model.InsertReleases(g.ctx, rels...) 366 } 367 368 // SyncTags syncs releases with tags in the database 369 func (g *GiteaLocalUploader) SyncTags() error { 370 return repo_module.SyncReleasesWithTags(g.ctx, g.repo, g.gitRepo) 371 } 372 373 // CreateIssues creates issues 374 func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { 375 iss := make([]*issues_model.Issue, 0, len(issues)) 376 for _, issue := range issues { 377 var labels []*issues_model.Label 378 for _, label := range issue.Labels { 379 lb, ok := g.labels[label.Name] 380 if ok { 381 labels = append(labels, lb) 382 } 383 } 384 385 milestoneID := g.milestones[issue.Milestone] 386 387 if issue.Created.IsZero() { 388 if issue.Closed != nil { 389 issue.Created = *issue.Closed 390 } else { 391 issue.Created = time.Now() 392 } 393 } 394 if issue.Updated.IsZero() { 395 if issue.Closed != nil { 396 issue.Updated = *issue.Closed 397 } else { 398 issue.Updated = time.Now() 399 } 400 } 401 402 // SECURITY: issue.Ref needs to be a valid reference 403 if !git.IsValidRefPattern(issue.Ref) { 404 log.Warn("Invalid issue.Ref[%s] in issue #%d in %s/%s", issue.Ref, issue.Number, g.repoOwner, g.repoName) 405 issue.Ref = "" 406 } 407 408 is := issues_model.Issue{ 409 RepoID: g.repo.ID, 410 Repo: g.repo, 411 Index: issue.Number, 412 Title: base_module.TruncateString(issue.Title, 255), 413 Content: issue.Content, 414 Ref: issue.Ref, 415 IsClosed: issue.State == "closed", 416 IsLocked: issue.IsLocked, 417 MilestoneID: milestoneID, 418 Labels: labels, 419 CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()), 420 UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()), 421 } 422 423 if err := g.remapUser(issue, &is); err != nil { 424 return err 425 } 426 427 if issue.Closed != nil { 428 is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix()) 429 } 430 // add reactions 431 for _, reaction := range issue.Reactions { 432 res := issues_model.Reaction{ 433 Type: reaction.Content, 434 CreatedUnix: timeutil.TimeStampNow(), 435 } 436 if err := g.remapUser(reaction, &res); err != nil { 437 return err 438 } 439 is.Reactions = append(is.Reactions, &res) 440 } 441 iss = append(iss, &is) 442 } 443 444 if len(iss) > 0 { 445 if err := issues_model.InsertIssues(g.ctx, iss...); err != nil { 446 return err 447 } 448 449 for _, is := range iss { 450 g.issues[is.Index] = is 451 } 452 } 453 454 return nil 455 } 456 457 // CreateComments creates comments of issues 458 func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { 459 cms := make([]*issues_model.Comment, 0, len(comments)) 460 for _, comment := range comments { 461 var issue *issues_model.Issue 462 issue, ok := g.issues[comment.IssueIndex] 463 if !ok { 464 return fmt.Errorf("comment references non existent IssueIndex %d", comment.IssueIndex) 465 } 466 467 if comment.Created.IsZero() { 468 comment.Created = time.Unix(int64(issue.CreatedUnix), 0) 469 } 470 if comment.Updated.IsZero() { 471 comment.Updated = comment.Created 472 } 473 if comment.CommentType == "" { 474 // if type field is missing, then assume a normal comment 475 comment.CommentType = issues_model.CommentTypeComment.String() 476 } 477 cm := issues_model.Comment{ 478 IssueID: issue.ID, 479 Type: issues_model.AsCommentType(comment.CommentType), 480 Content: comment.Content, 481 CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()), 482 UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()), 483 } 484 485 switch cm.Type { 486 case issues_model.CommentTypeReopen: 487 cm.Content = "" 488 case issues_model.CommentTypeClose: 489 cm.Content = "" 490 case issues_model.CommentTypeAssignees: 491 if assigneeID, ok := comment.Meta["AssigneeID"].(int); ok { 492 cm.AssigneeID = int64(assigneeID) 493 } 494 if comment.Meta["RemovedAssigneeID"] != nil { 495 cm.RemovedAssignee = true 496 } 497 case issues_model.CommentTypeChangeTitle: 498 if comment.Meta["OldTitle"] != nil { 499 cm.OldTitle = fmt.Sprint(comment.Meta["OldTitle"]) 500 } 501 if comment.Meta["NewTitle"] != nil { 502 cm.NewTitle = fmt.Sprint(comment.Meta["NewTitle"]) 503 } 504 case issues_model.CommentTypeChangeTargetBranch: 505 if comment.Meta["OldRef"] != nil && comment.Meta["NewRef"] != nil { 506 cm.OldRef = fmt.Sprint(comment.Meta["OldRef"]) 507 cm.NewRef = fmt.Sprint(comment.Meta["NewRef"]) 508 cm.Content = "" 509 } 510 case issues_model.CommentTypeMergePull: 511 cm.Content = "" 512 case issues_model.CommentTypePRScheduledToAutoMerge, issues_model.CommentTypePRUnScheduledToAutoMerge: 513 cm.Content = "" 514 default: 515 } 516 517 if err := g.remapUser(comment, &cm); err != nil { 518 return err 519 } 520 521 // add reactions 522 for _, reaction := range comment.Reactions { 523 res := issues_model.Reaction{ 524 Type: reaction.Content, 525 CreatedUnix: timeutil.TimeStampNow(), 526 } 527 if err := g.remapUser(reaction, &res); err != nil { 528 return err 529 } 530 cm.Reactions = append(cm.Reactions, &res) 531 } 532 533 cms = append(cms, &cm) 534 } 535 536 if len(cms) == 0 { 537 return nil 538 } 539 return issues_model.InsertIssueComments(g.ctx, cms) 540 } 541 542 // CreatePullRequests creates pull requests 543 func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error { 544 gprs := make([]*issues_model.PullRequest, 0, len(prs)) 545 for _, pr := range prs { 546 gpr, err := g.newPullRequest(pr) 547 if err != nil { 548 return err 549 } 550 551 if err := g.remapUser(pr, gpr.Issue); err != nil { 552 return err 553 } 554 555 gprs = append(gprs, gpr) 556 } 557 if err := issues_model.InsertPullRequests(g.ctx, gprs...); err != nil { 558 return err 559 } 560 for _, pr := range gprs { 561 g.issues[pr.Issue.Index] = pr.Issue 562 pull.AddToTaskQueue(g.ctx, pr) 563 } 564 return nil 565 } 566 567 func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head string, err error) { 568 // SECURITY: this pr must have been must have been ensured safe 569 if !pr.EnsuredSafe { 570 log.Error("PR #%d in %s/%s has not been checked for safety.", pr.Number, g.repoOwner, g.repoName) 571 return "", fmt.Errorf("the PR[%d] was not checked for safety", pr.Number) 572 } 573 574 // Anonymous function to download the patch file (allows us to use defer) 575 err = func() error { 576 // if the patchURL is empty there is nothing to download 577 if pr.PatchURL == "" { 578 return nil 579 } 580 581 // SECURITY: We will assume that the pr.PatchURL has been checked 582 // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe 583 ret, err := uri.Open(pr.PatchURL) // TODO: This probably needs to use the downloader as there may be rate limiting issues here 584 if err != nil { 585 return err 586 } 587 defer ret.Close() 588 589 pullDir := filepath.Join(g.repo.RepoPath(), "pulls") 590 if err = os.MkdirAll(pullDir, os.ModePerm); err != nil { 591 return err 592 } 593 594 f, err := os.Create(filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number))) 595 if err != nil { 596 return err 597 } 598 defer f.Close() 599 600 // TODO: Should there be limits on the size of this file? 601 _, err = io.Copy(f, ret) 602 603 return err 604 }() 605 if err != nil { 606 return "", err 607 } 608 609 head = "unknown repository" 610 if pr.IsForkPullRequest() && pr.State != "closed" { 611 // OK we want to fetch the current head as a branch from its CloneURL 612 613 // 1. Is there a head clone URL available? 614 // 2. Is there a head ref available? 615 if pr.Head.CloneURL == "" || pr.Head.Ref == "" { 616 return head, nil 617 } 618 619 // 3. We need to create a remote for this clone url 620 // ... maybe we already have a name for this remote 621 remote, ok := g.prHeadCache[pr.Head.CloneURL+":"] 622 if !ok { 623 // ... let's try ownername as a reasonable name 624 remote = pr.Head.OwnerName 625 if !git.IsValidRefPattern(remote) { 626 // ... let's try something less nice 627 remote = "head-pr-" + strconv.FormatInt(pr.Number, 10) 628 } 629 // ... now add the remote 630 err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true) 631 if err != nil { 632 log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err) 633 } else { 634 g.prHeadCache[pr.Head.CloneURL+":"] = remote 635 ok = true 636 } 637 } 638 if !ok { 639 return head, nil 640 } 641 642 // 4. Check if we already have this ref? 643 localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] 644 if !ok { 645 // ... We would normally name this migrated branch as <OwnerName>/<HeadRef> but we need to ensure that is safe 646 localRef = git.SanitizeRefPattern(pr.Head.OwnerName + "/" + pr.Head.Ref) 647 648 // ... Now we must assert that this does not exist 649 if g.gitRepo.IsBranchExist(localRef) { 650 localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef 651 i := 0 652 for g.gitRepo.IsBranchExist(localRef) { 653 if i > 5 { 654 // ... We tried, we really tried but this is just a seriously unfriendly repo 655 return head, nil 656 } 657 // OK just try some uuids! 658 localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String()) 659 i++ 660 } 661 } 662 663 fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef 664 if strings.HasPrefix(fetchArg, "-") { 665 fetchArg = git.BranchPrefix + fetchArg 666 } 667 668 _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) 669 if err != nil { 670 log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) 671 return head, nil 672 } 673 g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef 674 head = localRef 675 } 676 677 // 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch 678 if pr.Head.SHA == "" { 679 headSha, err := g.gitRepo.GetBranchCommitID(localRef) 680 if err != nil { 681 log.Error("unable to get head SHA of local head for PR #%d from %s in %s/%s. Error: %v", pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err) 682 return head, nil 683 } 684 pr.Head.SHA = headSha 685 } 686 687 _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) 688 if err != nil { 689 return "", err 690 } 691 692 return head, nil 693 } 694 695 if pr.Head.Ref != "" { 696 head = pr.Head.Ref 697 } 698 699 // Ensure the closed PR SHA still points to an existing ref 700 if pr.Head.SHA == "" { 701 // The SHA is empty 702 log.Warn("Empty reference, no pull head for PR #%d in %s/%s", pr.Number, g.repoOwner, g.repoName) 703 } else { 704 _, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1").AddDynamicArguments(pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) 705 if err != nil { 706 // Git update-ref remove bad references with a relative path 707 log.Warn("Deprecated local head %s for PR #%d in %s/%s, removing %s", pr.Head.SHA, pr.Number, g.repoOwner, g.repoName, pr.GetGitRefName()) 708 } else { 709 // set head information 710 _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()}) 711 if err != nil { 712 log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err) 713 } 714 } 715 } 716 717 return head, nil 718 } 719 720 func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model.PullRequest, error) { 721 var labels []*issues_model.Label 722 for _, label := range pr.Labels { 723 lb, ok := g.labels[label.Name] 724 if ok { 725 labels = append(labels, lb) 726 } 727 } 728 729 milestoneID := g.milestones[pr.Milestone] 730 731 head, err := g.updateGitForPullRequest(pr) 732 if err != nil { 733 return nil, fmt.Errorf("updateGitForPullRequest: %w", err) 734 } 735 736 // Now we may need to fix the mergebase 737 if pr.Base.SHA == "" { 738 if pr.Base.Ref != "" && pr.Head.SHA != "" { 739 // A PR against a tag base does not make sense - therefore pr.Base.Ref must be a branch 740 // TODO: should we be checking for the refs/heads/ prefix on the pr.Base.Ref? (i.e. are these actually branches or refs) 741 pr.Base.SHA, _, err = g.gitRepo.GetMergeBase("", git.BranchPrefix+pr.Base.Ref, pr.Head.SHA) 742 if err != nil { 743 log.Error("Cannot determine the merge base for PR #%d in %s/%s. Error: %v", pr.Number, g.repoOwner, g.repoName, err) 744 } 745 } else { 746 log.Error("Cannot determine the merge base for PR #%d in %s/%s. Not enough information", pr.Number, g.repoOwner, g.repoName) 747 } 748 } 749 750 if pr.Created.IsZero() { 751 if pr.Closed != nil { 752 pr.Created = *pr.Closed 753 } else if pr.MergedTime != nil { 754 pr.Created = *pr.MergedTime 755 } else { 756 pr.Created = time.Now() 757 } 758 } 759 if pr.Updated.IsZero() { 760 pr.Updated = pr.Created 761 } 762 763 issue := issues_model.Issue{ 764 RepoID: g.repo.ID, 765 Repo: g.repo, 766 Title: pr.Title, 767 Index: pr.Number, 768 Content: pr.Content, 769 MilestoneID: milestoneID, 770 IsPull: true, 771 IsClosed: pr.State == "closed", 772 IsLocked: pr.IsLocked, 773 Labels: labels, 774 CreatedUnix: timeutil.TimeStamp(pr.Created.Unix()), 775 UpdatedUnix: timeutil.TimeStamp(pr.Updated.Unix()), 776 } 777 778 if err := g.remapUser(pr, &issue); err != nil { 779 return nil, err 780 } 781 782 // add reactions 783 for _, reaction := range pr.Reactions { 784 res := issues_model.Reaction{ 785 Type: reaction.Content, 786 CreatedUnix: timeutil.TimeStampNow(), 787 } 788 if err := g.remapUser(reaction, &res); err != nil { 789 return nil, err 790 } 791 issue.Reactions = append(issue.Reactions, &res) 792 } 793 794 pullRequest := issues_model.PullRequest{ 795 HeadRepoID: g.repo.ID, 796 HeadBranch: head, 797 BaseRepoID: g.repo.ID, 798 BaseBranch: pr.Base.Ref, 799 MergeBase: pr.Base.SHA, 800 Index: pr.Number, 801 HasMerged: pr.Merged, 802 803 Issue: &issue, 804 } 805 806 if pullRequest.Issue.IsClosed && pr.Closed != nil { 807 pullRequest.Issue.ClosedUnix = timeutil.TimeStamp(pr.Closed.Unix()) 808 } 809 if pullRequest.HasMerged && pr.MergedTime != nil { 810 pullRequest.MergedUnix = timeutil.TimeStamp(pr.MergedTime.Unix()) 811 pullRequest.MergedCommitID = pr.MergeCommitSHA 812 pullRequest.MergerID = g.doer.ID 813 } 814 815 // TODO: assignees 816 817 return &pullRequest, nil 818 } 819 820 func convertReviewState(state string) issues_model.ReviewType { 821 switch state { 822 case base.ReviewStatePending: 823 return issues_model.ReviewTypePending 824 case base.ReviewStateApproved: 825 return issues_model.ReviewTypeApprove 826 case base.ReviewStateChangesRequested: 827 return issues_model.ReviewTypeReject 828 case base.ReviewStateCommented: 829 return issues_model.ReviewTypeComment 830 case base.ReviewStateRequestReview: 831 return issues_model.ReviewTypeRequest 832 default: 833 return issues_model.ReviewTypePending 834 } 835 } 836 837 // CreateReviews create pull request reviews of currently migrated issues 838 func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error { 839 cms := make([]*issues_model.Review, 0, len(reviews)) 840 for _, review := range reviews { 841 var issue *issues_model.Issue 842 issue, ok := g.issues[review.IssueIndex] 843 if !ok { 844 return fmt.Errorf("review references non existent IssueIndex %d", review.IssueIndex) 845 } 846 if review.CreatedAt.IsZero() { 847 review.CreatedAt = time.Unix(int64(issue.CreatedUnix), 0) 848 } 849 850 cm := issues_model.Review{ 851 Type: convertReviewState(review.State), 852 IssueID: issue.ID, 853 Content: review.Content, 854 Official: review.Official, 855 CreatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()), 856 UpdatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()), 857 } 858 859 if err := g.remapUser(review, &cm); err != nil { 860 return err 861 } 862 863 cms = append(cms, &cm) 864 865 // get pr 866 pr, ok := g.prCache[issue.ID] 867 if !ok { 868 var err error 869 pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(g.ctx, issue.ID) 870 if err != nil { 871 return err 872 } 873 g.prCache[issue.ID] = pr 874 } 875 if pr.MergeBase == "" { 876 // No mergebase -> no basis for any patches 877 log.Warn("PR #%d in %s/%s: does not have a merge base, all review comments will be ignored", pr.Index, g.repoOwner, g.repoName) 878 continue 879 } 880 881 headCommitID, err := g.gitRepo.GetRefCommitID(pr.GetGitRefName()) 882 if err != nil { 883 log.Warn("PR #%d GetRefCommitID[%s] in %s/%s: %v, all review comments will be ignored", pr.Index, pr.GetGitRefName(), g.repoOwner, g.repoName, err) 884 continue 885 } 886 887 for _, comment := range review.Comments { 888 line := comment.Line 889 if line != 0 { 890 comment.Position = 1 891 } else if comment.DiffHunk != "" { 892 _, _, line, _ = git.ParseDiffHunkString(comment.DiffHunk) 893 } 894 895 // SECURITY: The TreePath must be cleaned! use relative path 896 comment.TreePath = util.PathJoinRel(comment.TreePath) 897 898 var patch string 899 reader, writer := io.Pipe() 900 defer func() { 901 _ = reader.Close() 902 _ = writer.Close() 903 }() 904 go func(comment *base.ReviewComment) { 905 if err := git.GetRepoRawDiffForFile(g.gitRepo, pr.MergeBase, headCommitID, git.RawDiffNormal, comment.TreePath, writer); err != nil { 906 // We should ignore the error since the commit maybe removed when force push to the pull request 907 log.Warn("GetRepoRawDiffForFile failed when migrating [%s, %s, %s, %s]: %v", g.gitRepo.Path, pr.MergeBase, headCommitID, comment.TreePath, err) 908 } 909 _ = writer.Close() 910 }(comment) 911 912 patch, _ = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines) 913 914 if comment.CreatedAt.IsZero() { 915 comment.CreatedAt = review.CreatedAt 916 } 917 if comment.UpdatedAt.IsZero() { 918 comment.UpdatedAt = comment.CreatedAt 919 } 920 921 objectFormat := git.ObjectFormatFromName(g.repo.ObjectFormatName) 922 if !objectFormat.IsValid(comment.CommitID) { 923 log.Warn("Invalid comment CommitID[%s] on comment[%d] in PR #%d of %s/%s replaced with %s", comment.CommitID, pr.Index, g.repoOwner, g.repoName, headCommitID) 924 comment.CommitID = headCommitID 925 } 926 927 c := issues_model.Comment{ 928 Type: issues_model.CommentTypeCode, 929 IssueID: issue.ID, 930 Content: comment.Content, 931 Line: int64(line + comment.Position - 1), 932 TreePath: comment.TreePath, 933 CommitSHA: comment.CommitID, 934 Patch: patch, 935 CreatedUnix: timeutil.TimeStamp(comment.CreatedAt.Unix()), 936 UpdatedUnix: timeutil.TimeStamp(comment.UpdatedAt.Unix()), 937 } 938 939 if err := g.remapUser(review, &c); err != nil { 940 return err 941 } 942 943 cm.Comments = append(cm.Comments, &c) 944 } 945 } 946 947 return issues_model.InsertReviews(g.ctx, cms) 948 } 949 950 // Rollback when migrating failed, this will rollback all the changes. 951 func (g *GiteaLocalUploader) Rollback() error { 952 if g.repo != nil && g.repo.ID > 0 { 953 g.gitRepo.Close() 954 955 // do not delete the repository, otherwise the end users won't be able to see the last error message 956 } 957 return nil 958 } 959 960 // Finish when migrating success, this will do some status update things. 961 func (g *GiteaLocalUploader) Finish() error { 962 if g.repo == nil || g.repo.ID <= 0 { 963 return ErrRepoNotCreated 964 } 965 966 // update issue_index 967 if err := issues_model.RecalculateIssueIndexForRepo(g.ctx, g.repo.ID); err != nil { 968 return err 969 } 970 971 if err := models.UpdateRepoStats(g.ctx, g.repo.ID); err != nil { 972 return err 973 } 974 975 g.repo.Status = repo_model.RepositoryReady 976 return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "status") 977 } 978 979 func (g *GiteaLocalUploader) remapUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error { 980 var userid int64 981 var err error 982 if g.sameApp { 983 userid, err = g.remapLocalUser(source, target) 984 } else { 985 userid, err = g.remapExternalUser(source, target) 986 } 987 988 if err != nil { 989 return err 990 } 991 992 if userid > 0 { 993 return target.RemapExternalUser("", 0, userid) 994 } 995 return target.RemapExternalUser(source.GetExternalName(), source.GetExternalID(), g.doer.ID) 996 } 997 998 func (g *GiteaLocalUploader) remapLocalUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) (int64, error) { 999 userid, ok := g.userMap[source.GetExternalID()] 1000 if !ok { 1001 name, err := user_model.GetUserNameByID(g.ctx, source.GetExternalID()) 1002 if err != nil { 1003 return 0, err 1004 } 1005 // let's not reuse an ID when the user was deleted or has a different user name 1006 if name != source.GetExternalName() { 1007 userid = 0 1008 } else { 1009 userid = source.GetExternalID() 1010 } 1011 g.userMap[source.GetExternalID()] = userid 1012 } 1013 return userid, nil 1014 } 1015 1016 func (g *GiteaLocalUploader) remapExternalUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) (userid int64, err error) { 1017 userid, ok := g.userMap[source.GetExternalID()] 1018 if !ok { 1019 userid, err = user_model.GetUserIDByExternalUserID(g.ctx, g.gitServiceType.Name(), fmt.Sprintf("%d", source.GetExternalID())) 1020 if err != nil { 1021 log.Error("GetUserIDByExternalUserID: %v", err) 1022 return 0, err 1023 } 1024 g.userMap[source.GetExternalID()] = userid 1025 } 1026 return userid, nil 1027 }