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 30KB

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