gitea源码

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