gitea源码

codebase.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package migrations
  4. import (
  5. "context"
  6. "encoding/xml"
  7. "fmt"
  8. "net/http"
  9. "net/url"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/modules/log"
  14. base "code.gitea.io/gitea/modules/migration"
  15. "code.gitea.io/gitea/modules/proxy"
  16. "code.gitea.io/gitea/modules/structs"
  17. )
  18. var (
  19. _ base.Downloader = &CodebaseDownloader{}
  20. _ base.DownloaderFactory = &CodebaseDownloaderFactory{}
  21. )
  22. func init() {
  23. RegisterDownloaderFactory(&CodebaseDownloaderFactory{})
  24. }
  25. // CodebaseDownloaderFactory defines a downloader factory
  26. type CodebaseDownloaderFactory struct{}
  27. // New returns a downloader related to this factory according MigrateOptions
  28. func (f *CodebaseDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
  29. u, err := url.Parse(opts.CloneAddr)
  30. if err != nil {
  31. return nil, err
  32. }
  33. u.User = nil
  34. fields := strings.Split(strings.Trim(u.Path, "/"), "/")
  35. if len(fields) != 2 {
  36. return nil, fmt.Errorf("invalid path: %s", u.Path)
  37. }
  38. project := fields[0]
  39. repoName := strings.TrimSuffix(fields[1], ".git")
  40. log.Trace("Create Codebase downloader. BaseURL: %v RepoName: %s", u, repoName)
  41. return NewCodebaseDownloader(ctx, u, project, repoName, opts.AuthUsername, opts.AuthPassword), nil
  42. }
  43. // GitServiceType returns the type of git service
  44. func (f *CodebaseDownloaderFactory) GitServiceType() structs.GitServiceType {
  45. return structs.CodebaseService
  46. }
  47. type codebaseUser struct {
  48. ID int64 `json:"id"`
  49. Name string `json:"name"`
  50. Email string `json:"email"`
  51. }
  52. // CodebaseDownloader implements a Downloader interface to get repository information
  53. // from Codebase
  54. type CodebaseDownloader struct {
  55. base.NullDownloader
  56. client *http.Client
  57. baseURL *url.URL
  58. projectURL *url.URL
  59. project string
  60. repoName string
  61. maxIssueIndex int64
  62. userMap map[int64]*codebaseUser
  63. commitMap map[string]string
  64. }
  65. // NewCodebaseDownloader creates a new downloader
  66. func NewCodebaseDownloader(_ context.Context, projectURL *url.URL, project, repoName, username, password string) *CodebaseDownloader {
  67. baseURL, _ := url.Parse("https://api3.codebasehq.com")
  68. downloader := &CodebaseDownloader{
  69. baseURL: baseURL,
  70. projectURL: projectURL,
  71. project: project,
  72. repoName: repoName,
  73. client: &http.Client{
  74. Transport: &http.Transport{
  75. Proxy: func(req *http.Request) (*url.URL, error) {
  76. if len(username) > 0 && len(password) > 0 {
  77. req.SetBasicAuth(username, password)
  78. }
  79. return proxy.Proxy()(req)
  80. },
  81. },
  82. },
  83. userMap: make(map[int64]*codebaseUser),
  84. commitMap: make(map[string]string),
  85. }
  86. log.Trace("Create Codebase downloader. BaseURL: %s Project: %s RepoName: %s", baseURL, project, repoName)
  87. return downloader
  88. }
  89. // String implements Stringer
  90. func (d *CodebaseDownloader) String() string {
  91. return fmt.Sprintf("migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName)
  92. }
  93. func (d *CodebaseDownloader) LogString() string {
  94. if d == nil {
  95. return "<CodebaseDownloader nil>"
  96. }
  97. return fmt.Sprintf("<CodebaseDownloader %s %s/%s>", d.baseURL, d.project, d.repoName)
  98. }
  99. // FormatCloneURL add authentication into remote URLs
  100. func (d *CodebaseDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr string) (string, error) {
  101. return opts.CloneAddr, nil
  102. }
  103. func (d *CodebaseDownloader) callAPI(ctx context.Context, endpoint string, parameter map[string]string, result any) error {
  104. u, err := d.baseURL.Parse(endpoint)
  105. if err != nil {
  106. return err
  107. }
  108. if parameter != nil {
  109. query := u.Query()
  110. for k, v := range parameter {
  111. query.Set(k, v)
  112. }
  113. u.RawQuery = query.Encode()
  114. }
  115. req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
  116. if err != nil {
  117. return err
  118. }
  119. req.Header.Add("Accept", "application/xml")
  120. resp, err := d.client.Do(req)
  121. if err != nil {
  122. return err
  123. }
  124. defer resp.Body.Close()
  125. return xml.NewDecoder(resp.Body).Decode(&result)
  126. }
  127. // GetRepoInfo returns repository information
  128. // https://support.codebasehq.com/kb/projects
  129. func (d *CodebaseDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) {
  130. var rawRepository struct {
  131. XMLName xml.Name `xml:"repository"`
  132. Name string `xml:"name"`
  133. Description string `xml:"description"`
  134. Permalink string `xml:"permalink"`
  135. CloneURL string `xml:"clone-url"`
  136. Source string `xml:"source"`
  137. }
  138. err := d.callAPI(
  139. ctx,
  140. fmt.Sprintf("/%s/%s", d.project, d.repoName),
  141. nil,
  142. &rawRepository,
  143. )
  144. if err != nil {
  145. return nil, err
  146. }
  147. return &base.Repository{
  148. Name: rawRepository.Name,
  149. Description: rawRepository.Description,
  150. CloneURL: rawRepository.CloneURL,
  151. OriginalURL: d.projectURL.String(),
  152. }, nil
  153. }
  154. // GetMilestones returns milestones
  155. // https://support.codebasehq.com/kb/tickets-and-milestones/milestones
  156. func (d *CodebaseDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) {
  157. var rawMilestones struct {
  158. XMLName xml.Name `xml:"ticketing-milestone"`
  159. Type string `xml:"type,attr"`
  160. TicketingMilestone []struct {
  161. Text string `xml:",chardata"`
  162. ID struct {
  163. Value int64 `xml:",chardata"`
  164. Type string `xml:"type,attr"`
  165. } `xml:"id"`
  166. Identifier string `xml:"identifier"`
  167. Name string `xml:"name"`
  168. Deadline struct {
  169. Value string `xml:",chardata"`
  170. Type string `xml:"type,attr"`
  171. } `xml:"deadline"`
  172. Description string `xml:"description"`
  173. Status string `xml:"status"`
  174. } `xml:"ticketing-milestone"`
  175. }
  176. err := d.callAPI(
  177. ctx,
  178. fmt.Sprintf("/%s/milestones", d.project),
  179. nil,
  180. &rawMilestones,
  181. )
  182. if err != nil {
  183. return nil, err
  184. }
  185. milestones := make([]*base.Milestone, 0, len(rawMilestones.TicketingMilestone))
  186. for _, milestone := range rawMilestones.TicketingMilestone {
  187. var deadline *time.Time
  188. if len(milestone.Deadline.Value) > 0 {
  189. if val, err := time.Parse("2006-01-02", milestone.Deadline.Value); err == nil {
  190. deadline = &val
  191. }
  192. }
  193. closed := deadline
  194. state := "closed"
  195. if milestone.Status == "active" {
  196. closed = nil
  197. state = ""
  198. }
  199. milestones = append(milestones, &base.Milestone{
  200. Title: milestone.Name,
  201. Deadline: deadline,
  202. Closed: closed,
  203. State: state,
  204. })
  205. }
  206. return milestones, nil
  207. }
  208. // GetLabels returns labels
  209. // https://support.codebasehq.com/kb/tickets-and-milestones/statuses-priorities-and-categories
  210. func (d *CodebaseDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) {
  211. var rawTypes struct {
  212. XMLName xml.Name `xml:"ticketing-types"`
  213. Type string `xml:"type,attr"`
  214. TicketingType []struct {
  215. ID struct {
  216. Value int64 `xml:",chardata"`
  217. Type string `xml:"type,attr"`
  218. } `xml:"id"`
  219. Name string `xml:"name"`
  220. } `xml:"ticketing-type"`
  221. }
  222. err := d.callAPI(
  223. ctx,
  224. fmt.Sprintf("/%s/tickets/types", d.project),
  225. nil,
  226. &rawTypes,
  227. )
  228. if err != nil {
  229. return nil, err
  230. }
  231. labels := make([]*base.Label, 0, len(rawTypes.TicketingType))
  232. for _, label := range rawTypes.TicketingType {
  233. labels = append(labels, &base.Label{
  234. Name: label.Name,
  235. Color: "ffffff",
  236. })
  237. }
  238. return labels, nil
  239. }
  240. type codebaseIssueContext struct {
  241. Comments []*base.Comment
  242. }
  243. // GetIssues returns issues, limits are not supported
  244. // https://support.codebasehq.com/kb/tickets-and-milestones
  245. // https://support.codebasehq.com/kb/tickets-and-milestones/updating-tickets
  246. func (d *CodebaseDownloader) GetIssues(ctx context.Context, _, _ int) ([]*base.Issue, bool, error) {
  247. var rawIssues struct {
  248. XMLName xml.Name `xml:"tickets"`
  249. Type string `xml:"type,attr"`
  250. Ticket []struct {
  251. TicketID struct {
  252. Value int64 `xml:",chardata"`
  253. Type string `xml:"type,attr"`
  254. } `xml:"ticket-id"`
  255. Summary string `xml:"summary"`
  256. TicketType string `xml:"ticket-type"`
  257. ReporterID struct {
  258. Value int64 `xml:",chardata"`
  259. Type string `xml:"type,attr"`
  260. } `xml:"reporter-id"`
  261. Reporter string `xml:"reporter"`
  262. Type struct {
  263. Name string `xml:"name"`
  264. } `xml:"type"`
  265. Status struct {
  266. TreatAsClosed struct {
  267. Value bool `xml:",chardata"`
  268. Type string `xml:"type,attr"`
  269. } `xml:"treat-as-closed"`
  270. } `xml:"status"`
  271. Milestone struct {
  272. Name string `xml:"name"`
  273. } `xml:"milestone"`
  274. UpdatedAt struct {
  275. Value time.Time `xml:",chardata"`
  276. Type string `xml:"type,attr"`
  277. } `xml:"updated-at"`
  278. CreatedAt struct {
  279. Value time.Time `xml:",chardata"`
  280. Type string `xml:"type,attr"`
  281. } `xml:"created-at"`
  282. } `xml:"ticket"`
  283. }
  284. err := d.callAPI(
  285. ctx,
  286. fmt.Sprintf("/%s/tickets", d.project),
  287. nil,
  288. &rawIssues,
  289. )
  290. if err != nil {
  291. return nil, false, err
  292. }
  293. issues := make([]*base.Issue, 0, len(rawIssues.Ticket))
  294. for _, issue := range rawIssues.Ticket {
  295. var notes struct {
  296. XMLName xml.Name `xml:"ticket-notes"`
  297. Type string `xml:"type,attr"`
  298. TicketNote []struct {
  299. Content string `xml:"content"`
  300. CreatedAt struct {
  301. Value time.Time `xml:",chardata"`
  302. Type string `xml:"type,attr"`
  303. } `xml:"created-at"`
  304. UpdatedAt struct {
  305. Value time.Time `xml:",chardata"`
  306. Type string `xml:"type,attr"`
  307. } `xml:"updated-at"`
  308. ID struct {
  309. Value int64 `xml:",chardata"`
  310. Type string `xml:"type,attr"`
  311. } `xml:"id"`
  312. UserID struct {
  313. Value int64 `xml:",chardata"`
  314. Type string `xml:"type,attr"`
  315. } `xml:"user-id"`
  316. } `xml:"ticket-note"`
  317. }
  318. err := d.callAPI(
  319. ctx,
  320. fmt.Sprintf("/%s/tickets/%d/notes", d.project, issue.TicketID.Value),
  321. nil,
  322. &notes,
  323. )
  324. if err != nil {
  325. return nil, false, err
  326. }
  327. comments := make([]*base.Comment, 0, len(notes.TicketNote))
  328. for _, note := range notes.TicketNote {
  329. if len(note.Content) == 0 {
  330. continue
  331. }
  332. poster := d.tryGetUser(ctx, note.UserID.Value)
  333. comments = append(comments, &base.Comment{
  334. IssueIndex: issue.TicketID.Value,
  335. Index: note.ID.Value,
  336. PosterID: poster.ID,
  337. PosterName: poster.Name,
  338. PosterEmail: poster.Email,
  339. Content: note.Content,
  340. Created: note.CreatedAt.Value,
  341. Updated: note.UpdatedAt.Value,
  342. })
  343. }
  344. if len(comments) == 0 {
  345. comments = append(comments, &base.Comment{})
  346. }
  347. state := "open"
  348. if issue.Status.TreatAsClosed.Value {
  349. state = "closed"
  350. }
  351. poster := d.tryGetUser(ctx, issue.ReporterID.Value)
  352. issues = append(issues, &base.Issue{
  353. Title: issue.Summary,
  354. Number: issue.TicketID.Value,
  355. PosterName: poster.Name,
  356. PosterEmail: poster.Email,
  357. Content: comments[0].Content,
  358. Milestone: issue.Milestone.Name,
  359. State: state,
  360. Created: issue.CreatedAt.Value,
  361. Updated: issue.UpdatedAt.Value,
  362. Labels: []*base.Label{
  363. {Name: issue.Type.Name},
  364. },
  365. ForeignIndex: issue.TicketID.Value,
  366. Context: codebaseIssueContext{
  367. Comments: comments[1:],
  368. },
  369. })
  370. if d.maxIssueIndex < issue.TicketID.Value {
  371. d.maxIssueIndex = issue.TicketID.Value
  372. }
  373. }
  374. return issues, true, nil
  375. }
  376. // GetComments returns comments
  377. func (d *CodebaseDownloader) GetComments(_ context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
  378. context, ok := commentable.GetContext().(codebaseIssueContext)
  379. if !ok {
  380. return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
  381. }
  382. return context.Comments, true, nil
  383. }
  384. // GetPullRequests returns pull requests
  385. // https://support.codebasehq.com/kb/repositories/merge-requests
  386. func (d *CodebaseDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
  387. var rawMergeRequests struct {
  388. XMLName xml.Name `xml:"merge-requests"`
  389. Type string `xml:"type,attr"`
  390. MergeRequest []struct {
  391. ID struct {
  392. Value int64 `xml:",chardata"`
  393. Type string `xml:"type,attr"`
  394. } `xml:"id"`
  395. } `xml:"merge-request"`
  396. }
  397. err := d.callAPI(
  398. ctx,
  399. fmt.Sprintf("/%s/%s/merge_requests", d.project, d.repoName),
  400. map[string]string{
  401. "query": `"Target Project" is "` + d.repoName + `"`,
  402. "offset": strconv.Itoa((page - 1) * perPage),
  403. "count": strconv.Itoa(perPage),
  404. },
  405. &rawMergeRequests,
  406. )
  407. if err != nil {
  408. return nil, false, err
  409. }
  410. pullRequests := make([]*base.PullRequest, 0, len(rawMergeRequests.MergeRequest))
  411. for i, mr := range rawMergeRequests.MergeRequest {
  412. var rawMergeRequest struct {
  413. XMLName xml.Name `xml:"merge-request"`
  414. ID struct {
  415. Value int64 `xml:",chardata"`
  416. Type string `xml:"type,attr"`
  417. } `xml:"id"`
  418. SourceRef string `xml:"source-ref"` // NOTE: from the documentation these are actually just branches NOT full refs
  419. TargetRef string `xml:"target-ref"` // NOTE: from the documentation these are actually just branches NOT full refs
  420. Subject string `xml:"subject"`
  421. Status string `xml:"status"`
  422. UserID struct {
  423. Value int64 `xml:",chardata"`
  424. Type string `xml:"type,attr"`
  425. } `xml:"user-id"`
  426. CreatedAt struct {
  427. Value time.Time `xml:",chardata"`
  428. Type string `xml:"type,attr"`
  429. } `xml:"created-at"`
  430. UpdatedAt struct {
  431. Value time.Time `xml:",chardata"`
  432. Type string `xml:"type,attr"`
  433. } `xml:"updated-at"`
  434. Comments struct {
  435. Type string `xml:"type,attr"`
  436. Comment []struct {
  437. Content string `xml:"content"`
  438. ID struct {
  439. Value int64 `xml:",chardata"`
  440. Type string `xml:"type,attr"`
  441. } `xml:"id"`
  442. UserID struct {
  443. Value int64 `xml:",chardata"`
  444. Type string `xml:"type,attr"`
  445. } `xml:"user-id"`
  446. Action struct {
  447. Value string `xml:",chardata"`
  448. Nil string `xml:"nil,attr"`
  449. } `xml:"action"`
  450. CreatedAt struct {
  451. Value time.Time `xml:",chardata"`
  452. Type string `xml:"type,attr"`
  453. } `xml:"created-at"`
  454. } `xml:"comment"`
  455. } `xml:"comments"`
  456. }
  457. err := d.callAPI(
  458. ctx,
  459. fmt.Sprintf("/%s/%s/merge_requests/%d", d.project, d.repoName, mr.ID.Value),
  460. nil,
  461. &rawMergeRequest,
  462. )
  463. if err != nil {
  464. return nil, false, err
  465. }
  466. number := d.maxIssueIndex + int64(i) + 1
  467. state := "open"
  468. merged := false
  469. var closeTime *time.Time
  470. var mergedTime *time.Time
  471. if rawMergeRequest.Status != "new" {
  472. state = "closed"
  473. closeTime = &rawMergeRequest.UpdatedAt.Value
  474. }
  475. comments := make([]*base.Comment, 0, len(rawMergeRequest.Comments.Comment))
  476. for _, comment := range rawMergeRequest.Comments.Comment {
  477. if len(comment.Content) == 0 {
  478. if comment.Action.Value == "merging" {
  479. merged = true
  480. mergedTime = &comment.CreatedAt.Value
  481. }
  482. continue
  483. }
  484. poster := d.tryGetUser(ctx, comment.UserID.Value)
  485. comments = append(comments, &base.Comment{
  486. IssueIndex: number,
  487. Index: comment.ID.Value,
  488. PosterID: poster.ID,
  489. PosterName: poster.Name,
  490. PosterEmail: poster.Email,
  491. Content: comment.Content,
  492. Created: comment.CreatedAt.Value,
  493. Updated: comment.CreatedAt.Value,
  494. })
  495. }
  496. if len(comments) == 0 {
  497. comments = append(comments, &base.Comment{})
  498. }
  499. poster := d.tryGetUser(ctx, rawMergeRequest.UserID.Value)
  500. pullRequests = append(pullRequests, &base.PullRequest{
  501. Title: rawMergeRequest.Subject,
  502. Number: number,
  503. PosterName: poster.Name,
  504. PosterEmail: poster.Email,
  505. Content: comments[0].Content,
  506. State: state,
  507. Created: rawMergeRequest.CreatedAt.Value,
  508. Updated: rawMergeRequest.UpdatedAt.Value,
  509. Closed: closeTime,
  510. Merged: merged,
  511. MergedTime: mergedTime,
  512. Head: base.PullRequestBranch{
  513. Ref: rawMergeRequest.SourceRef,
  514. SHA: d.getHeadCommit(ctx, rawMergeRequest.SourceRef),
  515. RepoName: d.repoName,
  516. },
  517. Base: base.PullRequestBranch{
  518. Ref: rawMergeRequest.TargetRef,
  519. SHA: d.getHeadCommit(ctx, rawMergeRequest.TargetRef),
  520. RepoName: d.repoName,
  521. },
  522. ForeignIndex: rawMergeRequest.ID.Value,
  523. Context: codebaseIssueContext{
  524. Comments: comments[1:],
  525. },
  526. })
  527. // SECURITY: Ensure that the PR is safe
  528. _ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
  529. }
  530. return pullRequests, true, nil
  531. }
  532. func (d *CodebaseDownloader) tryGetUser(ctx context.Context, userID int64) *codebaseUser {
  533. if len(d.userMap) == 0 {
  534. var rawUsers struct {
  535. XMLName xml.Name `xml:"users"`
  536. Type string `xml:"type,attr"`
  537. User []struct {
  538. EmailAddress string `xml:"email-address"`
  539. ID struct {
  540. Value int64 `xml:",chardata"`
  541. Type string `xml:"type,attr"`
  542. } `xml:"id"`
  543. LastName string `xml:"last-name"`
  544. FirstName string `xml:"first-name"`
  545. Username string `xml:"username"`
  546. } `xml:"user"`
  547. }
  548. err := d.callAPI(
  549. ctx,
  550. "/users",
  551. nil,
  552. &rawUsers,
  553. )
  554. if err == nil {
  555. for _, user := range rawUsers.User {
  556. d.userMap[user.ID.Value] = &codebaseUser{
  557. Name: user.Username,
  558. Email: user.EmailAddress,
  559. }
  560. }
  561. }
  562. }
  563. user, ok := d.userMap[userID]
  564. if !ok {
  565. user = &codebaseUser{
  566. Name: fmt.Sprintf("User %d", userID),
  567. }
  568. d.userMap[userID] = user
  569. }
  570. return user
  571. }
  572. func (d *CodebaseDownloader) getHeadCommit(ctx context.Context, ref string) string {
  573. commitRef, ok := d.commitMap[ref]
  574. if !ok {
  575. var rawCommits struct {
  576. XMLName xml.Name `xml:"commits"`
  577. Type string `xml:"type,attr"`
  578. Commit []struct {
  579. Ref string `xml:"ref"`
  580. } `xml:"commit"`
  581. }
  582. err := d.callAPI(
  583. ctx,
  584. fmt.Sprintf("/%s/%s/commits/%s", d.project, d.repoName, ref),
  585. nil,
  586. &rawCommits,
  587. )
  588. if err == nil && len(rawCommits.Commit) > 0 {
  589. commitRef = rawCommits.Commit[0].Ref
  590. d.commitMap[ref] = commitRef
  591. }
  592. }
  593. return commitRef
  594. }