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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Copyright 2018 Jonas Franz. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package migrations
  6. import (
  7. "context"
  8. "fmt"
  9. "net/http"
  10. "net/url"
  11. "strings"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/migrations/base"
  14. "github.com/google/go-github/v24/github"
  15. "golang.org/x/oauth2"
  16. )
  17. var (
  18. _ base.Downloader = &GithubDownloaderV3{}
  19. _ base.DownloaderFactory = &GithubDownloaderV3Factory{}
  20. )
  21. func init() {
  22. RegisterDownloaderFactory(&GithubDownloaderV3Factory{})
  23. }
  24. // GithubDownloaderV3Factory defines a github downloader v3 factory
  25. type GithubDownloaderV3Factory struct {
  26. }
  27. // Match returns ture if the migration remote URL matched this downloader factory
  28. func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error) {
  29. u, err := url.Parse(opts.CloneAddr)
  30. if err != nil {
  31. return false, err
  32. }
  33. return u.Host == "github.com" && opts.AuthUsername != "", nil
  34. }
  35. // New returns a Downloader related to this factory according MigrateOptions
  36. func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Downloader, error) {
  37. u, err := url.Parse(opts.CloneAddr)
  38. if err != nil {
  39. return nil, err
  40. }
  41. fields := strings.Split(u.Path, "/")
  42. oldOwner := fields[1]
  43. oldName := strings.TrimSuffix(fields[2], ".git")
  44. log.Trace("Create github downloader: %s/%s", oldOwner, oldName)
  45. return NewGithubDownloaderV3(opts.AuthUsername, opts.AuthPassword, oldOwner, oldName), nil
  46. }
  47. // GithubDownloaderV3 implements a Downloader interface to get repository informations
  48. // from github via APIv3
  49. type GithubDownloaderV3 struct {
  50. ctx context.Context
  51. client *github.Client
  52. repoOwner string
  53. repoName string
  54. userName string
  55. password string
  56. }
  57. // NewGithubDownloaderV3 creates a github Downloader via github v3 API
  58. func NewGithubDownloaderV3(userName, password, repoOwner, repoName string) *GithubDownloaderV3 {
  59. var downloader = GithubDownloaderV3{
  60. userName: userName,
  61. password: password,
  62. ctx: context.Background(),
  63. repoOwner: repoOwner,
  64. repoName: repoName,
  65. }
  66. var client *http.Client
  67. if userName != "" {
  68. if password == "" {
  69. ts := oauth2.StaticTokenSource(
  70. &oauth2.Token{AccessToken: userName},
  71. )
  72. client = oauth2.NewClient(downloader.ctx, ts)
  73. } else {
  74. client = &http.Client{
  75. Transport: &http.Transport{
  76. Proxy: func(req *http.Request) (*url.URL, error) {
  77. req.SetBasicAuth(userName, password)
  78. return nil, nil
  79. },
  80. },
  81. }
  82. }
  83. }
  84. downloader.client = github.NewClient(client)
  85. return &downloader
  86. }
  87. // GetRepoInfo returns a repository information
  88. func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) {
  89. gr, _, err := g.client.Repositories.Get(g.ctx, g.repoOwner, g.repoName)
  90. if err != nil {
  91. return nil, err
  92. }
  93. // convert github repo to stand Repo
  94. return &base.Repository{
  95. Owner: g.repoOwner,
  96. Name: gr.GetName(),
  97. IsPrivate: *gr.Private,
  98. Description: gr.GetDescription(),
  99. OriginalURL: gr.GetHTMLURL(),
  100. CloneURL: gr.GetCloneURL(),
  101. }, nil
  102. }
  103. // GetTopics return github topics
  104. func (g *GithubDownloaderV3) GetTopics() ([]string, error) {
  105. r, _, err := g.client.Repositories.Get(g.ctx, g.repoOwner, g.repoName)
  106. return r.Topics, err
  107. }
  108. // GetMilestones returns milestones
  109. func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) {
  110. var perPage = 100
  111. var milestones = make([]*base.Milestone, 0, perPage)
  112. for i := 1; ; i++ {
  113. ms, _, err := g.client.Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName,
  114. &github.MilestoneListOptions{
  115. State: "all",
  116. ListOptions: github.ListOptions{
  117. Page: i,
  118. PerPage: perPage,
  119. }})
  120. if err != nil {
  121. return nil, err
  122. }
  123. for _, m := range ms {
  124. var desc string
  125. if m.Description != nil {
  126. desc = *m.Description
  127. }
  128. var state = "open"
  129. if m.State != nil {
  130. state = *m.State
  131. }
  132. milestones = append(milestones, &base.Milestone{
  133. Title: *m.Title,
  134. Description: desc,
  135. Deadline: m.DueOn,
  136. State: state,
  137. Created: *m.CreatedAt,
  138. Updated: m.UpdatedAt,
  139. Closed: m.ClosedAt,
  140. })
  141. }
  142. if len(ms) < perPage {
  143. break
  144. }
  145. }
  146. return milestones, nil
  147. }
  148. func convertGithubLabel(label *github.Label) *base.Label {
  149. var desc string
  150. if label.Description != nil {
  151. desc = *label.Description
  152. }
  153. return &base.Label{
  154. Name: *label.Name,
  155. Color: *label.Color,
  156. Description: desc,
  157. }
  158. }
  159. // GetLabels returns labels
  160. func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
  161. var perPage = 100
  162. var labels = make([]*base.Label, 0, perPage)
  163. for i := 1; ; i++ {
  164. ls, _, err := g.client.Issues.ListLabels(g.ctx, g.repoOwner, g.repoName,
  165. &github.ListOptions{
  166. Page: i,
  167. PerPage: perPage,
  168. })
  169. if err != nil {
  170. return nil, err
  171. }
  172. for _, label := range ls {
  173. labels = append(labels, convertGithubLabel(label))
  174. }
  175. if len(ls) < perPage {
  176. break
  177. }
  178. }
  179. return labels, nil
  180. }
  181. func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release {
  182. var (
  183. name string
  184. desc string
  185. )
  186. if rel.Body != nil {
  187. desc = *rel.Body
  188. }
  189. if rel.Name != nil {
  190. name = *rel.Name
  191. }
  192. var email string
  193. if rel.Author.Email != nil {
  194. email = *rel.Author.Email
  195. }
  196. r := &base.Release{
  197. TagName: *rel.TagName,
  198. TargetCommitish: *rel.TargetCommitish,
  199. Name: name,
  200. Body: desc,
  201. Draft: *rel.Draft,
  202. Prerelease: *rel.Prerelease,
  203. Created: rel.CreatedAt.Time,
  204. PublisherID: *rel.Author.ID,
  205. PublisherName: *rel.Author.Login,
  206. PublisherEmail: email,
  207. Published: rel.PublishedAt.Time,
  208. }
  209. for _, asset := range rel.Assets {
  210. u, _ := url.Parse(*asset.BrowserDownloadURL)
  211. u.User = url.UserPassword(g.userName, g.password)
  212. r.Assets = append(r.Assets, base.ReleaseAsset{
  213. URL: u.String(),
  214. Name: *asset.Name,
  215. ContentType: asset.ContentType,
  216. Size: asset.Size,
  217. DownloadCount: asset.DownloadCount,
  218. Created: asset.CreatedAt.Time,
  219. Updated: asset.UpdatedAt.Time,
  220. })
  221. }
  222. return r
  223. }
  224. // GetReleases returns releases
  225. func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
  226. var perPage = 100
  227. var releases = make([]*base.Release, 0, perPage)
  228. for i := 1; ; i++ {
  229. ls, _, err := g.client.Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName,
  230. &github.ListOptions{
  231. Page: i,
  232. PerPage: perPage,
  233. })
  234. if err != nil {
  235. return nil, err
  236. }
  237. for _, release := range ls {
  238. releases = append(releases, g.convertGithubRelease(release))
  239. }
  240. if len(ls) < perPage {
  241. break
  242. }
  243. }
  244. return releases, nil
  245. }
  246. func convertGithubReactions(reactions *github.Reactions) *base.Reactions {
  247. return &base.Reactions{
  248. TotalCount: *reactions.TotalCount,
  249. PlusOne: *reactions.PlusOne,
  250. MinusOne: *reactions.MinusOne,
  251. Laugh: *reactions.Laugh,
  252. Confused: *reactions.Confused,
  253. Heart: *reactions.Heart,
  254. Hooray: *reactions.Hooray,
  255. }
  256. }
  257. // GetIssues returns issues according start and limit
  258. func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
  259. opt := &github.IssueListByRepoOptions{
  260. Sort: "created",
  261. Direction: "asc",
  262. State: "all",
  263. ListOptions: github.ListOptions{
  264. PerPage: perPage,
  265. Page: page,
  266. },
  267. }
  268. var allIssues = make([]*base.Issue, 0, perPage)
  269. issues, _, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt)
  270. if err != nil {
  271. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  272. }
  273. for _, issue := range issues {
  274. if issue.IsPullRequest() {
  275. continue
  276. }
  277. var body string
  278. if issue.Body != nil {
  279. body = *issue.Body
  280. }
  281. var milestone string
  282. if issue.Milestone != nil {
  283. milestone = *issue.Milestone.Title
  284. }
  285. var labels = make([]*base.Label, 0, len(issue.Labels))
  286. for _, l := range issue.Labels {
  287. labels = append(labels, convertGithubLabel(&l))
  288. }
  289. var reactions *base.Reactions
  290. if issue.Reactions != nil {
  291. reactions = convertGithubReactions(issue.Reactions)
  292. }
  293. var email string
  294. if issue.User.Email != nil {
  295. email = *issue.User.Email
  296. }
  297. allIssues = append(allIssues, &base.Issue{
  298. Title: *issue.Title,
  299. Number: int64(*issue.Number),
  300. PosterID: *issue.User.ID,
  301. PosterName: *issue.User.Login,
  302. PosterEmail: email,
  303. Content: body,
  304. Milestone: milestone,
  305. State: *issue.State,
  306. Created: *issue.CreatedAt,
  307. Labels: labels,
  308. Reactions: reactions,
  309. Closed: issue.ClosedAt,
  310. IsLocked: *issue.Locked,
  311. })
  312. }
  313. return allIssues, len(issues) < perPage, nil
  314. }
  315. // GetComments returns comments according issueNumber
  316. func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, error) {
  317. var allComments = make([]*base.Comment, 0, 100)
  318. opt := &github.IssueListCommentsOptions{
  319. Sort: "created",
  320. Direction: "asc",
  321. ListOptions: github.ListOptions{
  322. PerPage: 100,
  323. },
  324. }
  325. for {
  326. comments, resp, err := g.client.Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(issueNumber), opt)
  327. if err != nil {
  328. return nil, fmt.Errorf("error while listing repos: %v", err)
  329. }
  330. for _, comment := range comments {
  331. var email string
  332. if comment.User.Email != nil {
  333. email = *comment.User.Email
  334. }
  335. var reactions *base.Reactions
  336. if comment.Reactions != nil {
  337. reactions = convertGithubReactions(comment.Reactions)
  338. }
  339. allComments = append(allComments, &base.Comment{
  340. IssueIndex: issueNumber,
  341. PosterID: *comment.User.ID,
  342. PosterName: *comment.User.Login,
  343. PosterEmail: email,
  344. Content: *comment.Body,
  345. Created: *comment.CreatedAt,
  346. Reactions: reactions,
  347. })
  348. }
  349. if resp.NextPage == 0 {
  350. break
  351. }
  352. opt.Page = resp.NextPage
  353. }
  354. return allComments, nil
  355. }
  356. // GetPullRequests returns pull requests according page and perPage
  357. func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, error) {
  358. opt := &github.PullRequestListOptions{
  359. Sort: "created",
  360. Direction: "asc",
  361. State: "all",
  362. ListOptions: github.ListOptions{
  363. PerPage: perPage,
  364. Page: page,
  365. },
  366. }
  367. var allPRs = make([]*base.PullRequest, 0, perPage)
  368. prs, _, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
  369. if err != nil {
  370. return nil, fmt.Errorf("error while listing repos: %v", err)
  371. }
  372. for _, pr := range prs {
  373. var body string
  374. if pr.Body != nil {
  375. body = *pr.Body
  376. }
  377. var milestone string
  378. if pr.Milestone != nil {
  379. milestone = *pr.Milestone.Title
  380. }
  381. var labels = make([]*base.Label, 0, len(pr.Labels))
  382. for _, l := range pr.Labels {
  383. labels = append(labels, convertGithubLabel(l))
  384. }
  385. // FIXME: This API missing reactions, we may need another extra request to get reactions
  386. var email string
  387. if pr.User.Email != nil {
  388. email = *pr.User.Email
  389. }
  390. var merged bool
  391. // pr.Merged is not valid, so use MergedAt to test if it's merged
  392. if pr.MergedAt != nil {
  393. merged = true
  394. }
  395. var (
  396. headRepoName string
  397. cloneURL string
  398. headRef string
  399. headSHA string
  400. )
  401. if pr.Head.Repo != nil {
  402. if pr.Head.Repo.Name != nil {
  403. headRepoName = *pr.Head.Repo.Name
  404. }
  405. if pr.Head.Repo.CloneURL != nil {
  406. cloneURL = *pr.Head.Repo.CloneURL
  407. }
  408. }
  409. if pr.Head.Ref != nil {
  410. headRef = *pr.Head.Ref
  411. }
  412. if pr.Head.SHA != nil {
  413. headSHA = *pr.Head.SHA
  414. }
  415. var mergeCommitSHA string
  416. if pr.MergeCommitSHA != nil {
  417. mergeCommitSHA = *pr.MergeCommitSHA
  418. }
  419. var headUserName string
  420. if pr.Head.User != nil && pr.Head.User.Login != nil {
  421. headUserName = *pr.Head.User.Login
  422. }
  423. allPRs = append(allPRs, &base.PullRequest{
  424. Title: *pr.Title,
  425. Number: int64(*pr.Number),
  426. PosterName: *pr.User.Login,
  427. PosterID: *pr.User.ID,
  428. PosterEmail: email,
  429. Content: body,
  430. Milestone: milestone,
  431. State: *pr.State,
  432. Created: *pr.CreatedAt,
  433. Closed: pr.ClosedAt,
  434. Labels: labels,
  435. Merged: merged,
  436. MergeCommitSHA: mergeCommitSHA,
  437. MergedTime: pr.MergedAt,
  438. IsLocked: pr.ActiveLockReason != nil,
  439. Head: base.PullRequestBranch{
  440. Ref: headRef,
  441. SHA: headSHA,
  442. RepoName: headRepoName,
  443. OwnerName: headUserName,
  444. CloneURL: cloneURL,
  445. },
  446. Base: base.PullRequestBranch{
  447. Ref: *pr.Base.Ref,
  448. SHA: *pr.Base.SHA,
  449. RepoName: *pr.Base.Repo.Name,
  450. OwnerName: *pr.Base.User.Login,
  451. },
  452. PatchURL: *pr.PatchURL,
  453. })
  454. }
  455. return allPRs, nil
  456. }