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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  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. "errors"
  8. "fmt"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/migrations/base"
  14. "code.gitea.io/gitea/modules/structs"
  15. "github.com/xanzy/go-gitlab"
  16. )
  17. var (
  18. _ base.Downloader = &GitlabDownloader{}
  19. _ base.DownloaderFactory = &GitlabDownloaderFactory{}
  20. )
  21. func init() {
  22. RegisterDownloaderFactory(&GitlabDownloaderFactory{})
  23. }
  24. // GitlabDownloaderFactory defines a gitlab downloader factory
  25. type GitlabDownloaderFactory struct {
  26. }
  27. // Match returns true if the migration remote URL matched this downloader factory
  28. func (f *GitlabDownloaderFactory) Match(opts base.MigrateOptions) (bool, error) {
  29. var matched bool
  30. u, err := url.Parse(opts.CloneAddr)
  31. if err != nil {
  32. return false, err
  33. }
  34. if strings.EqualFold(u.Host, "gitlab.com") && opts.AuthUsername != "" {
  35. matched = true
  36. }
  37. return matched, nil
  38. }
  39. // New returns a Downloader related to this factory according MigrateOptions
  40. func (f *GitlabDownloaderFactory) New(opts base.MigrateOptions) (base.Downloader, error) {
  41. u, err := url.Parse(opts.CloneAddr)
  42. if err != nil {
  43. return nil, err
  44. }
  45. baseURL := u.Scheme + "://" + u.Host
  46. repoNameSpace := strings.TrimPrefix(u.Path, "/")
  47. log.Trace("Create gitlab downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace)
  48. return NewGitlabDownloader(baseURL, repoNameSpace, opts.AuthUsername, opts.AuthPassword), nil
  49. }
  50. // GitServiceType returns the type of git service
  51. func (f *GitlabDownloaderFactory) GitServiceType() structs.GitServiceType {
  52. return structs.GitlabService
  53. }
  54. // GitlabDownloader implements a Downloader interface to get repository informations
  55. // from gitlab via go-gitlab
  56. // - issueCount is incremented in GetIssues() to ensure PR and Issue numbers do not overlap,
  57. // because Gitlab has individual Issue and Pull Request numbers.
  58. // - issueSeen, working alongside issueCount, is checked in GetComments() to see whether we
  59. // need to fetch the Issue or PR comments, as Gitlab stores them separately.
  60. type GitlabDownloader struct {
  61. ctx context.Context
  62. client *gitlab.Client
  63. repoID int
  64. repoName string
  65. issueCount int64
  66. fetchPRcomments bool
  67. }
  68. // NewGitlabDownloader creates a gitlab Downloader via gitlab API
  69. // Use either a username/password, personal token entered into the username field, or anonymous/public access
  70. // Note: Public access only allows very basic access
  71. func NewGitlabDownloader(baseURL, repoPath, username, password string) *GitlabDownloader {
  72. var gitlabClient *gitlab.Client
  73. var err error
  74. if username != "" {
  75. if password == "" {
  76. gitlabClient, err = gitlab.NewClient(username)
  77. } else {
  78. gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL))
  79. }
  80. }
  81. if err != nil {
  82. log.Trace("Error logging into gitlab: %v", err)
  83. return nil
  84. }
  85. // Grab and store project/repo ID here, due to issues using the URL escaped path
  86. gr, _, err := gitlabClient.Projects.GetProject(repoPath, nil, nil)
  87. if err != nil {
  88. log.Trace("Error retrieving project: %v", err)
  89. return nil
  90. }
  91. if gr == nil {
  92. log.Trace("Error getting project, project is nil")
  93. return nil
  94. }
  95. return &GitlabDownloader{
  96. ctx: context.Background(),
  97. client: gitlabClient,
  98. repoID: gr.ID,
  99. repoName: gr.Name,
  100. }
  101. }
  102. // SetContext set context
  103. func (g *GitlabDownloader) SetContext(ctx context.Context) {
  104. g.ctx = ctx
  105. }
  106. // GetRepoInfo returns a repository information
  107. func (g *GitlabDownloader) GetRepoInfo() (*base.Repository, error) {
  108. if g == nil {
  109. return nil, errors.New("error: GitlabDownloader is nil")
  110. }
  111. gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil)
  112. if err != nil {
  113. return nil, err
  114. }
  115. var private bool
  116. switch gr.Visibility {
  117. case gitlab.InternalVisibility:
  118. private = true
  119. case gitlab.PrivateVisibility:
  120. private = true
  121. }
  122. var owner string
  123. if gr.Owner == nil {
  124. log.Trace("gr.Owner is nil, trying to get owner from Namespace")
  125. if gr.Namespace != nil && gr.Namespace.Kind == "user" {
  126. owner = gr.Namespace.Path
  127. }
  128. } else {
  129. owner = gr.Owner.Username
  130. }
  131. // convert gitlab repo to stand Repo
  132. return &base.Repository{
  133. Owner: owner,
  134. Name: gr.Name,
  135. IsPrivate: private,
  136. Description: gr.Description,
  137. OriginalURL: gr.WebURL,
  138. CloneURL: gr.HTTPURLToRepo,
  139. }, nil
  140. }
  141. // GetTopics return gitlab topics
  142. func (g *GitlabDownloader) GetTopics() ([]string, error) {
  143. if g == nil {
  144. return nil, errors.New("error: GitlabDownloader is nil")
  145. }
  146. gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil)
  147. if err != nil {
  148. return nil, err
  149. }
  150. return gr.TagList, err
  151. }
  152. // GetMilestones returns milestones
  153. func (g *GitlabDownloader) GetMilestones() ([]*base.Milestone, error) {
  154. if g == nil {
  155. return nil, errors.New("error: GitlabDownloader is nil")
  156. }
  157. var perPage = 100
  158. var state = "all"
  159. var milestones = make([]*base.Milestone, 0, perPage)
  160. for i := 1; ; i++ {
  161. ms, _, err := g.client.Milestones.ListMilestones(g.repoID, &gitlab.ListMilestonesOptions{
  162. State: &state,
  163. ListOptions: gitlab.ListOptions{
  164. Page: i,
  165. PerPage: perPage,
  166. }}, nil)
  167. if err != nil {
  168. return nil, err
  169. }
  170. for _, m := range ms {
  171. var desc string
  172. if m.Description != "" {
  173. desc = m.Description
  174. }
  175. var state = "open"
  176. var closedAt *time.Time
  177. if m.State != "" {
  178. state = m.State
  179. if state == "closed" {
  180. closedAt = m.UpdatedAt
  181. }
  182. }
  183. var deadline *time.Time
  184. if m.DueDate != nil {
  185. deadlineParsed, err := time.Parse("2006-01-02", m.DueDate.String())
  186. if err != nil {
  187. log.Trace("Error parsing Milestone DueDate time")
  188. deadline = nil
  189. } else {
  190. deadline = &deadlineParsed
  191. }
  192. }
  193. milestones = append(milestones, &base.Milestone{
  194. Title: m.Title,
  195. Description: desc,
  196. Deadline: deadline,
  197. State: state,
  198. Created: *m.CreatedAt,
  199. Updated: m.UpdatedAt,
  200. Closed: closedAt,
  201. })
  202. }
  203. if len(ms) < perPage {
  204. break
  205. }
  206. }
  207. return milestones, nil
  208. }
  209. // GetLabels returns labels
  210. func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) {
  211. if g == nil {
  212. return nil, errors.New("error: GitlabDownloader is nil")
  213. }
  214. var perPage = 100
  215. var labels = make([]*base.Label, 0, perPage)
  216. for i := 1; ; i++ {
  217. ls, _, err := g.client.Labels.ListLabels(g.repoID, &gitlab.ListLabelsOptions{
  218. Page: i,
  219. PerPage: perPage,
  220. }, nil)
  221. if err != nil {
  222. return nil, err
  223. }
  224. for _, label := range ls {
  225. baseLabel := &base.Label{
  226. Name: label.Name,
  227. Color: strings.TrimLeft(label.Color, "#)"),
  228. Description: label.Description,
  229. }
  230. labels = append(labels, baseLabel)
  231. }
  232. if len(ls) < perPage {
  233. break
  234. }
  235. }
  236. return labels, nil
  237. }
  238. func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Release {
  239. r := &base.Release{
  240. TagName: rel.TagName,
  241. TargetCommitish: rel.Commit.ID,
  242. Name: rel.Name,
  243. Body: rel.Description,
  244. Created: *rel.CreatedAt,
  245. PublisherID: int64(rel.Author.ID),
  246. PublisherName: rel.Author.Username,
  247. }
  248. for k, asset := range rel.Assets.Links {
  249. r.Assets = append(r.Assets, base.ReleaseAsset{
  250. URL: asset.URL,
  251. Name: asset.Name,
  252. ContentType: &rel.Assets.Sources[k].Format,
  253. })
  254. }
  255. return r
  256. }
  257. // GetReleases returns releases
  258. func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) {
  259. var perPage = 100
  260. var releases = make([]*base.Release, 0, perPage)
  261. for i := 1; ; i++ {
  262. ls, _, err := g.client.Releases.ListReleases(g.repoID, &gitlab.ListReleasesOptions{
  263. Page: i,
  264. PerPage: perPage,
  265. }, nil)
  266. if err != nil {
  267. return nil, err
  268. }
  269. for _, release := range ls {
  270. releases = append(releases, g.convertGitlabRelease(release))
  271. }
  272. if len(ls) < perPage {
  273. break
  274. }
  275. }
  276. return releases, nil
  277. }
  278. // GetIssues returns issues according start and limit
  279. // Note: issue label description and colors are not supported by the go-gitlab library at this time
  280. // TODO: figure out how to transfer issue reactions
  281. func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
  282. state := "all"
  283. sort := "asc"
  284. opt := &gitlab.ListProjectIssuesOptions{
  285. State: &state,
  286. Sort: &sort,
  287. ListOptions: gitlab.ListOptions{
  288. PerPage: perPage,
  289. Page: page,
  290. },
  291. }
  292. var allIssues = make([]*base.Issue, 0, perPage)
  293. issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil)
  294. if err != nil {
  295. return nil, false, fmt.Errorf("error while listing issues: %v", err)
  296. }
  297. for _, issue := range issues {
  298. var labels = make([]*base.Label, 0, len(issue.Labels))
  299. for _, l := range issue.Labels {
  300. labels = append(labels, &base.Label{
  301. Name: l,
  302. })
  303. }
  304. var milestone string
  305. if issue.Milestone != nil {
  306. milestone = issue.Milestone.Title
  307. }
  308. allIssues = append(allIssues, &base.Issue{
  309. Title: issue.Title,
  310. Number: int64(issue.IID),
  311. PosterID: int64(issue.Author.ID),
  312. PosterName: issue.Author.Username,
  313. Content: issue.Description,
  314. Milestone: milestone,
  315. State: issue.State,
  316. Created: *issue.CreatedAt,
  317. Labels: labels,
  318. Closed: issue.ClosedAt,
  319. IsLocked: issue.DiscussionLocked,
  320. Updated: *issue.UpdatedAt,
  321. })
  322. // increment issueCount, to be used in GetPullRequests()
  323. g.issueCount++
  324. }
  325. return allIssues, len(issues) < perPage, nil
  326. }
  327. // GetComments returns comments according issueNumber
  328. func (g *GitlabDownloader) GetComments(issueNumber int64) ([]*base.Comment, error) {
  329. var allComments = make([]*base.Comment, 0, 100)
  330. var page = 1
  331. var realIssueNumber int64
  332. for {
  333. var comments []*gitlab.Discussion
  334. var resp *gitlab.Response
  335. var err error
  336. // fetchPRcomments decides whether to fetch Issue or PR comments
  337. if !g.fetchPRcomments {
  338. realIssueNumber = issueNumber
  339. comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(realIssueNumber), &gitlab.ListIssueDiscussionsOptions{
  340. Page: page,
  341. PerPage: 100,
  342. }, nil)
  343. } else {
  344. // If this is a PR, we need to figure out the Gitlab/original PR ID to be passed below
  345. realIssueNumber = issueNumber - g.issueCount
  346. comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(realIssueNumber), &gitlab.ListMergeRequestDiscussionsOptions{
  347. Page: page,
  348. PerPage: 100,
  349. }, nil)
  350. }
  351. if err != nil {
  352. return nil, fmt.Errorf("error while listing comments: %v %v", g.repoID, err)
  353. }
  354. for _, comment := range comments {
  355. // Flatten comment threads
  356. if !comment.IndividualNote {
  357. for _, note := range comment.Notes {
  358. allComments = append(allComments, &base.Comment{
  359. IssueIndex: realIssueNumber,
  360. PosterID: int64(note.Author.ID),
  361. PosterName: note.Author.Username,
  362. PosterEmail: note.Author.Email,
  363. Content: note.Body,
  364. Created: *note.CreatedAt,
  365. })
  366. }
  367. } else {
  368. c := comment.Notes[0]
  369. allComments = append(allComments, &base.Comment{
  370. IssueIndex: realIssueNumber,
  371. PosterID: int64(c.Author.ID),
  372. PosterName: c.Author.Username,
  373. PosterEmail: c.Author.Email,
  374. Content: c.Body,
  375. Created: *c.CreatedAt,
  376. })
  377. }
  378. }
  379. if resp.NextPage == 0 {
  380. break
  381. }
  382. page = resp.NextPage
  383. }
  384. return allComments, nil
  385. }
  386. // GetPullRequests returns pull requests according page and perPage
  387. func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, error) {
  388. opt := &gitlab.ListProjectMergeRequestsOptions{
  389. ListOptions: gitlab.ListOptions{
  390. PerPage: perPage,
  391. Page: page,
  392. },
  393. }
  394. // Set fetchPRcomments to true here, so PR comments are fetched instead of Issue comments
  395. g.fetchPRcomments = true
  396. var allPRs = make([]*base.PullRequest, 0, perPage)
  397. prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil)
  398. if err != nil {
  399. return nil, fmt.Errorf("error while listing merge requests: %v", err)
  400. }
  401. for _, pr := range prs {
  402. var labels = make([]*base.Label, 0, len(pr.Labels))
  403. for _, l := range pr.Labels {
  404. labels = append(labels, &base.Label{
  405. Name: l,
  406. })
  407. }
  408. var merged bool
  409. if pr.State == "merged" {
  410. merged = true
  411. pr.State = "closed"
  412. }
  413. var mergeTime = pr.MergedAt
  414. if merged && pr.MergedAt == nil {
  415. mergeTime = pr.UpdatedAt
  416. }
  417. var closeTime = pr.ClosedAt
  418. if merged && pr.ClosedAt == nil {
  419. closeTime = pr.UpdatedAt
  420. }
  421. var locked bool
  422. if pr.State == "locked" {
  423. locked = true
  424. }
  425. var milestone string
  426. if pr.Milestone != nil {
  427. milestone = pr.Milestone.Title
  428. }
  429. // Add the PR ID to the Issue Count because PR and Issues share ID space in Gitea
  430. newPRNumber := g.issueCount + int64(pr.IID)
  431. allPRs = append(allPRs, &base.PullRequest{
  432. Title: pr.Title,
  433. Number: newPRNumber,
  434. OriginalNumber: int64(pr.IID),
  435. PosterName: pr.Author.Username,
  436. PosterID: int64(pr.Author.ID),
  437. Content: pr.Description,
  438. Milestone: milestone,
  439. State: pr.State,
  440. Created: *pr.CreatedAt,
  441. Closed: closeTime,
  442. Labels: labels,
  443. Merged: merged,
  444. MergeCommitSHA: pr.MergeCommitSHA,
  445. MergedTime: mergeTime,
  446. IsLocked: locked,
  447. Head: base.PullRequestBranch{
  448. Ref: pr.SourceBranch,
  449. SHA: pr.SHA,
  450. RepoName: g.repoName,
  451. OwnerName: pr.Author.Username,
  452. CloneURL: pr.WebURL,
  453. },
  454. Base: base.PullRequestBranch{
  455. Ref: pr.TargetBranch,
  456. SHA: pr.DiffRefs.BaseSha,
  457. RepoName: g.repoName,
  458. OwnerName: pr.Author.Username,
  459. },
  460. PatchURL: pr.WebURL + ".patch",
  461. })
  462. }
  463. return allPRs, nil
  464. }
  465. // GetReviews returns pull requests review
  466. func (g *GitlabDownloader) GetReviews(pullRequestNumber int64) ([]*base.Review, error) {
  467. state, _, err := g.client.MergeRequestApprovals.GetApprovalState(g.repoID, int(pullRequestNumber))
  468. if err != nil {
  469. return nil, err
  470. }
  471. // GitLab's Approvals are equivalent to Gitea's approve reviews
  472. approvers := make(map[int]string)
  473. for i := range state.Rules {
  474. for u := range state.Rules[i].ApprovedBy {
  475. approvers[state.Rules[i].ApprovedBy[u].ID] = state.Rules[i].ApprovedBy[u].Username
  476. }
  477. }
  478. var reviews = make([]*base.Review, 0, len(approvers))
  479. for id, name := range approvers {
  480. reviews = append(reviews, &base.Review{
  481. ReviewerID: int64(id),
  482. ReviewerName: name,
  483. // GitLab API doesn't return a creation date
  484. CreatedAt: time.Now(),
  485. // All we get are approvals
  486. State: base.ReviewStateApproved,
  487. })
  488. }
  489. return reviews, nil
  490. }