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