aboutsummaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2021-12-02 23:24:02 +0100
committerGitHub <noreply@github.com>2021-12-02 23:24:02 +0100
commit87be76213a42ab03e7f5fa9fa7d85b0f270f7cf8 (patch)
treea19e92eb59bef741cd434d829264d69e2dd3e16e /services
parent957c3fcb5949b3f7ee348d3f7f609826cedb0e8b (diff)
downloadgitea-87be76213a42ab03e7f5fa9fa7d85b0f270f7cf8.tar.gz
gitea-87be76213a42ab03e7f5fa9fa7d85b0f270f7cf8.zip
Add migrate from Codebase (#16768)
This PR adds [Codebase](https://www.codebasehq.com/) as migration source. Supported: - Milestones - Issues - Pull Requests - Comments - Labels
Diffstat (limited to 'services')
-rw-r--r--services/migrations/codebase.go652
-rw-r--r--services/migrations/codebase_test.go154
-rw-r--r--services/migrations/main_test.go1
3 files changed, 807 insertions, 0 deletions
diff --git a/services/migrations/codebase.go b/services/migrations/codebase.go
new file mode 100644
index 0000000000..8999ee363e
--- /dev/null
+++ b/services/migrations/codebase.go
@@ -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,
+ &notes,
+ )
+ 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
+}
diff --git a/services/migrations/codebase_test.go b/services/migrations/codebase_test.go
new file mode 100644
index 0000000000..ef39b9f146
--- /dev/null
+++ b/services/migrations/codebase_test.go
@@ -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)
+}
diff --git a/services/migrations/main_test.go b/services/migrations/main_test.go
index 660f6dd845..ddf73df98e 100644
--- a/services/migrations/main_test.go
+++ b/services/migrations/main_test.go
@@ -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)
}
}