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.

gogs.go 8.5KB

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