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.

onedev.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package migrations
  4. import (
  5. "context"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "code.gitea.io/gitea/modules/json"
  13. "code.gitea.io/gitea/modules/log"
  14. base "code.gitea.io/gitea/modules/migration"
  15. "code.gitea.io/gitea/modules/structs"
  16. )
  17. var (
  18. _ base.Downloader = &OneDevDownloader{}
  19. _ base.DownloaderFactory = &OneDevDownloaderFactory{}
  20. )
  21. func init() {
  22. RegisterDownloaderFactory(&OneDevDownloaderFactory{})
  23. }
  24. // OneDevDownloaderFactory defines a downloader factory
  25. type OneDevDownloaderFactory struct{}
  26. // New returns a downloader related to this factory according MigrateOptions
  27. func (f *OneDevDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
  28. u, err := url.Parse(opts.CloneAddr)
  29. if err != nil {
  30. return nil, err
  31. }
  32. var repoName string
  33. fields := strings.Split(strings.Trim(u.Path, "/"), "/")
  34. if len(fields) == 2 && fields[0] == "projects" {
  35. repoName = fields[1]
  36. } else if len(fields) == 1 {
  37. repoName = fields[0]
  38. } else {
  39. return nil, fmt.Errorf("invalid path: %s", u.Path)
  40. }
  41. u.Path = ""
  42. u.Fragment = ""
  43. log.Trace("Create onedev downloader. BaseURL: %v RepoName: %s", u, repoName)
  44. return NewOneDevDownloader(ctx, u, opts.AuthUsername, opts.AuthPassword, repoName), nil
  45. }
  46. // GitServiceType returns the type of git service
  47. func (f *OneDevDownloaderFactory) GitServiceType() structs.GitServiceType {
  48. return structs.OneDevService
  49. }
  50. type onedevUser struct {
  51. ID int64 `json:"id"`
  52. Name string `json:"name"`
  53. Email string `json:"email"`
  54. }
  55. // OneDevDownloader implements a Downloader interface to get repository information
  56. // from OneDev
  57. type OneDevDownloader struct {
  58. base.NullDownloader
  59. ctx context.Context
  60. client *http.Client
  61. baseURL *url.URL
  62. repoName string
  63. repoID int64
  64. maxIssueIndex int64
  65. userMap map[int64]*onedevUser
  66. milestoneMap map[int64]string
  67. }
  68. // SetContext set context
  69. func (d *OneDevDownloader) SetContext(ctx context.Context) {
  70. d.ctx = ctx
  71. }
  72. // NewOneDevDownloader creates a new downloader
  73. func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, password, repoName string) *OneDevDownloader {
  74. downloader := &OneDevDownloader{
  75. ctx: ctx,
  76. baseURL: baseURL,
  77. repoName: repoName,
  78. client: &http.Client{
  79. Transport: &http.Transport{
  80. Proxy: func(req *http.Request) (*url.URL, error) {
  81. if len(username) > 0 && len(password) > 0 {
  82. req.SetBasicAuth(username, password)
  83. }
  84. return nil, nil
  85. },
  86. },
  87. },
  88. userMap: make(map[int64]*onedevUser),
  89. milestoneMap: make(map[int64]string),
  90. }
  91. return downloader
  92. }
  93. // String implements Stringer
  94. func (d *OneDevDownloader) String() string {
  95. return fmt.Sprintf("migration from oneDev server %s [%d]/%s", d.baseURL, d.repoID, d.repoName)
  96. }
  97. // ColorFormat provides a basic color format for a OneDevDownloader
  98. func (d *OneDevDownloader) ColorFormat(s fmt.State) {
  99. if d == nil {
  100. log.ColorFprintf(s, "<nil: OneDevDownloader>")
  101. return
  102. }
  103. log.ColorFprintf(s, "migration from oneDev server %s [%d]/%s", d.baseURL, d.repoID, d.repoName)
  104. }
  105. func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result interface{}) error {
  106. u, err := d.baseURL.Parse(endpoint)
  107. if err != nil {
  108. return err
  109. }
  110. if parameter != nil {
  111. query := u.Query()
  112. for k, v := range parameter {
  113. query.Set(k, v)
  114. }
  115. u.RawQuery = query.Encode()
  116. }
  117. req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil)
  118. if err != nil {
  119. return err
  120. }
  121. resp, err := d.client.Do(req)
  122. if err != nil {
  123. return err
  124. }
  125. defer resp.Body.Close()
  126. decoder := json.NewDecoder(resp.Body)
  127. return decoder.Decode(&result)
  128. }
  129. // GetRepoInfo returns repository information
  130. func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) {
  131. info := make([]struct {
  132. ID int64 `json:"id"`
  133. Name string `json:"name"`
  134. Description string `json:"description"`
  135. }, 0, 1)
  136. err := d.callAPI(
  137. "/api/projects",
  138. map[string]string{
  139. "query": `"Name" is "` + d.repoName + `"`,
  140. "offset": "0",
  141. "count": "1",
  142. },
  143. &info,
  144. )
  145. if err != nil {
  146. return nil, err
  147. }
  148. if len(info) != 1 {
  149. return nil, fmt.Errorf("Project %s not found", d.repoName)
  150. }
  151. d.repoID = info[0].ID
  152. cloneURL, err := d.baseURL.Parse(info[0].Name)
  153. if err != nil {
  154. return nil, err
  155. }
  156. originalURL, err := d.baseURL.Parse("/projects/" + info[0].Name)
  157. if err != nil {
  158. return nil, err
  159. }
  160. return &base.Repository{
  161. Name: info[0].Name,
  162. Description: info[0].Description,
  163. CloneURL: cloneURL.String(),
  164. OriginalURL: originalURL.String(),
  165. }, nil
  166. }
  167. // GetMilestones returns milestones
  168. func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) {
  169. rawMilestones := make([]struct {
  170. ID int64 `json:"id"`
  171. Name string `json:"name"`
  172. Description string `json:"description"`
  173. DueDate *time.Time `json:"dueDate"`
  174. Closed bool `json:"closed"`
  175. }, 0, 100)
  176. endpoint := fmt.Sprintf("/api/projects/%d/milestones", d.repoID)
  177. milestones := make([]*base.Milestone, 0, 100)
  178. offset := 0
  179. for {
  180. err := d.callAPI(
  181. endpoint,
  182. map[string]string{
  183. "offset": strconv.Itoa(offset),
  184. "count": "100",
  185. },
  186. &rawMilestones,
  187. )
  188. if err != nil {
  189. return nil, err
  190. }
  191. if len(rawMilestones) == 0 {
  192. break
  193. }
  194. offset += 100
  195. for _, milestone := range rawMilestones {
  196. d.milestoneMap[milestone.ID] = milestone.Name
  197. closed := milestone.DueDate
  198. if !milestone.Closed {
  199. closed = nil
  200. }
  201. milestones = append(milestones, &base.Milestone{
  202. Title: milestone.Name,
  203. Description: milestone.Description,
  204. Deadline: milestone.DueDate,
  205. Closed: closed,
  206. })
  207. }
  208. }
  209. return milestones, nil
  210. }
  211. // GetLabels returns labels
  212. func (d *OneDevDownloader) GetLabels() ([]*base.Label, error) {
  213. return []*base.Label{
  214. {
  215. Name: "Bug",
  216. Color: "f64e60",
  217. },
  218. {
  219. Name: "Build Failure",
  220. Color: "f64e60",
  221. },
  222. {
  223. Name: "Discussion",
  224. Color: "8950fc",
  225. },
  226. {
  227. Name: "Improvement",
  228. Color: "1bc5bd",
  229. },
  230. {
  231. Name: "New Feature",
  232. Color: "1bc5bd",
  233. },
  234. {
  235. Name: "Support Request",
  236. Color: "8950fc",
  237. },
  238. }, nil
  239. }
  240. type onedevIssueContext struct {
  241. IsPullRequest bool
  242. }
  243. // GetIssues returns issues
  244. func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
  245. rawIssues := make([]struct {
  246. ID int64 `json:"id"`
  247. Number int64 `json:"number"`
  248. State string `json:"state"`
  249. Title string `json:"title"`
  250. Description string `json:"description"`
  251. SubmitterID int64 `json:"submitterId"`
  252. SubmitDate time.Time `json:"submitDate"`
  253. }, 0, perPage)
  254. err := d.callAPI(
  255. "/api/issues",
  256. map[string]string{
  257. "query": `"Project" is "` + d.repoName + `"`,
  258. "offset": strconv.Itoa((page - 1) * perPage),
  259. "count": strconv.Itoa(perPage),
  260. },
  261. &rawIssues,
  262. )
  263. if err != nil {
  264. return nil, false, err
  265. }
  266. issues := make([]*base.Issue, 0, len(rawIssues))
  267. for _, issue := range rawIssues {
  268. fields := make([]struct {
  269. Name string `json:"name"`
  270. Value string `json:"value"`
  271. }, 0, 10)
  272. err := d.callAPI(
  273. fmt.Sprintf("/api/issues/%d/fields", issue.ID),
  274. nil,
  275. &fields,
  276. )
  277. if err != nil {
  278. return nil, false, err
  279. }
  280. var label *base.Label
  281. for _, field := range fields {
  282. if field.Name == "Type" {
  283. label = &base.Label{Name: field.Value}
  284. break
  285. }
  286. }
  287. milestones := make([]struct {
  288. ID int64 `json:"id"`
  289. Name string `json:"name"`
  290. }, 0, 10)
  291. err = d.callAPI(
  292. fmt.Sprintf("/api/issues/%d/milestones", issue.ID),
  293. nil,
  294. &milestones,
  295. )
  296. if err != nil {
  297. return nil, false, err
  298. }
  299. milestoneID := int64(0)
  300. if len(milestones) > 0 {
  301. milestoneID = milestones[0].ID
  302. }
  303. state := strings.ToLower(issue.State)
  304. if state == "released" {
  305. state = "closed"
  306. }
  307. poster := d.tryGetUser(issue.SubmitterID)
  308. issues = append(issues, &base.Issue{
  309. Title: issue.Title,
  310. Number: issue.Number,
  311. PosterName: poster.Name,
  312. PosterEmail: poster.Email,
  313. Content: issue.Description,
  314. Milestone: d.milestoneMap[milestoneID],
  315. State: state,
  316. Created: issue.SubmitDate,
  317. Updated: issue.SubmitDate,
  318. Labels: []*base.Label{label},
  319. ForeignIndex: issue.ID,
  320. Context: onedevIssueContext{IsPullRequest: false},
  321. })
  322. if d.maxIssueIndex < issue.Number {
  323. d.maxIssueIndex = issue.Number
  324. }
  325. }
  326. return issues, len(issues) == 0, nil
  327. }
  328. // GetComments returns comments
  329. func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
  330. context, ok := commentable.GetContext().(onedevIssueContext)
  331. if !ok {
  332. return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
  333. }
  334. rawComments := make([]struct {
  335. ID int64 `json:"id"`
  336. Date time.Time `json:"date"`
  337. UserID int64 `json:"userId"`
  338. Content string `json:"content"`
  339. }, 0, 100)
  340. var endpoint string
  341. if context.IsPullRequest {
  342. endpoint = fmt.Sprintf("/api/pull-requests/%d/comments", commentable.GetForeignIndex())
  343. } else {
  344. endpoint = fmt.Sprintf("/api/issues/%d/comments", commentable.GetForeignIndex())
  345. }
  346. err := d.callAPI(
  347. endpoint,
  348. nil,
  349. &rawComments,
  350. )
  351. if err != nil {
  352. return nil, false, err
  353. }
  354. rawChanges := make([]struct {
  355. Date time.Time `json:"date"`
  356. UserID int64 `json:"userId"`
  357. Data map[string]interface{} `json:"data"`
  358. }, 0, 100)
  359. if context.IsPullRequest {
  360. endpoint = fmt.Sprintf("/api/pull-requests/%d/changes", commentable.GetForeignIndex())
  361. } else {
  362. endpoint = fmt.Sprintf("/api/issues/%d/changes", commentable.GetForeignIndex())
  363. }
  364. err = d.callAPI(
  365. endpoint,
  366. nil,
  367. &rawChanges,
  368. )
  369. if err != nil {
  370. return nil, false, err
  371. }
  372. comments := make([]*base.Comment, 0, len(rawComments)+len(rawChanges))
  373. for _, comment := range rawComments {
  374. if len(comment.Content) == 0 {
  375. continue
  376. }
  377. poster := d.tryGetUser(comment.UserID)
  378. comments = append(comments, &base.Comment{
  379. IssueIndex: commentable.GetLocalIndex(),
  380. Index: comment.ID,
  381. PosterID: poster.ID,
  382. PosterName: poster.Name,
  383. PosterEmail: poster.Email,
  384. Content: comment.Content,
  385. Created: comment.Date,
  386. Updated: comment.Date,
  387. })
  388. }
  389. for _, change := range rawChanges {
  390. contentV, ok := change.Data["content"]
  391. if !ok {
  392. contentV, ok = change.Data["comment"]
  393. if !ok {
  394. continue
  395. }
  396. }
  397. content, ok := contentV.(string)
  398. if !ok || len(content) == 0 {
  399. continue
  400. }
  401. poster := d.tryGetUser(change.UserID)
  402. comments = append(comments, &base.Comment{
  403. IssueIndex: commentable.GetLocalIndex(),
  404. PosterID: poster.ID,
  405. PosterName: poster.Name,
  406. PosterEmail: poster.Email,
  407. Content: content,
  408. Created: change.Date,
  409. Updated: change.Date,
  410. })
  411. }
  412. return comments, true, nil
  413. }
  414. // GetPullRequests returns pull requests
  415. func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
  416. rawPullRequests := make([]struct {
  417. ID int64 `json:"id"`
  418. Number int64 `json:"number"`
  419. Title string `json:"title"`
  420. SubmitterID int64 `json:"submitterId"`
  421. SubmitDate time.Time `json:"submitDate"`
  422. Description string `json:"description"`
  423. TargetBranch string `json:"targetBranch"`
  424. SourceBranch string `json:"sourceBranch"`
  425. BaseCommitHash string `json:"baseCommitHash"`
  426. CloseInfo *struct {
  427. Date *time.Time `json:"date"`
  428. Status string `json:"status"`
  429. }
  430. }, 0, perPage)
  431. err := d.callAPI(
  432. "/api/pull-requests",
  433. map[string]string{
  434. "query": `"Target Project" is "` + d.repoName + `"`,
  435. "offset": strconv.Itoa((page - 1) * perPage),
  436. "count": strconv.Itoa(perPage),
  437. },
  438. &rawPullRequests,
  439. )
  440. if err != nil {
  441. return nil, false, err
  442. }
  443. pullRequests := make([]*base.PullRequest, 0, len(rawPullRequests))
  444. for _, pr := range rawPullRequests {
  445. var mergePreview struct {
  446. TargetHeadCommitHash string `json:"targetHeadCommitHash"`
  447. HeadCommitHash string `json:"headCommitHash"`
  448. MergeStrategy string `json:"mergeStrategy"`
  449. MergeCommitHash string `json:"mergeCommitHash"`
  450. }
  451. err := d.callAPI(
  452. fmt.Sprintf("/api/pull-requests/%d/merge-preview", pr.ID),
  453. nil,
  454. &mergePreview,
  455. )
  456. if err != nil {
  457. return nil, false, err
  458. }
  459. state := "open"
  460. merged := false
  461. var closeTime *time.Time
  462. var mergedTime *time.Time
  463. if pr.CloseInfo != nil {
  464. state = "closed"
  465. closeTime = pr.CloseInfo.Date
  466. if pr.CloseInfo.Status == "MERGED" { // "DISCARDED"
  467. merged = true
  468. mergedTime = pr.CloseInfo.Date
  469. }
  470. }
  471. poster := d.tryGetUser(pr.SubmitterID)
  472. number := pr.Number + d.maxIssueIndex
  473. pullRequests = append(pullRequests, &base.PullRequest{
  474. Title: pr.Title,
  475. Number: number,
  476. PosterName: poster.Name,
  477. PosterID: poster.ID,
  478. Content: pr.Description,
  479. State: state,
  480. Created: pr.SubmitDate,
  481. Updated: pr.SubmitDate,
  482. Closed: closeTime,
  483. Merged: merged,
  484. MergedTime: mergedTime,
  485. Head: base.PullRequestBranch{
  486. Ref: pr.SourceBranch,
  487. SHA: mergePreview.HeadCommitHash,
  488. RepoName: d.repoName,
  489. },
  490. Base: base.PullRequestBranch{
  491. Ref: pr.TargetBranch,
  492. SHA: mergePreview.TargetHeadCommitHash,
  493. RepoName: d.repoName,
  494. },
  495. ForeignIndex: pr.ID,
  496. Context: onedevIssueContext{IsPullRequest: true},
  497. })
  498. // SECURITY: Ensure that the PR is safe
  499. _ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
  500. }
  501. return pullRequests, len(pullRequests) == 0, nil
  502. }
  503. // GetReviews returns pull requests reviews
  504. func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
  505. rawReviews := make([]struct {
  506. ID int64 `json:"id"`
  507. UserID int64 `json:"userId"`
  508. Result *struct {
  509. Commit string `json:"commit"`
  510. Approved bool `json:"approved"`
  511. Comment string `json:"comment"`
  512. }
  513. }, 0, 100)
  514. err := d.callAPI(
  515. fmt.Sprintf("/api/pull-requests/%d/reviews", reviewable.GetForeignIndex()),
  516. nil,
  517. &rawReviews,
  518. )
  519. if err != nil {
  520. return nil, err
  521. }
  522. reviews := make([]*base.Review, 0, len(rawReviews))
  523. for _, review := range rawReviews {
  524. state := base.ReviewStatePending
  525. content := ""
  526. if review.Result != nil {
  527. if len(review.Result.Comment) > 0 {
  528. state = base.ReviewStateCommented
  529. content = review.Result.Comment
  530. }
  531. if review.Result.Approved {
  532. state = base.ReviewStateApproved
  533. }
  534. }
  535. poster := d.tryGetUser(review.UserID)
  536. reviews = append(reviews, &base.Review{
  537. IssueIndex: reviewable.GetLocalIndex(),
  538. ReviewerID: poster.ID,
  539. ReviewerName: poster.Name,
  540. Content: content,
  541. State: state,
  542. })
  543. }
  544. return reviews, nil
  545. }
  546. // GetTopics return repository topics
  547. func (d *OneDevDownloader) GetTopics() ([]string, error) {
  548. return []string{}, nil
  549. }
  550. func (d *OneDevDownloader) tryGetUser(userID int64) *onedevUser {
  551. user, ok := d.userMap[userID]
  552. if !ok {
  553. err := d.callAPI(
  554. fmt.Sprintf("/api/users/%d", userID),
  555. nil,
  556. &user,
  557. )
  558. if err != nil {
  559. user = &onedevUser{
  560. Name: fmt.Sprintf("User %d", userID),
  561. }
  562. }
  563. d.userMap[userID] = user
  564. }
  565. return user
  566. }