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.

codebase.go 17KB

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