gitea源码

gitea_uploader.go 30KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  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/git/gitcmd"
  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. 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(_ context.Context, doer *user_model.User, repoOwner, repoName string) *GiteaLocalUploader {
  55. return &GiteaLocalUploader{
  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(ctx context.Context, repo *base.Repository, opts base.MigrateOptions) error {
  87. owner, err := user_model.GetUserByName(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(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 || setting.Repository.ForcePrivate,
  99. IsMirror: opts.Mirror,
  100. Status: repo_model.RepositoryBeingMigrated,
  101. }, false)
  102. } else {
  103. r, err = repo_model.GetRepositoryByID(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_service.MigrateRepositoryGitData(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 = gitrepo.OpenRepository(ctx, g.repo)
  130. if err != nil {
  131. return err
  132. }
  133. // detect object format from git repository and update to database
  134. objectFormat, err := g.gitRepo.GetObjectFormat()
  135. if err != nil {
  136. return err
  137. }
  138. g.repo.ObjectFormatName = objectFormat.Name()
  139. return repo_model.UpdateRepositoryColsNoAutoTime(ctx, g.repo, "object_format_name")
  140. }
  141. // Close closes this uploader
  142. func (g *GiteaLocalUploader) Close() {
  143. if g.gitRepo != nil {
  144. g.gitRepo.Close()
  145. }
  146. }
  147. // CreateTopics creates topics
  148. func (g *GiteaLocalUploader) CreateTopics(ctx context.Context, topics ...string) error {
  149. // Ignore topics too long for the db
  150. c := 0
  151. for _, topic := range topics {
  152. if len(topic) > 50 {
  153. continue
  154. }
  155. topics[c] = topic
  156. c++
  157. }
  158. topics = topics[:c]
  159. return repo_model.SaveTopics(ctx, g.repo.ID, topics...)
  160. }
  161. // CreateMilestones creates milestones
  162. func (g *GiteaLocalUploader) CreateMilestones(ctx context.Context, milestones ...*base.Milestone) error {
  163. mss := make([]*issues_model.Milestone, 0, len(milestones))
  164. for _, milestone := range milestones {
  165. var deadline timeutil.TimeStamp
  166. if milestone.Deadline != nil {
  167. deadline = timeutil.TimeStamp(milestone.Deadline.Unix())
  168. }
  169. if deadline == 0 {
  170. deadline = timeutil.TimeStamp(time.Date(9999, 1, 1, 0, 0, 0, 0, setting.DefaultUILocation).Unix())
  171. }
  172. if milestone.Created.IsZero() {
  173. if milestone.Updated != nil {
  174. milestone.Created = *milestone.Updated
  175. } else if milestone.Deadline != nil {
  176. milestone.Created = *milestone.Deadline
  177. } else {
  178. milestone.Created = time.Now()
  179. }
  180. }
  181. if milestone.Updated == nil || milestone.Updated.IsZero() {
  182. milestone.Updated = &milestone.Created
  183. }
  184. ms := issues_model.Milestone{
  185. RepoID: g.repo.ID,
  186. Name: milestone.Title,
  187. Content: milestone.Description,
  188. IsClosed: milestone.State == "closed",
  189. CreatedUnix: timeutil.TimeStamp(milestone.Created.Unix()),
  190. UpdatedUnix: timeutil.TimeStamp(milestone.Updated.Unix()),
  191. DeadlineUnix: deadline,
  192. }
  193. if ms.IsClosed && milestone.Closed != nil {
  194. ms.ClosedDateUnix = timeutil.TimeStamp(milestone.Closed.Unix())
  195. }
  196. mss = append(mss, &ms)
  197. }
  198. err := issues_model.InsertMilestones(ctx, mss...)
  199. if err != nil {
  200. return err
  201. }
  202. for _, ms := range mss {
  203. g.milestones[ms.Name] = ms.ID
  204. }
  205. return nil
  206. }
  207. // CreateLabels creates labels
  208. func (g *GiteaLocalUploader) CreateLabels(ctx context.Context, labels ...*base.Label) error {
  209. lbs := make([]*issues_model.Label, 0, len(labels))
  210. for _, l := range labels {
  211. if color, err := label.NormalizeColor(l.Color); err != nil {
  212. log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", l.Color, l.Name, g.repoOwner, g.repoName)
  213. l.Color = "#ffffff"
  214. } else {
  215. l.Color = color
  216. }
  217. lbs = append(lbs, &issues_model.Label{
  218. RepoID: g.repo.ID,
  219. Name: l.Name,
  220. Exclusive: l.Exclusive,
  221. Description: l.Description,
  222. Color: l.Color,
  223. })
  224. }
  225. err := issues_model.NewLabels(ctx, lbs...)
  226. if err != nil {
  227. return err
  228. }
  229. for _, lb := range lbs {
  230. g.labels[lb.Name] = lb
  231. }
  232. return nil
  233. }
  234. // CreateReleases creates releases
  235. func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*base.Release) error {
  236. rels := make([]*repo_model.Release, 0, len(releases))
  237. for _, release := range releases {
  238. if release.Created.IsZero() {
  239. if !release.Published.IsZero() {
  240. release.Created = release.Published
  241. } else {
  242. release.Created = time.Now()
  243. }
  244. }
  245. // SECURITY: The TagName must be a valid git ref
  246. if release.TagName != "" && !git.IsValidRefPattern(release.TagName) {
  247. release.TagName = ""
  248. }
  249. // SECURITY: The TargetCommitish must be a valid git ref
  250. if release.TargetCommitish != "" && !git.IsValidRefPattern(release.TargetCommitish) {
  251. release.TargetCommitish = ""
  252. }
  253. rel := repo_model.Release{
  254. RepoID: g.repo.ID,
  255. TagName: release.TagName,
  256. LowerTagName: strings.ToLower(release.TagName),
  257. Target: release.TargetCommitish,
  258. Title: release.Name,
  259. Note: release.Body,
  260. IsDraft: release.Draft,
  261. IsPrerelease: release.Prerelease,
  262. IsTag: false,
  263. CreatedUnix: timeutil.TimeStamp(release.Created.Unix()),
  264. }
  265. if err := g.remapUser(ctx, release, &rel); err != nil {
  266. return err
  267. }
  268. // calc NumCommits if possible
  269. if rel.TagName != "" {
  270. commit, err := g.gitRepo.GetTagCommit(rel.TagName)
  271. if !git.IsErrNotExist(err) {
  272. if err != nil {
  273. return fmt.Errorf("GetTagCommit[%v]: %w", rel.TagName, err)
  274. }
  275. rel.Sha1 = commit.ID.String()
  276. rel.NumCommits, err = commit.CommitsCount()
  277. if err != nil {
  278. return fmt.Errorf("CommitsCount: %w", err)
  279. }
  280. }
  281. }
  282. for _, asset := range release.Assets {
  283. if asset.Created.IsZero() {
  284. if !asset.Updated.IsZero() {
  285. asset.Created = asset.Updated
  286. } else {
  287. asset.Created = release.Created
  288. }
  289. }
  290. attach := repo_model.Attachment{
  291. UUID: uuid.New().String(),
  292. Name: asset.Name,
  293. DownloadCount: int64(*asset.DownloadCount),
  294. Size: int64(*asset.Size),
  295. CreatedUnix: timeutil.TimeStamp(asset.Created.Unix()),
  296. }
  297. // SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here
  298. // ... we must assume that they are safe and simply download the attachment
  299. err := func() error {
  300. // asset.DownloadURL maybe a local file
  301. var rc io.ReadCloser
  302. var err error
  303. if asset.DownloadFunc != nil {
  304. rc, err = asset.DownloadFunc()
  305. if err != nil {
  306. return err
  307. }
  308. } else if asset.DownloadURL != nil {
  309. rc, err = uri.Open(*asset.DownloadURL)
  310. if err != nil {
  311. return err
  312. }
  313. }
  314. if rc == nil {
  315. return nil
  316. }
  317. _, err = storage.Attachments.Save(attach.RelativePath(), rc, int64(*asset.Size))
  318. rc.Close()
  319. return err
  320. }()
  321. if err != nil {
  322. return err
  323. }
  324. rel.Attachments = append(rel.Attachments, &attach)
  325. }
  326. rels = append(rels, &rel)
  327. }
  328. return repo_model.InsertReleases(ctx, rels...)
  329. }
  330. // SyncTags syncs releases with tags in the database
  331. func (g *GiteaLocalUploader) SyncTags(ctx context.Context) error {
  332. return repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo)
  333. }
  334. // CreateIssues creates issues
  335. func (g *GiteaLocalUploader) CreateIssues(ctx context.Context, issues ...*base.Issue) error {
  336. iss := make([]*issues_model.Issue, 0, len(issues))
  337. for _, issue := range issues {
  338. var labels []*issues_model.Label
  339. for _, label := range issue.Labels {
  340. lb, ok := g.labels[label.Name]
  341. if ok {
  342. labels = append(labels, lb)
  343. }
  344. }
  345. milestoneID := g.milestones[issue.Milestone]
  346. if issue.Created.IsZero() {
  347. if issue.Closed != nil {
  348. issue.Created = *issue.Closed
  349. } else {
  350. issue.Created = time.Now()
  351. }
  352. }
  353. if issue.Updated.IsZero() {
  354. if issue.Closed != nil {
  355. issue.Updated = *issue.Closed
  356. } else {
  357. issue.Updated = time.Now()
  358. }
  359. }
  360. // SECURITY: issue.Ref needs to be a valid reference
  361. if !git.IsValidRefPattern(issue.Ref) {
  362. log.Warn("Invalid issue.Ref[%s] in issue #%d in %s/%s", issue.Ref, issue.Number, g.repoOwner, g.repoName)
  363. issue.Ref = ""
  364. }
  365. is := issues_model.Issue{
  366. RepoID: g.repo.ID,
  367. Repo: g.repo,
  368. Index: issue.Number,
  369. Title: util.TruncateRunes(issue.Title, 255),
  370. Content: issue.Content,
  371. Ref: issue.Ref,
  372. IsClosed: issue.State == "closed",
  373. IsLocked: issue.IsLocked,
  374. MilestoneID: milestoneID,
  375. Labels: labels,
  376. CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()),
  377. UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()),
  378. }
  379. if err := g.remapUser(ctx, issue, &is); err != nil {
  380. return err
  381. }
  382. if issue.Closed != nil {
  383. is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix())
  384. }
  385. // add reactions
  386. for _, reaction := range issue.Reactions {
  387. res := issues_model.Reaction{
  388. Type: reaction.Content,
  389. CreatedUnix: timeutil.TimeStampNow(),
  390. }
  391. if err := g.remapUser(ctx, reaction, &res); err != nil {
  392. return err
  393. }
  394. is.Reactions = append(is.Reactions, &res)
  395. }
  396. iss = append(iss, &is)
  397. }
  398. if len(iss) > 0 {
  399. if err := issues_model.InsertIssues(ctx, iss...); err != nil {
  400. return err
  401. }
  402. for _, is := range iss {
  403. g.issues[is.Index] = is
  404. }
  405. }
  406. return nil
  407. }
  408. // CreateComments creates comments of issues
  409. func (g *GiteaLocalUploader) CreateComments(ctx context.Context, comments ...*base.Comment) error {
  410. cms := make([]*issues_model.Comment, 0, len(comments))
  411. for _, comment := range comments {
  412. var issue *issues_model.Issue
  413. issue, ok := g.issues[comment.IssueIndex]
  414. if !ok {
  415. return fmt.Errorf("comment references non existent IssueIndex %d", comment.IssueIndex)
  416. }
  417. if comment.Created.IsZero() {
  418. comment.Created = time.Unix(int64(issue.CreatedUnix), 0)
  419. }
  420. if comment.Updated.IsZero() {
  421. comment.Updated = comment.Created
  422. }
  423. if comment.CommentType == "" {
  424. // if type field is missing, then assume a normal comment
  425. comment.CommentType = issues_model.CommentTypeComment.String()
  426. }
  427. cm := issues_model.Comment{
  428. IssueID: issue.ID,
  429. Type: issues_model.AsCommentType(comment.CommentType),
  430. Content: comment.Content,
  431. CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()),
  432. UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()),
  433. }
  434. switch cm.Type {
  435. case issues_model.CommentTypeReopen:
  436. cm.Content = ""
  437. case issues_model.CommentTypeClose:
  438. cm.Content = ""
  439. case issues_model.CommentTypeAssignees:
  440. if assigneeID, ok := comment.Meta["AssigneeID"].(int); ok {
  441. cm.AssigneeID = int64(assigneeID)
  442. }
  443. if comment.Meta["RemovedAssigneeID"] != nil {
  444. cm.RemovedAssignee = true
  445. }
  446. case issues_model.CommentTypeChangeTitle:
  447. if comment.Meta["OldTitle"] != nil {
  448. cm.OldTitle = fmt.Sprint(comment.Meta["OldTitle"])
  449. }
  450. if comment.Meta["NewTitle"] != nil {
  451. cm.NewTitle = fmt.Sprint(comment.Meta["NewTitle"])
  452. }
  453. case issues_model.CommentTypeChangeTargetBranch:
  454. if comment.Meta["OldRef"] != nil && comment.Meta["NewRef"] != nil {
  455. cm.OldRef = fmt.Sprint(comment.Meta["OldRef"])
  456. cm.NewRef = fmt.Sprint(comment.Meta["NewRef"])
  457. cm.Content = ""
  458. }
  459. case issues_model.CommentTypeMergePull:
  460. cm.Content = ""
  461. case issues_model.CommentTypePRScheduledToAutoMerge, issues_model.CommentTypePRUnScheduledToAutoMerge:
  462. cm.Content = ""
  463. default:
  464. }
  465. if err := g.remapUser(ctx, comment, &cm); err != nil {
  466. return err
  467. }
  468. // add reactions
  469. for _, reaction := range comment.Reactions {
  470. res := issues_model.Reaction{
  471. Type: reaction.Content,
  472. CreatedUnix: timeutil.TimeStampNow(),
  473. }
  474. if err := g.remapUser(ctx, reaction, &res); err != nil {
  475. return err
  476. }
  477. cm.Reactions = append(cm.Reactions, &res)
  478. }
  479. cms = append(cms, &cm)
  480. }
  481. if len(cms) == 0 {
  482. return nil
  483. }
  484. return issues_model.InsertIssueComments(ctx, cms)
  485. }
  486. // CreatePullRequests creates pull requests
  487. func (g *GiteaLocalUploader) CreatePullRequests(ctx context.Context, prs ...*base.PullRequest) error {
  488. gprs := make([]*issues_model.PullRequest, 0, len(prs))
  489. for _, pr := range prs {
  490. gpr, err := g.newPullRequest(ctx, pr)
  491. if err != nil {
  492. return err
  493. }
  494. if err := g.remapUser(ctx, pr, gpr.Issue); err != nil {
  495. return err
  496. }
  497. gprs = append(gprs, gpr)
  498. }
  499. if err := issues_model.InsertPullRequests(ctx, gprs...); err != nil {
  500. return err
  501. }
  502. for _, pr := range gprs {
  503. g.issues[pr.Issue.Index] = pr.Issue
  504. pull.StartPullRequestCheckImmediately(ctx, pr)
  505. }
  506. return nil
  507. }
  508. func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *base.PullRequest) (head string, err error) {
  509. // SECURITY: this pr must have been must have been ensured safe
  510. if !pr.EnsuredSafe {
  511. log.Error("PR #%d in %s/%s has not been checked for safety.", pr.Number, g.repoOwner, g.repoName)
  512. return "", fmt.Errorf("the PR[%d] was not checked for safety", pr.Number)
  513. }
  514. // Anonymous function to download the patch file (allows us to use defer)
  515. err = func() error {
  516. // if the patchURL is empty there is nothing to download
  517. if pr.PatchURL == "" {
  518. return nil
  519. }
  520. // SECURITY: We will assume that the pr.PatchURL has been checked
  521. // pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
  522. ret, err := uri.Open(pr.PatchURL) // TODO: This probably needs to use the downloader as there may be rate limiting issues here
  523. if err != nil {
  524. return err
  525. }
  526. defer ret.Close()
  527. pullDir := filepath.Join(g.repo.RepoPath(), "pulls")
  528. if err = os.MkdirAll(pullDir, os.ModePerm); err != nil {
  529. return err
  530. }
  531. f, err := os.Create(filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number)))
  532. if err != nil {
  533. return err
  534. }
  535. defer f.Close()
  536. // TODO: Should there be limits on the size of this file?
  537. _, err = io.Copy(f, ret)
  538. return err
  539. }()
  540. if err != nil {
  541. return "", err
  542. }
  543. head = "unknown repository"
  544. if pr.IsForkPullRequest() && pr.State != "closed" {
  545. // OK we want to fetch the current head as a branch from its CloneURL
  546. // 1. Is there a head clone URL available?
  547. // 2. Is there a head ref available?
  548. if pr.Head.CloneURL == "" || pr.Head.Ref == "" {
  549. return head, nil
  550. }
  551. // 3. We need to create a remote for this clone url
  552. // ... maybe we already have a name for this remote
  553. remote, ok := g.prHeadCache[pr.Head.CloneURL+":"]
  554. if !ok {
  555. // ... let's try ownername as a reasonable name
  556. remote = pr.Head.OwnerName
  557. if !git.IsValidRefPattern(remote) {
  558. // ... let's try something less nice
  559. remote = "head-pr-" + strconv.FormatInt(pr.Number, 10)
  560. }
  561. // ... now add the remote
  562. err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
  563. if err != nil {
  564. log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err)
  565. } else {
  566. g.prHeadCache[pr.Head.CloneURL+":"] = remote
  567. ok = true
  568. }
  569. }
  570. if !ok {
  571. return head, nil
  572. }
  573. // 4. Check if we already have this ref?
  574. localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref]
  575. if !ok {
  576. // ... We would normally name this migrated branch as <OwnerName>/<HeadRef> but we need to ensure that is safe
  577. localRef = git.SanitizeRefPattern(pr.Head.OwnerName + "/" + pr.Head.Ref)
  578. // ... Now we must assert that this does not exist
  579. if g.gitRepo.IsBranchExist(localRef) {
  580. localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef
  581. i := 0
  582. for g.gitRepo.IsBranchExist(localRef) {
  583. if i > 5 {
  584. // ... We tried, we really tried but this is just a seriously unfriendly repo
  585. return head, nil
  586. }
  587. // OK just try some uuids!
  588. localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String())
  589. i++
  590. }
  591. }
  592. fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef
  593. if strings.HasPrefix(fetchArg, "-") {
  594. fetchArg = git.BranchPrefix + fetchArg
  595. }
  596. _, _, err = gitcmd.NewCommand("fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(ctx, &gitcmd.RunOpts{Dir: g.repo.RepoPath()})
  597. if err != nil {
  598. log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
  599. return head, nil
  600. }
  601. g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef
  602. head = localRef
  603. }
  604. // 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch
  605. if pr.Head.SHA == "" {
  606. headSha, err := g.gitRepo.GetBranchCommitID(localRef)
  607. if err != nil {
  608. 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)
  609. return head, nil
  610. }
  611. pr.Head.SHA = headSha
  612. }
  613. if err = gitrepo.UpdateRef(ctx, g.repo, pr.GetGitHeadRefName(), pr.Head.SHA); err != nil {
  614. return "", err
  615. }
  616. return head, nil
  617. }
  618. if pr.Head.Ref != "" {
  619. head = pr.Head.Ref
  620. }
  621. // Ensure the closed PR SHA still points to an existing ref
  622. if pr.Head.SHA == "" {
  623. // The SHA is empty
  624. log.Warn("Empty reference, no pull head for PR #%d in %s/%s", pr.Number, g.repoOwner, g.repoName)
  625. } else {
  626. _, _, err = gitcmd.NewCommand("rev-list", "--quiet", "-1").AddDynamicArguments(pr.Head.SHA).RunStdString(ctx, &gitcmd.RunOpts{Dir: g.repo.RepoPath()})
  627. if err != nil {
  628. // Git update-ref remove bad references with a relative path
  629. log.Warn("Deprecated local head %s for PR #%d in %s/%s, removing %s", pr.Head.SHA, pr.Number, g.repoOwner, g.repoName, pr.GetGitHeadRefName())
  630. } else {
  631. // set head information
  632. if err = gitrepo.UpdateRef(ctx, g.repo, pr.GetGitHeadRefName(), pr.Head.SHA); err != nil {
  633. 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)
  634. }
  635. }
  636. }
  637. return head, nil
  638. }
  639. func (g *GiteaLocalUploader) newPullRequest(ctx context.Context, pr *base.PullRequest) (*issues_model.PullRequest, error) {
  640. var labels []*issues_model.Label
  641. for _, label := range pr.Labels {
  642. lb, ok := g.labels[label.Name]
  643. if ok {
  644. labels = append(labels, lb)
  645. }
  646. }
  647. milestoneID := g.milestones[pr.Milestone]
  648. head, err := g.updateGitForPullRequest(ctx, pr)
  649. if err != nil {
  650. return nil, fmt.Errorf("updateGitForPullRequest: %w", err)
  651. }
  652. // Now we may need to fix the mergebase
  653. if pr.Base.SHA == "" {
  654. if pr.Base.Ref != "" && pr.Head.SHA != "" {
  655. // A PR against a tag base does not make sense - therefore pr.Base.Ref must be a branch
  656. // TODO: should we be checking for the refs/heads/ prefix on the pr.Base.Ref? (i.e. are these actually branches or refs)
  657. pr.Base.SHA, _, err = g.gitRepo.GetMergeBase("", git.BranchPrefix+pr.Base.Ref, pr.Head.SHA)
  658. if err != nil {
  659. log.Error("Cannot determine the merge base for PR #%d in %s/%s. Error: %v", pr.Number, g.repoOwner, g.repoName, err)
  660. }
  661. } else {
  662. log.Error("Cannot determine the merge base for PR #%d in %s/%s. Not enough information", pr.Number, g.repoOwner, g.repoName)
  663. }
  664. }
  665. if pr.Created.IsZero() {
  666. if pr.Closed != nil {
  667. pr.Created = *pr.Closed
  668. } else if pr.MergedTime != nil {
  669. pr.Created = *pr.MergedTime
  670. } else {
  671. pr.Created = time.Now()
  672. }
  673. }
  674. if pr.Updated.IsZero() {
  675. pr.Updated = pr.Created
  676. }
  677. prTitle := pr.Title
  678. if pr.IsDraft && !issues_model.HasWorkInProgressPrefix(pr.Title) {
  679. prTitle = fmt.Sprintf("%s %s", setting.Repository.PullRequest.WorkInProgressPrefixes[0], pr.Title)
  680. }
  681. issue := issues_model.Issue{
  682. RepoID: g.repo.ID,
  683. Repo: g.repo,
  684. Title: util.TruncateRunes(prTitle, 255),
  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(ctx, 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(ctx, 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(ctx context.Context, 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(ctx, 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(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.GetGitHeadRefName())
  786. if err != nil {
  787. log.Warn("PR #%d GetRefCommitID[%s] in %s/%s: %v, all review comments will be ignored", pr.Index, pr.GetGitHeadRefName(), 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(ctx, review, &c); err != nil {
  836. return err
  837. }
  838. cm.Comments = append(cm.Comments, &c)
  839. }
  840. }
  841. return issues_model.InsertReviews(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(ctx context.Context) error {
  853. if g.repo == nil || g.repo.ID <= 0 {
  854. return ErrRepoNotCreated
  855. }
  856. // update issue_index
  857. if err := issues_model.RecalculateIssueIndexForRepo(ctx, g.repo.ID); err != nil {
  858. return err
  859. }
  860. if err := models.UpdateRepoStats(ctx, g.repo.ID); err != nil {
  861. return err
  862. }
  863. g.repo.Status = repo_model.RepositoryReady
  864. return repo_model.UpdateRepositoryColsWithAutoTime(ctx, g.repo, "status")
  865. }
  866. func (g *GiteaLocalUploader) remapUser(ctx context.Context, 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(ctx, source)
  871. } else {
  872. userID, err = g.remapExternalUser(ctx, 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(ctx context.Context, source user_model.ExternalUserMigrated) (int64, error) {
  883. userid, ok := g.userMap[source.GetExternalID()]
  884. if !ok {
  885. name, err := user_model.GetUserNameByID(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(ctx context.Context, source user_model.ExternalUserMigrated) (userid int64, err error) {
  900. userid, ok := g.userMap[source.GetExternalID()]
  901. if !ok {
  902. userid, err = user_model.GetUserIDByExternalUserID(ctx, g.gitServiceType.Name(), strconv.FormatInt(source.GetExternalID(), 10))
  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. }