code.gitea.io/gitea@v1.21.7/services/migrations/dump.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 "os" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "time" 18 19 user_model "code.gitea.io/gitea/models/user" 20 "code.gitea.io/gitea/modules/git" 21 "code.gitea.io/gitea/modules/log" 22 base "code.gitea.io/gitea/modules/migration" 23 "code.gitea.io/gitea/modules/repository" 24 "code.gitea.io/gitea/modules/setting" 25 "code.gitea.io/gitea/modules/structs" 26 27 "github.com/google/uuid" 28 "gopkg.in/yaml.v3" 29 ) 30 31 var _ base.Uploader = &RepositoryDumper{} 32 33 // RepositoryDumper implements an Uploader to the local directory 34 type RepositoryDumper struct { 35 ctx context.Context 36 baseDir string 37 repoOwner string 38 repoName string 39 opts base.MigrateOptions 40 milestoneFile *os.File 41 labelFile *os.File 42 releaseFile *os.File 43 issueFile *os.File 44 commentFiles map[int64]*os.File 45 pullrequestFile *os.File 46 reviewFiles map[int64]*os.File 47 48 gitRepo *git.Repository 49 prHeadCache map[string]string 50 } 51 52 // NewRepositoryDumper creates an gitea Uploader 53 func NewRepositoryDumper(ctx context.Context, baseDir, repoOwner, repoName string, opts base.MigrateOptions) (*RepositoryDumper, error) { 54 baseDir = filepath.Join(baseDir, repoOwner, repoName) 55 if err := os.MkdirAll(baseDir, os.ModePerm); err != nil { 56 return nil, err 57 } 58 return &RepositoryDumper{ 59 ctx: ctx, 60 opts: opts, 61 baseDir: baseDir, 62 repoOwner: repoOwner, 63 repoName: repoName, 64 prHeadCache: make(map[string]string), 65 commentFiles: make(map[int64]*os.File), 66 reviewFiles: make(map[int64]*os.File), 67 }, nil 68 } 69 70 // MaxBatchInsertSize returns the table's max batch insert size 71 func (g *RepositoryDumper) MaxBatchInsertSize(tp string) int { 72 return 1000 73 } 74 75 func (g *RepositoryDumper) gitPath() string { 76 return filepath.Join(g.baseDir, "git") 77 } 78 79 func (g *RepositoryDumper) wikiPath() string { 80 return filepath.Join(g.baseDir, "wiki") 81 } 82 83 func (g *RepositoryDumper) commentDir() string { 84 return filepath.Join(g.baseDir, "comments") 85 } 86 87 func (g *RepositoryDumper) reviewDir() string { 88 return filepath.Join(g.baseDir, "reviews") 89 } 90 91 func (g *RepositoryDumper) setURLToken(remoteAddr string) (string, error) { 92 if len(g.opts.AuthToken) > 0 || len(g.opts.AuthUsername) > 0 { 93 u, err := url.Parse(remoteAddr) 94 if err != nil { 95 return "", err 96 } 97 u.User = url.UserPassword(g.opts.AuthUsername, g.opts.AuthPassword) 98 if len(g.opts.AuthToken) > 0 { 99 u.User = url.UserPassword("oauth2", g.opts.AuthToken) 100 } 101 remoteAddr = u.String() 102 } 103 104 return remoteAddr, nil 105 } 106 107 // CreateRepo creates a repository 108 func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error { 109 f, err := os.Create(filepath.Join(g.baseDir, "repo.yml")) 110 if err != nil { 111 return err 112 } 113 defer f.Close() 114 115 bs, err := yaml.Marshal(map[string]any{ 116 "name": repo.Name, 117 "owner": repo.Owner, 118 "description": repo.Description, 119 "clone_addr": opts.CloneAddr, 120 "original_url": repo.OriginalURL, 121 "is_private": opts.Private, 122 "service_type": opts.GitServiceType, 123 "wiki": opts.Wiki, 124 "issues": opts.Issues, 125 "milestones": opts.Milestones, 126 "labels": opts.Labels, 127 "releases": opts.Releases, 128 "comments": opts.Comments, 129 "pulls": opts.PullRequests, 130 "assets": opts.ReleaseAssets, 131 }) 132 if err != nil { 133 return err 134 } 135 136 if _, err := f.Write(bs); err != nil { 137 return err 138 } 139 140 repoPath := g.gitPath() 141 if err := os.MkdirAll(repoPath, os.ModePerm); err != nil { 142 return err 143 } 144 145 migrateTimeout := 2 * time.Hour 146 147 remoteAddr, err := g.setURLToken(repo.CloneURL) 148 if err != nil { 149 return err 150 } 151 152 err = git.Clone(g.ctx, remoteAddr, repoPath, git.CloneRepoOptions{ 153 Mirror: true, 154 Quiet: true, 155 Timeout: migrateTimeout, 156 SkipTLSVerify: setting.Migrations.SkipTLSVerify, 157 }) 158 if err != nil { 159 return fmt.Errorf("Clone: %w", err) 160 } 161 if err := git.WriteCommitGraph(g.ctx, repoPath); err != nil { 162 return err 163 } 164 165 if opts.Wiki { 166 wikiPath := g.wikiPath() 167 wikiRemotePath := repository.WikiRemoteURL(g.ctx, remoteAddr) 168 if len(wikiRemotePath) > 0 { 169 if err := os.MkdirAll(wikiPath, os.ModePerm); err != nil { 170 return fmt.Errorf("Failed to remove %s: %w", wikiPath, err) 171 } 172 173 if err := git.Clone(g.ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{ 174 Mirror: true, 175 Quiet: true, 176 Timeout: migrateTimeout, 177 Branch: "master", 178 SkipTLSVerify: setting.Migrations.SkipTLSVerify, 179 }); err != nil { 180 log.Warn("Clone wiki: %v", err) 181 if err := os.RemoveAll(wikiPath); err != nil { 182 return fmt.Errorf("Failed to remove %s: %w", wikiPath, err) 183 } 184 } else if err := git.WriteCommitGraph(g.ctx, wikiPath); err != nil { 185 return err 186 } 187 } 188 } 189 190 g.gitRepo, err = git.OpenRepository(g.ctx, g.gitPath()) 191 return err 192 } 193 194 // Close closes this uploader 195 func (g *RepositoryDumper) Close() { 196 if g.gitRepo != nil { 197 g.gitRepo.Close() 198 } 199 if g.milestoneFile != nil { 200 g.milestoneFile.Close() 201 } 202 if g.labelFile != nil { 203 g.labelFile.Close() 204 } 205 if g.releaseFile != nil { 206 g.releaseFile.Close() 207 } 208 if g.issueFile != nil { 209 g.issueFile.Close() 210 } 211 for _, f := range g.commentFiles { 212 f.Close() 213 } 214 if g.pullrequestFile != nil { 215 g.pullrequestFile.Close() 216 } 217 for _, f := range g.reviewFiles { 218 f.Close() 219 } 220 } 221 222 // CreateTopics creates topics 223 func (g *RepositoryDumper) CreateTopics(topics ...string) error { 224 f, err := os.Create(filepath.Join(g.baseDir, "topic.yml")) 225 if err != nil { 226 return err 227 } 228 defer f.Close() 229 230 bs, err := yaml.Marshal(map[string]any{ 231 "topics": topics, 232 }) 233 if err != nil { 234 return err 235 } 236 237 if _, err := f.Write(bs); err != nil { 238 return err 239 } 240 241 return nil 242 } 243 244 // CreateMilestones creates milestones 245 func (g *RepositoryDumper) CreateMilestones(milestones ...*base.Milestone) error { 246 var err error 247 if g.milestoneFile == nil { 248 g.milestoneFile, err = os.Create(filepath.Join(g.baseDir, "milestone.yml")) 249 if err != nil { 250 return err 251 } 252 } 253 254 bs, err := yaml.Marshal(milestones) 255 if err != nil { 256 return err 257 } 258 259 if _, err := g.milestoneFile.Write(bs); err != nil { 260 return err 261 } 262 263 return nil 264 } 265 266 // CreateLabels creates labels 267 func (g *RepositoryDumper) CreateLabels(labels ...*base.Label) error { 268 var err error 269 if g.labelFile == nil { 270 g.labelFile, err = os.Create(filepath.Join(g.baseDir, "label.yml")) 271 if err != nil { 272 return err 273 } 274 } 275 276 bs, err := yaml.Marshal(labels) 277 if err != nil { 278 return err 279 } 280 281 if _, err := g.labelFile.Write(bs); err != nil { 282 return err 283 } 284 285 return nil 286 } 287 288 // CreateReleases creates releases 289 func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error { 290 if g.opts.ReleaseAssets { 291 for _, release := range releases { 292 attachDir := filepath.Join("release_assets", release.TagName) 293 if err := os.MkdirAll(filepath.Join(g.baseDir, attachDir), os.ModePerm); err != nil { 294 return err 295 } 296 for _, asset := range release.Assets { 297 attachLocalPath := filepath.Join(attachDir, asset.Name) 298 299 // SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here 300 // ... we must assume that they are safe and simply download the attachment 301 // download attachment 302 err := func(attachPath string) error { 303 var rc io.ReadCloser 304 var err error 305 if asset.DownloadURL == nil { 306 rc, err = asset.DownloadFunc() 307 if err != nil { 308 return err 309 } 310 } else { 311 resp, err := http.Get(*asset.DownloadURL) 312 if err != nil { 313 return err 314 } 315 rc = resp.Body 316 } 317 defer rc.Close() 318 319 fw, err := os.Create(attachPath) 320 if err != nil { 321 return fmt.Errorf("create: %w", err) 322 } 323 defer fw.Close() 324 325 _, err = io.Copy(fw, rc) 326 return err 327 }(filepath.Join(g.baseDir, attachLocalPath)) 328 if err != nil { 329 return err 330 } 331 asset.DownloadURL = &attachLocalPath // to save the filepath on the yml file, change the source 332 } 333 } 334 } 335 336 var err error 337 if g.releaseFile == nil { 338 g.releaseFile, err = os.Create(filepath.Join(g.baseDir, "release.yml")) 339 if err != nil { 340 return err 341 } 342 } 343 344 bs, err := yaml.Marshal(releases) 345 if err != nil { 346 return err 347 } 348 349 if _, err := g.releaseFile.Write(bs); err != nil { 350 return err 351 } 352 353 return nil 354 } 355 356 // SyncTags syncs releases with tags in the database 357 func (g *RepositoryDumper) SyncTags() error { 358 return nil 359 } 360 361 // CreateIssues creates issues 362 func (g *RepositoryDumper) CreateIssues(issues ...*base.Issue) error { 363 var err error 364 if g.issueFile == nil { 365 g.issueFile, err = os.Create(filepath.Join(g.baseDir, "issue.yml")) 366 if err != nil { 367 return err 368 } 369 } 370 371 bs, err := yaml.Marshal(issues) 372 if err != nil { 373 return err 374 } 375 376 if _, err := g.issueFile.Write(bs); err != nil { 377 return err 378 } 379 380 return nil 381 } 382 383 func (g *RepositoryDumper) createItems(dir string, itemFiles map[int64]*os.File, itemsMap map[int64][]any) error { 384 if err := os.MkdirAll(dir, os.ModePerm); err != nil { 385 return err 386 } 387 388 for number, items := range itemsMap { 389 if err := g.encodeItems(number, items, dir, itemFiles); err != nil { 390 return err 391 } 392 } 393 394 return nil 395 } 396 397 func (g *RepositoryDumper) encodeItems(number int64, items []any, dir string, itemFiles map[int64]*os.File) error { 398 itemFile := itemFiles[number] 399 if itemFile == nil { 400 var err error 401 itemFile, err = os.Create(filepath.Join(dir, fmt.Sprintf("%d.yml", number))) 402 if err != nil { 403 return err 404 } 405 itemFiles[number] = itemFile 406 } 407 408 encoder := yaml.NewEncoder(itemFile) 409 defer encoder.Close() 410 411 return encoder.Encode(items) 412 } 413 414 // CreateComments creates comments of issues 415 func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error { 416 commentsMap := make(map[int64][]any, len(comments)) 417 for _, comment := range comments { 418 commentsMap[comment.IssueIndex] = append(commentsMap[comment.IssueIndex], comment) 419 } 420 421 return g.createItems(g.commentDir(), g.commentFiles, commentsMap) 422 } 423 424 func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error { 425 // SECURITY: this pr must have been ensured safe 426 if !pr.EnsuredSafe { 427 log.Error("PR #%d in %s/%s has not been checked for safety ... We will ignore this.", pr.Number, g.repoOwner, g.repoName) 428 return fmt.Errorf("unsafe PR #%d", pr.Number) 429 } 430 431 // First we download the patch file 432 err := func() error { 433 // if the patchURL is empty there is nothing to download 434 if pr.PatchURL == "" { 435 return nil 436 } 437 438 // SECURITY: We will assume that the pr.PatchURL has been checked 439 // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe 440 u, err := g.setURLToken(pr.PatchURL) 441 if err != nil { 442 return err 443 } 444 445 // SECURITY: We will assume that the pr.PatchURL has been checked 446 // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe 447 resp, err := http.Get(u) // TODO: This probably needs to use the downloader as there may be rate limiting issues here 448 if err != nil { 449 return err 450 } 451 defer resp.Body.Close() 452 pullDir := filepath.Join(g.gitPath(), "pulls") 453 if err = os.MkdirAll(pullDir, os.ModePerm); err != nil { 454 return err 455 } 456 fPath := filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number)) 457 f, err := os.Create(fPath) 458 if err != nil { 459 return err 460 } 461 defer f.Close() 462 463 // TODO: Should there be limits on the size of this file? 464 if _, err = io.Copy(f, resp.Body); err != nil { 465 return err 466 } 467 pr.PatchURL = "git/pulls/" + fmt.Sprintf("%d.patch", pr.Number) 468 469 return nil 470 }() 471 if err != nil { 472 log.Error("PR #%d in %s/%s unable to download patch: %v", pr.Number, g.repoOwner, g.repoName, err) 473 return err 474 } 475 476 isFork := pr.IsForkPullRequest() 477 478 // Even if it's a forked repo PR, we have to change head info as the same as the base info 479 oldHeadOwnerName := pr.Head.OwnerName 480 pr.Head.OwnerName, pr.Head.RepoName = pr.Base.OwnerName, pr.Base.RepoName 481 482 if !isFork || pr.State == "closed" { 483 return nil 484 } 485 486 // OK we want to fetch the current head as a branch from its CloneURL 487 488 // 1. Is there a head clone URL available? 489 // 2. Is there a head ref available? 490 if pr.Head.CloneURL == "" || pr.Head.Ref == "" { 491 // Set head information if pr.Head.SHA is available 492 if pr.Head.SHA != "" { 493 _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) 494 if err != nil { 495 log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err) 496 } 497 } 498 return nil 499 } 500 501 // 3. We need to create a remote for this clone url 502 // ... maybe we already have a name for this remote 503 remote, ok := g.prHeadCache[pr.Head.CloneURL+":"] 504 if !ok { 505 // ... let's try ownername as a reasonable name 506 remote = oldHeadOwnerName 507 if !git.IsValidRefPattern(remote) { 508 // ... let's try something less nice 509 remote = "head-pr-" + strconv.FormatInt(pr.Number, 10) 510 } 511 // ... now add the remote 512 err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true) 513 if err != nil { 514 log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err) 515 } else { 516 g.prHeadCache[pr.Head.CloneURL+":"] = remote 517 ok = true 518 } 519 } 520 if !ok { 521 // Set head information if pr.Head.SHA is available 522 if pr.Head.SHA != "" { 523 _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) 524 if err != nil { 525 log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err) 526 } 527 } 528 529 return nil 530 } 531 532 // 4. Check if we already have this ref? 533 localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] 534 if !ok { 535 // ... We would normally name this migrated branch as <OwnerName>/<HeadRef> but we need to ensure that is safe 536 localRef = git.SanitizeRefPattern(oldHeadOwnerName + "/" + pr.Head.Ref) 537 538 // ... Now we must assert that this does not exist 539 if g.gitRepo.IsBranchExist(localRef) { 540 localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef 541 i := 0 542 for g.gitRepo.IsBranchExist(localRef) { 543 if i > 5 { 544 // ... We tried, we really tried but this is just a seriously unfriendly repo 545 return fmt.Errorf("unable to create unique local reference from %s", pr.Head.Ref) 546 } 547 // OK just try some uuids! 548 localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String()) 549 i++ 550 } 551 } 552 553 fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef 554 if strings.HasPrefix(fetchArg, "-") { 555 fetchArg = git.BranchPrefix + fetchArg 556 } 557 558 _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()}) 559 if err != nil { 560 log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) 561 // We need to continue here so that the Head.Ref is reset and we attempt to set the gitref for the PR 562 // (This last step will likely fail but we should try to do as much as we can.) 563 } else { 564 // Cache the localRef as the Head.Ref - if we've failed we can always try again. 565 g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef 566 } 567 } 568 569 // Set the pr.Head.Ref to the localRef 570 pr.Head.Ref = localRef 571 572 // 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch 573 if pr.Head.SHA == "" { 574 headSha, err := g.gitRepo.GetBranchCommitID(localRef) 575 if err != nil { 576 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) 577 return nil 578 } 579 pr.Head.SHA = headSha 580 } 581 if pr.Head.SHA != "" { 582 _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()}) 583 if err != nil { 584 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) 585 } 586 } 587 588 return nil 589 } 590 591 // CreatePullRequests creates pull requests 592 func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error { 593 var err error 594 if g.pullrequestFile == nil { 595 if err := os.MkdirAll(g.baseDir, os.ModePerm); err != nil { 596 return err 597 } 598 g.pullrequestFile, err = os.Create(filepath.Join(g.baseDir, "pull_request.yml")) 599 if err != nil { 600 return err 601 } 602 } 603 604 encoder := yaml.NewEncoder(g.pullrequestFile) 605 defer encoder.Close() 606 607 count := 0 608 for i := 0; i < len(prs); i++ { 609 pr := prs[i] 610 if err := g.handlePullRequest(pr); err != nil { 611 log.Error("PR #%d in %s/%s failed - skipping", pr.Number, g.repoOwner, g.repoName, err) 612 continue 613 } 614 prs[count] = pr 615 count++ 616 } 617 prs = prs[:count] 618 619 return encoder.Encode(prs) 620 } 621 622 // CreateReviews create pull request reviews 623 func (g *RepositoryDumper) CreateReviews(reviews ...*base.Review) error { 624 reviewsMap := make(map[int64][]any, len(reviews)) 625 for _, review := range reviews { 626 reviewsMap[review.IssueIndex] = append(reviewsMap[review.IssueIndex], review) 627 } 628 629 return g.createItems(g.reviewDir(), g.reviewFiles, reviewsMap) 630 } 631 632 // Rollback when migrating failed, this will rollback all the changes. 633 func (g *RepositoryDumper) Rollback() error { 634 g.Close() 635 return os.RemoveAll(g.baseDir) 636 } 637 638 // Finish when migrating succeed, this will update something. 639 func (g *RepositoryDumper) Finish() error { 640 return nil 641 } 642 643 // DumpRepository dump repository according MigrateOptions to a local directory 644 func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error { 645 doer, err := user_model.GetAdminUser(ctx) 646 if err != nil { 647 return err 648 } 649 downloader, err := newDownloader(ctx, ownerName, opts) 650 if err != nil { 651 return err 652 } 653 uploader, err := NewRepositoryDumper(ctx, baseDir, ownerName, opts.RepoName, opts) 654 if err != nil { 655 return err 656 } 657 658 if err := migrateRepository(ctx, doer, downloader, uploader, opts, nil); err != nil { 659 if err1 := uploader.Rollback(); err1 != nil { 660 log.Error("rollback failed: %v", err1) 661 } 662 return err 663 } 664 return nil 665 } 666 667 func updateOptionsUnits(opts *base.MigrateOptions, units []string) error { 668 if len(units) == 0 { 669 opts.Wiki = true 670 opts.Issues = true 671 opts.Milestones = true 672 opts.Labels = true 673 opts.Releases = true 674 opts.Comments = true 675 opts.PullRequests = true 676 opts.ReleaseAssets = true 677 } else { 678 for _, unit := range units { 679 switch strings.ToLower(strings.TrimSpace(unit)) { 680 case "": 681 continue 682 case "wiki": 683 opts.Wiki = true 684 case "issues": 685 opts.Issues = true 686 case "milestones": 687 opts.Milestones = true 688 case "labels": 689 opts.Labels = true 690 case "releases": 691 opts.Releases = true 692 case "release_assets": 693 opts.ReleaseAssets = true 694 case "comments": 695 opts.Comments = true 696 case "pull_requests": 697 opts.PullRequests = true 698 default: 699 return errors.New("invalid unit: " + unit) 700 } 701 } 702 } 703 return nil 704 } 705 706 // RestoreRepository restore a repository from the disk directory 707 func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string, validation bool) error { 708 doer, err := user_model.GetAdminUser(ctx) 709 if err != nil { 710 return err 711 } 712 uploader := NewGiteaLocalUploader(ctx, doer, ownerName, repoName) 713 downloader, err := NewRepositoryRestorer(ctx, baseDir, ownerName, repoName, validation) 714 if err != nil { 715 return err 716 } 717 opts, err := downloader.getRepoOptions() 718 if err != nil { 719 return err 720 } 721 tp, _ := strconv.Atoi(opts["service_type"]) 722 723 migrateOpts := base.MigrateOptions{ 724 GitServiceType: structs.GitServiceType(tp), 725 } 726 if err := updateOptionsUnits(&migrateOpts, units); err != nil { 727 return err 728 } 729 730 if err = migrateRepository(ctx, doer, downloader, uploader, migrateOpts, nil); err != nil { 731 if err1 := uploader.Rollback(); err1 != nil { 732 log.Error("rollback failed: %v", err1) 733 } 734 return err 735 } 736 return updateMigrationPosterIDByGitService(ctx, structs.GitServiceType(tp)) 737 }