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

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