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

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