gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package migrations
  4. import (
  5. "context"
  6. "fmt"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. "time"
  11. "code.gitea.io/gitea/modules/log"
  12. base "code.gitea.io/gitea/modules/migration"
  13. "code.gitea.io/gitea/modules/structs"
  14. "github.com/gogs/go-gogs-client"
  15. )
  16. var (
  17. _ base.Downloader = &GogsDownloader{}
  18. _ base.DownloaderFactory = &GogsDownloaderFactory{}
  19. )
  20. func init() {
  21. RegisterDownloaderFactory(&GogsDownloaderFactory{})
  22. }
  23. // GogsDownloaderFactory defines a gogs downloader factory
  24. type GogsDownloaderFactory struct{}
  25. // New returns a Downloader related to this factory according MigrateOptions
  26. func (f *GogsDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
  27. u, err := url.Parse(opts.CloneAddr)
  28. if err != nil {
  29. return nil, err
  30. }
  31. baseURL := u.Scheme + "://" + u.Host
  32. repoNameSpace := strings.TrimSuffix(u.Path, ".git")
  33. repoNameSpace = strings.Trim(repoNameSpace, "/")
  34. fields := strings.Split(repoNameSpace, "/")
  35. if len(fields) < 2 {
  36. return nil, fmt.Errorf("invalid path: %s", repoNameSpace)
  37. }
  38. log.Trace("Create gogs downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, fields[0], fields[1])
  39. return NewGogsDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, fields[0], fields[1]), nil
  40. }
  41. // GitServiceType returns the type of git service
  42. func (f *GogsDownloaderFactory) GitServiceType() structs.GitServiceType {
  43. return structs.GogsService
  44. }
  45. // GogsDownloader implements a Downloader interface to get repository information
  46. // from gogs via API
  47. type GogsDownloader struct {
  48. base.NullDownloader
  49. baseURL string
  50. repoOwner string
  51. repoName string
  52. userName string
  53. password string
  54. token string
  55. openIssuesFinished bool
  56. openIssuesPages int
  57. }
  58. // String implements Stringer
  59. func (g *GogsDownloader) String() string {
  60. return fmt.Sprintf("migration from gogs server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
  61. }
  62. func (g *GogsDownloader) LogString() string {
  63. if g == nil {
  64. return "<GogsDownloader nil>"
  65. }
  66. return fmt.Sprintf("<GogsDownloader %s %s/%s>", g.baseURL, g.repoOwner, g.repoName)
  67. }
  68. // NewGogsDownloader creates a gogs Downloader via gogs API
  69. func NewGogsDownloader(_ context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GogsDownloader {
  70. downloader := GogsDownloader{
  71. baseURL: baseURL,
  72. userName: userName,
  73. password: password,
  74. token: token,
  75. repoOwner: repoOwner,
  76. repoName: repoName,
  77. }
  78. return &downloader
  79. }
  80. type roundTripperFunc func(req *http.Request) (*http.Response, error)
  81. func (rt roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
  82. return rt(r)
  83. }
  84. func (g *GogsDownloader) client(ctx context.Context) *gogs.Client {
  85. // Gogs client lacks the context support, so we use a custom transport
  86. // Then each request uses a dedicated client with its own context
  87. httpTransport := NewMigrationHTTPTransport()
  88. gogsClient := gogs.NewClient(g.baseURL, g.token)
  89. gogsClient.SetHTTPClient(&http.Client{
  90. Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
  91. if g.password != "" {
  92. // Gogs client lacks the support for basic auth, this is the only way to set it
  93. req.SetBasicAuth(g.userName, g.password)
  94. }
  95. return httpTransport.RoundTrip(req.WithContext(ctx))
  96. }),
  97. })
  98. return gogsClient
  99. }
  100. // GetRepoInfo returns a repository information
  101. func (g *GogsDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) {
  102. gr, err := g.client(ctx).GetRepo(g.repoOwner, g.repoName)
  103. if err != nil {
  104. return nil, err
  105. }
  106. // convert gogs repo to stand Repo
  107. return &base.Repository{
  108. Owner: g.repoOwner,
  109. Name: g.repoName,
  110. IsPrivate: gr.Private,
  111. Description: gr.Description,
  112. CloneURL: gr.CloneURL,
  113. OriginalURL: gr.HTMLURL,
  114. DefaultBranch: gr.DefaultBranch,
  115. }, nil
  116. }
  117. // GetMilestones returns milestones
  118. func (g *GogsDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) {
  119. perPage := 100
  120. milestones := make([]*base.Milestone, 0, perPage)
  121. ms, err := g.client(ctx).ListRepoMilestones(g.repoOwner, g.repoName)
  122. if err != nil {
  123. return nil, err
  124. }
  125. for _, m := range ms {
  126. milestones = append(milestones, &base.Milestone{
  127. Title: m.Title,
  128. Description: m.Description,
  129. Deadline: m.Deadline,
  130. State: string(m.State),
  131. Closed: m.Closed,
  132. })
  133. }
  134. return milestones, nil
  135. }
  136. // GetLabels returns labels
  137. func (g *GogsDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) {
  138. perPage := 100
  139. labels := make([]*base.Label, 0, perPage)
  140. ls, err := g.client(ctx).ListRepoLabels(g.repoOwner, g.repoName)
  141. if err != nil {
  142. return nil, err
  143. }
  144. for _, label := range ls {
  145. labels = append(labels, convertGogsLabel(label))
  146. }
  147. return labels, nil
  148. }
  149. // GetIssues returns issues according start and limit, perPage is not supported
  150. func (g *GogsDownloader) GetIssues(ctx context.Context, page, _ int) ([]*base.Issue, bool, error) {
  151. var state string
  152. if g.openIssuesFinished {
  153. state = string(gogs.STATE_CLOSED)
  154. page -= g.openIssuesPages
  155. } else {
  156. state = string(gogs.STATE_OPEN)
  157. g.openIssuesPages = page
  158. }
  159. issues, isEnd, err := g.getIssues(ctx, page, state)
  160. if err != nil {
  161. return nil, false, err
  162. }
  163. if isEnd {
  164. if g.openIssuesFinished {
  165. return issues, true, nil
  166. }
  167. g.openIssuesFinished = true
  168. }
  169. return issues, false, nil
  170. }
  171. func (g *GogsDownloader) getIssues(ctx context.Context, page int, state string) ([]*base.Issue, bool, error) {
  172. allIssues := make([]*base.Issue, 0, 10)
  173. issues, err := g.client(ctx).ListRepoIssues(g.repoOwner, g.repoName, gogs.ListIssueOption{
  174. Page: page,
  175. State: state,
  176. })
  177. if err != nil {
  178. return nil, false, fmt.Errorf("error while listing repos: %w", err)
  179. }
  180. for _, issue := range issues {
  181. if issue.PullRequest != nil {
  182. continue
  183. }
  184. allIssues = append(allIssues, convertGogsIssue(issue))
  185. }
  186. return allIssues, len(issues) == 0, nil
  187. }
  188. // GetComments returns comments according issueNumber
  189. func (g *GogsDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
  190. allComments := make([]*base.Comment, 0, 100)
  191. comments, err := g.client(ctx).ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex())
  192. if err != nil {
  193. return nil, false, fmt.Errorf("error while listing repos: %w", err)
  194. }
  195. for _, comment := range comments {
  196. if len(comment.Body) == 0 || comment.Poster == nil {
  197. continue
  198. }
  199. allComments = append(allComments, &base.Comment{
  200. IssueIndex: commentable.GetLocalIndex(),
  201. Index: comment.ID,
  202. PosterID: comment.Poster.ID,
  203. PosterName: comment.Poster.Login,
  204. PosterEmail: comment.Poster.Email,
  205. Content: comment.Body,
  206. Created: comment.Created,
  207. Updated: comment.Updated,
  208. })
  209. }
  210. return allComments, true, nil
  211. }
  212. // GetTopics return repository topics
  213. func (g *GogsDownloader) GetTopics(_ context.Context) ([]string, error) {
  214. return []string{}, nil
  215. }
  216. // FormatCloneURL add authentication into remote URLs
  217. func (g *GogsDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) {
  218. if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 {
  219. u, err := url.Parse(remoteAddr)
  220. if err != nil {
  221. return "", err
  222. }
  223. if len(opts.AuthToken) != 0 {
  224. u.User = url.UserPassword(opts.AuthToken, "")
  225. } else {
  226. u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword)
  227. }
  228. return u.String(), nil
  229. }
  230. return remoteAddr, nil
  231. }
  232. func convertGogsIssue(issue *gogs.Issue) *base.Issue {
  233. var milestone string
  234. if issue.Milestone != nil {
  235. milestone = issue.Milestone.Title
  236. }
  237. labels := make([]*base.Label, 0, len(issue.Labels))
  238. for _, l := range issue.Labels {
  239. labels = append(labels, convertGogsLabel(l))
  240. }
  241. var closed *time.Time
  242. if issue.State == gogs.STATE_CLOSED {
  243. // gogs client haven't provide closed, so we use updated instead
  244. closed = &issue.Updated
  245. }
  246. return &base.Issue{
  247. Title: issue.Title,
  248. Number: issue.Index,
  249. PosterID: issue.Poster.ID,
  250. PosterName: issue.Poster.Login,
  251. PosterEmail: issue.Poster.Email,
  252. Content: issue.Body,
  253. Milestone: milestone,
  254. State: string(issue.State),
  255. Created: issue.Created,
  256. Updated: issue.Updated,
  257. Labels: labels,
  258. Closed: closed,
  259. ForeignIndex: issue.Index,
  260. }
  261. }
  262. func convertGogsLabel(label *gogs.Label) *base.Label {
  263. return &base.Label{
  264. Name: label.Name,
  265. Color: label.Color,
  266. }
  267. }