Add migrate from Codebase (#16768)
This PR adds [Codebase](https://www.codebasehq.com/) as migration source. Supported: - Milestones - Issues - Pull Requests - Comments - Labelstokarchuk/v1.17
parent
957c3fcb59
commit
87be76213a
After Width: | Height: | Size: 5.4 KiB |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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_merge_requests" | 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: 5.5 KiB |
Loading…
Reference in new issue