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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  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. CloneURL: gr.GetCloneURL(),
  100. }, nil
  101. }
  102. // GetMilestones returns milestones
  103. func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) {
  104. var perPage = 100
  105. var milestones = make([]*base.Milestone, 0, perPage)
  106. for i := 1; ; i++ {
  107. ms, _, err := g.client.Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName,
  108. &github.MilestoneListOptions{
  109. State: "all",
  110. ListOptions: github.ListOptions{
  111. Page: i,
  112. PerPage: perPage,
  113. }})
  114. if err != nil {
  115. return nil, err
  116. }
  117. for _, m := range ms {
  118. var desc string
  119. if m.Description != nil {
  120. desc = *m.Description
  121. }
  122. var state = "open"
  123. if m.State != nil {
  124. state = *m.State
  125. }
  126. milestones = append(milestones, &base.Milestone{
  127. Title: *m.Title,
  128. Description: desc,
  129. Deadline: m.DueOn,
  130. State: state,
  131. Created: *m.CreatedAt,
  132. Updated: m.UpdatedAt,
  133. Closed: m.ClosedAt,
  134. })
  135. }
  136. if len(ms) < perPage {
  137. break
  138. }
  139. }
  140. return milestones, nil
  141. }
  142. func convertGithubLabel(label *github.Label) *base.Label {
  143. var desc string
  144. if label.Description != nil {
  145. desc = *label.Description
  146. }
  147. return &base.Label{
  148. Name: *label.Name,
  149. Color: *label.Color,
  150. Description: desc,
  151. }
  152. }
  153. // GetLabels returns labels
  154. func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
  155. var perPage = 100
  156. var labels = make([]*base.Label, 0, perPage)
  157. for i := 1; ; i++ {
  158. ls, _, err := g.client.Issues.ListLabels(g.ctx, g.repoOwner, g.repoName,
  159. &github.ListOptions{
  160. Page: i,
  161. PerPage: perPage,
  162. })
  163. if err != nil {
  164. return nil, err
  165. }
  166. for _, label := range ls {
  167. labels = append(labels, convertGithubLabel(label))
  168. }
  169. if len(ls) < perPage {
  170. break
  171. }
  172. }
  173. return labels, nil
  174. }
  175. func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release {
  176. var (
  177. name string
  178. desc string
  179. )
  180. if rel.Body != nil {
  181. desc = *rel.Body
  182. }
  183. if rel.Name != nil {
  184. name = *rel.Name
  185. }
  186. r := &base.Release{
  187. TagName: *rel.TagName,
  188. TargetCommitish: *rel.TargetCommitish,
  189. Name: name,
  190. Body: desc,
  191. Draft: *rel.Draft,
  192. Prerelease: *rel.Prerelease,
  193. Created: rel.CreatedAt.Time,
  194. Published: rel.PublishedAt.Time,
  195. }
  196. for _, asset := range rel.Assets {
  197. u, _ := url.Parse(*asset.BrowserDownloadURL)
  198. u.User = url.UserPassword(g.userName, g.password)
  199. r.Assets = append(r.Assets, base.ReleaseAsset{
  200. URL: u.String(),
  201. Name: *asset.Name,
  202. ContentType: asset.ContentType,
  203. Size: asset.Size,
  204. DownloadCount: asset.DownloadCount,
  205. Created: asset.CreatedAt.Time,
  206. Updated: asset.UpdatedAt.Time,
  207. })
  208. }
  209. return r
  210. }
  211. // GetReleases returns releases
  212. func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
  213. var perPage = 100
  214. var releases = make([]*base.Release, 0, perPage)
  215. for i := 1; ; i++ {
  216. ls, _, err := g.client.Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName,
  217. &github.ListOptions{
  218. Page: i,
  219. PerPage: perPage,
  220. })
  221. if err != nil {
  222. return nil, err
  223. }
  224. for _, release := range ls {
  225. releases = append(releases, g.convertGithubRelease(release))
  226. }
  227. if len(ls) < perPage {
  228. break
  229. }
  230. }
  231. return releases, nil
  232. }
  233. func convertGithubReactions(reactions *github.Reactions) *base.Reactions {
  234. return &base.Reactions{
  235. TotalCount: *reactions.TotalCount,
  236. PlusOne: *reactions.PlusOne,
  237. MinusOne: *reactions.MinusOne,
  238. Laugh: *reactions.Laugh,
  239. Confused: *reactions.Confused,
  240. Heart: *reactions.Heart,
  241. Hooray: *reactions.Hooray,
  242. }
  243. }
  244. // GetIssues returns issues according start and limit
  245. func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
  246. opt := &github.IssueListByRepoOptions{
  247. Sort: "created",
  248. Direction: "asc",
  249. State: "all",
  250. ListOptions: github.ListOptions{
  251. PerPage: perPage,
  252. Page: page,
  253. },
  254. }
  255. var allIssues = make([]*base.Issue, 0, perPage)
  256. issues, _, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt)
  257. if err != nil {
  258. return nil, false, fmt.Errorf("error while listing repos: %v", err)
  259. }
  260. for _, issue := range issues {
  261. if issue.IsPullRequest() {
  262. continue
  263. }
  264. var body string
  265. if issue.Body != nil {
  266. body = *issue.Body
  267. }
  268. var milestone string
  269. if issue.Milestone != nil {
  270. milestone = *issue.Milestone.Title
  271. }
  272. var labels = make([]*base.Label, 0, len(issue.Labels))
  273. for _, l := range issue.Labels {
  274. labels = append(labels, convertGithubLabel(&l))
  275. }
  276. var reactions *base.Reactions
  277. if issue.Reactions != nil {
  278. reactions = convertGithubReactions(issue.Reactions)
  279. }
  280. var email string
  281. if issue.User.Email != nil {
  282. email = *issue.User.Email
  283. }
  284. allIssues = append(allIssues, &base.Issue{
  285. Title: *issue.Title,
  286. Number: int64(*issue.Number),
  287. PosterName: *issue.User.Login,
  288. PosterEmail: email,
  289. Content: body,
  290. Milestone: milestone,
  291. State: *issue.State,
  292. Created: *issue.CreatedAt,
  293. Labels: labels,
  294. Reactions: reactions,
  295. Closed: issue.ClosedAt,
  296. IsLocked: *issue.Locked,
  297. })
  298. }
  299. return allIssues, len(issues) < perPage, nil
  300. }
  301. // GetComments returns comments according issueNumber
  302. func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, error) {
  303. var allComments = make([]*base.Comment, 0, 100)
  304. opt := &github.IssueListCommentsOptions{
  305. Sort: "created",
  306. Direction: "asc",
  307. ListOptions: github.ListOptions{
  308. PerPage: 100,
  309. },
  310. }
  311. for {
  312. comments, resp, err := g.client.Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(issueNumber), opt)
  313. if err != nil {
  314. return nil, fmt.Errorf("error while listing repos: %v", err)
  315. }
  316. for _, comment := range comments {
  317. var email string
  318. if comment.User.Email != nil {
  319. email = *comment.User.Email
  320. }
  321. var reactions *base.Reactions
  322. if comment.Reactions != nil {
  323. reactions = convertGithubReactions(comment.Reactions)
  324. }
  325. allComments = append(allComments, &base.Comment{
  326. IssueIndex: issueNumber,
  327. PosterName: *comment.User.Login,
  328. PosterEmail: email,
  329. Content: *comment.Body,
  330. Created: *comment.CreatedAt,
  331. Reactions: reactions,
  332. })
  333. }
  334. if resp.NextPage == 0 {
  335. break
  336. }
  337. opt.Page = resp.NextPage
  338. }
  339. return allComments, nil
  340. }
  341. // GetPullRequests returns pull requests according page and perPage
  342. func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, error) {
  343. opt := &github.PullRequestListOptions{
  344. Sort: "created",
  345. Direction: "asc",
  346. State: "all",
  347. ListOptions: github.ListOptions{
  348. PerPage: perPage,
  349. Page: page,
  350. },
  351. }
  352. var allPRs = make([]*base.PullRequest, 0, perPage)
  353. prs, _, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
  354. if err != nil {
  355. return nil, fmt.Errorf("error while listing repos: %v", err)
  356. }
  357. for _, pr := range prs {
  358. var body string
  359. if pr.Body != nil {
  360. body = *pr.Body
  361. }
  362. var milestone string
  363. if pr.Milestone != nil {
  364. milestone = *pr.Milestone.Title
  365. }
  366. var labels = make([]*base.Label, 0, len(pr.Labels))
  367. for _, l := range pr.Labels {
  368. labels = append(labels, convertGithubLabel(l))
  369. }
  370. // FIXME: This API missing reactions, we may need another extra request to get reactions
  371. var email string
  372. if pr.User.Email != nil {
  373. email = *pr.User.Email
  374. }
  375. var merged bool
  376. // pr.Merged is not valid, so use MergedAt to test if it's merged
  377. if pr.MergedAt != nil {
  378. merged = true
  379. }
  380. var (
  381. headRepoName string
  382. cloneURL string
  383. headRef string
  384. headSHA string
  385. )
  386. if pr.Head.Repo != nil {
  387. if pr.Head.Repo.Name != nil {
  388. headRepoName = *pr.Head.Repo.Name
  389. }
  390. if pr.Head.Repo.CloneURL != nil {
  391. cloneURL = *pr.Head.Repo.CloneURL
  392. }
  393. }
  394. if pr.Head.Ref != nil {
  395. headRef = *pr.Head.Ref
  396. }
  397. if pr.Head.SHA != nil {
  398. headSHA = *pr.Head.SHA
  399. }
  400. var mergeCommitSHA string
  401. if pr.MergeCommitSHA != nil {
  402. mergeCommitSHA = *pr.MergeCommitSHA
  403. }
  404. var headUserName string
  405. if pr.Head.User != nil && pr.Head.User.Login != nil {
  406. headUserName = *pr.Head.User.Login
  407. }
  408. allPRs = append(allPRs, &base.PullRequest{
  409. Title: *pr.Title,
  410. Number: int64(*pr.Number),
  411. PosterName: *pr.User.Login,
  412. PosterEmail: email,
  413. Content: body,
  414. Milestone: milestone,
  415. State: *pr.State,
  416. Created: *pr.CreatedAt,
  417. Closed: pr.ClosedAt,
  418. Labels: labels,
  419. Merged: merged,
  420. MergeCommitSHA: mergeCommitSHA,
  421. MergedTime: pr.MergedAt,
  422. IsLocked: pr.ActiveLockReason != nil,
  423. Head: base.PullRequestBranch{
  424. Ref: headRef,
  425. SHA: headSHA,
  426. RepoName: headRepoName,
  427. OwnerName: headUserName,
  428. CloneURL: cloneURL,
  429. },
  430. Base: base.PullRequestBranch{
  431. Ref: *pr.Base.Ref,
  432. SHA: *pr.Base.SHA,
  433. RepoName: *pr.Base.Repo.Name,
  434. OwnerName: *pr.Base.User.Login,
  435. },
  436. PatchURL: *pr.PatchURL,
  437. })
  438. }
  439. return allPRs, nil
  440. }