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.6KB

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