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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  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. // New returns a Downloader related to this factory according MigrateOptions
  35. func (f *GithubDownloaderV3Factory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
  36. u, err := url.Parse(opts.CloneAddr)
  37. if err != nil {
  38. return nil, err
  39. }
  40. baseURL := u.Scheme + "://" + u.Host
  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(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil
  46. }
  47. // GitServiceType returns the type of git service
  48. func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType {
  49. return structs.GithubService
  50. }
  51. // GithubDownloaderV3 implements a Downloader interface to get repository information
  52. // from github via APIv3
  53. type GithubDownloaderV3 struct {
  54. base.NullDownloader
  55. ctx context.Context
  56. clients []*github.Client
  57. repoOwner string
  58. repoName string
  59. userName string
  60. password string
  61. rates []*github.Rate
  62. curClientIdx int
  63. maxPerPage int
  64. SkipReactions bool
  65. }
  66. // NewGithubDownloaderV3 creates a github Downloader via github v3 API
  67. func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
  68. downloader := GithubDownloaderV3{
  69. userName: userName,
  70. password: password,
  71. ctx: ctx,
  72. repoOwner: repoOwner,
  73. repoName: repoName,
  74. maxPerPage: 100,
  75. }
  76. if token != "" {
  77. tokens := strings.Split(token, ",")
  78. for _, token := range tokens {
  79. token = strings.TrimSpace(token)
  80. ts := oauth2.StaticTokenSource(
  81. &oauth2.Token{AccessToken: token},
  82. )
  83. client := &http.Client{
  84. Transport: &oauth2.Transport{
  85. Base: NewMigrationHTTPTransport(),
  86. Source: oauth2.ReuseTokenSource(nil, ts),
  87. },
  88. }
  89. downloader.addClient(client, baseURL)
  90. }
  91. } else {
  92. transport := NewMigrationHTTPTransport()
  93. transport.Proxy = func(req *http.Request) (*url.URL, error) {
  94. req.SetBasicAuth(userName, password)
  95. return proxy.Proxy()(req)
  96. }
  97. client := &http.Client{
  98. Transport: transport,
  99. }
  100. downloader.addClient(client, baseURL)
  101. }
  102. return &downloader
  103. }
  104. func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
  105. githubClient := github.NewClient(client)
  106. if baseURL != "https://github.com" {
  107. githubClient, _ = github.NewEnterpriseClient(baseURL, baseURL, client)
  108. }
  109. g.clients = append(g.clients, githubClient)
  110. g.rates = append(g.rates, nil)
  111. }
  112. // SetContext set context
  113. func (g *GithubDownloaderV3) SetContext(ctx context.Context) {
  114. g.ctx = ctx
  115. }
  116. func (g *GithubDownloaderV3) waitAndPickClient() {
  117. var recentIdx int
  118. var maxRemaining int
  119. for i := 0; i < len(g.clients); i++ {
  120. if g.rates[i] != nil && g.rates[i].Remaining > maxRemaining {
  121. maxRemaining = g.rates[i].Remaining
  122. recentIdx = i
  123. }
  124. }
  125. g.curClientIdx = recentIdx // if no max remain, it will always pick the first client.
  126. for g.rates[g.curClientIdx] != nil && g.rates[g.curClientIdx].Remaining <= GithubLimitRateRemaining {
  127. timer := time.NewTimer(time.Until(g.rates[g.curClientIdx].Reset.Time))
  128. select {
  129. case <-g.ctx.Done():
  130. util.StopTimer(timer)
  131. return
  132. case <-timer.C:
  133. }
  134. err := g.RefreshRate()
  135. if err != nil {
  136. log.Error("g.getClient().RateLimits: %s", err)
  137. }
  138. }
  139. }
  140. // RefreshRate update the current rate (doesn't count in rate limit)
  141. func (g *GithubDownloaderV3) RefreshRate() error {
  142. rates, _, err := g.getClient().RateLimits(g.ctx)
  143. if err != nil {
  144. // if rate limit is not enabled, ignore it
  145. if strings.Contains(err.Error(), "404") {
  146. g.setRate(nil)
  147. return nil
  148. }
  149. return err
  150. }
  151. g.setRate(rates.GetCore())
  152. return nil
  153. }
  154. func (g *GithubDownloaderV3) getClient() *github.Client {
  155. return g.clients[g.curClientIdx]
  156. }
  157. func (g *GithubDownloaderV3) setRate(rate *github.Rate) {
  158. g.rates[g.curClientIdx] = rate
  159. }
  160. // GetRepoInfo returns a repository information
  161. func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) {
  162. g.waitAndPickClient()
  163. gr, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName)
  164. if err != nil {
  165. return nil, err
  166. }
  167. g.setRate(&resp.Rate)
  168. // convert github repo to stand Repo
  169. return &base.Repository{
  170. Owner: g.repoOwner,
  171. Name: gr.GetName(),
  172. IsPrivate: gr.GetPrivate(),
  173. Description: gr.GetDescription(),
  174. OriginalURL: gr.GetHTMLURL(),
  175. CloneURL: gr.GetCloneURL(),
  176. DefaultBranch: gr.GetDefaultBranch(),
  177. }, nil
  178. }
  179. // GetTopics return github topics
  180. func (g *GithubDownloaderV3) GetTopics() ([]string, error) {
  181. g.waitAndPickClient()
  182. r, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName)
  183. if err != nil {
  184. return nil, err
  185. }
  186. g.setRate(&resp.Rate)
  187. return r.Topics, nil
  188. }
  189. // GetMilestones returns milestones
  190. func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) {
  191. perPage := g.maxPerPage
  192. milestones := make([]*base.Milestone, 0, perPage)
  193. for i := 1; ; i++ {
  194. g.waitAndPickClient()
  195. ms, resp, err := g.getClient().Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName,
  196. &github.MilestoneListOptions{
  197. State: "all",
  198. ListOptions: github.ListOptions{
  199. Page: i,
  200. PerPage: perPage,
  201. },
  202. })
  203. if err != nil {
  204. return nil, err
  205. }
  206. g.setRate(&resp.Rate)
  207. for _, m := range ms {
  208. 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. perPage := g.maxPerPage
  238. 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. 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. perPage := g.maxPerPage
  323. 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. 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. 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. ForeignIndex: int64(*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(commentable base.Commentable) ([]*base.Comment, bool, error) {
  430. comments, err := g.getComments(commentable)
  431. return comments, false, err
  432. }
  433. func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.Comment, error) {
  434. var (
  435. allComments = make([]*base.Comment, 0, g.maxPerPage)
  436. created = "created"
  437. asc = "asc"
  438. )
  439. opt := &github.IssueListCommentsOptions{
  440. Sort: &created,
  441. Direction: &asc,
  442. ListOptions: github.ListOptions{
  443. PerPage: g.maxPerPage,
  444. },
  445. }
  446. for {
  447. g.waitAndPickClient()
  448. comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(commentable.GetForeignIndex()), opt)
  449. if err != nil {
  450. return nil, fmt.Errorf("error while listing repos: %v", err)
  451. }
  452. g.setRate(&resp.Rate)
  453. for _, comment := range comments {
  454. // get reactions
  455. var reactions []*base.Reaction
  456. if !g.SkipReactions {
  457. for i := 1; ; i++ {
  458. g.waitAndPickClient()
  459. res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
  460. Page: i,
  461. PerPage: g.maxPerPage,
  462. })
  463. if err != nil {
  464. return nil, err
  465. }
  466. g.setRate(&resp.Rate)
  467. if len(res) == 0 {
  468. break
  469. }
  470. for _, reaction := range res {
  471. reactions = append(reactions, &base.Reaction{
  472. UserID: reaction.User.GetID(),
  473. UserName: reaction.User.GetLogin(),
  474. Content: reaction.GetContent(),
  475. })
  476. }
  477. }
  478. }
  479. allComments = append(allComments, &base.Comment{
  480. IssueIndex: commentable.GetLocalIndex(),
  481. Index: comment.GetID(),
  482. PosterID: comment.GetUser().GetID(),
  483. PosterName: comment.GetUser().GetLogin(),
  484. PosterEmail: comment.GetUser().GetEmail(),
  485. Content: comment.GetBody(),
  486. Created: comment.GetCreatedAt(),
  487. Updated: comment.GetUpdatedAt(),
  488. Reactions: reactions,
  489. })
  490. }
  491. if resp.NextPage == 0 {
  492. break
  493. }
  494. opt.Page = resp.NextPage
  495. }
  496. return allComments, nil
  497. }
  498. // GetAllComments returns repository comments according page and perPageSize
  499. func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, bool, error) {
  500. var (
  501. allComments = make([]*base.Comment, 0, perPage)
  502. created = "created"
  503. asc = "asc"
  504. )
  505. if perPage > g.maxPerPage {
  506. perPage = g.maxPerPage
  507. }
  508. opt := &github.IssueListCommentsOptions{
  509. Sort: &created,
  510. Direction: &asc,
  511. ListOptions: github.ListOptions{
  512. Page: page,
  513. PerPage: perPage,
  514. },
  515. }
  516. g.waitAndPickClient()
  517. comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, 0, opt)
  518. if err != nil {
  519. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  520. }
  521. isEnd := resp.NextPage == 0
  522. log.Trace("Request get comments %d/%d, but in fact get %d, next page is %d", perPage, page, len(comments), resp.NextPage)
  523. g.setRate(&resp.Rate)
  524. for _, comment := range comments {
  525. // get reactions
  526. var reactions []*base.Reaction
  527. if !g.SkipReactions {
  528. for i := 1; ; i++ {
  529. g.waitAndPickClient()
  530. res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
  531. Page: i,
  532. PerPage: g.maxPerPage,
  533. })
  534. if err != nil {
  535. return nil, false, err
  536. }
  537. g.setRate(&resp.Rate)
  538. if len(res) == 0 {
  539. break
  540. }
  541. for _, reaction := range res {
  542. reactions = append(reactions, &base.Reaction{
  543. UserID: reaction.User.GetID(),
  544. UserName: reaction.User.GetLogin(),
  545. Content: reaction.GetContent(),
  546. })
  547. }
  548. }
  549. }
  550. idx := strings.LastIndex(*comment.IssueURL, "/")
  551. issueIndex, _ := strconv.ParseInt((*comment.IssueURL)[idx+1:], 10, 64)
  552. allComments = append(allComments, &base.Comment{
  553. IssueIndex: issueIndex,
  554. Index: comment.GetID(),
  555. PosterID: comment.GetUser().GetID(),
  556. PosterName: comment.GetUser().GetLogin(),
  557. PosterEmail: comment.GetUser().GetEmail(),
  558. Content: comment.GetBody(),
  559. Created: comment.GetCreatedAt(),
  560. Updated: comment.GetUpdatedAt(),
  561. Reactions: reactions,
  562. })
  563. }
  564. return allComments, isEnd, nil
  565. }
  566. // GetPullRequests returns pull requests according page and perPage
  567. func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
  568. if perPage > g.maxPerPage {
  569. perPage = g.maxPerPage
  570. }
  571. opt := &github.PullRequestListOptions{
  572. Sort: "created",
  573. Direction: "asc",
  574. State: "all",
  575. ListOptions: github.ListOptions{
  576. PerPage: perPage,
  577. Page: page,
  578. },
  579. }
  580. allPRs := make([]*base.PullRequest, 0, perPage)
  581. g.waitAndPickClient()
  582. prs, resp, err := g.getClient().PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
  583. if err != nil {
  584. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  585. }
  586. log.Trace("Request get pull requests %d/%d, but in fact get %d", perPage, page, len(prs))
  587. g.setRate(&resp.Rate)
  588. for _, pr := range prs {
  589. labels := make([]*base.Label, 0, len(pr.Labels))
  590. for _, l := range pr.Labels {
  591. labels = append(labels, convertGithubLabel(l))
  592. }
  593. // get reactions
  594. var reactions []*base.Reaction
  595. if !g.SkipReactions {
  596. for i := 1; ; i++ {
  597. g.waitAndPickClient()
  598. res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{
  599. Page: i,
  600. PerPage: perPage,
  601. })
  602. if err != nil {
  603. return nil, false, err
  604. }
  605. g.setRate(&resp.Rate)
  606. if len(res) == 0 {
  607. break
  608. }
  609. for _, reaction := range res {
  610. reactions = append(reactions, &base.Reaction{
  611. UserID: reaction.User.GetID(),
  612. UserName: reaction.User.GetLogin(),
  613. Content: reaction.GetContent(),
  614. })
  615. }
  616. }
  617. }
  618. // download patch and saved as tmp file
  619. g.waitAndPickClient()
  620. allPRs = append(allPRs, &base.PullRequest{
  621. Title: pr.GetTitle(),
  622. Number: int64(pr.GetNumber()),
  623. PosterID: pr.GetUser().GetID(),
  624. PosterName: pr.GetUser().GetLogin(),
  625. PosterEmail: pr.GetUser().GetEmail(),
  626. Content: pr.GetBody(),
  627. Milestone: pr.GetMilestone().GetTitle(),
  628. State: pr.GetState(),
  629. Created: pr.GetCreatedAt(),
  630. Updated: pr.GetUpdatedAt(),
  631. Closed: pr.ClosedAt,
  632. Labels: labels,
  633. Merged: pr.MergedAt != nil,
  634. MergeCommitSHA: pr.GetMergeCommitSHA(),
  635. MergedTime: pr.MergedAt,
  636. IsLocked: pr.ActiveLockReason != nil,
  637. Head: base.PullRequestBranch{
  638. Ref: pr.GetHead().GetRef(),
  639. SHA: pr.GetHead().GetSHA(),
  640. OwnerName: pr.GetHead().GetUser().GetLogin(),
  641. RepoName: pr.GetHead().GetRepo().GetName(),
  642. CloneURL: pr.GetHead().GetRepo().GetCloneURL(),
  643. },
  644. Base: base.PullRequestBranch{
  645. Ref: pr.GetBase().GetRef(),
  646. SHA: pr.GetBase().GetSHA(),
  647. RepoName: pr.GetBase().GetRepo().GetName(),
  648. OwnerName: pr.GetBase().GetUser().GetLogin(),
  649. },
  650. PatchURL: pr.GetPatchURL(),
  651. Reactions: reactions,
  652. ForeignIndex: int64(*pr.Number),
  653. })
  654. }
  655. return allPRs, len(prs) < perPage, nil
  656. }
  657. func convertGithubReview(r *github.PullRequestReview) *base.Review {
  658. return &base.Review{
  659. ID: r.GetID(),
  660. ReviewerID: r.GetUser().GetID(),
  661. ReviewerName: r.GetUser().GetLogin(),
  662. CommitID: r.GetCommitID(),
  663. Content: r.GetBody(),
  664. CreatedAt: r.GetSubmittedAt(),
  665. State: r.GetState(),
  666. }
  667. }
  668. func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullRequestComment) ([]*base.ReviewComment, error) {
  669. rcs := make([]*base.ReviewComment, 0, len(cs))
  670. for _, c := range cs {
  671. // get reactions
  672. var reactions []*base.Reaction
  673. if !g.SkipReactions {
  674. for i := 1; ; i++ {
  675. g.waitAndPickClient()
  676. res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{
  677. Page: i,
  678. PerPage: g.maxPerPage,
  679. })
  680. if err != nil {
  681. return nil, err
  682. }
  683. g.setRate(&resp.Rate)
  684. if len(res) == 0 {
  685. break
  686. }
  687. for _, reaction := range res {
  688. reactions = append(reactions, &base.Reaction{
  689. UserID: reaction.User.GetID(),
  690. UserName: reaction.User.GetLogin(),
  691. Content: reaction.GetContent(),
  692. })
  693. }
  694. }
  695. }
  696. rcs = append(rcs, &base.ReviewComment{
  697. ID: c.GetID(),
  698. InReplyTo: c.GetInReplyTo(),
  699. Content: c.GetBody(),
  700. TreePath: c.GetPath(),
  701. DiffHunk: c.GetDiffHunk(),
  702. Position: c.GetPosition(),
  703. CommitID: c.GetCommitID(),
  704. PosterID: c.GetUser().GetID(),
  705. Reactions: reactions,
  706. CreatedAt: c.GetCreatedAt(),
  707. UpdatedAt: c.GetUpdatedAt(),
  708. })
  709. }
  710. return rcs, nil
  711. }
  712. // GetReviews returns pull requests review
  713. func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
  714. allReviews := make([]*base.Review, 0, g.maxPerPage)
  715. opt := &github.ListOptions{
  716. PerPage: g.maxPerPage,
  717. }
  718. for {
  719. g.waitAndPickClient()
  720. reviews, resp, err := g.getClient().PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt)
  721. if err != nil {
  722. return nil, fmt.Errorf("error while listing repos: %v", err)
  723. }
  724. g.setRate(&resp.Rate)
  725. for _, review := range reviews {
  726. r := convertGithubReview(review)
  727. r.IssueIndex = reviewable.GetLocalIndex()
  728. // retrieve all review comments
  729. opt2 := &github.ListOptions{
  730. PerPage: g.maxPerPage,
  731. }
  732. for {
  733. g.waitAndPickClient()
  734. reviewComments, resp, err := g.getClient().PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), review.GetID(), opt2)
  735. if err != nil {
  736. return nil, fmt.Errorf("error while listing repos: %v", err)
  737. }
  738. g.setRate(&resp.Rate)
  739. cs, err := g.convertGithubReviewComments(reviewComments)
  740. if err != nil {
  741. return nil, err
  742. }
  743. r.Comments = append(r.Comments, cs...)
  744. if resp.NextPage == 0 {
  745. break
  746. }
  747. opt2.Page = resp.NextPage
  748. }
  749. allReviews = append(allReviews, r)
  750. }
  751. if resp.NextPage == 0 {
  752. break
  753. }
  754. opt.Page = resp.NextPage
  755. }
  756. return allReviews, nil
  757. }