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.

gitlab.go 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package migrations
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "path"
  12. "regexp"
  13. "strings"
  14. "time"
  15. issues_model "code.gitea.io/gitea/models/issues"
  16. "code.gitea.io/gitea/modules/container"
  17. "code.gitea.io/gitea/modules/log"
  18. base "code.gitea.io/gitea/modules/migration"
  19. "code.gitea.io/gitea/modules/structs"
  20. "github.com/xanzy/go-gitlab"
  21. )
  22. var (
  23. _ base.Downloader = &GitlabDownloader{}
  24. _ base.DownloaderFactory = &GitlabDownloaderFactory{}
  25. )
  26. func init() {
  27. RegisterDownloaderFactory(&GitlabDownloaderFactory{})
  28. }
  29. // GitlabDownloaderFactory defines a gitlab downloader factory
  30. type GitlabDownloaderFactory struct{}
  31. // New returns a Downloader related to this factory according MigrateOptions
  32. func (f *GitlabDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
  33. u, err := url.Parse(opts.CloneAddr)
  34. if err != nil {
  35. return nil, err
  36. }
  37. baseURL := u.Scheme + "://" + u.Host
  38. repoNameSpace := strings.TrimPrefix(u.Path, "/")
  39. repoNameSpace = strings.TrimSuffix(repoNameSpace, ".git")
  40. log.Trace("Create gitlab downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace)
  41. return NewGitlabDownloader(ctx, baseURL, repoNameSpace, opts.AuthUsername, opts.AuthPassword, opts.AuthToken)
  42. }
  43. // GitServiceType returns the type of git service
  44. func (f *GitlabDownloaderFactory) GitServiceType() structs.GitServiceType {
  45. return structs.GitlabService
  46. }
  47. type gitlabIIDResolver struct {
  48. maxIssueIID int64
  49. frozen bool
  50. }
  51. func (r *gitlabIIDResolver) recordIssueIID(issueIID int) {
  52. if r.frozen {
  53. panic("cannot record issue IID after pull request IID generation has started")
  54. }
  55. r.maxIssueIID = max(r.maxIssueIID, int64(issueIID))
  56. }
  57. func (r *gitlabIIDResolver) generatePullRequestNumber(mrIID int) int64 {
  58. r.frozen = true
  59. return r.maxIssueIID + int64(mrIID)
  60. }
  61. // GitlabDownloader implements a Downloader interface to get repository information
  62. // from gitlab via go-gitlab
  63. // - issueCount is incremented in GetIssues() to ensure PR and Issue numbers do not overlap,
  64. // because Gitlab has individual Issue and Pull Request numbers.
  65. type GitlabDownloader struct {
  66. base.NullDownloader
  67. ctx context.Context
  68. client *gitlab.Client
  69. baseURL string
  70. repoID int
  71. repoName string
  72. iidResolver gitlabIIDResolver
  73. maxPerPage int
  74. }
  75. // NewGitlabDownloader creates a gitlab Downloader via gitlab API
  76. //
  77. // Use either a username/password, personal token entered into the username field, or anonymous/public access
  78. // Note: Public access only allows very basic access
  79. func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, password, token string) (*GitlabDownloader, error) {
  80. gitlabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient()))
  81. // Only use basic auth if token is blank and password is NOT
  82. // Basic auth will fail with empty strings, but empty token will allow anonymous public API usage
  83. if token == "" && password != "" {
  84. gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient()))
  85. }
  86. if err != nil {
  87. log.Trace("Error logging into gitlab: %v", err)
  88. return nil, err
  89. }
  90. // split namespace and subdirectory
  91. pathParts := strings.Split(strings.Trim(repoPath, "/"), "/")
  92. var resp *gitlab.Response
  93. u, _ := url.Parse(baseURL)
  94. for len(pathParts) >= 2 {
  95. _, resp, err = gitlabClient.Version.GetVersion()
  96. if err == nil || resp != nil && resp.StatusCode == http.StatusUnauthorized {
  97. err = nil // if no authentication given, this still should work
  98. break
  99. }
  100. u.Path = path.Join(u.Path, pathParts[0])
  101. baseURL = u.String()
  102. pathParts = pathParts[1:]
  103. _ = gitlab.WithBaseURL(baseURL)(gitlabClient)
  104. repoPath = strings.Join(pathParts, "/")
  105. }
  106. if err != nil {
  107. log.Trace("Error could not get gitlab version: %v", err)
  108. return nil, err
  109. }
  110. log.Trace("gitlab downloader: use BaseURL: '%s' and RepoPath: '%s'", baseURL, repoPath)
  111. // Grab and store project/repo ID here, due to issues using the URL escaped path
  112. gr, _, err := gitlabClient.Projects.GetProject(repoPath, nil, nil, gitlab.WithContext(ctx))
  113. if err != nil {
  114. log.Trace("Error retrieving project: %v", err)
  115. return nil, err
  116. }
  117. if gr == nil {
  118. log.Trace("Error getting project, project is nil")
  119. return nil, errors.New("Error getting project, project is nil")
  120. }
  121. return &GitlabDownloader{
  122. ctx: ctx,
  123. client: gitlabClient,
  124. baseURL: baseURL,
  125. repoID: gr.ID,
  126. repoName: gr.Name,
  127. maxPerPage: 100,
  128. }, nil
  129. }
  130. // String implements Stringer
  131. func (g *GitlabDownloader) String() string {
  132. return fmt.Sprintf("migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName)
  133. }
  134. func (g *GitlabDownloader) LogString() string {
  135. if g == nil {
  136. return "<GitlabDownloader nil>"
  137. }
  138. return fmt.Sprintf("<GitlabDownloader %s [%d]/%s>", g.baseURL, g.repoID, g.repoName)
  139. }
  140. // SetContext set context
  141. func (g *GitlabDownloader) SetContext(ctx context.Context) {
  142. g.ctx = ctx
  143. }
  144. // GetRepoInfo returns a repository information
  145. func (g *GitlabDownloader) GetRepoInfo() (*base.Repository, error) {
  146. gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx))
  147. if err != nil {
  148. return nil, err
  149. }
  150. var private bool
  151. switch gr.Visibility {
  152. case gitlab.InternalVisibility:
  153. private = true
  154. case gitlab.PrivateVisibility:
  155. private = true
  156. }
  157. var owner string
  158. if gr.Owner == nil {
  159. log.Trace("gr.Owner is nil, trying to get owner from Namespace")
  160. if gr.Namespace != nil && gr.Namespace.Kind == "user" {
  161. owner = gr.Namespace.Path
  162. }
  163. } else {
  164. owner = gr.Owner.Username
  165. }
  166. // convert gitlab repo to stand Repo
  167. return &base.Repository{
  168. Owner: owner,
  169. Name: gr.Name,
  170. IsPrivate: private,
  171. Description: gr.Description,
  172. OriginalURL: gr.WebURL,
  173. CloneURL: gr.HTTPURLToRepo,
  174. DefaultBranch: gr.DefaultBranch,
  175. }, nil
  176. }
  177. // GetTopics return gitlab topics
  178. func (g *GitlabDownloader) GetTopics() ([]string, error) {
  179. gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx))
  180. if err != nil {
  181. return nil, err
  182. }
  183. return gr.TagList, err
  184. }
  185. // GetMilestones returns milestones
  186. func (g *GitlabDownloader) GetMilestones() ([]*base.Milestone, error) {
  187. perPage := g.maxPerPage
  188. state := "all"
  189. milestones := make([]*base.Milestone, 0, perPage)
  190. for i := 1; ; i++ {
  191. ms, _, err := g.client.Milestones.ListMilestones(g.repoID, &gitlab.ListMilestonesOptions{
  192. State: &state,
  193. ListOptions: gitlab.ListOptions{
  194. Page: i,
  195. PerPage: perPage,
  196. },
  197. }, nil, gitlab.WithContext(g.ctx))
  198. if err != nil {
  199. return nil, err
  200. }
  201. for _, m := range ms {
  202. var desc string
  203. if m.Description != "" {
  204. desc = m.Description
  205. }
  206. state := "open"
  207. var closedAt *time.Time
  208. if m.State != "" {
  209. state = m.State
  210. if state == "closed" {
  211. closedAt = m.UpdatedAt
  212. }
  213. }
  214. var deadline *time.Time
  215. if m.DueDate != nil {
  216. deadlineParsed, err := time.Parse("2006-01-02", m.DueDate.String())
  217. if err != nil {
  218. log.Trace("Error parsing Milestone DueDate time")
  219. deadline = nil
  220. } else {
  221. deadline = &deadlineParsed
  222. }
  223. }
  224. milestones = append(milestones, &base.Milestone{
  225. Title: m.Title,
  226. Description: desc,
  227. Deadline: deadline,
  228. State: state,
  229. Created: *m.CreatedAt,
  230. Updated: m.UpdatedAt,
  231. Closed: closedAt,
  232. })
  233. }
  234. if len(ms) < perPage {
  235. break
  236. }
  237. }
  238. return milestones, nil
  239. }
  240. func (g *GitlabDownloader) normalizeColor(val string) string {
  241. val = strings.TrimLeft(val, "#")
  242. val = strings.ToLower(val)
  243. if len(val) == 3 {
  244. c := []rune(val)
  245. val = fmt.Sprintf("%c%c%c%c%c%c", c[0], c[0], c[1], c[1], c[2], c[2])
  246. }
  247. if len(val) != 6 {
  248. return ""
  249. }
  250. return val
  251. }
  252. // GetLabels returns labels
  253. func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) {
  254. perPage := g.maxPerPage
  255. labels := make([]*base.Label, 0, perPage)
  256. for i := 1; ; i++ {
  257. ls, _, err := g.client.Labels.ListLabels(g.repoID, &gitlab.ListLabelsOptions{ListOptions: gitlab.ListOptions{
  258. Page: i,
  259. PerPage: perPage,
  260. }}, nil, gitlab.WithContext(g.ctx))
  261. if err != nil {
  262. return nil, err
  263. }
  264. for _, label := range ls {
  265. baseLabel := &base.Label{
  266. Name: label.Name,
  267. Color: g.normalizeColor(label.Color),
  268. Description: label.Description,
  269. }
  270. labels = append(labels, baseLabel)
  271. }
  272. if len(ls) < perPage {
  273. break
  274. }
  275. }
  276. return labels, nil
  277. }
  278. func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Release {
  279. var zero int
  280. r := &base.Release{
  281. TagName: rel.TagName,
  282. TargetCommitish: rel.Commit.ID,
  283. Name: rel.Name,
  284. Body: rel.Description,
  285. Created: *rel.CreatedAt,
  286. PublisherID: int64(rel.Author.ID),
  287. PublisherName: rel.Author.Username,
  288. }
  289. httpClient := NewMigrationHTTPClient()
  290. for k, asset := range rel.Assets.Links {
  291. assetID := asset.ID // Don't optimize this, for closure we need a local variable
  292. r.Assets = append(r.Assets, &base.ReleaseAsset{
  293. ID: int64(asset.ID),
  294. Name: asset.Name,
  295. ContentType: &rel.Assets.Sources[k].Format,
  296. Size: &zero,
  297. DownloadCount: &zero,
  298. DownloadFunc: func() (io.ReadCloser, error) {
  299. link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(g.ctx))
  300. if err != nil {
  301. return nil, err
  302. }
  303. if !hasBaseURL(link.URL, g.baseURL) {
  304. WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, link.URL)
  305. return io.NopCloser(strings.NewReader(link.URL)), nil
  306. }
  307. req, err := http.NewRequest("GET", link.URL, nil)
  308. if err != nil {
  309. return nil, err
  310. }
  311. req = req.WithContext(g.ctx)
  312. resp, err := httpClient.Do(req)
  313. if err != nil {
  314. return nil, err
  315. }
  316. // resp.Body is closed by the uploader
  317. return resp.Body, nil
  318. },
  319. })
  320. }
  321. return r
  322. }
  323. // GetReleases returns releases
  324. func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) {
  325. perPage := g.maxPerPage
  326. releases := make([]*base.Release, 0, perPage)
  327. for i := 1; ; i++ {
  328. ls, _, err := g.client.Releases.ListReleases(g.repoID, &gitlab.ListReleasesOptions{
  329. ListOptions: gitlab.ListOptions{
  330. Page: i,
  331. PerPage: perPage,
  332. },
  333. }, nil, gitlab.WithContext(g.ctx))
  334. if err != nil {
  335. return nil, err
  336. }
  337. for _, release := range ls {
  338. releases = append(releases, g.convertGitlabRelease(release))
  339. }
  340. if len(ls) < perPage {
  341. break
  342. }
  343. }
  344. return releases, nil
  345. }
  346. type gitlabIssueContext struct {
  347. IsMergeRequest bool
  348. }
  349. // GetIssues returns issues according start and limit
  350. //
  351. // Note: issue label description and colors are not supported by the go-gitlab library at this time
  352. func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
  353. state := "all"
  354. sort := "asc"
  355. if perPage > g.maxPerPage {
  356. perPage = g.maxPerPage
  357. }
  358. opt := &gitlab.ListProjectIssuesOptions{
  359. State: &state,
  360. Sort: &sort,
  361. ListOptions: gitlab.ListOptions{
  362. PerPage: perPage,
  363. Page: page,
  364. },
  365. }
  366. allIssues := make([]*base.Issue, 0, perPage)
  367. issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
  368. if err != nil {
  369. return nil, false, fmt.Errorf("error while listing issues: %w", err)
  370. }
  371. for _, issue := range issues {
  372. labels := make([]*base.Label, 0, len(issue.Labels))
  373. for _, l := range issue.Labels {
  374. labels = append(labels, &base.Label{
  375. Name: l,
  376. })
  377. }
  378. var milestone string
  379. if issue.Milestone != nil {
  380. milestone = issue.Milestone.Title
  381. }
  382. var reactions []*gitlab.AwardEmoji
  383. awardPage := 1
  384. for {
  385. awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
  386. if err != nil {
  387. return nil, false, fmt.Errorf("error while listing issue awards: %w", err)
  388. }
  389. reactions = append(reactions, awards...)
  390. if len(awards) < perPage {
  391. break
  392. }
  393. awardPage++
  394. }
  395. allIssues = append(allIssues, &base.Issue{
  396. Title: issue.Title,
  397. Number: int64(issue.IID),
  398. PosterID: int64(issue.Author.ID),
  399. PosterName: issue.Author.Username,
  400. Content: issue.Description,
  401. Milestone: milestone,
  402. State: issue.State,
  403. Created: *issue.CreatedAt,
  404. Labels: labels,
  405. Reactions: g.awardsToReactions(reactions),
  406. Closed: issue.ClosedAt,
  407. IsLocked: issue.DiscussionLocked,
  408. Updated: *issue.UpdatedAt,
  409. ForeignIndex: int64(issue.IID),
  410. Context: gitlabIssueContext{IsMergeRequest: false},
  411. })
  412. // record the issue IID, to be used in GetPullRequests()
  413. g.iidResolver.recordIssueIID(issue.IID)
  414. }
  415. return allIssues, len(issues) < perPage, nil
  416. }
  417. // GetComments returns comments according issueNumber
  418. // TODO: figure out how to transfer comment reactions
  419. func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
  420. context, ok := commentable.GetContext().(gitlabIssueContext)
  421. if !ok {
  422. return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
  423. }
  424. allComments := make([]*base.Comment, 0, g.maxPerPage)
  425. page := 1
  426. for {
  427. var comments []*gitlab.Discussion
  428. var resp *gitlab.Response
  429. var err error
  430. if !context.IsMergeRequest {
  431. comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListIssueDiscussionsOptions{
  432. Page: page,
  433. PerPage: g.maxPerPage,
  434. }, nil, gitlab.WithContext(g.ctx))
  435. } else {
  436. comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListMergeRequestDiscussionsOptions{
  437. Page: page,
  438. PerPage: g.maxPerPage,
  439. }, nil, gitlab.WithContext(g.ctx))
  440. }
  441. if err != nil {
  442. return nil, false, fmt.Errorf("error while listing comments: %v %w", g.repoID, err)
  443. }
  444. for _, comment := range comments {
  445. for _, note := range comment.Notes {
  446. allComments = append(allComments, g.convertNoteToComment(commentable.GetLocalIndex(), note))
  447. }
  448. }
  449. if resp.NextPage == 0 {
  450. break
  451. }
  452. page = resp.NextPage
  453. }
  454. page = 1
  455. for {
  456. var stateEvents []*gitlab.StateEvent
  457. var resp *gitlab.Response
  458. var err error
  459. if context.IsMergeRequest {
  460. stateEvents, resp, err = g.client.ResourceStateEvents.ListMergeStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
  461. ListOptions: gitlab.ListOptions{
  462. Page: page,
  463. PerPage: g.maxPerPage,
  464. },
  465. }, nil, gitlab.WithContext(g.ctx))
  466. } else {
  467. stateEvents, resp, err = g.client.ResourceStateEvents.ListIssueStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
  468. ListOptions: gitlab.ListOptions{
  469. Page: page,
  470. PerPage: g.maxPerPage,
  471. },
  472. }, nil, gitlab.WithContext(g.ctx))
  473. }
  474. if err != nil {
  475. return nil, false, fmt.Errorf("error while listing state events: %v %w", g.repoID, err)
  476. }
  477. for _, stateEvent := range stateEvents {
  478. comment := &base.Comment{
  479. IssueIndex: commentable.GetLocalIndex(),
  480. Index: int64(stateEvent.ID),
  481. PosterID: int64(stateEvent.User.ID),
  482. PosterName: stateEvent.User.Username,
  483. Content: "",
  484. Created: *stateEvent.CreatedAt,
  485. }
  486. switch stateEvent.State {
  487. case gitlab.ClosedEventType:
  488. comment.CommentType = issues_model.CommentTypeClose.String()
  489. case gitlab.MergedEventType:
  490. comment.CommentType = issues_model.CommentTypeMergePull.String()
  491. case gitlab.ReopenedEventType:
  492. comment.CommentType = issues_model.CommentTypeReopen.String()
  493. default:
  494. // Ignore other event types
  495. continue
  496. }
  497. allComments = append(allComments, comment)
  498. }
  499. if resp.NextPage == 0 {
  500. break
  501. }
  502. page = resp.NextPage
  503. }
  504. return allComments, true, nil
  505. }
  506. var targetBranchChangeRegexp = regexp.MustCompile("^changed target branch from `(.*?)` to `(.*?)`$")
  507. func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.Note) *base.Comment {
  508. comment := &base.Comment{
  509. IssueIndex: localIndex,
  510. Index: int64(note.ID),
  511. PosterID: int64(note.Author.ID),
  512. PosterName: note.Author.Username,
  513. PosterEmail: note.Author.Email,
  514. Content: note.Body,
  515. Created: *note.CreatedAt,
  516. Meta: map[string]any{},
  517. }
  518. // Try to find the underlying event of system notes.
  519. if note.System {
  520. if match := targetBranchChangeRegexp.FindStringSubmatch(note.Body); match != nil {
  521. comment.CommentType = issues_model.CommentTypeChangeTargetBranch.String()
  522. comment.Meta["OldRef"] = match[1]
  523. comment.Meta["NewRef"] = match[2]
  524. } else if strings.HasPrefix(note.Body, "enabled an automatic merge") {
  525. comment.CommentType = issues_model.CommentTypePRScheduledToAutoMerge.String()
  526. } else if note.Body == "canceled the automatic merge" {
  527. comment.CommentType = issues_model.CommentTypePRUnScheduledToAutoMerge.String()
  528. }
  529. }
  530. return comment
  531. }
  532. // GetPullRequests returns pull requests according page and perPage
  533. func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
  534. if perPage > g.maxPerPage {
  535. perPage = g.maxPerPage
  536. }
  537. view := "simple"
  538. opt := &gitlab.ListProjectMergeRequestsOptions{
  539. ListOptions: gitlab.ListOptions{
  540. PerPage: perPage,
  541. Page: page,
  542. },
  543. View: &view,
  544. }
  545. allPRs := make([]*base.PullRequest, 0, perPage)
  546. prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
  547. if err != nil {
  548. return nil, false, fmt.Errorf("error while listing merge requests: %w", err)
  549. }
  550. for _, simplePR := range prs {
  551. // Load merge request again by itself, as not all fields are populated in the ListProjectMergeRequests endpoint.
  552. // See https://gitlab.com/gitlab-org/gitlab/-/issues/29620
  553. pr, _, err := g.client.MergeRequests.GetMergeRequest(g.repoID, simplePR.IID, nil)
  554. if err != nil {
  555. return nil, false, fmt.Errorf("error while loading merge request: %w", err)
  556. }
  557. labels := make([]*base.Label, 0, len(pr.Labels))
  558. for _, l := range pr.Labels {
  559. labels = append(labels, &base.Label{
  560. Name: l,
  561. })
  562. }
  563. var merged bool
  564. if pr.State == "merged" {
  565. merged = true
  566. pr.State = "closed"
  567. }
  568. mergeTime := pr.MergedAt
  569. if merged && pr.MergedAt == nil {
  570. mergeTime = pr.UpdatedAt
  571. }
  572. closeTime := pr.ClosedAt
  573. if merged && pr.ClosedAt == nil {
  574. closeTime = pr.UpdatedAt
  575. }
  576. mergeCommitSHA := pr.MergeCommitSHA
  577. if mergeCommitSHA == "" {
  578. mergeCommitSHA = pr.SquashCommitSHA
  579. }
  580. var locked bool
  581. if pr.State == "locked" {
  582. locked = true
  583. }
  584. var milestone string
  585. if pr.Milestone != nil {
  586. milestone = pr.Milestone.Title
  587. }
  588. var reactions []*gitlab.AwardEmoji
  589. awardPage := 1
  590. for {
  591. awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
  592. if err != nil {
  593. return nil, false, fmt.Errorf("error while listing merge requests awards: %w", err)
  594. }
  595. reactions = append(reactions, awards...)
  596. if len(awards) < perPage {
  597. break
  598. }
  599. awardPage++
  600. }
  601. // Generate new PR Numbers by the known Issue Numbers, because they share the same number space in Gitea, but they are independent in Gitlab
  602. newPRNumber := g.iidResolver.generatePullRequestNumber(pr.IID)
  603. allPRs = append(allPRs, &base.PullRequest{
  604. Title: pr.Title,
  605. Number: newPRNumber,
  606. PosterName: pr.Author.Username,
  607. PosterID: int64(pr.Author.ID),
  608. Content: pr.Description,
  609. Milestone: milestone,
  610. State: pr.State,
  611. Created: *pr.CreatedAt,
  612. Closed: closeTime,
  613. Labels: labels,
  614. Merged: merged,
  615. MergeCommitSHA: mergeCommitSHA,
  616. MergedTime: mergeTime,
  617. IsLocked: locked,
  618. Reactions: g.awardsToReactions(reactions),
  619. Head: base.PullRequestBranch{
  620. Ref: pr.SourceBranch,
  621. SHA: pr.SHA,
  622. RepoName: g.repoName,
  623. OwnerName: pr.Author.Username,
  624. CloneURL: pr.WebURL,
  625. },
  626. Base: base.PullRequestBranch{
  627. Ref: pr.TargetBranch,
  628. SHA: pr.DiffRefs.BaseSha,
  629. RepoName: g.repoName,
  630. OwnerName: pr.Author.Username,
  631. },
  632. PatchURL: pr.WebURL + ".patch",
  633. ForeignIndex: int64(pr.IID),
  634. Context: gitlabIssueContext{IsMergeRequest: true},
  635. })
  636. // SECURITY: Ensure that the PR is safe
  637. _ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
  638. }
  639. return allPRs, len(prs) < perPage, nil
  640. }
  641. // GetReviews returns pull requests review
  642. func (g *GitlabDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
  643. approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(reviewable.GetForeignIndex()), gitlab.WithContext(g.ctx))
  644. if err != nil {
  645. if resp != nil && resp.StatusCode == http.StatusNotFound {
  646. log.Error(fmt.Sprintf("GitlabDownloader: while migrating a error occurred: '%s'", err.Error()))
  647. return []*base.Review{}, nil
  648. }
  649. return nil, err
  650. }
  651. var createdAt time.Time
  652. if approvals.CreatedAt != nil {
  653. createdAt = *approvals.CreatedAt
  654. } else if approvals.UpdatedAt != nil {
  655. createdAt = *approvals.UpdatedAt
  656. } else {
  657. createdAt = time.Now()
  658. }
  659. reviews := make([]*base.Review, 0, len(approvals.ApprovedBy))
  660. for _, user := range approvals.ApprovedBy {
  661. reviews = append(reviews, &base.Review{
  662. IssueIndex: reviewable.GetLocalIndex(),
  663. ReviewerID: int64(user.User.ID),
  664. ReviewerName: user.User.Username,
  665. CreatedAt: createdAt,
  666. // All we get are approvals
  667. State: base.ReviewStateApproved,
  668. })
  669. }
  670. return reviews, nil
  671. }
  672. func (g *GitlabDownloader) awardsToReactions(awards []*gitlab.AwardEmoji) []*base.Reaction {
  673. result := make([]*base.Reaction, 0, len(awards))
  674. uniqCheck := make(container.Set[string])
  675. for _, award := range awards {
  676. uid := fmt.Sprintf("%s%d", award.Name, award.User.ID)
  677. if uniqCheck.Add(uid) {
  678. result = append(result, &base.Reaction{
  679. UserID: int64(award.User.ID),
  680. UserName: award.User.Username,
  681. Content: award.Name,
  682. })
  683. }
  684. }
  685. return result
  686. }