summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--modules/structs/repo.go4
-rw-r--r--options/locale/locale_en-US.ini1
-rw-r--r--public/img/svg/gitea-codebase.svg1
-rw-r--r--services/migrations/codebase.go652
-rw-r--r--services/migrations/codebase_test.go154
-rw-r--r--services/migrations/main_test.go1
-rw-r--r--templates/repo/migrate/codebase.tmpl117
-rw-r--r--web_src/svg/gitea-codebase.svg13
8 files changed, 943 insertions, 0 deletions
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 8482a2128d..b1a3781d05 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -250,6 +250,7 @@ const (
GogsService // 5 gogs service
OneDevService // 6 onedev service
GitBucketService // 7 gitbucket service
+ CodebaseService // 8 codebase service
)
// Name represents the service type's name
@@ -273,6 +274,8 @@ func (gt GitServiceType) Title() string {
return "OneDev"
case GitBucketService:
return "GitBucket"
+ case CodebaseService:
+ return "Codebase"
case PlainGitService:
return "Git"
}
@@ -330,5 +333,6 @@ var (
GogsService,
OneDevService,
GitBucketService,
+ CodebaseService,
}
)
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index a2f6389e5c..0a3c0c064c 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -926,6 +926,7 @@ migrate.gitlab.description = Migrate data from gitlab.com or other GitLab instan
migrate.gitea.description = Migrate data from gitea.com or other Gitea instances.
migrate.gogs.description = Migrate data from notabug.org or other Gogs instances.
migrate.onedev.description = Migrate data from code.onedev.io or other OneDev instances.
+migrate.codebase.description = Migrate data from codebasehq.com.
migrate.gitbucket.description = Migrate data from GitBucket instances.
migrate.migrating_git = Migrating Git Data
migrate.migrating_topics = Migrating Topics
diff --git a/public/img/svg/gitea-codebase.svg b/public/img/svg/gitea-codebase.svg
new file mode 100644
index 0000000000..2438230db2
--- /dev/null
+++ b/public/img/svg/gitea-codebase.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 2516 543" class="svg gitea-codebase" width="16" height="16" aria-hidden="true"><path d="M760 121.1v80l-6.2-7.2c-11.4-13.2-30.2-25.7-48.3-32.2-13.4-4.7-22.6-6.1-41-6.1-24.5 0-39 3.3-60 13.3-28.8 13.8-54.9 43.1-67.4 75.8L535 250l33 33.4c27.4 27.7 33 33.8 33 36.1 0 2.4-5.7 8.5-34.1 37.1l-34.2 34.3 2.8 7.8c17.3 49.2 57.4 83.9 106 91.9 12.2 1.9 36.1 1.5 47.5-1 24.2-5.3 44.8-16.5 62.2-33.8l8.8-8.7V484h101V41H760v80.1zM716.8 248c14.5 3.5 27.4 10.4 38 20.3l5.2 5v99.6l-5.1 5c-6.8 6.7-14.2 11.5-24.7 16.1-11.5 4.9-21 7-32.6 7-12.6 0-18.2-1.3-29.6-6.7-20.6-9.9-35.1-30.8-39.1-56.4-1.6-10.5-.6-30.5 2.1-39.9 2.8-9.8 8.8-21.1 15.4-29.2 8.3-10 17.4-16 31.1-20.4 9.6-3.1 27.4-3.3 39.3-.4zM1289 262.5V484h35v-50.2l4.2 5.4c20.9 26.6 54.4 46.5 86.7 51.3 44.2 6.7 87.5-7.2 117.1-37.5 15-15.3 29-38.7 36-59.9 13.6-41.5 13.4-100.2-.6-140.2-17.5-50.4-54.4-84.6-102.4-95-14.7-3.2-41.6-3.2-56.7 0-31.4 6.6-58.1 23.3-78.4 49.1l-6.4 8.2-.3-87.1-.2-87.1h-34v221.5zm155-75.4c12.2 1.5 19.3 3.5 29.6 8.4 17.8 8.3 31.2 19.8 41.9 36 14.7 22 22.2 45.7 24.7 77.5 2.5 32.6-3.2 65.9-15.4 90.5-15.3 30.7-37.3 49.5-67.6 57.7-9.6 2.6-33 3.5-45.6 1.9-25.9-3.5-53.7-17.3-72.6-36.1-4.8-4.7-10.3-11-12.3-14l-3.7-5.5V245.3l4.9-6.9c9.4-13.4 25.8-27.2 44.1-37.2 22.6-12.3 47.5-17.2 72-14.1zM167.5 155.6c-29.2 3-49.7 8.7-70.2 19.4-43.4 22.6-72.8 61.4-82.9 109.4-3.5 16.4-4.4 48.2-2 65.7 5 35.7 18.6 64.3 42 88.7 21.3 22.1 43.4 35.8 73 45.2 28.2 8.9 67.6 10.5 98.4 4 19.8-4.2 41.6-13.1 57.6-23.8 7.1-4.7 26.4-23 30.4-28.8l2.2-3.2-32.8-30.9-32.7-30.8-9.5 9.5c-7.5 7.6-11.1 10.4-17.1 13.3-11.2 5.5-21.1 7.7-34.4 7.7-35.9 0-63.7-21.3-72.6-55.6-2.6-10.1-2.9-32.5-.6-42.5 6.2-26.1 23-45.1 47.6-53.7 6.8-2.3 9.4-2.7 23.6-3 14.5-.3 16.8-.1 24.1 2.1 12.3 3.5 22.3 9.8 31.2 19.3l7.6 8.1 32.7-30.5c26.8-24.9 32.6-30.8 31.9-32.2-1.8-3.3-15.1-17.3-21.8-22.9-13.7-11.5-30.2-20.2-50.1-26.5-19.3-6.1-30.1-7.8-52.1-8.1-10.7-.2-21.3-.1-23.5.1zM1042.7 156.1c-33.4 3.2-68.4 18.2-91.1 39-22.6 20.8-38.6 44.6-48 71.8-7 20-8 27.5-8 55.6 0 26.7.8 34.3 5.9 52.6 10.1 35.8 32.1 65.7 64.5 87.4 31.8 21.4 70.3 30.8 115.6 28.5 21.2-1.1 37.4-3.7 57.2-9.2 26.3-7.2 42.8-15.3 62.1-30.7l2.5-1.9-22.3-33.5-22.3-33.5-8.1 5.5c-10.3 7.1-19.4 11.3-32.7 15.3-15.2 4.5-25.5 6-40.5 6-16.4 0-27.9-2.6-42.3-9.6-8-4-11.1-6.2-17.7-12.8-8.2-8.2-14.6-18.7-16.1-26.4l-.6-3.2H1228v-21c0-31.3-3-52.3-10.6-74.2-8.6-24.5-21.1-44.4-39.6-62.4-25.9-25.4-56.5-39.6-93.7-43.3-11.8-1.2-28.5-1.2-41.4 0zm52.7 84.3c20.5 5.7 36.1 21.1 41 40.6l1.3 5.5-65 .3c-35.8.1-65.2 0-65.5-.3-1-1 3-13.1 6.3-19 8.7-15.6 22.4-25.1 41.3-28.5 7.4-1.4 33.8-.4 40.6 1.4zM1733 156c-25.4 3.1-50 12.3-71.5 26.6-8.2 5.5-21.9 16.8-27.7 23l-2.7 2.9 8.5 11c4.7 6.1 8.9 11.4 9.3 11.8.5.5 4.8-3.2 9.7-8 16-15.9 37.1-28 59.4-33.8 9-2.4 13.1-2.8 28.2-3.2 23.7-.7 34 1.2 50.4 9.4 15.6 7.7 26.4 18.1 33 31.8 6.4 13.5 6.7 15.1 7.1 57.2.3 21.1.2 38.3-.2 38.3s-4.2-3.3-8.4-7.3c-18.2-17.3-43.3-29.9-68-34.3-13.1-2.3-35.1-3.4-46-2.3-37.6 3.8-72.9 26.4-87.9 56.4-3.9 7.8-7.7 20-9.2 29.6-1.9 11.6-.8 38.3 1.9 48.2 8.7 32.6 33 58.3 67.1 71 15.4 5.7 23.2 7 42.5 7 40.5.1 72.7-12 99.9-37.5l8.6-8V484h34V365.7c0-97.8-.3-119.8-1.5-127.7-4.2-28.2-16.3-47.8-38.2-62.2-21-13.9-42-19.7-73.3-20.3-9.6-.2-20.9 0-25 .5zm26 151c29.9 3.7 56.2 17.1 73.1 37.3l4.9 5.9v71l-10.2 10.2c-21 20.9-44.6 30.8-77.4 32.5-24 1.3-37.5-.9-52.8-8.5-10.1-5.1-15.3-8.9-23.7-17.5-8.7-8.8-15.6-21.4-18.5-33.4-2.4-9.9-2.4-30-.1-38.5 8.2-30.7 34.9-53.7 68.7-59.3 5.2-.9 27.9-.7 36 .3zM2334.5 155.7c-38.2 3.5-73.9 22.8-98.8 53.5-32.7 40.5-45 88.3-37.2 144.7 5.3 37.4 20.1 67.9 45.4 93.2 12 11.9 23.4 20.4 37 27.3 13.3 6.7 21.6 9.7 36.1 12.9 50.8 11.3 100.4 1.8 140.2-26.6 10.2-7.3 24-19.5 23.6-20.9-.8-2.2-16.4-21.7-17.5-21.7-.6-.1-1.8.7-2.5 1.7-2.6 3.6-14.4 13.6-22.3 18.7-16.1 10.6-34.2 17.8-52.9 21.2-13.1 2.4-42.2 2.4-53.6-.1-22.8-4.8-41.4-14.5-57.7-29.9-21.1-20.1-35.8-49.7-39.8-80.4-.8-6.3-1.5-12.3-1.5-13.4 0-1.9 1.8-1.9 134-1.9h134v-11.8c0-41.6-12.4-81.6-34.6-111.4-7.7-10.3-23.7-25.8-33.1-32.1-14.4-9.6-31.5-16.7-48.3-20.1-14.4-2.9-36.7-4.2-50.5-2.9zm35.1 30.4c17.1 2.4 34.5 9.3 47.6 19.1 18.6 13.8 34.8 37.8 42.8 63.7 3.1 9.9 6 26.3 6 33.8v4.3h-233.2l.7-6.3c4.9-45.2 28.8-84.1 63.6-103.6 7.8-4.3 20.6-8.9 29.7-10.5 9.4-1.7 32.3-2 42.8-.5zM2011.5 157.5c-19.4 3-39.4 10.7-51.5 19.7-25.6 19.3-36.9 41.9-35.8 71.7.4 9.6 1 13.1 3.5 20.2 5.5 15.9 16 28.3 33.6 39.6 14.5 9.4 25.9 13.3 73.7 25.2 37.7 9.4 46.8 12.5 62.6 21.1 19.6 10.8 28.3 24 28.4 43 0 29.9-22.9 54-58.5 61.5-12.1 2.6-37.7 3.1-49.5 1.1-13.4-2.3-24.8-6-37.5-12-13.8-6.6-23.5-13.4-34.8-24.5l-8.8-8.6-2.2 2.6c-1.2 1.5-5.9 7.2-10.4 12.7l-8.3 10 9.9 9.5c21.4 20.5 46.3 33 77.9 38.9 10 1.9 15.3 2.2 35.7 2.2 25.8 0 33.1-.8 50.5-6.1 42.9-12.9 69-46.3 69-88.5 0-22.7-8.1-42.2-23.8-57-20.2-19-33.7-24.7-96.2-40.4-28.4-7.1-38.8-10.7-54.4-18.8-18.4-9.5-25.6-19-26.4-35-.8-15.9 3.8-27.9 15.3-39.2 8.4-8.3 20.7-15 34-18.5 11-3 39.5-3.8 54-1.5 27.5 4.2 47.6 14.2 63.9 31.8 6.6 7.1 7.8 8 9.1 6.7.7-.8 5.2-6.1 10-11.9l8.6-10.5-7.3-6.9c-22.8-21.5-47.5-33.3-79.9-38-12.8-1.9-42.7-2-54.4-.1z"/><path d="M373.6 186.1c-4.8 5-35.3 36.3-67.9 69.5-51.1 52.3-59.2 60.9-59.2 63.5 0 2.5 9.1 12.2 68.3 72.4l68.3 69.5 11.5-12c6.3-6.5 12.4-13 13.5-14.4 1.1-1.4 2.6-2.6 3.2-2.6.7 0 7.7 6.6 15.8 14.6l14.6 14.6 68.9-69.6c51.4-51.9 69-70.2 69.2-72.2.3-2.3-2.7-5.8-22-25.3-12.3-12.4-43.4-43.9-69.1-70l-46.6-47.4-15.5 14.7-15.4 14.6-13.8-14.5c-7.5-8-14.1-14.5-14.5-14.5-.3 0-4.6 4.1-9.3 9.1zm72.8 100.8 31.5 32.4-31.6 32.4c-17.4 17.8-32 32.2-32.4 32.1-.4-.2-15.6-14.7-33.7-32.3l-33.1-32 32.7-32.8c18-18 33.2-32.6 33.9-32.5.7.2 15.4 14.9 32.7 32.7z" fill="#e22c2c"/></svg> \ No newline at end of file
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)
}
}
diff --git a/templates/repo/migrate/codebase.tmpl b/templates/repo/migrate/codebase.tmpl
new file mode 100644
index 0000000000..10727229c6
--- /dev/null
+++ b/templates/repo/migrate/codebase.tmpl
@@ -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" .}}
diff --git a/web_src/svg/gitea-codebase.svg b/web_src/svg/gitea-codebase.svg
new file mode 100644
index 0000000000..13c67e7e7f
--- /dev/null
+++ b/web_src/svg/gitea-codebase.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2516.000000 543.000000">
+<g transform="translate(0.000000,543.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
+<path d="M7600 4219 l0 -800 -62 72 c-114 132 -302 257 -483 322 -134 47 -226 61 -410 61 -245 0 -390 -33 -600 -133 -288 -138 -549 -431 -674 -758 l-21 -53 330 -334 c274 -277 330 -338 330 -361 0 -24 -57 -85 -341 -371 l-342 -343 28 -78 c173 -492 574 -839 1060 -919 122 -19 361 -15 475 10 242 53 448 165 622 338 l88 87 0 -184 0 -185 505 0 505 0 0 2215 0 2215 -505 0 -505 0 0 -801z m-432 -1269 c145 -35 274 -104 380 -203 l52 -50 0 -498 0 -498 -51 -50 c-68 -67 -142 -115 -247 -161 -115 -49 -210 -70 -326 -70 -126 0 -182 13 -296 67 -206 99 -351 308 -391 564 -16 105 -6 305 21 399 28 98 88 211 154 292 83 100 174 160 311 204 96 31 274 33 393 4z"/>
+<path d="M12890 2805 l0 -2215 175 0 175 0 0 251 0 251 42 -54 c209 -266 544 -465 867 -513 442 -67 875 72 1171 375 150 153 290 387 360 599 136 415 134 1002 -6 1402 -175 504 -544 846 -1024 950 -147 32 -416 32 -567 0 -314 -66 -581 -233 -784 -491 l-64 -82 -3 871 -2 871 -170 0 -170 0 0 -2215z m1550 754 c122 -15 193 -35 296 -84 178 -83 312 -198 419 -360 147 -220 222 -457 247 -775 25 -326 -32 -659 -154 -905 -153 -307 -373 -495 -676 -577 -96 -26 -330 -35 -456 -19 -259 35 -537 173 -726 361 -48 47 -103 110 -123 140 l-37 55 0 791 0 791 49 69 c94 134 258 272 441 372 226 123 475 172 720 141z"/>
+<path d="M1675 3874 c-292 -30 -497 -87 -702 -194 -434 -226 -728 -614 -829 -1094 -35 -164 -44 -482 -20 -657 50 -357 186 -643 420 -887 213 -221 434 -358 730 -452 282 -89 676 -105 984 -40 198 42 416 131 576 238 71 47 264 230 304 288 l22 32 -328 309 -327 308 -95 -95 c-75 -76 -111 -104 -171 -133 -112 -55 -211 -77 -344 -77 -359 0 -637 213 -726 556 -26 101 -29 325 -6 425 62 261 230 451 476 537 68 23 94 27 236 30 145 3 168 1 241 -21 123 -35 223 -98 312 -193 l76 -81 327 305 c268 249 326 308 319 322 -18 33 -151 173 -218 229 -137 115 -302 202 -501 265 -193 61 -301 78 -521 81 -107 2 -213 1 -235 -1z"/>
+<path d="M10427 3869 c-334 -32 -684 -182 -911 -390 -226 -208 -386 -446 -480 -718 -70 -200 -80 -275 -80 -556 0 -267 8 -343 59 -526 101 -358 321 -657 645 -874 318 -214 703 -308 1156 -285 212 11 374 37 572 92 263 72 428 153 621 307 l25 19 -223 335 -223 335 -81 -55 c-103 -71 -194 -113 -327 -153 -152 -45 -255 -60 -405 -60 -164 0 -279 26 -423 96 -80 40 -111 62 -177 128 -82 82 -146 187 -161 264 l-6 32 1136 0 1136 0 0 210 c0 313 -30 523 -106 742 -86 245 -211 444 -396 624 -259 254 -565 396 -937 433 -118 12 -285 12 -414 0z m527 -843 c205 -57 361 -211 410 -406 l13 -55 -650 -3 c-358 -1 -652 0 -655 3 -10 10 30 131 63 190 87 156 224 251 413 285 74 14 338 4 406 -14z"/>
+<path d="M17330 3870 c-254 -31 -500 -123 -715 -266 -82 -55 -219 -168 -277 -230 l-27 -29 85 -110 c47 -61 89 -114 93 -118 5 -5 48 32 97 80 160 159 371 280 594 338 90 24 131 28 282 32 237 7 340 -12 504 -94 156 -77 264 -181 330 -318 64 -135 67 -151 71 -572 3 -211 2 -383 -2 -383 -4 0 -42 33 -84 73 -182 173 -433 299 -680 343 -131 23 -351 34 -460 23 -376 -38 -729 -264 -879 -564 -39 -78 -77 -200 -92 -296 -19 -116 -8 -383 19 -482 87 -326 330 -583 671 -710 154 -57 232 -70 425 -70 405 -1 727 120 999 375 l86 80 0 -191 0 -191 170 0 170 0 0 1183 c0 978 -3 1198 -15 1277 -42 282 -163 478 -382 622 -210 139 -420 197 -733 203 -96 2 -209 0 -250 -5z m260 -1510 c299 -37 562 -171 731 -373 l49 -59 0 -355 0 -355 -102 -102 c-210 -209 -446 -308 -774 -325 -240 -13 -375 9 -528 85 -101 51 -153 89 -237 175 -87 88 -156 214 -185 334 -24 99 -24 300 -1 385 82 307 349 537 687 593 52 9 279 7 360 -3z"/>
+<path d="M23345 3873 c-382 -35 -739 -228 -988 -535 -327 -405 -450 -883 -372 -1447 53 -374 201 -679 454 -932 120 -119 234 -204 370 -273 133 -67 216 -97 361 -129 508 -113 1004 -18 1402 266 102 73 240 195 236 209 -8 22 -164 217 -175 217 -6 1 -18 -7 -25 -17 -26 -36 -144 -136 -223 -187 -161 -106 -342 -178 -529 -212 -131 -24 -422 -24 -536 1 -228 48 -414 145 -577 299 -211 201 -358 497 -398 804 -8 63 -15 123 -15 134 0 19 18 19 1340 19 l1340 0 0 118 c0 416 -124 816 -346 1114 -77 103 -237 258 -331 321 -144 96 -315 167 -483 201 -144 29 -367 42 -505 29z m351 -304 c171 -24 345 -93 476 -191 186 -138 348 -378 428 -637 31 -99 60 -263 60 -338 l0 -43 -1166 0 -1166 0 7 63 c49 452 288 841 636 1036 78 43 206 89 297 105 94 17 323 20 428 5z"/>
+<path d="M20115 3855 c-194 -30 -394 -107 -515 -197 -256 -193 -369 -419 -358 -717 4 -96 10 -131 35 -202 55 -159 160 -283 336 -396 145 -94 259 -133 737 -252 377 -94 468 -125 626 -211 196 -108 283 -240 284 -430 0 -299 -229 -540 -585 -615 -121 -26 -377 -31 -495 -11 -134 23 -248 60 -375 120 -138 66 -235 134 -348 245 l-88 86 -22 -26 c-12 -15 -59 -72 -104 -127 l-83 -100 99 -95 c214 -205 463 -330 779 -389 100 -19 153 -22 357 -22 258 0 331 8 505 61 429 129 690 463 690 885 0 227 -81 422 -238 570 -202 190 -337 247 -962 404 -284 71 -388 107 -544 188 -184 95 -256 190 -264 350 -8 159 38 279 153 392 84 83 207 150 340 185 110 30 395 38 540 15 275 -42 476 -142 639 -318 66 -71 78 -80 91 -67 7 8 52 61 100 119 l86 105 -73 69 c-228 215 -475 333 -799 380 -128 19 -427 20 -544 1z"/>
+<path d="M3736 3569 c-48 -50 -353 -363 -679 -695 -511 -523 -592 -609 -592 -635 0 -25 91 -122 683 -724 l683 -695 115 120 c63 65 124 130 135 144 11 14 26 26 32 26 7 0 77 -66 158 -146 l146 -146 689 696 c514 519 690 702 692 722 3 23 -27 58 -220 253 -123 124 -434 439 -691 700 l-466 474 -155 -147 -154 -146 -138 145 c-75 80 -141 145 -145 145 -3 0 -46 -41 -93 -91z m728 -1008 l315 -324 -316 -324 c-174 -178 -320 -322 -324 -321 -4 2 -156 147 -337 323 l-331 320 327 328 c180 180 332 326 339 325 7 -2 154 -149 327 -327z" fill="#e22c2c"/>
+</g>
+</svg> \ No newline at end of file