Add migrate from Codebase (#16768)
This PR adds [Codebase](https://www.codebasehq.com/) as migration source. Supported: - Milestones - Issues - Pull Requests - Comments - Labels
This commit is contained in:
parent
957c3fcb59
commit
87be76213a
8 changed files with 943 additions and 0 deletions
652
services/migrations/codebase.go
Normal file
652
services/migrations/codebase.go
Normal file
|
@ -0,0 +1,652 @@
|
|||
// 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"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
base "code.gitea.io/gitea/modules/migration"
|
||||
"code.gitea.io/gitea/modules/proxy"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
)
|
||||
|
||||
var (
|
||||
_ base.Downloader = &CodebaseDownloader{}
|
||||
_ base.DownloaderFactory = &CodebaseDownloaderFactory{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterDownloaderFactory(&CodebaseDownloaderFactory{})
|
||||
}
|
||||
|
||||
// CodebaseDownloaderFactory defines a downloader factory
|
||||
type CodebaseDownloaderFactory struct {
|
||||
}
|
||||
|
||||
// New returns a downloader related to this factory according MigrateOptions
|
||||
func (f *CodebaseDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
|
||||
u, err := url.Parse(opts.CloneAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.User = nil
|
||||
|
||||
fields := strings.Split(strings.Trim(u.Path, "/"), "/")
|
||||
if len(fields) != 2 {
|
||||
return nil, fmt.Errorf("invalid path: %s", u.Path)
|
||||
}
|
||||
project := fields[0]
|
||||
repoName := strings.TrimSuffix(fields[1], ".git")
|
||||
|
||||
log.Trace("Create Codebase downloader. BaseURL: %v RepoName: %s", u, repoName)
|
||||
|
||||
return NewCodebaseDownloader(ctx, u, project, repoName, opts.AuthUsername, opts.AuthPassword), nil
|
||||
}
|
||||
|
||||
// GitServiceType returns the type of git service
|
||||
func (f *CodebaseDownloaderFactory) GitServiceType() structs.GitServiceType {
|
||||
return structs.CodebaseService
|
||||
}
|
||||
|
||||
type codebaseUser struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// CodebaseDownloader implements a Downloader interface to get repository informations
|
||||
// from Codebase
|
||||
type CodebaseDownloader struct {
|
||||
base.NullDownloader
|
||||
ctx context.Context
|
||||
client *http.Client
|
||||
baseURL *url.URL
|
||||
projectURL *url.URL
|
||||
project string
|
||||
repoName string
|
||||
maxIssueIndex int64
|
||||
userMap map[int64]*codebaseUser
|
||||
commitMap map[string]string
|
||||
}
|
||||
|
||||
// SetContext set context
|
||||
func (d *CodebaseDownloader) SetContext(ctx context.Context) {
|
||||
d.ctx = ctx
|
||||
}
|
||||
|
||||
// NewCodebaseDownloader creates a new downloader
|
||||
func NewCodebaseDownloader(ctx context.Context, projectURL *url.URL, project, repoName, username, password string) *CodebaseDownloader {
|
||||
baseURL, _ := url.Parse("https://api3.codebasehq.com")
|
||||
|
||||
var downloader = &CodebaseDownloader{
|
||||
ctx: ctx,
|
||||
baseURL: baseURL,
|
||||
projectURL: projectURL,
|
||||
project: project,
|
||||
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 proxy.Proxy()(req)
|
||||
},
|
||||
},
|
||||
},
|
||||
userMap: make(map[int64]*codebaseUser),
|
||||
commitMap: make(map[string]string),
|
||||
}
|
||||
|
||||
return downloader
|
||||
}
|
||||
|
||||
// FormatCloneURL add authentification into remote URLs
|
||||
func (d *CodebaseDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr string) (string, error) {
|
||||
return opts.CloneAddr, nil
|
||||
}
|
||||
|
||||
func (d *CodebaseDownloader) 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
|
||||
}
|
||||
req.Header.Add("Accept", "application/xml")
|
||||
|
||||
resp, err := d.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return xml.NewDecoder(resp.Body).Decode(&result)
|
||||
}
|
||||
|
||||
// GetRepoInfo returns repository information
|
||||
// https://support.codebasehq.com/kb/projects
|
||||
func (d *CodebaseDownloader) GetRepoInfo() (*base.Repository, error) {
|
||||
var rawRepository struct {
|
||||
XMLName xml.Name `xml:"repository"`
|
||||
Name string `xml:"name"`
|
||||
Description string `xml:"description"`
|
||||
Permalink string `xml:"permalink"`
|
||||
CloneURL string `xml:"clone-url"`
|
||||
Source string `xml:"source"`
|
||||
}
|
||||
|
||||
err := d.callAPI(
|
||||
fmt.Sprintf("/%s/%s", d.project, d.repoName),
|
||||
nil,
|
||||
&rawRepository,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &base.Repository{
|
||||
Name: rawRepository.Name,
|
||||
Description: rawRepository.Description,
|
||||
CloneURL: rawRepository.CloneURL,
|
||||
OriginalURL: d.projectURL.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMilestones returns milestones
|
||||
// https://support.codebasehq.com/kb/tickets-and-milestones/milestones
|
||||
func (d *CodebaseDownloader) GetMilestones() ([]*base.Milestone, error) {
|
||||
var rawMilestones struct {
|
||||
XMLName xml.Name `xml:"ticketing-milestone"`
|
||||
Type string `xml:"type,attr"`
|
||||
TicketingMilestone []struct {
|
||||
Text string `xml:",chardata"`
|
||||
ID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"id"`
|
||||
Identifier string `xml:"identifier"`
|
||||
Name string `xml:"name"`
|
||||
Deadline struct {
|
||||
Value string `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"deadline"`
|
||||
Description string `xml:"description"`
|
||||
Status string `xml:"status"`
|
||||
} `xml:"ticketing-milestone"`
|
||||
}
|
||||
|
||||
err := d.callAPI(
|
||||
fmt.Sprintf("/%s/milestones", d.project),
|
||||
nil,
|
||||
&rawMilestones,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var milestones = make([]*base.Milestone, 0, len(rawMilestones.TicketingMilestone))
|
||||
for _, milestone := range rawMilestones.TicketingMilestone {
|
||||
var deadline *time.Time
|
||||
if len(milestone.Deadline.Value) > 0 {
|
||||
if val, err := time.Parse("2006-01-02", milestone.Deadline.Value); err == nil {
|
||||
deadline = &val
|
||||
}
|
||||
}
|
||||
|
||||
closed := deadline
|
||||
state := "closed"
|
||||
if milestone.Status == "active" {
|
||||
closed = nil
|
||||
state = ""
|
||||
}
|
||||
|
||||
milestones = append(milestones, &base.Milestone{
|
||||
Title: milestone.Name,
|
||||
Deadline: deadline,
|
||||
Closed: closed,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
return milestones, nil
|
||||
}
|
||||
|
||||
// GetLabels returns labels
|
||||
// https://support.codebasehq.com/kb/tickets-and-milestones/statuses-priorities-and-categories
|
||||
func (d *CodebaseDownloader) GetLabels() ([]*base.Label, error) {
|
||||
var rawTypes struct {
|
||||
XMLName xml.Name `xml:"ticketing-types"`
|
||||
Type string `xml:"type,attr"`
|
||||
TicketingType []struct {
|
||||
ID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"id"`
|
||||
Name string `xml:"name"`
|
||||
} `xml:"ticketing-type"`
|
||||
}
|
||||
|
||||
err := d.callAPI(
|
||||
fmt.Sprintf("/%s/tickets/types", d.project),
|
||||
nil,
|
||||
&rawTypes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var labels = make([]*base.Label, 0, len(rawTypes.TicketingType))
|
||||
for _, label := range rawTypes.TicketingType {
|
||||
labels = append(labels, &base.Label{
|
||||
Name: label.Name,
|
||||
Color: "ffffff",
|
||||
})
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
type codebaseIssueContext struct {
|
||||
foreignID int64
|
||||
localID int64
|
||||
Comments []*base.Comment
|
||||
}
|
||||
|
||||
func (c codebaseIssueContext) LocalID() int64 {
|
||||
return c.localID
|
||||
}
|
||||
|
||||
func (c codebaseIssueContext) ForeignID() int64 {
|
||||
return c.foreignID
|
||||
}
|
||||
|
||||
// GetIssues returns issues, limits are not supported
|
||||
// https://support.codebasehq.com/kb/tickets-and-milestones
|
||||
// https://support.codebasehq.com/kb/tickets-and-milestones/updating-tickets
|
||||
func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
|
||||
var rawIssues struct {
|
||||
XMLName xml.Name `xml:"tickets"`
|
||||
Type string `xml:"type,attr"`
|
||||
Ticket []struct {
|
||||
TicketID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"ticket-id"`
|
||||
Summary string `xml:"summary"`
|
||||
TicketType string `xml:"ticket-type"`
|
||||
ReporterID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"reporter-id"`
|
||||
Reporter string `xml:"reporter"`
|
||||
Type struct {
|
||||
Name string `xml:"name"`
|
||||
} `xml:"type"`
|
||||
Status struct {
|
||||
TreatAsClosed struct {
|
||||
Value bool `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"treat-as-closed"`
|
||||
} `xml:"status"`
|
||||
Milestone struct {
|
||||
Name string `xml:"name"`
|
||||
} `xml:"milestone"`
|
||||
UpdatedAt struct {
|
||||
Value time.Time `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"updated-at"`
|
||||
CreatedAt struct {
|
||||
Value time.Time `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"created-at"`
|
||||
} `xml:"ticket"`
|
||||
}
|
||||
|
||||
err := d.callAPI(
|
||||
fmt.Sprintf("/%s/tickets", d.project),
|
||||
nil,
|
||||
&rawIssues,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
issues := make([]*base.Issue, 0, len(rawIssues.Ticket))
|
||||
for _, issue := range rawIssues.Ticket {
|
||||
var notes struct {
|
||||
XMLName xml.Name `xml:"ticket-notes"`
|
||||
Type string `xml:"type,attr"`
|
||||
TicketNote []struct {
|
||||
Content string `xml:"content"`
|
||||
CreatedAt struct {
|
||||
Value time.Time `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"created-at"`
|
||||
UpdatedAt struct {
|
||||
Value time.Time `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"updated-at"`
|
||||
ID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"id"`
|
||||
UserID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"user-id"`
|
||||
} `xml:"ticket-note"`
|
||||
}
|
||||
err := d.callAPI(
|
||||
fmt.Sprintf("/%s/tickets/%d/notes", d.project, issue.TicketID.Value),
|
||||
nil,
|
||||
¬es,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
comments := make([]*base.Comment, 0, len(notes.TicketNote))
|
||||
for _, note := range notes.TicketNote {
|
||||
if len(note.Content) == 0 {
|
||||
continue
|
||||
}
|
||||
poster := d.tryGetUser(note.UserID.Value)
|
||||
comments = append(comments, &base.Comment{
|
||||
IssueIndex: issue.TicketID.Value,
|
||||
PosterID: poster.ID,
|
||||
PosterName: poster.Name,
|
||||
PosterEmail: poster.Email,
|
||||
Content: note.Content,
|
||||
Created: note.CreatedAt.Value,
|
||||
Updated: note.UpdatedAt.Value,
|
||||
})
|
||||
}
|
||||
if len(comments) == 0 {
|
||||
comments = append(comments, &base.Comment{})
|
||||
}
|
||||
|
||||
state := "open"
|
||||
if issue.Status.TreatAsClosed.Value {
|
||||
state = "closed"
|
||||
}
|
||||
poster := d.tryGetUser(issue.ReporterID.Value)
|
||||
issues = append(issues, &base.Issue{
|
||||
Title: issue.Summary,
|
||||
Number: issue.TicketID.Value,
|
||||
PosterName: poster.Name,
|
||||
PosterEmail: poster.Email,
|
||||
Content: comments[0].Content,
|
||||
Milestone: issue.Milestone.Name,
|
||||
State: state,
|
||||
Created: issue.CreatedAt.Value,
|
||||
Updated: issue.UpdatedAt.Value,
|
||||
Labels: []*base.Label{
|
||||
{Name: issue.Type.Name}},
|
||||
Context: codebaseIssueContext{
|
||||
foreignID: issue.TicketID.Value,
|
||||
localID: issue.TicketID.Value,
|
||||
Comments: comments[1:],
|
||||
},
|
||||
})
|
||||
|
||||
if d.maxIssueIndex < issue.TicketID.Value {
|
||||
d.maxIssueIndex = issue.TicketID.Value
|
||||
}
|
||||
}
|
||||
|
||||
return issues, true, nil
|
||||
}
|
||||
|
||||
// GetComments returns comments
|
||||
func (d *CodebaseDownloader) GetComments(opts base.GetCommentOptions) ([]*base.Comment, bool, error) {
|
||||
context, ok := opts.Context.(codebaseIssueContext)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("unexpected comment context: %+v", opts.Context)
|
||||
}
|
||||
|
||||
return context.Comments, true, nil
|
||||
}
|
||||
|
||||
// GetPullRequests returns pull requests
|
||||
// https://support.codebasehq.com/kb/repositories/merge-requests
|
||||
func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
|
||||
var rawMergeRequests struct {
|
||||
XMLName xml.Name `xml:"merge-requests"`
|
||||
Type string `xml:"type,attr"`
|
||||
MergeRequest []struct {
|
||||
ID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"id"`
|
||||
} `xml:"merge-request"`
|
||||
}
|
||||
|
||||
err := d.callAPI(
|
||||
fmt.Sprintf("/%s/%s/merge_requests", d.project, d.repoName),
|
||||
map[string]string{
|
||||
"query": `"Target Project" is "` + d.repoName + `"`,
|
||||
"offset": strconv.Itoa((page - 1) * perPage),
|
||||
"count": strconv.Itoa(perPage),
|
||||
},
|
||||
&rawMergeRequests,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
pullRequests := make([]*base.PullRequest, 0, len(rawMergeRequests.MergeRequest))
|
||||
for i, mr := range rawMergeRequests.MergeRequest {
|
||||
var rawMergeRequest struct {
|
||||
XMLName xml.Name `xml:"merge-request"`
|
||||
ID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"id"`
|
||||
SourceRef string `xml:"source-ref"`
|
||||
TargetRef string `xml:"target-ref"`
|
||||
Subject string `xml:"subject"`
|
||||
Status string `xml:"status"`
|
||||
UserID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"user-id"`
|
||||
CreatedAt struct {
|
||||
Value time.Time `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"created-at"`
|
||||
UpdatedAt struct {
|
||||
Value time.Time `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"updated-at"`
|
||||
Comments struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Comment []struct {
|
||||
Content string `xml:"content"`
|
||||
UserID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"user-id"`
|
||||
Action struct {
|
||||
Value string `xml:",chardata"`
|
||||
Nil string `xml:"nil,attr"`
|
||||
} `xml:"action"`
|
||||
CreatedAt struct {
|
||||
Value time.Time `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"created-at"`
|
||||
} `xml:"comment"`
|
||||
} `xml:"comments"`
|
||||
}
|
||||
err := d.callAPI(
|
||||
fmt.Sprintf("/%s/%s/merge_requests/%d", d.project, d.repoName, mr.ID.Value),
|
||||
nil,
|
||||
&rawMergeRequest,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
number := d.maxIssueIndex + int64(i) + 1
|
||||
|
||||
state := "open"
|
||||
merged := false
|
||||
var closeTime *time.Time
|
||||
var mergedTime *time.Time
|
||||
if rawMergeRequest.Status != "new" {
|
||||
state = "closed"
|
||||
closeTime = &rawMergeRequest.UpdatedAt.Value
|
||||
}
|
||||
|
||||
comments := make([]*base.Comment, 0, len(rawMergeRequest.Comments.Comment))
|
||||
for _, comment := range rawMergeRequest.Comments.Comment {
|
||||
if len(comment.Content) == 0 {
|
||||
if comment.Action.Value == "merging" {
|
||||
merged = true
|
||||
mergedTime = &comment.CreatedAt.Value
|
||||
}
|
||||
continue
|
||||
}
|
||||
poster := d.tryGetUser(comment.UserID.Value)
|
||||
comments = append(comments, &base.Comment{
|
||||
IssueIndex: number,
|
||||
PosterID: poster.ID,
|
||||
PosterName: poster.Name,
|
||||
PosterEmail: poster.Email,
|
||||
Content: comment.Content,
|
||||
Created: comment.CreatedAt.Value,
|
||||
Updated: comment.CreatedAt.Value,
|
||||
})
|
||||
}
|
||||
if len(comments) == 0 {
|
||||
comments = append(comments, &base.Comment{})
|
||||
}
|
||||
|
||||
poster := d.tryGetUser(rawMergeRequest.UserID.Value)
|
||||
|
||||
pullRequests = append(pullRequests, &base.PullRequest{
|
||||
Title: rawMergeRequest.Subject,
|
||||
Number: number,
|
||||
PosterName: poster.Name,
|
||||
PosterEmail: poster.Email,
|
||||
Content: comments[0].Content,
|
||||
State: state,
|
||||
Created: rawMergeRequest.CreatedAt.Value,
|
||||
Updated: rawMergeRequest.UpdatedAt.Value,
|
||||
Closed: closeTime,
|
||||
Merged: merged,
|
||||
MergedTime: mergedTime,
|
||||
Head: base.PullRequestBranch{
|
||||
Ref: rawMergeRequest.SourceRef,
|
||||
SHA: d.getHeadCommit(rawMergeRequest.SourceRef),
|
||||
RepoName: d.repoName,
|
||||
},
|
||||
Base: base.PullRequestBranch{
|
||||
Ref: rawMergeRequest.TargetRef,
|
||||
SHA: d.getHeadCommit(rawMergeRequest.TargetRef),
|
||||
RepoName: d.repoName,
|
||||
},
|
||||
Context: codebaseIssueContext{
|
||||
foreignID: rawMergeRequest.ID.Value,
|
||||
localID: number,
|
||||
Comments: comments[1:],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return pullRequests, true, nil
|
||||
}
|
||||
|
||||
// GetReviews returns pull requests reviews
|
||||
func (d *CodebaseDownloader) GetReviews(context base.IssueContext) ([]*base.Review, error) {
|
||||
return []*base.Review{}, nil
|
||||
}
|
||||
|
||||
// GetTopics return repository topics
|
||||
func (d *CodebaseDownloader) GetTopics() ([]string, error) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
func (d *CodebaseDownloader) tryGetUser(userID int64) *codebaseUser {
|
||||
if len(d.userMap) == 0 {
|
||||
var rawUsers struct {
|
||||
XMLName xml.Name `xml:"users"`
|
||||
Type string `xml:"type,attr"`
|
||||
User []struct {
|
||||
EmailAddress string `xml:"email-address"`
|
||||
ID struct {
|
||||
Value int64 `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
} `xml:"id"`
|
||||
LastName string `xml:"last-name"`
|
||||
FirstName string `xml:"first-name"`
|
||||
Username string `xml:"username"`
|
||||
} `xml:"user"`
|
||||
}
|
||||
|
||||
err := d.callAPI(
|
||||
"/users",
|
||||
nil,
|
||||
&rawUsers,
|
||||
)
|
||||
if err == nil {
|
||||
for _, user := range rawUsers.User {
|
||||
d.userMap[user.ID.Value] = &codebaseUser{
|
||||
Name: user.Username,
|
||||
Email: user.EmailAddress,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user, ok := d.userMap[userID]
|
||||
if !ok {
|
||||
user = &codebaseUser{
|
||||
Name: fmt.Sprintf("User %d", userID),
|
||||
}
|
||||
d.userMap[userID] = user
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func (d *CodebaseDownloader) getHeadCommit(ref string) string {
|
||||
commitRef, ok := d.commitMap[ref]
|
||||
if !ok {
|
||||
var rawCommits struct {
|
||||
XMLName xml.Name `xml:"commits"`
|
||||
Type string `xml:"type,attr"`
|
||||
Commit []struct {
|
||||
Ref string `xml:"ref"`
|
||||
} `xml:"commit"`
|
||||
}
|
||||
err := d.callAPI(
|
||||
fmt.Sprintf("/%s/%s/commits/%s", d.project, d.repoName, ref),
|
||||
nil,
|
||||
&rawCommits,
|
||||
)
|
||||
if err == nil && len(rawCommits.Commit) > 0 {
|
||||
commitRef = rawCommits.Commit[0].Ref
|
||||
d.commitMap[ref] = commitRef
|
||||
}
|
||||
}
|
||||
return commitRef
|
||||
}
|
154
services/migrations/codebase_test.go
Normal file
154
services/migrations/codebase_test.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
// 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/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
base "code.gitea.io/gitea/modules/migration"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCodebaseDownloadRepo(t *testing.T) {
|
||||
// Skip tests if Codebase token is not found
|
||||
cloneUser := os.Getenv("CODEBASE_CLONE_USER")
|
||||
clonePassword := os.Getenv("CODEBASE_CLONE_PASSWORD")
|
||||
apiUser := os.Getenv("CODEBASE_API_USER")
|
||||
apiPassword := os.Getenv("CODEBASE_API_TOKEN")
|
||||
if apiUser == "" || apiPassword == "" {
|
||||
t.Skip("skipped test because a CODEBASE_ variable was not in the environment")
|
||||
}
|
||||
|
||||
cloneAddr := "https://gitea-test.codebasehq.com/gitea-test/test.git"
|
||||
u, _ := url.Parse(cloneAddr)
|
||||
if cloneUser != "" {
|
||||
u.User = url.UserPassword(cloneUser, clonePassword)
|
||||
}
|
||||
|
||||
factory := &CodebaseDownloaderFactory{}
|
||||
downloader, err := factory.New(context.Background(), base.MigrateOptions{
|
||||
CloneAddr: u.String(),
|
||||
AuthUsername: apiUser,
|
||||
AuthPassword: apiPassword,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(fmt.Sprintf("Error creating Codebase downloader: %v", err))
|
||||
}
|
||||
repo, err := downloader.GetRepoInfo()
|
||||
assert.NoError(t, err)
|
||||
assertRepositoryEqual(t, &base.Repository{
|
||||
Name: "test",
|
||||
Owner: "",
|
||||
Description: "Repository Description",
|
||||
CloneURL: "git@codebasehq.com:gitea-test/gitea-test/test.git",
|
||||
OriginalURL: cloneAddr,
|
||||
}, repo)
|
||||
|
||||
milestones, err := downloader.GetMilestones()
|
||||
assert.NoError(t, err)
|
||||
assertMilestonesEqual(t, []*base.Milestone{
|
||||
{
|
||||
Title: "Milestone1",
|
||||
Deadline: timePtr(time.Date(2021, time.September, 16, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
{
|
||||
Title: "Milestone2",
|
||||
Deadline: timePtr(time.Date(2021, time.September, 17, 0, 0, 0, 0, time.UTC)),
|
||||
Closed: timePtr(time.Date(2021, time.September, 17, 0, 0, 0, 0, time.UTC)),
|
||||
State: "closed",
|
||||
},
|
||||
}, milestones)
|
||||
|
||||
labels, err := downloader.GetLabels()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, 4)
|
||||
|
||||
issues, isEnd, err := downloader.GetIssues(1, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isEnd)
|
||||
assertIssuesEqual(t, []*base.Issue{
|
||||
{
|
||||
Number: 2,
|
||||
Title: "Open Ticket",
|
||||
Content: "Open Ticket Message",
|
||||
PosterName: "gitea-test-43",
|
||||
PosterEmail: "gitea-codebase@smack.email",
|
||||
State: "open",
|
||||
Created: time.Date(2021, time.September, 26, 19, 19, 14, 0, time.UTC),
|
||||
Updated: time.Date(2021, time.September, 26, 19, 19, 34, 0, time.UTC),
|
||||
Labels: []*base.Label{
|
||||
{
|
||||
Name: "Feature",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Number: 1,
|
||||
Title: "Closed Ticket",
|
||||
Content: "Closed Ticket Message",
|
||||
PosterName: "gitea-test-43",
|
||||
PosterEmail: "gitea-codebase@smack.email",
|
||||
State: "closed",
|
||||
Milestone: "Milestone1",
|
||||
Created: time.Date(2021, time.September, 26, 19, 18, 33, 0, time.UTC),
|
||||
Updated: time.Date(2021, time.September, 26, 19, 18, 55, 0, time.UTC),
|
||||
Labels: []*base.Label{
|
||||
{
|
||||
Name: "Bug",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, issues)
|
||||
|
||||
comments, _, err := downloader.GetComments(base.GetCommentOptions{
|
||||
Context: issues[0].Context,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assertCommentsEqual(t, []*base.Comment{
|
||||
{
|
||||
IssueIndex: 2,
|
||||
PosterName: "gitea-test-43",
|
||||
PosterEmail: "gitea-codebase@smack.email",
|
||||
Created: time.Date(2021, time.September, 26, 19, 19, 34, 0, time.UTC),
|
||||
Updated: time.Date(2021, time.September, 26, 19, 19, 34, 0, time.UTC),
|
||||
Content: "open comment",
|
||||
},
|
||||
}, comments)
|
||||
|
||||
prs, _, err := downloader.GetPullRequests(1, 1)
|
||||
assert.NoError(t, err)
|
||||
assertPullRequestsEqual(t, []*base.PullRequest{
|
||||
{
|
||||
Number: 3,
|
||||
Title: "Readme Change",
|
||||
Content: "Merge Request comment",
|
||||
PosterName: "gitea-test-43",
|
||||
PosterEmail: "gitea-codebase@smack.email",
|
||||
State: "open",
|
||||
Created: time.Date(2021, time.September, 26, 20, 25, 47, 0, time.UTC),
|
||||
Updated: time.Date(2021, time.September, 26, 20, 25, 47, 0, time.UTC),
|
||||
Head: base.PullRequestBranch{
|
||||
Ref: "readme-mr",
|
||||
SHA: "1287f206b888d4d13540e0a8e1c07458f5420059",
|
||||
RepoName: "test",
|
||||
},
|
||||
Base: base.PullRequestBranch{
|
||||
Ref: "master",
|
||||
SHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||
RepoName: "test",
|
||||
},
|
||||
},
|
||||
}, prs)
|
||||
|
||||
rvs, err := downloader.GetReviews(prs[0].Context)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, rvs)
|
||||
}
|
|
@ -32,6 +32,7 @@ func assertTimePtrEqual(t *testing.T, expected, actual *time.Time) {
|
|||
if expected == nil {
|
||||
assert.Nil(t, actual)
|
||||
} else {
|
||||
assert.NotNil(t, actual)
|
||||
assertTimeEqual(t, *expected, *actual)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue