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.

486 lines
12 KiB

  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Copyright 2018 Jonas Franz. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package migrations
  6. import (
  7. "context"
  8. "fmt"
  9. "net/http"
  10. "net/url"
  11. "strings"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/migrations/base"
  14. "github.com/google/go-github/v24/github"
  15. "golang.org/x/oauth2"
  16. )
  17. var (
  18. _ base.Downloader = &GithubDownloaderV3{}
  19. _ base.DownloaderFactory = &GithubDownloaderV3Factory{}
  20. )
  21. func init() {
  22. RegisterDownloaderFactory(&GithubDownloaderV3Factory{})
  23. }
  24. // GithubDownloaderV3Factory defines a github downloader v3 factory
  25. type GithubDownloaderV3Factory struct {
  26. }
  27. // Match returns ture if the migration remote URL matched this downloader factory
  28. func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error) {
  29. u, err := url.Parse(opts.RemoteURL)
  30. if err != nil {
  31. return false, err
  32. }
  33. return u.Host == "github.com" && opts.AuthUsername != "", nil
  34. }
  35. // New returns a Downloader related to this factory according MigrateOptions
  36. func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Downloader, error) {
  37. u, err := url.Parse(opts.RemoteURL)
  38. if err != nil {
  39. return nil, err
  40. }
  41. fields := strings.Split(u.Path, "/")
  42. oldOwner := fields[1]
  43. oldName := strings.TrimSuffix(fields[2], ".git")
  44. log.Trace("Create github downloader: %s/%s", oldOwner, oldName)
  45. return NewGithubDownloaderV3(opts.AuthUsername, opts.AuthPassword, oldOwner, oldName), nil
  46. }
  47. // GithubDownloaderV3 implements a Downloader interface to get repository informations
  48. // from github via APIv3
  49. type GithubDownloaderV3 struct {
  50. ctx context.Context
  51. client *github.Client
  52. repoOwner string
  53. repoName string
  54. userName string
  55. password string
  56. }
  57. // NewGithubDownloaderV3 creates a github Downloader via github v3 API
  58. func NewGithubDownloaderV3(userName, password, repoOwner, repoName string) *GithubDownloaderV3 {
  59. var downloader = GithubDownloaderV3{
  60. userName: userName,
  61. password: password,
  62. ctx: context.Background(),
  63. repoOwner: repoOwner,
  64. repoName: repoName,
  65. }
  66. var client *http.Client
  67. if userName != "" {
  68. if password == "" {
  69. ts := oauth2.StaticTokenSource(
  70. &oauth2.Token{AccessToken: userName},
  71. )
  72. client = oauth2.NewClient(downloader.ctx, ts)
  73. } else {
  74. client = &http.Client{
  75. Transport: &http.Transport{
  76. Proxy: func(req *http.Request) (*url.URL, error) {
  77. req.SetBasicAuth(userName, password)
  78. return nil, nil
  79. },
  80. },
  81. }
  82. }
  83. }
  84. downloader.client = github.NewClient(client)
  85. return &downloader
  86. }
  87. // GetRepoInfo returns a repository information
  88. func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) {
  89. gr, _, err := g.client.Repositories.Get(g.ctx, g.repoOwner, g.repoName)
  90. if err != nil {
  91. return nil, err
  92. }
  93. // convert github repo to stand Repo
  94. return &base.Repository{
  95. Owner: g.repoOwner,
  96. Name: gr.GetName(),
  97. IsPrivate: *gr.Private,
  98. Description: gr.GetDescription(),
  99. OriginalURL: gr.GetHTMLURL(),
  100. CloneURL: gr.GetCloneURL(),
  101. }, nil
  102. }
  103. // GetMilestones returns milestones
  104. func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) {
  105. var perPage = 100
  106. var milestones = make([]*base.Milestone, 0, perPage)
  107. for i := 1; ; i++ {
  108. ms, _, err := g.client.Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName,
  109. &github.MilestoneListOptions{
  110. State: "all",
  111. ListOptions: github.ListOptions{
  112. Page: i,
  113. PerPage: perPage,
  114. }})
  115. if err != nil {
  116. return nil, err
  117. }
  118. for _, m := range ms {
  119. var desc string
  120. if m.Description != nil {
  121. desc = *m.Description
  122. }
  123. var state = "open"
  124. if m.State != nil {
  125. state = *m.State
  126. }
  127. milestones = append(milestones, &base.Milestone{
  128. Title: *m.Title,
  129. Description: desc,
  130. Deadline: m.DueOn,
  131. State: state,
  132. Created: *m.CreatedAt,
  133. Updated: m.UpdatedAt,
  134. Closed: m.ClosedAt,
  135. })
  136. }
  137. if len(ms) < perPage {
  138. break
  139. }
  140. }
  141. return milestones, nil
  142. }
  143. func convertGithubLabel(label *github.Label) *base.Label {
  144. var desc string
  145. if label.Description != nil {
  146. desc = *label.Description
  147. }
  148. return &base.Label{
  149. Name: *label.Name,
  150. Color: *label.Color,
  151. Description: desc,
  152. }
  153. }
  154. // GetLabels returns labels
  155. func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
  156. var perPage = 100
  157. var labels = make([]*base.Label, 0, perPage)
  158. for i := 1; ; i++ {
  159. ls, _, err := g.client.Issues.ListLabels(g.ctx, g.repoOwner, g.repoName,
  160. &github.ListOptions{
  161. Page: i,
  162. PerPage: perPage,
  163. })
  164. if err != nil {
  165. return nil, err
  166. }
  167. for _, label := range ls {
  168. labels = append(labels, convertGithubLabel(label))
  169. }
  170. if len(ls) < perPage {
  171. break
  172. }
  173. }
  174. return labels, nil
  175. }
  176. func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release {
  177. var (
  178. name string
  179. desc string
  180. )
  181. if rel.Body != nil {
  182. desc = *rel.Body
  183. }
  184. if rel.Name != nil {
  185. name = *rel.Name
  186. }
  187. r := &base.Release{
  188. TagName: *rel.TagName,
  189. TargetCommitish: *rel.TargetCommitish,
  190. Name: name,
  191. Body: desc,
  192. Draft: *rel.Draft,
  193. Prerelease: *rel.Prerelease,
  194. Created: rel.CreatedAt.Time,
  195. Published: rel.PublishedAt.Time,
  196. }
  197. for _, asset := range rel.Assets {
  198. u, _ := url.Parse(*asset.BrowserDownloadURL)
  199. u.User = url.UserPassword(g.userName, g.password)
  200. r.Assets = append(r.Assets, base.ReleaseAsset{
  201. URL: u.String(),
  202. Name: *asset.Name,
  203. ContentType: asset.ContentType,
  204. Size: asset.Size,
  205. DownloadCount: asset.DownloadCount,
  206. Created: asset.CreatedAt.Time,
  207. Updated: asset.UpdatedAt.Time,
  208. })
  209. }
  210. return r
  211. }
  212. // GetReleases returns releases
  213. func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
  214. var perPage = 100
  215. var releases = make([]*base.Release, 0, perPage)
  216. for i := 1; ; i++ {
  217. ls, _, err := g.client.Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName,
  218. &github.ListOptions{
  219. Page: i,
  220. PerPage: perPage,
  221. })
  222. if err != nil {
  223. return nil, err
  224. }
  225. for _, release := range ls {
  226. releases = append(releases, g.convertGithubRelease(release))
  227. }
  228. if len(ls) < perPage {
  229. break
  230. }
  231. }
  232. return releases, nil
  233. }
  234. func convertGithubReactions(reactions *github.Reactions) *base.Reactions {
  235. return &base.Reactions{
  236. TotalCount: *reactions.TotalCount,
  237. PlusOne: *reactions.PlusOne,
  238. MinusOne: *reactions.MinusOne,
  239. Laugh: *reactions.Laugh,
  240. Confused: *reactions.Confused,
  241. Heart: *reactions.Heart,
  242. Hooray: *reactions.Hooray,
  243. }
  244. }
  245. // GetIssues returns issues according start and limit
  246. func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
  247. opt := &github.IssueListByRepoOptions{
  248. Sort: "created",
  249. Direction: "asc",
  250. State: "all",
  251. ListOptions: github.ListOptions{
  252. PerPage: perPage,
  253. Page: page,
  254. },
  255. }
  256. var allIssues = make([]*base.Issue, 0, perPage)
  257. issues, _, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt)
  258. if err != nil {
  259. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  260. }
  261. for _, issue := range issues {
  262. if issue.IsPullRequest() {
  263. continue
  264. }
  265. var body string
  266. if issue.Body != nil {
  267. body = *issue.Body
  268. }
  269. var milestone string
  270. if issue.Milestone != nil {
  271. milestone = *issue.Milestone.Title
  272. }
  273. var labels = make([]*base.Label, 0, len(issue.Labels))
  274. for _, l := range issue.Labels {
  275. labels = append(labels, convertGithubLabel(&l))
  276. }
  277. var reactions *base.Reactions
  278. if issue.Reactions != nil {
  279. reactions = convertGithubReactions(issue.Reactions)
  280. }
  281. var email string
  282. if issue.User.Email != nil {
  283. email = *issue.User.Email
  284. }
  285. allIssues = append(allIssues, &base.Issue{
  286. Title: *issue.Title,
  287. Number: int64(*issue.Number),
  288. PosterID: *issue.User.ID,
  289. PosterName: *issue.User.Login,
  290. PosterEmail: email,
  291. Content: body,
  292. Milestone: milestone,
  293. State: *issue.State,
  294. Created: *issue.CreatedAt,
  295. Labels: labels,
  296. Reactions: reactions,
  297. Closed: issue.ClosedAt,
  298. IsLocked: *issue.Locked,
  299. })
  300. }
  301. return allIssues, len(issues) < perPage, nil
  302. }
  303. // GetComments returns comments according issueNumber
  304. func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, error) {
  305. var allComments = make([]*base.Comment, 0, 100)
  306. opt := &github.IssueListCommentsOptions{
  307. Sort: "created",
  308. Direction: "asc",
  309. ListOptions: github.ListOptions{
  310. PerPage: 100,
  311. },
  312. }
  313. for {
  314. comments, resp, err := g.client.Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(issueNumber), opt)
  315. if err != nil {
  316. return nil, fmt.Errorf("error while listing repos: %v", err)
  317. }
  318. for _, comment := range comments {
  319. var email string
  320. if comment.User.Email != nil {
  321. email = *comment.User.Email
  322. }
  323. var reactions *base.Reactions
  324. if comment.Reactions != nil {
  325. reactions = convertGithubReactions(comment.Reactions)
  326. }
  327. allComments = append(allComments, &base.Comment{
  328. IssueIndex: issueNumber,
  329. PosterID: *comment.User.ID,
  330. PosterName: *comment.User.Login,
  331. PosterEmail: email,
  332. Content: *comment.Body,
  333. Created: *comment.CreatedAt,
  334. Reactions: reactions,
  335. })
  336. }
  337. if resp.NextPage == 0 {
  338. break
  339. }
  340. opt.Page = resp.NextPage
  341. }
  342. return allComments, nil
  343. }
  344. // GetPullRequests returns pull requests according page and perPage
  345. func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, error) {
  346. opt := &github.PullRequestListOptions{
  347. Sort: "created",
  348. Direction: "asc",
  349. State: "all",
  350. ListOptions: github.ListOptions{
  351. PerPage: perPage,
  352. Page: page,
  353. },
  354. }
  355. var allPRs = make([]*base.PullRequest, 0, perPage)
  356. prs, _, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
  357. if err != nil {
  358. return nil, fmt.Errorf("error while listing repos: %v", err)
  359. }
  360. for _, pr := range prs {
  361. var body string
  362. if pr.Body != nil {
  363. body = *pr.Body
  364. }
  365. var milestone string
  366. if pr.Milestone != nil {
  367. milestone = *pr.Milestone.Title
  368. }
  369. var labels = make([]*base.Label, 0, len(pr.Labels))
  370. for _, l := range pr.Labels {
  371. labels = append(labels, convertGithubLabel(l))
  372. }
  373. // FIXME: This API missing reactions, we may need another extra request to get reactions
  374. var email string
  375. if pr.User.Email != nil {
  376. email = *pr.User.Email
  377. }
  378. var merged bool
  379. // pr.Merged is not valid, so use MergedAt to test if it's merged
  380. if pr.MergedAt != nil {
  381. merged = true
  382. }
  383. var (
  384. headRepoName string
  385. cloneURL string
  386. headRef string
  387. headSHA string
  388. )
  389. if pr.Head.Repo != nil {
  390. if pr.Head.Repo.Name != nil {
  391. headRepoName = *pr.Head.Repo.Name
  392. }
  393. if pr.Head.Repo.CloneURL != nil {
  394. cloneURL = *pr.Head.Repo.CloneURL
  395. }
  396. }
  397. if pr.Head.Ref != nil {
  398. headRef = *pr.Head.Ref
  399. }
  400. if pr.Head.SHA != nil {
  401. headSHA = *pr.Head.SHA
  402. }
  403. var mergeCommitSHA string
  404. if pr.MergeCommitSHA != nil {
  405. mergeCommitSHA = *pr.MergeCommitSHA
  406. }
  407. var headUserName string
  408. if pr.Head.User != nil && pr.Head.User.Login != nil {
  409. headUserName = *pr.Head.User.Login
  410. }
  411. allPRs = append(allPRs, &base.PullRequest{
  412. Title: *pr.Title,
  413. Number: int64(*pr.Number),
  414. PosterName: *pr.User.Login,
  415. PosterID: *pr.User.ID,
  416. PosterEmail: email,
  417. Content: body,
  418. Milestone: milestone,
  419. State: *pr.State,
  420. Created: *pr.CreatedAt,
  421. Closed: pr.ClosedAt,
  422. Labels: labels,
  423. Merged: merged,
  424. MergeCommitSHA: mergeCommitSHA,
  425. MergedTime: pr.MergedAt,
  426. IsLocked: pr.ActiveLockReason != nil,
  427. Head: base.PullRequestBranch{
  428. Ref: headRef,
  429. SHA: headSHA,
  430. RepoName: headRepoName,
  431. OwnerName: headUserName,
  432. CloneURL: cloneURL,
  433. },
  434. Base: base.PullRequestBranch{
  435. Ref: *pr.Base.Ref,
  436. SHA: *pr.Base.SHA,
  437. RepoName: *pr.Base.Repo.Name,
  438. OwnerName: *pr.Base.User.Login,
  439. },
  440. PatchURL: *pr.PatchURL,
  441. })
  442. }
  443. return allPRs, nil
  444. }