Add migrate from OneDev (#16356)
	
		
	
				
					
				
			* Use context to simplify logic. * Added migration from OneDev. This PR adds [OneDev](https://code.onedev.io/) as migration source. Supported: - [x] Milestones - [x] Issues - [x] Pull Requests - [x] Comments - [x] Reviews - [x] Labelstokarchuk/v1.17
							parent
							
								
									2d1935acc7
								
							
						
					
					
						commit
						cee5f7c5e2
					
				@ -0,0 +1,619 @@ | 
				
			||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||
// Use of this source code is governed by a MIT-style
 | 
				
			||||
// license that can be found in the LICENSE file.
 | 
				
			||||
 | 
				
			||||
package migrations | 
				
			||||
 | 
				
			||||
import ( | 
				
			||||
	"context" | 
				
			||||
	"fmt" | 
				
			||||
	"net/http" | 
				
			||||
	"net/url" | 
				
			||||
	"strconv" | 
				
			||||
	"strings" | 
				
			||||
	"time" | 
				
			||||
 | 
				
			||||
	"code.gitea.io/gitea/modules/json" | 
				
			||||
	"code.gitea.io/gitea/modules/log" | 
				
			||||
	"code.gitea.io/gitea/modules/migrations/base" | 
				
			||||
	"code.gitea.io/gitea/modules/structs" | 
				
			||||
) | 
				
			||||
 | 
				
			||||
var ( | 
				
			||||
	_ base.Downloader        = &OneDevDownloader{} | 
				
			||||
	_ base.DownloaderFactory = &OneDevDownloaderFactory{} | 
				
			||||
) | 
				
			||||
 | 
				
			||||
func init() { | 
				
			||||
	RegisterDownloaderFactory(&OneDevDownloaderFactory{}) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// OneDevDownloaderFactory defines a downloader factory
 | 
				
			||||
type OneDevDownloaderFactory struct { | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// New returns a downloader related to this factory according MigrateOptions
 | 
				
			||||
func (f *OneDevDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { | 
				
			||||
	u, err := url.Parse(opts.CloneAddr) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	repoName := "" | 
				
			||||
 | 
				
			||||
	fields := strings.Split(strings.Trim(u.Path, "/"), "/") | 
				
			||||
	if len(fields) == 2 && fields[0] == "projects" { | 
				
			||||
		repoName = fields[1] | 
				
			||||
	} else if len(fields) == 1 { | 
				
			||||
		repoName = fields[0] | 
				
			||||
	} else { | 
				
			||||
		return nil, fmt.Errorf("invalid path: %s", u.Path) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	u.Path = "" | 
				
			||||
	u.Fragment = "" | 
				
			||||
 | 
				
			||||
	log.Trace("Create onedev downloader. BaseURL: %v RepoName: %s", u, repoName) | 
				
			||||
 | 
				
			||||
	return NewOneDevDownloader(ctx, u, opts.AuthUsername, opts.AuthPassword, repoName), nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GitServiceType returns the type of git service
 | 
				
			||||
func (f *OneDevDownloaderFactory) GitServiceType() structs.GitServiceType { | 
				
			||||
	return structs.OneDevService | 
				
			||||
} | 
				
			||||
 | 
				
			||||
type onedevUser struct { | 
				
			||||
	ID    int64  `json:"id"` | 
				
			||||
	Name  string `json:"name"` | 
				
			||||
	Email string `json:"email"` | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// OneDevDownloader implements a Downloader interface to get repository informations
 | 
				
			||||
// from OneDev
 | 
				
			||||
type OneDevDownloader struct { | 
				
			||||
	base.NullDownloader | 
				
			||||
	ctx           context.Context | 
				
			||||
	client        *http.Client | 
				
			||||
	baseURL       *url.URL | 
				
			||||
	repoName      string | 
				
			||||
	repoID        int64 | 
				
			||||
	maxIssueIndex int64 | 
				
			||||
	userMap       map[int64]*onedevUser | 
				
			||||
	milestoneMap  map[int64]string | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// SetContext set context
 | 
				
			||||
func (d *OneDevDownloader) SetContext(ctx context.Context) { | 
				
			||||
	d.ctx = ctx | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// NewOneDevDownloader creates a new downloader
 | 
				
			||||
func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, password, repoName string) *OneDevDownloader { | 
				
			||||
	var downloader = &OneDevDownloader{ | 
				
			||||
		ctx:      ctx, | 
				
			||||
		baseURL:  baseURL, | 
				
			||||
		repoName: repoName, | 
				
			||||
		client: &http.Client{ | 
				
			||||
			Transport: &http.Transport{ | 
				
			||||
				Proxy: func(req *http.Request) (*url.URL, error) { | 
				
			||||
					if len(username) > 0 && len(password) > 0 { | 
				
			||||
						req.SetBasicAuth(username, password) | 
				
			||||
					} | 
				
			||||
					return nil, nil | 
				
			||||
				}, | 
				
			||||
			}, | 
				
			||||
		}, | 
				
			||||
		userMap:      make(map[int64]*onedevUser), | 
				
			||||
		milestoneMap: make(map[int64]string), | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	return downloader | 
				
			||||
} | 
				
			||||
 | 
				
			||||
func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result interface{}) error { | 
				
			||||
	u, err := d.baseURL.Parse(endpoint) | 
				
			||||
	if err != nil { | 
				
			||||
		return err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	if parameter != nil { | 
				
			||||
		query := u.Query() | 
				
			||||
		for k, v := range parameter { | 
				
			||||
			query.Set(k, v) | 
				
			||||
		} | 
				
			||||
		u.RawQuery = query.Encode() | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil) | 
				
			||||
	if err != nil { | 
				
			||||
		return err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	resp, err := d.client.Do(req) | 
				
			||||
	if err != nil { | 
				
			||||
		return err | 
				
			||||
	} | 
				
			||||
	defer resp.Body.Close() | 
				
			||||
 | 
				
			||||
	decoder := json.NewDecoder(resp.Body) | 
				
			||||
	return decoder.Decode(&result) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GetRepoInfo returns repository information
 | 
				
			||||
func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) { | 
				
			||||
	info := make([]struct { | 
				
			||||
		ID          int64  `json:"id"` | 
				
			||||
		Name        string `json:"name"` | 
				
			||||
		Description string `json:"description"` | 
				
			||||
	}, 0, 1) | 
				
			||||
 | 
				
			||||
	err := d.callAPI( | 
				
			||||
		"/api/projects", | 
				
			||||
		map[string]string{ | 
				
			||||
			"query":  `"Name" is "` + d.repoName + `"`, | 
				
			||||
			"offset": "0", | 
				
			||||
			"count":  "1", | 
				
			||||
		}, | 
				
			||||
		&info, | 
				
			||||
	) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, err | 
				
			||||
	} | 
				
			||||
	if len(info) != 1 { | 
				
			||||
		return nil, fmt.Errorf("Project %s not found", d.repoName) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	d.repoID = info[0].ID | 
				
			||||
 | 
				
			||||
	cloneURL, err := d.baseURL.Parse(info[0].Name) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, err | 
				
			||||
	} | 
				
			||||
	originalURL, err := d.baseURL.Parse("/projects/" + info[0].Name) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	return &base.Repository{ | 
				
			||||
		Name:        info[0].Name, | 
				
			||||
		Description: info[0].Description, | 
				
			||||
		CloneURL:    cloneURL.String(), | 
				
			||||
		OriginalURL: originalURL.String(), | 
				
			||||
	}, nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GetMilestones returns milestones
 | 
				
			||||
func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) { | 
				
			||||
	rawMilestones := make([]struct { | 
				
			||||
		ID          int64      `json:"id"` | 
				
			||||
		Name        string     `json:"name"` | 
				
			||||
		Description string     `json:"description"` | 
				
			||||
		DueDate     *time.Time `json:"dueDate"` | 
				
			||||
		Closed      bool       `json:"closed"` | 
				
			||||
	}, 0, 100) | 
				
			||||
 | 
				
			||||
	endpoint := fmt.Sprintf("/api/projects/%d/milestones", d.repoID) | 
				
			||||
 | 
				
			||||
	var milestones = make([]*base.Milestone, 0, 100) | 
				
			||||
	offset := 0 | 
				
			||||
	for { | 
				
			||||
		err := d.callAPI( | 
				
			||||
			endpoint, | 
				
			||||
			map[string]string{ | 
				
			||||
				"offset": strconv.Itoa(offset), | 
				
			||||
				"count":  "100", | 
				
			||||
			}, | 
				
			||||
			&rawMilestones, | 
				
			||||
		) | 
				
			||||
		if err != nil { | 
				
			||||
			return nil, err | 
				
			||||
		} | 
				
			||||
		if len(rawMilestones) == 0 { | 
				
			||||
			break | 
				
			||||
		} | 
				
			||||
		offset += 100 | 
				
			||||
 | 
				
			||||
		for _, milestone := range rawMilestones { | 
				
			||||
			d.milestoneMap[milestone.ID] = milestone.Name | 
				
			||||
			closed := milestone.DueDate | 
				
			||||
			if !milestone.Closed { | 
				
			||||
				closed = nil | 
				
			||||
			} | 
				
			||||
 | 
				
			||||
			milestones = append(milestones, &base.Milestone{ | 
				
			||||
				Title:       milestone.Name, | 
				
			||||
				Description: milestone.Description, | 
				
			||||
				Deadline:    milestone.DueDate, | 
				
			||||
				Closed:      closed, | 
				
			||||
			}) | 
				
			||||
		} | 
				
			||||
	} | 
				
			||||
	return milestones, nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GetLabels returns labels
 | 
				
			||||
func (d *OneDevDownloader) GetLabels() ([]*base.Label, error) { | 
				
			||||
	return []*base.Label{ | 
				
			||||
		{ | 
				
			||||
			Name:  "Bug", | 
				
			||||
			Color: "f64e60", | 
				
			||||
		}, | 
				
			||||
		{ | 
				
			||||
			Name:  "Build Failure", | 
				
			||||
			Color: "f64e60", | 
				
			||||
		}, | 
				
			||||
		{ | 
				
			||||
			Name:  "Discussion", | 
				
			||||
			Color: "8950fc", | 
				
			||||
		}, | 
				
			||||
		{ | 
				
			||||
			Name:  "Improvement", | 
				
			||||
			Color: "1bc5bd", | 
				
			||||
		}, | 
				
			||||
		{ | 
				
			||||
			Name:  "New Feature", | 
				
			||||
			Color: "1bc5bd", | 
				
			||||
		}, | 
				
			||||
		{ | 
				
			||||
			Name:  "Support Request", | 
				
			||||
			Color: "8950fc", | 
				
			||||
		}, | 
				
			||||
	}, nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
type onedevIssueContext struct { | 
				
			||||
	foreignID     int64 | 
				
			||||
	localID       int64 | 
				
			||||
	IsPullRequest bool | 
				
			||||
} | 
				
			||||
 | 
				
			||||
func (c onedevIssueContext) LocalID() int64 { | 
				
			||||
	return c.localID | 
				
			||||
} | 
				
			||||
 | 
				
			||||
func (c onedevIssueContext) ForeignID() int64 { | 
				
			||||
	return c.foreignID | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GetIssues returns issues
 | 
				
			||||
func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | 
				
			||||
	rawIssues := make([]struct { | 
				
			||||
		ID          int64     `json:"id"` | 
				
			||||
		Number      int64     `json:"number"` | 
				
			||||
		State       string    `json:"state"` | 
				
			||||
		Title       string    `json:"title"` | 
				
			||||
		Description string    `json:"description"` | 
				
			||||
		MilestoneID int64     `json:"milestoneId"` | 
				
			||||
		SubmitterID int64     `json:"submitterId"` | 
				
			||||
		SubmitDate  time.Time `json:"submitDate"` | 
				
			||||
	}, 0, perPage) | 
				
			||||
 | 
				
			||||
	err := d.callAPI( | 
				
			||||
		"/api/issues", | 
				
			||||
		map[string]string{ | 
				
			||||
			"query":  `"Project" is "` + d.repoName + `"`, | 
				
			||||
			"offset": strconv.Itoa((page - 1) * perPage), | 
				
			||||
			"count":  strconv.Itoa(perPage), | 
				
			||||
		}, | 
				
			||||
		&rawIssues, | 
				
			||||
	) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, false, err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	issues := make([]*base.Issue, 0, len(rawIssues)) | 
				
			||||
	for _, issue := range rawIssues { | 
				
			||||
		fields := make([]struct { | 
				
			||||
			Name  string `json:"name"` | 
				
			||||
			Value string `json:"value"` | 
				
			||||
		}, 0, 10) | 
				
			||||
		err := d.callAPI( | 
				
			||||
			fmt.Sprintf("/api/issues/%d/fields", issue.ID), | 
				
			||||
			nil, | 
				
			||||
			&fields, | 
				
			||||
		) | 
				
			||||
		if err != nil { | 
				
			||||
			return nil, false, err | 
				
			||||
		} | 
				
			||||
 | 
				
			||||
		var label *base.Label | 
				
			||||
		for _, field := range fields { | 
				
			||||
			if field.Name == "Type" { | 
				
			||||
				label = &base.Label{Name: field.Value} | 
				
			||||
				break | 
				
			||||
			} | 
				
			||||
		} | 
				
			||||
 | 
				
			||||
		state := strings.ToLower(issue.State) | 
				
			||||
		if state == "released" { | 
				
			||||
			state = "closed" | 
				
			||||
		} | 
				
			||||
		poster := d.tryGetUser(issue.SubmitterID) | 
				
			||||
		issues = append(issues, &base.Issue{ | 
				
			||||
			Title:       issue.Title, | 
				
			||||
			Number:      issue.Number, | 
				
			||||
			PosterName:  poster.Name, | 
				
			||||
			PosterEmail: poster.Email, | 
				
			||||
			Content:     issue.Description, | 
				
			||||
			Milestone:   d.milestoneMap[issue.MilestoneID], | 
				
			||||
			State:       state, | 
				
			||||
			Created:     issue.SubmitDate, | 
				
			||||
			Updated:     issue.SubmitDate, | 
				
			||||
			Labels:      []*base.Label{label}, | 
				
			||||
			Context: onedevIssueContext{ | 
				
			||||
				foreignID:     issue.ID, | 
				
			||||
				localID:       issue.Number, | 
				
			||||
				IsPullRequest: false, | 
				
			||||
			}, | 
				
			||||
		}) | 
				
			||||
 | 
				
			||||
		if d.maxIssueIndex < issue.Number { | 
				
			||||
			d.maxIssueIndex = issue.Number | 
				
			||||
		} | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	return issues, len(issues) == 0, nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GetComments returns comments
 | 
				
			||||
func (d *OneDevDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) { | 
				
			||||
	context, ok := opts.Context.(onedevIssueContext) | 
				
			||||
	if !ok { | 
				
			||||
		return nil, false, fmt.Errorf("unexpected comment context: %+v", opts.Context) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	rawComments := make([]struct { | 
				
			||||
		Date    time.Time `json:"date"` | 
				
			||||
		UserID  int64     `json:"userId"` | 
				
			||||
		Content string    `json:"content"` | 
				
			||||
	}, 0, 100) | 
				
			||||
 | 
				
			||||
	var endpoint string | 
				
			||||
	if context.IsPullRequest { | 
				
			||||
		endpoint = fmt.Sprintf("/api/pull-requests/%d/comments", context.ForeignID()) | 
				
			||||
	} else { | 
				
			||||
		endpoint = fmt.Sprintf("/api/issues/%d/comments", context.ForeignID()) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	err := d.callAPI( | 
				
			||||
		endpoint, | 
				
			||||
		nil, | 
				
			||||
		&rawComments, | 
				
			||||
	) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, false, err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	rawChanges := make([]struct { | 
				
			||||
		Date   time.Time              `json:"date"` | 
				
			||||
		UserID int64                  `json:"userId"` | 
				
			||||
		Data   map[string]interface{} `json:"data"` | 
				
			||||
	}, 0, 100) | 
				
			||||
 | 
				
			||||
	if context.IsPullRequest { | 
				
			||||
		endpoint = fmt.Sprintf("/api/pull-requests/%d/changes", context.ForeignID()) | 
				
			||||
	} else { | 
				
			||||
		endpoint = fmt.Sprintf("/api/issues/%d/changes", context.ForeignID()) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	err = d.callAPI( | 
				
			||||
		endpoint, | 
				
			||||
		nil, | 
				
			||||
		&rawChanges, | 
				
			||||
	) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, false, err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	comments := make([]*base.Comment, 0, len(rawComments)+len(rawChanges)) | 
				
			||||
	for _, comment := range rawComments { | 
				
			||||
		if len(comment.Content) == 0 { | 
				
			||||
			continue | 
				
			||||
		} | 
				
			||||
		poster := d.tryGetUser(comment.UserID) | 
				
			||||
		comments = append(comments, &base.Comment{ | 
				
			||||
			IssueIndex:  context.LocalID(), | 
				
			||||
			PosterID:    poster.ID, | 
				
			||||
			PosterName:  poster.Name, | 
				
			||||
			PosterEmail: poster.Email, | 
				
			||||
			Content:     comment.Content, | 
				
			||||
			Created:     comment.Date, | 
				
			||||
			Updated:     comment.Date, | 
				
			||||
		}) | 
				
			||||
	} | 
				
			||||
	for _, change := range rawChanges { | 
				
			||||
		contentV, ok := change.Data["content"] | 
				
			||||
		if !ok { | 
				
			||||
			contentV, ok = change.Data["comment"] | 
				
			||||
			if !ok { | 
				
			||||
				continue | 
				
			||||
			} | 
				
			||||
		} | 
				
			||||
		content, ok := contentV.(string) | 
				
			||||
		if !ok || len(content) == 0 { | 
				
			||||
			continue | 
				
			||||
		} | 
				
			||||
 | 
				
			||||
		poster := d.tryGetUser(change.UserID) | 
				
			||||
		comments = append(comments, &base.Comment{ | 
				
			||||
			IssueIndex:  context.LocalID(), | 
				
			||||
			PosterID:    poster.ID, | 
				
			||||
			PosterName:  poster.Name, | 
				
			||||
			PosterEmail: poster.Email, | 
				
			||||
			Content:     content, | 
				
			||||
			Created:     change.Date, | 
				
			||||
			Updated:     change.Date, | 
				
			||||
		}) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	return comments, true, nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GetPullRequests returns pull requests
 | 
				
			||||
func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { | 
				
			||||
	rawPullRequests := make([]struct { | 
				
			||||
		ID             int64     `json:"id"` | 
				
			||||
		Number         int64     `json:"number"` | 
				
			||||
		Title          string    `json:"title"` | 
				
			||||
		SubmitterID    int64     `json:"submitterId"` | 
				
			||||
		SubmitDate     time.Time `json:"submitDate"` | 
				
			||||
		Description    string    `json:"description"` | 
				
			||||
		TargetBranch   string    `json:"targetBranch"` | 
				
			||||
		SourceBranch   string    `json:"sourceBranch"` | 
				
			||||
		BaseCommitHash string    `json:"baseCommitHash"` | 
				
			||||
		CloseInfo      *struct { | 
				
			||||
			Date   *time.Time `json:"date"` | 
				
			||||
			Status string     `json:"status"` | 
				
			||||
		} | 
				
			||||
	}, 0, perPage) | 
				
			||||
 | 
				
			||||
	err := d.callAPI( | 
				
			||||
		"/api/pull-requests", | 
				
			||||
		map[string]string{ | 
				
			||||
			"query":  `"Target Project" is "` + d.repoName + `"`, | 
				
			||||
			"offset": strconv.Itoa((page - 1) * perPage), | 
				
			||||
			"count":  strconv.Itoa(perPage), | 
				
			||||
		}, | 
				
			||||
		&rawPullRequests, | 
				
			||||
	) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, false, err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	pullRequests := make([]*base.PullRequest, 0, len(rawPullRequests)) | 
				
			||||
	for _, pr := range rawPullRequests { | 
				
			||||
		var mergePreview struct { | 
				
			||||
			TargetHeadCommitHash string `json:"targetHeadCommitHash"` | 
				
			||||
			HeadCommitHash       string `json:"headCommitHash"` | 
				
			||||
			MergeStrategy        string `json:"mergeStrategy"` | 
				
			||||
			MergeCommitHash      string `json:"mergeCommitHash"` | 
				
			||||
		} | 
				
			||||
		err := d.callAPI( | 
				
			||||
			fmt.Sprintf("/api/pull-requests/%d/merge-preview", pr.ID), | 
				
			||||
			nil, | 
				
			||||
			&mergePreview, | 
				
			||||
		) | 
				
			||||
		if err != nil { | 
				
			||||
			return nil, false, err | 
				
			||||
		} | 
				
			||||
 | 
				
			||||
		state := "open" | 
				
			||||
		merged := false | 
				
			||||
		var closeTime *time.Time | 
				
			||||
		var mergedTime *time.Time | 
				
			||||
		if pr.CloseInfo != nil { | 
				
			||||
			state = "closed" | 
				
			||||
			closeTime = pr.CloseInfo.Date | 
				
			||||
			if pr.CloseInfo.Status == "MERGED" { // "DISCARDED"
 | 
				
			||||
				merged = true | 
				
			||||
				mergedTime = pr.CloseInfo.Date | 
				
			||||
			} | 
				
			||||
		} | 
				
			||||
		poster := d.tryGetUser(pr.SubmitterID) | 
				
			||||
 | 
				
			||||
		number := pr.Number + d.maxIssueIndex | 
				
			||||
		pullRequests = append(pullRequests, &base.PullRequest{ | 
				
			||||
			Title:      pr.Title, | 
				
			||||
			Number:     number, | 
				
			||||
			PosterName: poster.Name, | 
				
			||||
			PosterID:   poster.ID, | 
				
			||||
			Content:    pr.Description, | 
				
			||||
			State:      state, | 
				
			||||
			Created:    pr.SubmitDate, | 
				
			||||
			Updated:    pr.SubmitDate, | 
				
			||||
			Closed:     closeTime, | 
				
			||||
			Merged:     merged, | 
				
			||||
			MergedTime: mergedTime, | 
				
			||||
			Head: base.PullRequestBranch{ | 
				
			||||
				Ref:      pr.SourceBranch, | 
				
			||||
				SHA:      mergePreview.HeadCommitHash, | 
				
			||||
				RepoName: d.repoName, | 
				
			||||
			}, | 
				
			||||
			Base: base.PullRequestBranch{ | 
				
			||||
				Ref:      pr.TargetBranch, | 
				
			||||
				SHA:      mergePreview.TargetHeadCommitHash, | 
				
			||||
				RepoName: d.repoName, | 
				
			||||
			}, | 
				
			||||
			Context: onedevIssueContext{ | 
				
			||||
				foreignID:     pr.ID, | 
				
			||||
				localID:       number, | 
				
			||||
				IsPullRequest: true, | 
				
			||||
			}, | 
				
			||||
		}) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	return pullRequests, len(pullRequests) == 0, nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GetReviews returns pull requests reviews
 | 
				
			||||
func (d *OneDevDownloader) GetReviews(context base.IssueContext) ([]*base.Review, error) { | 
				
			||||
	rawReviews := make([]struct { | 
				
			||||
		ID     int64 `json:"id"` | 
				
			||||
		UserID int64 `json:"userId"` | 
				
			||||
		Result *struct { | 
				
			||||
			Commit   string `json:"commit"` | 
				
			||||
			Approved bool   `json:"approved"` | 
				
			||||
			Comment  string `json:"comment"` | 
				
			||||
		} | 
				
			||||
	}, 0, 100) | 
				
			||||
 | 
				
			||||
	err := d.callAPI( | 
				
			||||
		fmt.Sprintf("/api/pull-requests/%d/reviews", context.ForeignID()), | 
				
			||||
		nil, | 
				
			||||
		&rawReviews, | 
				
			||||
	) | 
				
			||||
	if err != nil { | 
				
			||||
		return nil, err | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	var reviews = make([]*base.Review, 0, len(rawReviews)) | 
				
			||||
	for _, review := range rawReviews { | 
				
			||||
		state := base.ReviewStatePending | 
				
			||||
		content := "" | 
				
			||||
		if review.Result != nil { | 
				
			||||
			if len(review.Result.Comment) > 0 { | 
				
			||||
				state = base.ReviewStateCommented | 
				
			||||
				content = review.Result.Comment | 
				
			||||
			} | 
				
			||||
			if review.Result.Approved { | 
				
			||||
				state = base.ReviewStateApproved | 
				
			||||
			} | 
				
			||||
		} | 
				
			||||
 | 
				
			||||
		poster := d.tryGetUser(review.UserID) | 
				
			||||
		reviews = append(reviews, &base.Review{ | 
				
			||||
			IssueIndex:   context.LocalID(), | 
				
			||||
			ReviewerID:   poster.ID, | 
				
			||||
			ReviewerName: poster.Name, | 
				
			||||
			Content:      content, | 
				
			||||
			State:        state, | 
				
			||||
		}) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	return reviews, nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// GetTopics return repository topics
 | 
				
			||||
func (d *OneDevDownloader) GetTopics() ([]string, error) { | 
				
			||||
	return []string{}, nil | 
				
			||||
} | 
				
			||||
 | 
				
			||||
func (d *OneDevDownloader) tryGetUser(userID int64) *onedevUser { | 
				
			||||
	user, ok := d.userMap[userID] | 
				
			||||
	if !ok { | 
				
			||||
		err := d.callAPI( | 
				
			||||
			fmt.Sprintf("/api/users/%d", userID), | 
				
			||||
			nil, | 
				
			||||
			&user, | 
				
			||||
		) | 
				
			||||
		if err != nil { | 
				
			||||
			user = &onedevUser{ | 
				
			||||
				Name: fmt.Sprintf("User %d", userID), | 
				
			||||
			} | 
				
			||||
		} | 
				
			||||
		d.userMap[userID] = user | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	return user | 
				
			||||
} | 
				
			||||
@ -0,0 +1,169 @@ | 
				
			||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
				
			||||
// Use of this source code is governed by a MIT-style
 | 
				
			||||
// license that can be found in the LICENSE file.
 | 
				
			||||
 | 
				
			||||
package migrations | 
				
			||||
 | 
				
			||||
import ( | 
				
			||||
	"context" | 
				
			||||
	"fmt" | 
				
			||||
	"net/http" | 
				
			||||
	"net/url" | 
				
			||||
	"testing" | 
				
			||||
	"time" | 
				
			||||
 | 
				
			||||
	"code.gitea.io/gitea/modules/migrations/base" | 
				
			||||
 | 
				
			||||
	"github.com/stretchr/testify/assert" | 
				
			||||
) | 
				
			||||
 | 
				
			||||
func TestOneDevDownloadRepo(t *testing.T) { | 
				
			||||
	resp, err := http.Get("https://code.onedev.io/projects/go-gitea-test_repo") | 
				
			||||
	if err != nil || resp.StatusCode != 200 { | 
				
			||||
		t.Skipf("Can't access test repo, skipping %s", t.Name()) | 
				
			||||
	} | 
				
			||||
 | 
				
			||||
	u, _ := url.Parse("https://code.onedev.io") | 
				
			||||
	downloader := NewOneDevDownloader(context.Background(), u, "", "", "go-gitea-test_repo") | 
				
			||||
	if err != nil { | 
				
			||||
		t.Fatal(fmt.Sprintf("NewOneDevDownloader is nil: %v", err)) | 
				
			||||
	} | 
				
			||||
	repo, err := downloader.GetRepoInfo() | 
				
			||||
	assert.NoError(t, err) | 
				
			||||
	assert.EqualValues(t, &base.Repository{ | 
				
			||||
		Name:        "go-gitea-test_repo", | 
				
			||||
		Owner:       "", | 
				
			||||
		Description: "Test repository for testing migration from OneDev to gitea", | 
				
			||||
		CloneURL:    "https://code.onedev.io/go-gitea-test_repo", | 
				
			||||
		OriginalURL: "https://code.onedev.io/projects/go-gitea-test_repo", | 
				
			||||
	}, repo) | 
				
			||||
 | 
				
			||||
	milestones, err := downloader.GetMilestones() | 
				
			||||
	assert.NoError(t, err) | 
				
			||||
	assert.Len(t, milestones, 2) | 
				
			||||
	deadline := time.Unix(1620086400, 0) | 
				
			||||
	assert.EqualValues(t, []*base.Milestone{ | 
				
			||||
		{ | 
				
			||||
			Title:    "1.0.0", | 
				
			||||
			Deadline: &deadline, | 
				
			||||
			Closed:   &deadline, | 
				
			||||
		}, | 
				
			||||
		{ | 
				
			||||
			Title:       "1.1.0", | 
				
			||||
			Description: "next things?", | 
				
			||||
		}, | 
				
			||||
	}, milestones) | 
				
			||||
 | 
				
			||||
	labels, err := downloader.GetLabels() | 
				
			||||
	assert.NoError(t, err) | 
				
			||||
	assert.Len(t, labels, 6) | 
				
			||||
 | 
				
			||||
	issues, isEnd, err := downloader.GetIssues(1, 2) | 
				
			||||
	assert.NoError(t, err) | 
				
			||||
	assert.Len(t, issues, 2) | 
				
			||||
	assert.False(t, isEnd) | 
				
			||||
	assert.EqualValues(t, []*base.Issue{ | 
				
			||||
		{ | 
				
			||||
			Number:     4, | 
				
			||||
			Title:      "Hi there", | 
				
			||||
			Content:    "an issue not assigned to a milestone", | 
				
			||||
			PosterName: "User 336", | 
				
			||||
			State:      "open", | 
				
			||||
			Created:    time.Unix(1628549776, 734000000), | 
				
			||||
			Updated:    time.Unix(1628549776, 734000000), | 
				
			||||
			Labels: []*base.Label{ | 
				
			||||
				{ | 
				
			||||
					Name: "Improvement", | 
				
			||||
				}, | 
				
			||||
			}, | 
				
			||||
			Context: onedevIssueContext{ | 
				
			||||
				foreignID:     398, | 
				
			||||
				localID:       4, | 
				
			||||
				IsPullRequest: false, | 
				
			||||
			}, | 
				
			||||
		}, | 
				
			||||
		{ | 
				
			||||
			Number:     3, | 
				
			||||
			Title:      "Add an awesome feature", | 
				
			||||
			Content:    "just another issue to test against", | 
				
			||||
			PosterName: "User 336", | 
				
			||||
			State:      "open", | 
				
			||||
			Milestone:  "1.1.0", | 
				
			||||
			Created:    time.Unix(1628549749, 878000000), | 
				
			||||
			Updated:    time.Unix(1628549749, 878000000), | 
				
			||||
			Labels: []*base.Label{ | 
				
			||||
				{ | 
				
			||||
					Name: "New Feature", | 
				
			||||
				}, | 
				
			||||
			}, | 
				
			||||
			Context: onedevIssueContext{ | 
				
			||||
				foreignID:     397, | 
				
			||||
				localID:       3, | 
				
			||||
				IsPullRequest: false, | 
				
			||||
			}, | 
				
			||||
		}, | 
				
			||||
	}, issues) | 
				
			||||
 | 
				
			||||
	comments, _, err := downloader.GetComments(base.GetCommentOptions{ | 
				
			||||
		Context: onedevIssueContext{ | 
				
			||||
			foreignID:     398, | 
				
			||||
			localID:       4, | 
				
			||||
			IsPullRequest: false, | 
				
			||||
		}, | 
				
			||||
	}) | 
				
			||||
	assert.NoError(t, err) | 
				
			||||
	assert.Len(t, comments, 1) | 
				
			||||
	assert.EqualValues(t, []*base.Comment{ | 
				
			||||
		{ | 
				
			||||
			IssueIndex: 4, | 
				
			||||
			PosterName: "User 336", | 
				
			||||
			Created:    time.Unix(1628549791, 128000000), | 
				
			||||
			Updated:    time.Unix(1628549791, 128000000), | 
				
			||||
			Content:    "it has a comment\r\n\r\nEDIT: that got edited", | 
				
			||||
		}, | 
				
			||||
	}, comments) | 
				
			||||
 | 
				
			||||
	prs, _, err := downloader.GetPullRequests(1, 1) | 
				
			||||
	assert.NoError(t, err) | 
				
			||||
	assert.Len(t, prs, 1) | 
				
			||||
	assert.EqualValues(t, []*base.PullRequest{ | 
				
			||||
		{ | 
				
			||||
			Number:     5, | 
				
			||||
			Title:      "Pull to add a new file", | 
				
			||||
			Content:    "just do some git stuff", | 
				
			||||
			PosterName: "User 336", | 
				
			||||
			State:      "open", | 
				
			||||
			Created:    time.Unix(1628550076, 25000000), | 
				
			||||
			Updated:    time.Unix(1628550076, 25000000), | 
				
			||||
			Head: base.PullRequestBranch{ | 
				
			||||
				Ref:      "branch-for-a-pull", | 
				
			||||
				SHA:      "343deffe3526b9bc84e873743ff7f6e6d8b827c0", | 
				
			||||
				RepoName: "go-gitea-test_repo", | 
				
			||||
			}, | 
				
			||||
			Base: base.PullRequestBranch{ | 
				
			||||
				Ref:      "master", | 
				
			||||
				SHA:      "f32b0a9dfd09a60f616f29158f772cedd89942d2", | 
				
			||||
				RepoName: "go-gitea-test_repo", | 
				
			||||
			}, | 
				
			||||
			Context: onedevIssueContext{ | 
				
			||||
				foreignID:     186, | 
				
			||||
				localID:       5, | 
				
			||||
				IsPullRequest: true, | 
				
			||||
			}, | 
				
			||||
		}, | 
				
			||||
	}, prs) | 
				
			||||
 | 
				
			||||
	rvs, err := downloader.GetReviews(onedevIssueContext{ | 
				
			||||
		foreignID: 186, | 
				
			||||
		localID:   5, | 
				
			||||
	}) | 
				
			||||
	assert.NoError(t, err) | 
				
			||||
	assert.Len(t, rvs, 1) | 
				
			||||
	assert.EqualValues(t, []*base.Review{ | 
				
			||||
		{ | 
				
			||||
			IssueIndex:   5, | 
				
			||||
			ReviewerName: "User 317", | 
				
			||||
			State:        "PENDING", | 
				
			||||
		}, | 
				
			||||
	}, rvs) | 
				
			||||
} | 
				
			||||
| 
		 After Width: | Height: | Size: 2.2 KiB  | 
@ -0,0 +1,117 @@ | 
				
			||||
{{template "base/head" .}} | 
				
			||||
<div class="page-content repository new migrate"> | 
				
			||||
	<div class="ui middle very relaxed page grid"> | 
				
			||||
		<div class="column"> | 
				
			||||
			<form class="ui form" action="{{.Link}}" method="post"> | 
				
			||||
				{{.CsrfTokenHtml}} | 
				
			||||
				<h3 class="ui top attached header"> | 
				
			||||
					{{.i18n.Tr "repo.migrate.migrate" .service.Title}} | 
				
			||||
					<input id="service_type" type="hidden" name="service" value="{{.service}}"> | 
				
			||||
				</h3> | 
				
			||||
				<div class="ui attached segment"> | 
				
			||||
					{{template "base/alert" .}} | 
				
			||||
					<div class="inline required field {{if .Err_CloneAddr}}error{{end}}"> | 
				
			||||
						<label for="clone_addr">{{.i18n.Tr "repo.migrate.clone_address"}}</label> | 
				
			||||
						<input id="clone_addr" name="clone_addr" value="{{.clone_addr}}" autofocus required> | 
				
			||||
						<span class="help"> | 
				
			||||
						{{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} | 
				
			||||
						</span> | 
				
			||||
					</div> | 
				
			||||
 | 
				
			||||
					<div class="inline field {{if .Err_Auth}}error{{end}}"> | 
				
			||||
						<label for="auth_username">{{.i18n.Tr "username"}}</label> | 
				
			||||
						<input id="auth_username" name="auth_username" value="{{.auth_username}}" {{if not .auth_username}}data-need-clear="true"{{end}}> | 
				
			||||
					</div> | 
				
			||||
					<input class="fake" type="password"> | 
				
			||||
					<div class="inline field {{if .Err_Auth}}error{{end}}"> | 
				
			||||
						<label for="auth_password">{{.i18n.Tr "password"}}</label> | 
				
			||||
						<input id="auth_password" name="auth_password" type="password" value="{{.auth_password}}"> | 
				
			||||
					</div> | 
				
			||||
 | 
				
			||||
					{{template "repo/migrate/options" .}} | 
				
			||||
 | 
				
			||||
					<div id="migrate_items"> | 
				
			||||
						<div class="inline field"> | 
				
			||||
							<label>{{.i18n.Tr "repo.migrate_items"}}</label> | 
				
			||||
							<div class="ui checkbox"> | 
				
			||||
								<input name="milestones" type="checkbox" {{if .milestones}}checked{{end}}> | 
				
			||||
								<label>{{.i18n.Tr "repo.migrate_items_milestones" | Safe}}</label> | 
				
			||||
							</div> | 
				
			||||
							<div class="ui checkbox"> | 
				
			||||
								<input name="labels" type="checkbox" {{if .labels}}checked{{end}}> | 
				
			||||
								<label>{{.i18n.Tr "repo.migrate_items_labels" | Safe}}</label> | 
				
			||||
							</div> | 
				
			||||
						</div> | 
				
			||||
						<div class="inline field"> | 
				
			||||
							<label></label> | 
				
			||||
							<div class="ui checkbox"> | 
				
			||||
								<input name="issues" type="checkbox" {{if .issues}}checked{{end}}> | 
				
			||||
								<label>{{.i18n.Tr "repo.migrate_items_issues" | Safe}}</label> | 
				
			||||
							</div> | 
				
			||||
							<div class="ui checkbox"> | 
				
			||||
								<input name="pull_requests" type="checkbox" {{if .pull_requests}}checked{{end}}> | 
				
			||||
								<label>{{.i18n.Tr "repo.migrate_items_pullrequests" | Safe}}</label> | 
				
			||||
							</div> | 
				
			||||
						</div> | 
				
			||||
					</div> | 
				
			||||
 | 
				
			||||
					<div class="ui divider"></div> | 
				
			||||
 | 
				
			||||
					<div class="inline required field {{if .Err_Owner}}error{{end}}"> | 
				
			||||
						<label>{{.i18n.Tr "repo.owner"}}</label> | 
				
			||||
						<div class="ui selection owner dropdown"> | 
				
			||||
							<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> | 
				
			||||
							<span class="text truncated-item-container" title="{{.ContextUser.Name}}"> | 
				
			||||
								{{avatar .ContextUser 28 "mini"}} | 
				
			||||
								<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> | 
				
			||||
							</span> | 
				
			||||
							{{svg "octicon-triangle-down" 14 "dropdown icon"}} | 
				
			||||
							<div class="menu" title="{{.SignedUser.Name}}"> | 
				
			||||
								<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}"> | 
				
			||||
									{{avatar .SignedUser 28 "mini"}} | 
				
			||||
									<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> | 
				
			||||
								</div> | 
				
			||||
								{{range .Orgs}} | 
				
			||||
									<div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> | 
				
			||||
										{{avatar . 28 "mini"}} | 
				
			||||
										<span class="truncated-item-name">{{.ShortName 40}}</span> | 
				
			||||
									</div> | 
				
			||||
								{{end}} | 
				
			||||
							</div> | 
				
			||||
						</div> | 
				
			||||
					</div> | 
				
			||||
 | 
				
			||||
					<div class="inline required field {{if .Err_RepoName}}error{{end}}"> | 
				
			||||
						<label for="repo_name">{{.i18n.Tr "repo.repo_name"}}</label> | 
				
			||||
						<input id="repo_name" name="repo_name" value="{{.repo_name}}" required> | 
				
			||||
					</div> | 
				
			||||
					<div class="inline field"> | 
				
			||||
						<label>{{.i18n.Tr "repo.visibility"}}</label> | 
				
			||||
						<div class="ui checkbox"> | 
				
			||||
							{{if .IsForcedPrivate}} | 
				
			||||
								<input name="private" type="checkbox" checked readonly> | 
				
			||||
								<label>{{.i18n.Tr "repo.visibility_helper_forced" | Safe}}</label> | 
				
			||||
							{{else}} | 
				
			||||
								<input name="private" type="checkbox" {{if .private}}checked{{end}}> | 
				
			||||
								<label>{{.i18n.Tr "repo.visibility_helper" | Safe}}</label> | 
				
			||||
							{{end}} | 
				
			||||
						</div> | 
				
			||||
					</div> | 
				
			||||
					<div class="inline field {{if .Err_Description}}error{{end}}"> | 
				
			||||
						<label for="description">{{.i18n.Tr "repo.repo_desc"}}</label> | 
				
			||||
						<textarea id="description" name="description">{{.description}}</textarea> | 
				
			||||
					</div> | 
				
			||||
 | 
				
			||||
					<div class="inline field"> | 
				
			||||
						<label></label> | 
				
			||||
						<button class="ui green button"> | 
				
			||||
							{{.i18n.Tr "repo.migrate_repo"}} | 
				
			||||
						</button> | 
				
			||||
						<a class="ui button" href="{{AppSubUrl}}/">{{.i18n.Tr "cancel"}}</a> | 
				
			||||
					</div> | 
				
			||||
				</div> | 
				
			||||
			</form> | 
				
			||||
		</div> | 
				
			||||
	</div> | 
				
			||||
</div> | 
				
			||||
{{template "base/footer" .}} | 
				
			||||
| 
		 After Width: | Height: | Size: 2.5 KiB  | 
					Loading…
					
					
				
		Reference in new issue