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

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