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