You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dump.go 20KB

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