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

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