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.

github.go 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  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. "io"
  10. "net/http"
  11. "net/url"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "code.gitea.io/gitea/modules/log"
  16. base "code.gitea.io/gitea/modules/migration"
  17. "code.gitea.io/gitea/modules/proxy"
  18. "code.gitea.io/gitea/modules/structs"
  19. "code.gitea.io/gitea/modules/util"
  20. "github.com/google/go-github/v39/github"
  21. "golang.org/x/oauth2"
  22. )
  23. var (
  24. _ base.Downloader = &GithubDownloaderV3{}
  25. _ base.DownloaderFactory = &GithubDownloaderV3Factory{}
  26. // GithubLimitRateRemaining limit to wait for new rate to apply
  27. GithubLimitRateRemaining = 0
  28. )
  29. func init() {
  30. RegisterDownloaderFactory(&GithubDownloaderV3Factory{})
  31. }
  32. // GithubDownloaderV3Factory defines a github downloader v3 factory
  33. type GithubDownloaderV3Factory struct {
  34. }
  35. // New returns a Downloader related to this factory according MigrateOptions
  36. func (f *GithubDownloaderV3Factory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
  37. u, err := url.Parse(opts.CloneAddr)
  38. if err != nil {
  39. return nil, err
  40. }
  41. baseURL := u.Scheme + "://" + u.Host
  42. fields := strings.Split(u.Path, "/")
  43. oldOwner := fields[1]
  44. oldName := strings.TrimSuffix(fields[2], ".git")
  45. log.Trace("Create github downloader: %s/%s", oldOwner, oldName)
  46. return NewGithubDownloaderV3(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
  47. }
  48. // GitServiceType returns the type of git service
  49. func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType {
  50. return structs.GithubService
  51. }
  52. // GithubDownloaderV3 implements a Downloader interface to get repository information
  53. // from github via APIv3
  54. type GithubDownloaderV3 struct {
  55. base.NullDownloader
  56. ctx context.Context
  57. clients []*github.Client
  58. repoOwner string
  59. repoName string
  60. userName string
  61. password string
  62. rates []*github.Rate
  63. curClientIdx int
  64. maxPerPage int
  65. SkipReactions bool
  66. }
  67. // NewGithubDownloaderV3 creates a github Downloader via github v3 API
  68. func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
  69. var downloader = GithubDownloaderV3{
  70. userName: userName,
  71. password: password,
  72. ctx: ctx,
  73. repoOwner: repoOwner,
  74. repoName: repoName,
  75. maxPerPage: 100,
  76. }
  77. if token != "" {
  78. tokens := strings.Split(token, ",")
  79. for _, token := range tokens {
  80. token = strings.TrimSpace(token)
  81. ts := oauth2.StaticTokenSource(
  82. &oauth2.Token{AccessToken: token},
  83. )
  84. var client = &http.Client{
  85. Transport: &oauth2.Transport{
  86. Base: NewMigrationHTTPTransport(),
  87. Source: oauth2.ReuseTokenSource(nil, ts),
  88. },
  89. }
  90. downloader.addClient(client, baseURL)
  91. }
  92. } else {
  93. var transport = NewMigrationHTTPTransport()
  94. transport.Proxy = func(req *http.Request) (*url.URL, error) {
  95. req.SetBasicAuth(userName, password)
  96. return proxy.Proxy()(req)
  97. }
  98. var client = &http.Client{
  99. Transport: transport,
  100. }
  101. downloader.addClient(client, baseURL)
  102. }
  103. return &downloader
  104. }
  105. func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
  106. githubClient := github.NewClient(client)
  107. if baseURL != "https://github.com" {
  108. githubClient, _ = github.NewEnterpriseClient(baseURL, baseURL, client)
  109. }
  110. g.clients = append(g.clients, githubClient)
  111. g.rates = append(g.rates, nil)
  112. }
  113. // SetContext set context
  114. func (g *GithubDownloaderV3) SetContext(ctx context.Context) {
  115. g.ctx = ctx
  116. }
  117. func (g *GithubDownloaderV3) waitAndPickClient() {
  118. var recentIdx int
  119. var maxRemaining int
  120. for i := 0; i < len(g.clients); i++ {
  121. if g.rates[i] != nil && g.rates[i].Remaining > maxRemaining {
  122. maxRemaining = g.rates[i].Remaining
  123. recentIdx = i
  124. }
  125. }
  126. g.curClientIdx = recentIdx // if no max remain, it will always pick the first client.
  127. for g.rates[g.curClientIdx] != nil && g.rates[g.curClientIdx].Remaining <= GithubLimitRateRemaining {
  128. timer := time.NewTimer(time.Until(g.rates[g.curClientIdx].Reset.Time))
  129. select {
  130. case <-g.ctx.Done():
  131. util.StopTimer(timer)
  132. return
  133. case <-timer.C:
  134. }
  135. err := g.RefreshRate()
  136. if err != nil {
  137. log.Error("g.getClient().RateLimits: %s", err)
  138. }
  139. }
  140. }
  141. // RefreshRate update the current rate (doesn't count in rate limit)
  142. func (g *GithubDownloaderV3) RefreshRate() error {
  143. rates, _, err := g.getClient().RateLimits(g.ctx)
  144. if err != nil {
  145. // if rate limit is not enabled, ignore it
  146. if strings.Contains(err.Error(), "404") {
  147. g.setRate(nil)
  148. return nil
  149. }
  150. return err
  151. }
  152. g.setRate(rates.GetCore())
  153. return nil
  154. }
  155. func (g *GithubDownloaderV3) getClient() *github.Client {
  156. return g.clients[g.curClientIdx]
  157. }
  158. func (g *GithubDownloaderV3) setRate(rate *github.Rate) {
  159. g.rates[g.curClientIdx] = rate
  160. }
  161. // GetRepoInfo returns a repository information
  162. func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) {
  163. g.waitAndPickClient()
  164. gr, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName)
  165. if err != nil {
  166. return nil, err
  167. }
  168. g.setRate(&resp.Rate)
  169. // convert github repo to stand Repo
  170. return &base.Repository{
  171. Owner: g.repoOwner,
  172. Name: gr.GetName(),
  173. IsPrivate: gr.GetPrivate(),
  174. Description: gr.GetDescription(),
  175. OriginalURL: gr.GetHTMLURL(),
  176. CloneURL: gr.GetCloneURL(),
  177. DefaultBranch: gr.GetDefaultBranch(),
  178. }, nil
  179. }
  180. // GetTopics return github topics
  181. func (g *GithubDownloaderV3) GetTopics() ([]string, error) {
  182. g.waitAndPickClient()
  183. r, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName)
  184. if err != nil {
  185. return nil, err
  186. }
  187. g.setRate(&resp.Rate)
  188. return r.Topics, nil
  189. }
  190. // GetMilestones returns milestones
  191. func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) {
  192. var perPage = g.maxPerPage
  193. var milestones = make([]*base.Milestone, 0, perPage)
  194. for i := 1; ; i++ {
  195. g.waitAndPickClient()
  196. ms, resp, err := g.getClient().Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName,
  197. &github.MilestoneListOptions{
  198. State: "all",
  199. ListOptions: github.ListOptions{
  200. Page: i,
  201. PerPage: perPage,
  202. }})
  203. if err != nil {
  204. return nil, err
  205. }
  206. g.setRate(&resp.Rate)
  207. for _, m := range ms {
  208. var state = "open"
  209. if m.State != nil {
  210. state = *m.State
  211. }
  212. milestones = append(milestones, &base.Milestone{
  213. Title: m.GetTitle(),
  214. Description: m.GetDescription(),
  215. Deadline: m.DueOn,
  216. State: state,
  217. Created: m.GetCreatedAt(),
  218. Updated: m.UpdatedAt,
  219. Closed: m.ClosedAt,
  220. })
  221. }
  222. if len(ms) < perPage {
  223. break
  224. }
  225. }
  226. return milestones, nil
  227. }
  228. func convertGithubLabel(label *github.Label) *base.Label {
  229. return &base.Label{
  230. Name: label.GetName(),
  231. Color: label.GetColor(),
  232. Description: label.GetDescription(),
  233. }
  234. }
  235. // GetLabels returns labels
  236. func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
  237. var perPage = g.maxPerPage
  238. var labels = make([]*base.Label, 0, perPage)
  239. for i := 1; ; i++ {
  240. g.waitAndPickClient()
  241. ls, resp, err := g.getClient().Issues.ListLabels(g.ctx, g.repoOwner, g.repoName,
  242. &github.ListOptions{
  243. Page: i,
  244. PerPage: perPage,
  245. })
  246. if err != nil {
  247. return nil, err
  248. }
  249. g.setRate(&resp.Rate)
  250. for _, label := range ls {
  251. labels = append(labels, convertGithubLabel(label))
  252. }
  253. if len(ls) < perPage {
  254. break
  255. }
  256. }
  257. return labels, nil
  258. }
  259. func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release {
  260. r := &base.Release{
  261. Name: rel.GetName(),
  262. TagName: rel.GetTagName(),
  263. TargetCommitish: rel.GetTargetCommitish(),
  264. Draft: rel.GetDraft(),
  265. Prerelease: rel.GetPrerelease(),
  266. Created: rel.GetCreatedAt().Time,
  267. PublisherID: rel.GetAuthor().GetID(),
  268. PublisherName: rel.GetAuthor().GetLogin(),
  269. PublisherEmail: rel.GetAuthor().GetEmail(),
  270. Body: rel.GetBody(),
  271. }
  272. if rel.PublishedAt != nil {
  273. r.Published = rel.PublishedAt.Time
  274. }
  275. httpClient := NewMigrationHTTPClient()
  276. for _, asset := range rel.Assets {
  277. var assetID = *asset.ID // Don't optimize this, for closure we need a local variable
  278. r.Assets = append(r.Assets, &base.ReleaseAsset{
  279. ID: asset.GetID(),
  280. Name: asset.GetName(),
  281. ContentType: asset.ContentType,
  282. Size: asset.Size,
  283. DownloadCount: asset.DownloadCount,
  284. Created: asset.CreatedAt.Time,
  285. Updated: asset.UpdatedAt.Time,
  286. DownloadFunc: func() (io.ReadCloser, error) {
  287. g.waitAndPickClient()
  288. asset, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil)
  289. if err != nil {
  290. return nil, err
  291. }
  292. if err := g.RefreshRate(); err != nil {
  293. log.Error("g.getClient().RateLimits: %s", err)
  294. }
  295. if asset == nil {
  296. if redirectURL != "" {
  297. g.waitAndPickClient()
  298. req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil)
  299. if err != nil {
  300. return nil, err
  301. }
  302. resp, err := httpClient.Do(req)
  303. err1 := g.RefreshRate()
  304. if err1 != nil {
  305. log.Error("g.getClient().RateLimits: %s", err1)
  306. }
  307. if err != nil {
  308. return nil, err
  309. }
  310. return resp.Body, nil
  311. }
  312. return nil, fmt.Errorf("No release asset found for %d", assetID)
  313. }
  314. return asset, nil
  315. },
  316. })
  317. }
  318. return r
  319. }
  320. // GetReleases returns releases
  321. func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
  322. var perPage = g.maxPerPage
  323. var releases = make([]*base.Release, 0, perPage)
  324. for i := 1; ; i++ {
  325. g.waitAndPickClient()
  326. ls, resp, err := g.getClient().Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName,
  327. &github.ListOptions{
  328. Page: i,
  329. PerPage: perPage,
  330. })
  331. if err != nil {
  332. return nil, err
  333. }
  334. g.setRate(&resp.Rate)
  335. for _, release := range ls {
  336. releases = append(releases, g.convertGithubRelease(release))
  337. }
  338. if len(ls) < perPage {
  339. break
  340. }
  341. }
  342. return releases, nil
  343. }
  344. // GetIssues returns issues according start and limit
  345. func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
  346. if perPage > g.maxPerPage {
  347. perPage = g.maxPerPage
  348. }
  349. opt := &github.IssueListByRepoOptions{
  350. Sort: "created",
  351. Direction: "asc",
  352. State: "all",
  353. ListOptions: github.ListOptions{
  354. PerPage: perPage,
  355. Page: page,
  356. },
  357. }
  358. var allIssues = make([]*base.Issue, 0, perPage)
  359. g.waitAndPickClient()
  360. issues, resp, err := g.getClient().Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt)
  361. if err != nil {
  362. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  363. }
  364. log.Trace("Request get issues %d/%d, but in fact get %d", perPage, page, len(issues))
  365. g.setRate(&resp.Rate)
  366. for _, issue := range issues {
  367. if issue.IsPullRequest() {
  368. continue
  369. }
  370. var labels = make([]*base.Label, 0, len(issue.Labels))
  371. for _, l := range issue.Labels {
  372. labels = append(labels, convertGithubLabel(l))
  373. }
  374. // get reactions
  375. var reactions []*base.Reaction
  376. if !g.SkipReactions {
  377. for i := 1; ; i++ {
  378. g.waitAndPickClient()
  379. res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{
  380. Page: i,
  381. PerPage: perPage,
  382. })
  383. if err != nil {
  384. return nil, false, err
  385. }
  386. g.setRate(&resp.Rate)
  387. if len(res) == 0 {
  388. break
  389. }
  390. for _, reaction := range res {
  391. reactions = append(reactions, &base.Reaction{
  392. UserID: reaction.User.GetID(),
  393. UserName: reaction.User.GetLogin(),
  394. Content: reaction.GetContent(),
  395. })
  396. }
  397. }
  398. }
  399. var assignees []string
  400. for i := range issue.Assignees {
  401. assignees = append(assignees, issue.Assignees[i].GetLogin())
  402. }
  403. allIssues = append(allIssues, &base.Issue{
  404. Title: *issue.Title,
  405. Number: int64(*issue.Number),
  406. PosterID: issue.GetUser().GetID(),
  407. PosterName: issue.GetUser().GetLogin(),
  408. PosterEmail: issue.GetUser().GetEmail(),
  409. Content: issue.GetBody(),
  410. Milestone: issue.GetMilestone().GetTitle(),
  411. State: issue.GetState(),
  412. Created: issue.GetCreatedAt(),
  413. Updated: issue.GetUpdatedAt(),
  414. Labels: labels,
  415. Reactions: reactions,
  416. Closed: issue.ClosedAt,
  417. IsLocked: issue.GetLocked(),
  418. Assignees: assignees,
  419. Context: base.BasicIssueContext(*issue.Number),
  420. })
  421. }
  422. return allIssues, len(issues) < perPage, nil
  423. }
  424. // SupportGetRepoComments return true if it supports get repo comments
  425. func (g *GithubDownloaderV3) SupportGetRepoComments() bool {
  426. return true
  427. }
  428. // GetComments returns comments according issueNumber
  429. func (g *GithubDownloaderV3) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
  430. if opts.Context != nil {
  431. comments, err := g.getComments(opts.Context)
  432. return comments, false, err
  433. }
  434. return g.GetAllComments(opts.Page, opts.PageSize)
  435. }
  436. func (g *GithubDownloaderV3) getComments(issueContext base.IssueContext) ([]*base.Comment, error) {
  437. var (
  438. allComments = make([]*base.Comment, 0, g.maxPerPage)
  439. created = "created"
  440. asc = "asc"
  441. )
  442. opt := &github.IssueListCommentsOptions{
  443. Sort: &created,
  444. Direction: &asc,
  445. ListOptions: github.ListOptions{
  446. PerPage: g.maxPerPage,
  447. },
  448. }
  449. for {
  450. g.waitAndPickClient()
  451. comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(issueContext.ForeignID()), opt)
  452. if err != nil {
  453. return nil, fmt.Errorf("error while listing repos: %v", err)
  454. }
  455. g.setRate(&resp.Rate)
  456. for _, comment := range comments {
  457. // get reactions
  458. var reactions []*base.Reaction
  459. if !g.SkipReactions {
  460. for i := 1; ; i++ {
  461. g.waitAndPickClient()
  462. res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
  463. Page: i,
  464. PerPage: g.maxPerPage,
  465. })
  466. if err != nil {
  467. return nil, err
  468. }
  469. g.setRate(&resp.Rate)
  470. if len(res) == 0 {
  471. break
  472. }
  473. for _, reaction := range res {
  474. reactions = append(reactions, &base.Reaction{
  475. UserID: reaction.User.GetID(),
  476. UserName: reaction.User.GetLogin(),
  477. Content: reaction.GetContent(),
  478. })
  479. }
  480. }
  481. }
  482. allComments = append(allComments, &base.Comment{
  483. IssueIndex: issueContext.LocalID(),
  484. PosterID: comment.GetUser().GetID(),
  485. PosterName: comment.GetUser().GetLogin(),
  486. PosterEmail: comment.GetUser().GetEmail(),
  487. Content: comment.GetBody(),
  488. Created: comment.GetCreatedAt(),
  489. Updated: comment.GetUpdatedAt(),
  490. Reactions: reactions,
  491. })
  492. }
  493. if resp.NextPage == 0 {
  494. break
  495. }
  496. opt.Page = resp.NextPage
  497. }
  498. return allComments, nil
  499. }
  500. // GetAllComments returns repository comments according page and perPageSize
  501. func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, bool, error) {
  502. var (
  503. allComments = make([]*base.Comment, 0, perPage)
  504. created = "created"
  505. asc = "asc"
  506. )
  507. if perPage > g.maxPerPage {
  508. perPage = g.maxPerPage
  509. }
  510. opt := &github.IssueListCommentsOptions{
  511. Sort: &created,
  512. Direction: &asc,
  513. ListOptions: github.ListOptions{
  514. Page: page,
  515. PerPage: perPage,
  516. },
  517. }
  518. g.waitAndPickClient()
  519. comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, 0, opt)
  520. if err != nil {
  521. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  522. }
  523. var isEnd = resp.NextPage == 0
  524. log.Trace("Request get comments %d/%d, but in fact get %d, next page is %d", perPage, page, len(comments), resp.NextPage)
  525. g.setRate(&resp.Rate)
  526. for _, comment := range comments {
  527. // get reactions
  528. var reactions []*base.Reaction
  529. if !g.SkipReactions {
  530. for i := 1; ; i++ {
  531. g.waitAndPickClient()
  532. res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
  533. Page: i,
  534. PerPage: g.maxPerPage,
  535. })
  536. if err != nil {
  537. return nil, false, err
  538. }
  539. g.setRate(&resp.Rate)
  540. if len(res) == 0 {
  541. break
  542. }
  543. for _, reaction := range res {
  544. reactions = append(reactions, &base.Reaction{
  545. UserID: reaction.User.GetID(),
  546. UserName: reaction.User.GetLogin(),
  547. Content: reaction.GetContent(),
  548. })
  549. }
  550. }
  551. }
  552. idx := strings.LastIndex(*comment.IssueURL, "/")
  553. issueIndex, _ := strconv.ParseInt((*comment.IssueURL)[idx+1:], 10, 64)
  554. allComments = append(allComments, &base.Comment{
  555. IssueIndex: issueIndex,
  556. PosterID: comment.GetUser().GetID(),
  557. PosterName: comment.GetUser().GetLogin(),
  558. PosterEmail: comment.GetUser().GetEmail(),
  559. Content: comment.GetBody(),
  560. Created: comment.GetCreatedAt(),
  561. Updated: comment.GetUpdatedAt(),
  562. Reactions: reactions,
  563. })
  564. }
  565. return allComments, isEnd, nil
  566. }
  567. // GetPullRequests returns pull requests according page and perPage
  568. func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
  569. if perPage > g.maxPerPage {
  570. perPage = g.maxPerPage
  571. }
  572. opt := &github.PullRequestListOptions{
  573. Sort: "created",
  574. Direction: "asc",
  575. State: "all",
  576. ListOptions: github.ListOptions{
  577. PerPage: perPage,
  578. Page: page,
  579. },
  580. }
  581. var allPRs = make([]*base.PullRequest, 0, perPage)
  582. g.waitAndPickClient()
  583. prs, resp, err := g.getClient().PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
  584. if err != nil {
  585. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  586. }
  587. log.Trace("Request get pull requests %d/%d, but in fact get %d", perPage, page, len(prs))
  588. g.setRate(&resp.Rate)
  589. for _, pr := range prs {
  590. var labels = make([]*base.Label, 0, len(pr.Labels))
  591. for _, l := range pr.Labels {
  592. labels = append(labels, convertGithubLabel(l))
  593. }
  594. // get reactions
  595. var reactions []*base.Reaction
  596. if !g.SkipReactions {
  597. for i := 1; ; i++ {
  598. g.waitAndPickClient()
  599. res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{
  600. Page: i,
  601. PerPage: perPage,
  602. })
  603. if err != nil {
  604. return nil, false, err
  605. }
  606. g.setRate(&resp.Rate)
  607. if len(res) == 0 {
  608. break
  609. }
  610. for _, reaction := range res {
  611. reactions = append(reactions, &base.Reaction{
  612. UserID: reaction.User.GetID(),
  613. UserName: reaction.User.GetLogin(),
  614. Content: reaction.GetContent(),
  615. })
  616. }
  617. }
  618. }
  619. // download patch and saved as tmp file
  620. g.waitAndPickClient()
  621. allPRs = append(allPRs, &base.PullRequest{
  622. Title: pr.GetTitle(),
  623. Number: int64(pr.GetNumber()),
  624. PosterID: pr.GetUser().GetID(),
  625. PosterName: pr.GetUser().GetLogin(),
  626. PosterEmail: pr.GetUser().GetEmail(),
  627. Content: pr.GetBody(),
  628. Milestone: pr.GetMilestone().GetTitle(),
  629. State: pr.GetState(),
  630. Created: pr.GetCreatedAt(),
  631. Updated: pr.GetUpdatedAt(),
  632. Closed: pr.ClosedAt,
  633. Labels: labels,
  634. Merged: pr.MergedAt != nil,
  635. MergeCommitSHA: pr.GetMergeCommitSHA(),
  636. MergedTime: pr.MergedAt,
  637. IsLocked: pr.ActiveLockReason != nil,
  638. Head: base.PullRequestBranch{
  639. Ref: pr.GetHead().GetRef(),
  640. SHA: pr.GetHead().GetSHA(),
  641. OwnerName: pr.GetHead().GetUser().GetLogin(),
  642. RepoName: pr.GetHead().GetRepo().GetName(),
  643. CloneURL: pr.GetHead().GetRepo().GetCloneURL(),
  644. },
  645. Base: base.PullRequestBranch{
  646. Ref: pr.GetBase().GetRef(),
  647. SHA: pr.GetBase().GetSHA(),
  648. RepoName: pr.GetBase().GetRepo().GetName(),
  649. OwnerName: pr.GetBase().GetUser().GetLogin(),
  650. },
  651. PatchURL: pr.GetPatchURL(),
  652. Reactions: reactions,
  653. Context: base.BasicIssueContext(*pr.Number),
  654. })
  655. }
  656. return allPRs, len(prs) < perPage, nil
  657. }
  658. func convertGithubReview(r *github.PullRequestReview) *base.Review {
  659. return &base.Review{
  660. ID: r.GetID(),
  661. ReviewerID: r.GetUser().GetID(),
  662. ReviewerName: r.GetUser().GetLogin(),
  663. CommitID: r.GetCommitID(),
  664. Content: r.GetBody(),
  665. CreatedAt: r.GetSubmittedAt(),
  666. State: r.GetState(),
  667. }
  668. }
  669. func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullRequestComment) ([]*base.ReviewComment, error) {
  670. var rcs = make([]*base.ReviewComment, 0, len(cs))
  671. for _, c := range cs {
  672. // get reactions
  673. var reactions []*base.Reaction
  674. if !g.SkipReactions {
  675. for i := 1; ; i++ {
  676. g.waitAndPickClient()
  677. res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{
  678. Page: i,
  679. PerPage: g.maxPerPage,
  680. })
  681. if err != nil {
  682. return nil, err
  683. }
  684. g.setRate(&resp.Rate)
  685. if len(res) == 0 {
  686. break
  687. }
  688. for _, reaction := range res {
  689. reactions = append(reactions, &base.Reaction{
  690. UserID: reaction.User.GetID(),
  691. UserName: reaction.User.GetLogin(),
  692. Content: reaction.GetContent(),
  693. })
  694. }
  695. }
  696. }
  697. rcs = append(rcs, &base.ReviewComment{
  698. ID: c.GetID(),
  699. InReplyTo: c.GetInReplyTo(),
  700. Content: c.GetBody(),
  701. TreePath: c.GetPath(),
  702. DiffHunk: c.GetDiffHunk(),
  703. Position: c.GetPosition(),
  704. CommitID: c.GetCommitID(),
  705. PosterID: c.GetUser().GetID(),
  706. Reactions: reactions,
  707. CreatedAt: c.GetCreatedAt(),
  708. UpdatedAt: c.GetUpdatedAt(),
  709. })
  710. }
  711. return rcs, nil
  712. }
  713. // GetReviews returns pull requests review
  714. func (g *GithubDownloaderV3) GetReviews(context base.IssueContext) ([]*base.Review, error) {
  715. var allReviews = make([]*base.Review, 0, g.maxPerPage)
  716. opt := &github.ListOptions{
  717. PerPage: g.maxPerPage,
  718. }
  719. for {
  720. g.waitAndPickClient()
  721. reviews, resp, err := g.getClient().PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(context.ForeignID()), opt)
  722. if err != nil {
  723. return nil, fmt.Errorf("error while listing repos: %v", err)
  724. }
  725. g.setRate(&resp.Rate)
  726. for _, review := range reviews {
  727. r := convertGithubReview(review)
  728. r.IssueIndex = context.LocalID()
  729. // retrieve all review comments
  730. opt2 := &github.ListOptions{
  731. PerPage: g.maxPerPage,
  732. }
  733. for {
  734. g.waitAndPickClient()
  735. reviewComments, resp, err := g.getClient().PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(context.ForeignID()), review.GetID(), opt2)
  736. if err != nil {
  737. return nil, fmt.Errorf("error while listing repos: %v", err)
  738. }
  739. g.setRate(&resp.Rate)
  740. cs, err := g.convertGithubReviewComments(reviewComments)
  741. if err != nil {
  742. return nil, err
  743. }
  744. r.Comments = append(r.Comments, cs...)
  745. if resp.NextPage == 0 {
  746. break
  747. }
  748. opt2.Page = resp.NextPage
  749. }
  750. allReviews = append(allReviews, r)
  751. }
  752. if resp.NextPage == 0 {
  753. break
  754. }
  755. opt.Page = resp.NextPage
  756. }
  757. return allReviews, nil
  758. }