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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  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.RemoteURL)
  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.RemoteURL)
  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. r := &base.Release{
  193. TagName: *rel.TagName,
  194. TargetCommitish: *rel.TargetCommitish,
  195. Name: name,
  196. Body: desc,
  197. Draft: *rel.Draft,
  198. Prerelease: *rel.Prerelease,
  199. Created: rel.CreatedAt.Time,
  200. Published: rel.PublishedAt.Time,
  201. }
  202. for _, asset := range rel.Assets {
  203. u, _ := url.Parse(*asset.BrowserDownloadURL)
  204. u.User = url.UserPassword(g.userName, g.password)
  205. r.Assets = append(r.Assets, base.ReleaseAsset{
  206. URL: u.String(),
  207. Name: *asset.Name,
  208. ContentType: asset.ContentType,
  209. Size: asset.Size,
  210. DownloadCount: asset.DownloadCount,
  211. Created: asset.CreatedAt.Time,
  212. Updated: asset.UpdatedAt.Time,
  213. })
  214. }
  215. return r
  216. }
  217. // GetReleases returns releases
  218. func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
  219. var perPage = 100
  220. var releases = make([]*base.Release, 0, perPage)
  221. for i := 1; ; i++ {
  222. ls, _, err := g.client.Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName,
  223. &github.ListOptions{
  224. Page: i,
  225. PerPage: perPage,
  226. })
  227. if err != nil {
  228. return nil, err
  229. }
  230. for _, release := range ls {
  231. releases = append(releases, g.convertGithubRelease(release))
  232. }
  233. if len(ls) < perPage {
  234. break
  235. }
  236. }
  237. return releases, nil
  238. }
  239. func convertGithubReactions(reactions *github.Reactions) *base.Reactions {
  240. return &base.Reactions{
  241. TotalCount: *reactions.TotalCount,
  242. PlusOne: *reactions.PlusOne,
  243. MinusOne: *reactions.MinusOne,
  244. Laugh: *reactions.Laugh,
  245. Confused: *reactions.Confused,
  246. Heart: *reactions.Heart,
  247. Hooray: *reactions.Hooray,
  248. }
  249. }
  250. // GetIssues returns issues according start and limit
  251. func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
  252. opt := &github.IssueListByRepoOptions{
  253. Sort: "created",
  254. Direction: "asc",
  255. State: "all",
  256. ListOptions: github.ListOptions{
  257. PerPage: perPage,
  258. Page: page,
  259. },
  260. }
  261. var allIssues = make([]*base.Issue, 0, perPage)
  262. issues, _, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt)
  263. if err != nil {
  264. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  265. }
  266. for _, issue := range issues {
  267. if issue.IsPullRequest() {
  268. continue
  269. }
  270. var body string
  271. if issue.Body != nil {
  272. body = *issue.Body
  273. }
  274. var milestone string
  275. if issue.Milestone != nil {
  276. milestone = *issue.Milestone.Title
  277. }
  278. var labels = make([]*base.Label, 0, len(issue.Labels))
  279. for _, l := range issue.Labels {
  280. labels = append(labels, convertGithubLabel(&l))
  281. }
  282. var reactions *base.Reactions
  283. if issue.Reactions != nil {
  284. reactions = convertGithubReactions(issue.Reactions)
  285. }
  286. var email string
  287. if issue.User.Email != nil {
  288. email = *issue.User.Email
  289. }
  290. allIssues = append(allIssues, &base.Issue{
  291. Title: *issue.Title,
  292. Number: int64(*issue.Number),
  293. PosterID: *issue.User.ID,
  294. PosterName: *issue.User.Login,
  295. PosterEmail: email,
  296. Content: body,
  297. Milestone: milestone,
  298. State: *issue.State,
  299. Created: *issue.CreatedAt,
  300. Labels: labels,
  301. Reactions: reactions,
  302. Closed: issue.ClosedAt,
  303. IsLocked: *issue.Locked,
  304. })
  305. }
  306. return allIssues, len(issues) < perPage, nil
  307. }
  308. // GetComments returns comments according issueNumber
  309. func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, error) {
  310. var allComments = make([]*base.Comment, 0, 100)
  311. opt := &github.IssueListCommentsOptions{
  312. Sort: "created",
  313. Direction: "asc",
  314. ListOptions: github.ListOptions{
  315. PerPage: 100,
  316. },
  317. }
  318. for {
  319. comments, resp, err := g.client.Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(issueNumber), opt)
  320. if err != nil {
  321. return nil, fmt.Errorf("error while listing repos: %v", err)
  322. }
  323. for _, comment := range comments {
  324. var email string
  325. if comment.User.Email != nil {
  326. email = *comment.User.Email
  327. }
  328. var reactions *base.Reactions
  329. if comment.Reactions != nil {
  330. reactions = convertGithubReactions(comment.Reactions)
  331. }
  332. allComments = append(allComments, &base.Comment{
  333. IssueIndex: issueNumber,
  334. PosterID: *comment.User.ID,
  335. PosterName: *comment.User.Login,
  336. PosterEmail: email,
  337. Content: *comment.Body,
  338. Created: *comment.CreatedAt,
  339. Reactions: reactions,
  340. })
  341. }
  342. if resp.NextPage == 0 {
  343. break
  344. }
  345. opt.Page = resp.NextPage
  346. }
  347. return allComments, nil
  348. }
  349. // GetPullRequests returns pull requests according page and perPage
  350. func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, error) {
  351. opt := &github.PullRequestListOptions{
  352. Sort: "created",
  353. Direction: "asc",
  354. State: "all",
  355. ListOptions: github.ListOptions{
  356. PerPage: perPage,
  357. Page: page,
  358. },
  359. }
  360. var allPRs = make([]*base.PullRequest, 0, perPage)
  361. prs, _, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
  362. if err != nil {
  363. return nil, fmt.Errorf("error while listing repos: %v", err)
  364. }
  365. for _, pr := range prs {
  366. var body string
  367. if pr.Body != nil {
  368. body = *pr.Body
  369. }
  370. var milestone string
  371. if pr.Milestone != nil {
  372. milestone = *pr.Milestone.Title
  373. }
  374. var labels = make([]*base.Label, 0, len(pr.Labels))
  375. for _, l := range pr.Labels {
  376. labels = append(labels, convertGithubLabel(l))
  377. }
  378. // FIXME: This API missing reactions, we may need another extra request to get reactions
  379. var email string
  380. if pr.User.Email != nil {
  381. email = *pr.User.Email
  382. }
  383. var merged bool
  384. // pr.Merged is not valid, so use MergedAt to test if it's merged
  385. if pr.MergedAt != nil {
  386. merged = true
  387. }
  388. var (
  389. headRepoName string
  390. cloneURL string
  391. headRef string
  392. headSHA string
  393. )
  394. if pr.Head.Repo != nil {
  395. if pr.Head.Repo.Name != nil {
  396. headRepoName = *pr.Head.Repo.Name
  397. }
  398. if pr.Head.Repo.CloneURL != nil {
  399. cloneURL = *pr.Head.Repo.CloneURL
  400. }
  401. }
  402. if pr.Head.Ref != nil {
  403. headRef = *pr.Head.Ref
  404. }
  405. if pr.Head.SHA != nil {
  406. headSHA = *pr.Head.SHA
  407. }
  408. var mergeCommitSHA string
  409. if pr.MergeCommitSHA != nil {
  410. mergeCommitSHA = *pr.MergeCommitSHA
  411. }
  412. var headUserName string
  413. if pr.Head.User != nil && pr.Head.User.Login != nil {
  414. headUserName = *pr.Head.User.Login
  415. }
  416. allPRs = append(allPRs, &base.PullRequest{
  417. Title: *pr.Title,
  418. Number: int64(*pr.Number),
  419. PosterName: *pr.User.Login,
  420. PosterID: *pr.User.ID,
  421. PosterEmail: email,
  422. Content: body,
  423. Milestone: milestone,
  424. State: *pr.State,
  425. Created: *pr.CreatedAt,
  426. Closed: pr.ClosedAt,
  427. Labels: labels,
  428. Merged: merged,
  429. MergeCommitSHA: mergeCommitSHA,
  430. MergedTime: pr.MergedAt,
  431. IsLocked: pr.ActiveLockReason != nil,
  432. Head: base.PullRequestBranch{
  433. Ref: headRef,
  434. SHA: headSHA,
  435. RepoName: headRepoName,
  436. OwnerName: headUserName,
  437. CloneURL: cloneURL,
  438. },
  439. Base: base.PullRequestBranch{
  440. Ref: *pr.Base.Ref,
  441. SHA: *pr.Base.SHA,
  442. RepoName: *pr.Base.Repo.Name,
  443. OwnerName: *pr.Base.User.Login,
  444. },
  445. PatchURL: *pr.PatchURL,
  446. })
  447. }
  448. return allPRs, nil
  449. }