gitea源码

github.go 26KB

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