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  }