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