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

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