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

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