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

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