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.

gitea_uploader.go 29KB

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