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