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

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