* Dump github/gitlab repository data to a local directory * Fix lint * Adjust directory structure * Allow migration special units * Allow migration ignore release assets * Fix lint * Add restore repository * stage the changes * Merge * Fix lint * Update the interface * Add some restore methods * Finish restore * Add comments * Fix restore * Add a token flag * Fix bug * Fix test * Fix test * Fix bug * Fix bug * Fix lint * Fix restore * refactor downloader * fmt * Fix bug isEnd detection on getIssues * Refactor maxPerPage * Remove unused codes * Remove unused codes * Fix bug * Fix restore * Fix dump * Uploader should not depend downloader * use release attachment name but not id * Fix restore bug * Fix lint * Fix restore bug * Add a method of DownloadFunc for base.Release to make uploader not depend on downloader * fix Release yml marshal * Fix trace information * Fix bug when dump & restore * Save relative path on yml file * Fix bug * Use relative path * Update docs * Use git service string but not int * Recognize clone addr to service typetags/v1.15.0-dev
@@ -0,0 +1,162 @@ | |||||
// Copyright 2020 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 cmd | |||||
import ( | |||||
"context" | |||||
"errors" | |||||
"strings" | |||||
"code.gitea.io/gitea/modules/convert" | |||||
"code.gitea.io/gitea/modules/log" | |||||
"code.gitea.io/gitea/modules/migrations" | |||||
"code.gitea.io/gitea/modules/migrations/base" | |||||
"code.gitea.io/gitea/modules/setting" | |||||
"code.gitea.io/gitea/modules/structs" | |||||
"github.com/urfave/cli" | |||||
) | |||||
// CmdDumpRepository represents the available dump repository sub-command. | |||||
var CmdDumpRepository = cli.Command{ | |||||
Name: "dump-repo", | |||||
Usage: "Dump the repository from git/github/gitea/gitlab", | |||||
Description: "This is a command for dumping the repository data.", | |||||
Action: runDumpRepository, | |||||
Flags: []cli.Flag{ | |||||
cli.StringFlag{ | |||||
Name: "git_service", | |||||
Value: "", | |||||
Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "repo_dir, r", | |||||
Value: "./data", | |||||
Usage: "Repository dir path to store the data", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "clone_addr", | |||||
Value: "", | |||||
Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "auth_username", | |||||
Value: "", | |||||
Usage: "The username to visit the clone_addr", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "auth_password", | |||||
Value: "", | |||||
Usage: "The password to visit the clone_addr", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "auth_token", | |||||
Value: "", | |||||
Usage: "The personal token to visit the clone_addr", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "owner_name", | |||||
Value: "", | |||||
Usage: "The data will be stored on a directory with owner name if not empty", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "repo_name", | |||||
Value: "", | |||||
Usage: "The data will be stored on a directory with repository name if not empty", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "units", | |||||
Value: "", | |||||
Usage: `Which items will be migrated, one or more units should be separated as comma. | |||||
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, | |||||
}, | |||||
}, | |||||
} | |||||
func runDumpRepository(ctx *cli.Context) error { | |||||
if err := initDB(); err != nil { | |||||
return err | |||||
} | |||||
log.Trace("AppPath: %s", setting.AppPath) | |||||
log.Trace("AppWorkPath: %s", setting.AppWorkPath) | |||||
log.Trace("Custom path: %s", setting.CustomPath) | |||||
log.Trace("Log path: %s", setting.LogRootPath) | |||||
setting.InitDBConfig() | |||||
var ( | |||||
serviceType structs.GitServiceType | |||||
cloneAddr = ctx.String("clone_addr") | |||||
serviceStr = ctx.String("git_service") | |||||
) | |||||
if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") { | |||||
serviceStr = "github" | |||||
} else if strings.HasPrefix(strings.ToLower(cloneAddr), "https://gitlab.com/") { | |||||
serviceStr = "gitlab" | |||||
} else if strings.HasPrefix(strings.ToLower(cloneAddr), "https://gitea.com/") { | |||||
serviceStr = "gitea" | |||||
} | |||||
if serviceStr == "" { | |||||
return errors.New("git_service missed or clone_addr cannot be recognized") | |||||
} | |||||
serviceType = convert.ToGitServiceType(serviceStr) | |||||
var opts = base.MigrateOptions{ | |||||
GitServiceType: serviceType, | |||||
CloneAddr: cloneAddr, | |||||
AuthUsername: ctx.String("auth_username"), | |||||
AuthPassword: ctx.String("auth_password"), | |||||
AuthToken: ctx.String("auth_token"), | |||||
RepoName: ctx.String("repo_name"), | |||||
} | |||||
if len(ctx.String("units")) == 0 { | |||||
opts.Wiki = true | |||||
opts.Issues = true | |||||
opts.Milestones = true | |||||
opts.Labels = true | |||||
opts.Releases = true | |||||
opts.Comments = true | |||||
opts.PullRequests = true | |||||
opts.ReleaseAssets = true | |||||
} else { | |||||
units := strings.Split(ctx.String("units"), ",") | |||||
for _, unit := range units { | |||||
switch strings.ToLower(unit) { | |||||
case "wiki": | |||||
opts.Wiki = true | |||||
case "issues": | |||||
opts.Issues = true | |||||
case "milestones": | |||||
opts.Milestones = true | |||||
case "labels": | |||||
opts.Labels = true | |||||
case "releases": | |||||
opts.Releases = true | |||||
case "release_assets": | |||||
opts.ReleaseAssets = true | |||||
case "comments": | |||||
opts.Comments = true | |||||
case "pull_requests": | |||||
opts.PullRequests = true | |||||
} | |||||
} | |||||
} | |||||
if err := migrations.DumpRepository( | |||||
context.Background(), | |||||
ctx.String("repo_dir"), | |||||
ctx.String("owner_name"), | |||||
opts, | |||||
); err != nil { | |||||
log.Fatal("Failed to dump repository: %v", err) | |||||
return err | |||||
} | |||||
log.Trace("Dump finished!!!") | |||||
return nil | |||||
} |
@@ -0,0 +1,119 @@ | |||||
// Copyright 2020 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 cmd | |||||
import ( | |||||
"context" | |||||
"strings" | |||||
"code.gitea.io/gitea/modules/log" | |||||
"code.gitea.io/gitea/modules/migrations" | |||||
"code.gitea.io/gitea/modules/migrations/base" | |||||
"code.gitea.io/gitea/modules/setting" | |||||
"code.gitea.io/gitea/modules/storage" | |||||
pull_service "code.gitea.io/gitea/services/pull" | |||||
"github.com/urfave/cli" | |||||
) | |||||
// CmdRestoreRepository represents the available restore a repository sub-command. | |||||
var CmdRestoreRepository = cli.Command{ | |||||
Name: "restore-repo", | |||||
Usage: "Restore the repository from disk", | |||||
Description: "This is a command for restoring the repository data.", | |||||
Action: runRestoreRepository, | |||||
Flags: []cli.Flag{ | |||||
cli.StringFlag{ | |||||
Name: "repo_dir, r", | |||||
Value: "./data", | |||||
Usage: "Repository dir path to restore from", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "owner_name", | |||||
Value: "", | |||||
Usage: "Restore destination owner name", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "repo_name", | |||||
Value: "", | |||||
Usage: "Restore destination repository name", | |||||
}, | |||||
cli.StringFlag{ | |||||
Name: "units", | |||||
Value: "", | |||||
Usage: `Which items will be restored, one or more units should be separated as comma. | |||||
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, | |||||
}, | |||||
}, | |||||
} | |||||
func runRestoreRepository(ctx *cli.Context) error { | |||||
if err := initDB(); err != nil { | |||||
return err | |||||
} | |||||
log.Trace("AppPath: %s", setting.AppPath) | |||||
log.Trace("AppWorkPath: %s", setting.AppWorkPath) | |||||
log.Trace("Custom path: %s", setting.CustomPath) | |||||
log.Trace("Log path: %s", setting.LogRootPath) | |||||
setting.InitDBConfig() | |||||
if err := storage.Init(); err != nil { | |||||
return err | |||||
} | |||||
if err := pull_service.Init(); err != nil { | |||||
return err | |||||
} | |||||
var opts = base.MigrateOptions{ | |||||
RepoName: ctx.String("repo_name"), | |||||
} | |||||
if len(ctx.String("units")) == 0 { | |||||
opts.Wiki = true | |||||
opts.Issues = true | |||||
opts.Milestones = true | |||||
opts.Labels = true | |||||
opts.Releases = true | |||||
opts.Comments = true | |||||
opts.PullRequests = true | |||||
opts.ReleaseAssets = true | |||||
} else { | |||||
units := strings.Split(ctx.String("units"), ",") | |||||
for _, unit := range units { | |||||
switch strings.ToLower(unit) { | |||||
case "wiki": | |||||
opts.Wiki = true | |||||
case "issues": | |||||
opts.Issues = true | |||||
case "milestones": | |||||
opts.Milestones = true | |||||
case "labels": | |||||
opts.Labels = true | |||||
case "releases": | |||||
opts.Releases = true | |||||
case "release_assets": | |||||
opts.ReleaseAssets = true | |||||
case "comments": | |||||
opts.Comments = true | |||||
case "pull_requests": | |||||
opts.PullRequests = true | |||||
} | |||||
} | |||||
} | |||||
if err := migrations.RestoreRepository( | |||||
context.Background(), | |||||
ctx.String("repo_dir"), | |||||
ctx.String("owner_name"), | |||||
ctx.String("repo_name"), | |||||
); err != nil { | |||||
log.Fatal("Failed to restore repository: %v", err) | |||||
return err | |||||
} | |||||
return nil | |||||
} |
@@ -441,3 +441,28 @@ Manage running server operations: | |||||
- `--host value`, `-H value`: Mail server host (defaults to: 127.0.0.1:25) | - `--host value`, `-H value`: Mail server host (defaults to: 127.0.0.1:25) | ||||
- `--send-to value`, `-s value`: Email address(es) to send to | - `--send-to value`, `-s value`: Email address(es) to send to | ||||
- `--subject value`, `-S value`: Subject header of sent emails | - `--subject value`, `-S value`: Subject header of sent emails | ||||
### dump-repo | |||||
Dump-repo dumps repository data from git/github/gitea/gitlab: | |||||
- Options: | |||||
- `--git_service service` : Git service, it could be `git`, `github`, `gitea`, `gitlab`, If clone_addr could be recognized, this could be ignored. | |||||
- `--repo_dir dir`, `-r dir`: Repository dir path to store the data | |||||
- `--clone_addr addr`: The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL. i.e. https://github.com/lunny/tango.git | |||||
- `--auth_username lunny`: The username to visit the clone_addr | |||||
- `--auth_password <password>`: The password to visit the clone_addr | |||||
- `--auth_token <token>`: The personal token to visit the clone_addr | |||||
- `--owner_name lunny`: The data will be stored on a directory with owner name if not empty | |||||
- `--repo_name tango`: The data will be stored on a directory with repository name if not empty | |||||
- `--units <units>`: Which items will be migrated, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units. | |||||
### restore-repo | |||||
Restore-repo restore repository data from disk dir: | |||||
- Options: | |||||
- `--repo_dir dir`, `-r dir`: Repository dir path to restore from | |||||
- `--owner_name lunny`: Restore destination owner name | |||||
- `--repo_name tango`: Restore destination repository name | |||||
- `--units <units>`: Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units. |
@@ -72,6 +72,8 @@ arguments - which can alternatively be run by running the subcommand web.` | |||||
cmd.Cmdembedded, | cmd.Cmdembedded, | ||||
cmd.CmdMigrateStorage, | cmd.CmdMigrateStorage, | ||||
cmd.CmdDocs, | cmd.CmdDocs, | ||||
cmd.CmdDumpRepository, | |||||
cmd.CmdRestoreRepository, | |||||
} | } | ||||
// Now adjust these commands to add our global configuration options | // Now adjust these commands to add our global configuration options | ||||
@@ -132,3 +132,16 @@ func DeleteNoticesByIDs(ids []int64) error { | |||||
Delete(new(Notice)) | Delete(new(Notice)) | ||||
return err | return err | ||||
} | } | ||||
// GetAdminUser returns the first administrator | |||||
func GetAdminUser() (*User, error) { | |||||
var admin User | |||||
has, err := x.Where("is_admin=?", true).Get(&admin) | |||||
if err != nil { | |||||
return nil, err | |||||
} else if !has { | |||||
return nil, ErrUserNotExist{} | |||||
} | |||||
return &admin, nil | |||||
} |
@@ -211,10 +211,6 @@ func FinishMigrateTask(task *Task) error { | |||||
if _, err := sess.ID(task.ID).Cols("status", "end_time").Update(task); err != nil { | if _, err := sess.ID(task.ID).Cols("status", "end_time").Update(task); err != nil { | ||||
return err | return err | ||||
} | } | ||||
task.Repo.Status = RepositoryReady | |||||
if _, err := sess.ID(task.RepoID).Cols("status").Update(task.Repo); err != nil { | |||||
return err | |||||
} | |||||
return sess.Commit() | return sess.Commit() | ||||
} | } |
@@ -9,10 +9,10 @@ import "time" | |||||
// Comment is a standard comment information | // Comment is a standard comment information | ||||
type Comment struct { | type Comment struct { | ||||
IssueIndex int64 | |||||
PosterID int64 | |||||
PosterName string | |||||
PosterEmail string | |||||
IssueIndex int64 `yaml:"issue_index"` | |||||
PosterID int64 `yaml:"poster_id"` | |||||
PosterName string `yaml:"poster_name"` | |||||
PosterEmail string `yaml:"poster_email"` | |||||
Created time.Time | Created time.Time | ||||
Updated time.Time | Updated time.Time | ||||
Content string | Content string | ||||
@@ -7,20 +7,13 @@ package base | |||||
import ( | import ( | ||||
"context" | "context" | ||||
"io" | |||||
"time" | "time" | ||||
"code.gitea.io/gitea/modules/structs" | "code.gitea.io/gitea/modules/structs" | ||||
) | ) | ||||
// AssetDownloader downloads an asset (attachment) for a release | |||||
type AssetDownloader interface { | |||||
GetAsset(relTag string, relID, id int64) (io.ReadCloser, error) | |||||
} | |||||
// Downloader downloads the site repo informations | // Downloader downloads the site repo informations | ||||
type Downloader interface { | type Downloader interface { | ||||
AssetDownloader | |||||
SetContext(context.Context) | SetContext(context.Context) | ||||
GetRepoInfo() (*Repository, error) | GetRepoInfo() (*Repository, error) | ||||
GetTopics() ([]string, error) | GetTopics() ([]string, error) | ||||
@@ -10,15 +10,15 @@ import "time" | |||||
// Issue is a standard issue information | // Issue is a standard issue information | ||||
type Issue struct { | type Issue struct { | ||||
Number int64 | Number int64 | ||||
PosterID int64 | |||||
PosterName string | |||||
PosterEmail string | |||||
PosterID int64 `yaml:"poster_id"` | |||||
PosterName string `yaml:"poster_name"` | |||||
PosterEmail string `yaml:"poster_email"` | |||||
Title string | Title string | ||||
Content string | Content string | ||||
Ref string | Ref string | ||||
Milestone string | Milestone string | ||||
State string // closed, open | State string // closed, open | ||||
IsLocked bool | |||||
IsLocked bool `yaml:"is_locked"` | |||||
Created time.Time | Created time.Time | ||||
Updated time.Time | Updated time.Time | ||||
Closed *time.Time | Closed *time.Time | ||||
@@ -31,5 +31,6 @@ type MigrateOptions struct { | |||||
Releases bool | Releases bool | ||||
Comments bool | Comments bool | ||||
PullRequests bool | PullRequests bool | ||||
ReleaseAssets bool | |||||
MigrateToRepoID int64 | MigrateToRepoID int64 | ||||
} | } |
@@ -13,11 +13,11 @@ import ( | |||||
// PullRequest defines a standard pull request information | // PullRequest defines a standard pull request information | ||||
type PullRequest struct { | type PullRequest struct { | ||||
Number int64 | Number int64 | ||||
OriginalNumber int64 | |||||
OriginalNumber int64 `yaml:"original_number"` | |||||
Title string | Title string | ||||
PosterName string | |||||
PosterID int64 | |||||
PosterEmail string | |||||
PosterName string `yaml:"poster_name"` | |||||
PosterID int64 `yaml:"poster_id"` | |||||
PosterEmail string `yaml:"poster_email"` | |||||
Content string | Content string | ||||
Milestone string | Milestone string | ||||
State string | State string | ||||
@@ -25,14 +25,14 @@ type PullRequest struct { | |||||
Updated time.Time | Updated time.Time | ||||
Closed *time.Time | Closed *time.Time | ||||
Labels []*Label | Labels []*Label | ||||
PatchURL string | |||||
PatchURL string `yaml:"patch_url"` | |||||
Merged bool | Merged bool | ||||
MergedTime *time.Time | |||||
MergeCommitSHA string | |||||
MergedTime *time.Time `yaml:"merged_time"` | |||||
MergeCommitSHA string `yaml:"merge_commit_sha"` | |||||
Head PullRequestBranch | Head PullRequestBranch | ||||
Base PullRequestBranch | Base PullRequestBranch | ||||
Assignees []string | Assignees []string | ||||
IsLocked bool | |||||
IsLocked bool `yaml:"is_locked"` | |||||
Reactions []*Reaction | Reactions []*Reaction | ||||
} | } | ||||
@@ -43,11 +43,11 @@ func (p *PullRequest) IsForkPullRequest() bool { | |||||
// PullRequestBranch represents a pull request branch | // PullRequestBranch represents a pull request branch | ||||
type PullRequestBranch struct { | type PullRequestBranch struct { | ||||
CloneURL string | |||||
CloneURL string `yaml:"clone_url"` | |||||
Ref string | Ref string | ||||
SHA string | SHA string | ||||
RepoName string | |||||
OwnerName string | |||||
RepoName string `yaml:"repo_name"` | |||||
OwnerName string `yaml:"owner_name"` | |||||
} | } | ||||
// RepoPath returns pull request repo path | // RepoPath returns pull request repo path | ||||
@@ -6,7 +6,7 @@ package base | |||||
// Reaction represents a reaction to an issue/pr/comment. | // Reaction represents a reaction to an issue/pr/comment. | ||||
type Reaction struct { | type Reaction struct { | ||||
UserID int64 | |||||
UserName string | |||||
UserID int64 `yaml:"user_id"` | |||||
UserName string `yaml:"user_name"` | |||||
Content string | Content string | ||||
} | } |
@@ -4,32 +4,37 @@ | |||||
package base | package base | ||||
import "time" | |||||
import ( | |||||
"io" | |||||
"time" | |||||
) | |||||
// ReleaseAsset represents a release asset | // ReleaseAsset represents a release asset | ||||
type ReleaseAsset struct { | type ReleaseAsset struct { | ||||
ID int64 | ID int64 | ||||
Name string | Name string | ||||
ContentType *string | |||||
ContentType *string `yaml:"content_type"` | |||||
Size *int | Size *int | ||||
DownloadCount *int | |||||
DownloadCount *int `yaml:"download_count"` | |||||
Created time.Time | Created time.Time | ||||
Updated time.Time | Updated time.Time | ||||
DownloadURL *string | |||||
DownloadURL *string `yaml:"download_url"` | |||||
// if DownloadURL is nil, the function should be invoked | |||||
DownloadFunc func() (io.ReadCloser, error) `yaml:"-"` | |||||
} | } | ||||
// Release represents a release | // Release represents a release | ||||
type Release struct { | type Release struct { | ||||
TagName string | |||||
TargetCommitish string | |||||
TagName string `yaml:"tag_name"` | |||||
TargetCommitish string `yaml:"target_commitish"` | |||||
Name string | Name string | ||||
Body string | Body string | ||||
Draft bool | Draft bool | ||||
Prerelease bool | Prerelease bool | ||||
PublisherID int64 | |||||
PublisherName string | |||||
PublisherEmail string | |||||
Assets []ReleaseAsset | |||||
PublisherID int64 `yaml:"publisher_id"` | |||||
PublisherName string `yaml:"publisher_name"` | |||||
PublisherEmail string `yaml:"publisher_email"` | |||||
Assets []*ReleaseAsset | |||||
Created time.Time | Created time.Time | ||||
Published time.Time | Published time.Time | ||||
} | } |
@@ -9,10 +9,10 @@ package base | |||||
type Repository struct { | type Repository struct { | ||||
Name string | Name string | ||||
Owner string | Owner string | ||||
IsPrivate bool | |||||
IsMirror bool | |||||
IsPrivate bool `yaml:"is_private"` | |||||
IsMirror bool `yaml:"is_mirror"` | |||||
Description string | Description string | ||||
CloneURL string | |||||
OriginalURL string | |||||
CloneURL string `yaml:"clone_url"` | |||||
OriginalURL string `yaml:"original_url"` | |||||
DefaultBranch string | DefaultBranch string | ||||
} | } |
@@ -17,29 +17,29 @@ const ( | |||||
// Review is a standard review information | // Review is a standard review information | ||||
type Review struct { | type Review struct { | ||||
ID int64 | ID int64 | ||||
IssueIndex int64 | |||||
ReviewerID int64 | |||||
ReviewerName string | |||||
IssueIndex int64 `yaml:"issue_index"` | |||||
ReviewerID int64 `yaml:"reviewer_id"` | |||||
ReviewerName string `yaml:"reviewer_name"` | |||||
Official bool | Official bool | ||||
CommitID string | |||||
CommitID string `yaml:"commit_id"` | |||||
Content string | Content string | ||||
CreatedAt time.Time | |||||
State string // PENDING, APPROVED, REQUEST_CHANGES, or COMMENT | |||||
CreatedAt time.Time `yaml:"created_at"` | |||||
State string // PENDING, APPROVED, REQUEST_CHANGES, or COMMENT | |||||
Comments []*ReviewComment | Comments []*ReviewComment | ||||
} | } | ||||
// ReviewComment represents a review comment | // ReviewComment represents a review comment | ||||
type ReviewComment struct { | type ReviewComment struct { | ||||
ID int64 | ID int64 | ||||
InReplyTo int64 | |||||
InReplyTo int64 `yaml:"in_reply_to"` | |||||
Content string | Content string | ||||
TreePath string | |||||
DiffHunk string | |||||
TreePath string `yaml:"tree_path"` | |||||
DiffHunk string `yaml:"diff_hunk"` | |||||
Position int | Position int | ||||
Line int | Line int | ||||
CommitID string | |||||
PosterID int64 | |||||
CommitID string `yaml:"commit_id"` | |||||
PosterID int64 `yaml:"poster_id"` | |||||
Reactions []*Reaction | Reactions []*Reaction | ||||
CreatedAt time.Time | |||||
UpdatedAt time.Time | |||||
CreatedAt time.Time `yaml:"created_at"` | |||||
UpdatedAt time.Time `yaml:"updated_at"` | |||||
} | } |
@@ -11,7 +11,7 @@ type Uploader interface { | |||||
CreateRepo(repo *Repository, opts MigrateOptions) error | CreateRepo(repo *Repository, opts MigrateOptions) error | ||||
CreateTopics(topic ...string) error | CreateTopics(topic ...string) error | ||||
CreateMilestones(milestones ...*Milestone) error | CreateMilestones(milestones ...*Milestone) error | ||||
CreateReleases(downloader Downloader, releases ...*Release) error | |||||
CreateReleases(releases ...*Release) error | |||||
SyncTags() error | SyncTags() error | ||||
CreateLabels(labels ...*Label) error | CreateLabels(labels ...*Label) error | ||||
CreateIssues(issues ...*Issue) error | CreateIssues(issues ...*Issue) error | ||||
@@ -19,5 +19,6 @@ type Uploader interface { | |||||
CreatePullRequests(prs ...*PullRequest) error | CreatePullRequests(prs ...*PullRequest) error | ||||
CreateReviews(reviews ...*Review) error | CreateReviews(reviews ...*Review) error | ||||
Rollback() error | Rollback() error | ||||
Finish() error | |||||
Close() | Close() | ||||
} | } |
@@ -0,0 +1,591 @@ | |||||
// Copyright 2020 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" | |||||
"io" | |||||
"net/http" | |||||
"net/url" | |||||
"os" | |||||
"path/filepath" | |||||
"time" | |||||
"code.gitea.io/gitea/models" | |||||
"code.gitea.io/gitea/modules/git" | |||||
"code.gitea.io/gitea/modules/log" | |||||
"code.gitea.io/gitea/modules/migrations/base" | |||||
"code.gitea.io/gitea/modules/repository" | |||||
"gopkg.in/yaml.v2" | |||||
) | |||||
var ( | |||||
_ base.Uploader = &RepositoryDumper{} | |||||
) | |||||
// RepositoryDumper implements an Uploader to the local directory | |||||
type RepositoryDumper struct { | |||||
ctx context.Context | |||||
baseDir string | |||||
repoOwner string | |||||
repoName string | |||||
opts base.MigrateOptions | |||||
milestoneFile *os.File | |||||
labelFile *os.File | |||||
releaseFile *os.File | |||||
issueFile *os.File | |||||
commentFiles map[int64]*os.File | |||||
pullrequestFile *os.File | |||||
reviewFiles map[int64]*os.File | |||||
gitRepo *git.Repository | |||||
prHeadCache map[string]struct{} | |||||
} | |||||
// NewRepositoryDumper creates an gitea Uploader | |||||
func NewRepositoryDumper(ctx context.Context, baseDir, repoOwner, repoName string, opts base.MigrateOptions) (*RepositoryDumper, error) { | |||||
baseDir = filepath.Join(baseDir, repoOwner, repoName) | |||||
if err := os.MkdirAll(baseDir, os.ModePerm); err != nil { | |||||
return nil, err | |||||
} | |||||
return &RepositoryDumper{ | |||||
ctx: ctx, | |||||
opts: opts, | |||||
baseDir: baseDir, | |||||
repoOwner: repoOwner, | |||||
repoName: repoName, | |||||
prHeadCache: make(map[string]struct{}), | |||||
commentFiles: make(map[int64]*os.File), | |||||
reviewFiles: make(map[int64]*os.File), | |||||
}, nil | |||||
} | |||||
// MaxBatchInsertSize returns the table's max batch insert size | |||||
func (g *RepositoryDumper) MaxBatchInsertSize(tp string) int { | |||||
return 1000 | |||||
} | |||||
func (g *RepositoryDumper) gitPath() string { | |||||
return filepath.Join(g.baseDir, "git") | |||||
} | |||||
func (g *RepositoryDumper) wikiPath() string { | |||||
return filepath.Join(g.baseDir, "wiki") | |||||
} | |||||
func (g *RepositoryDumper) commentDir() string { | |||||
return filepath.Join(g.baseDir, "comments") | |||||
} | |||||
func (g *RepositoryDumper) reviewDir() string { | |||||
return filepath.Join(g.baseDir, "reviews") | |||||
} | |||||
func (g *RepositoryDumper) setURLToken(remoteAddr string) (string, error) { | |||||
if len(g.opts.AuthToken) > 0 || len(g.opts.AuthUsername) > 0 { | |||||
u, err := url.Parse(remoteAddr) | |||||
if err != nil { | |||||
return "", err | |||||
} | |||||
u.User = url.UserPassword(g.opts.AuthUsername, g.opts.AuthPassword) | |||||
if len(g.opts.AuthToken) > 0 { | |||||
u.User = url.UserPassword("oauth2", g.opts.AuthToken) | |||||
} | |||||
remoteAddr = u.String() | |||||
} | |||||
return remoteAddr, nil | |||||
} | |||||
// CreateRepo creates a repository | |||||
func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error { | |||||
f, err := os.Create(filepath.Join(g.baseDir, "repo.yml")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer f.Close() | |||||
bs, err := yaml.Marshal(map[string]interface{}{ | |||||
"name": repo.Name, | |||||
"owner": repo.Owner, | |||||
"description": repo.Description, | |||||
"clone_addr": opts.CloneAddr, | |||||
"original_url": repo.OriginalURL, | |||||
"is_private": opts.Private, | |||||
"service_type": opts.GitServiceType, | |||||
"wiki": opts.Wiki, | |||||
"issues": opts.Issues, | |||||
"milestones": opts.Milestones, | |||||
"labels": opts.Labels, | |||||
"releases": opts.Releases, | |||||
"comments": opts.Comments, | |||||
"pulls": opts.PullRequests, | |||||
"assets": opts.ReleaseAssets, | |||||
}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if _, err := f.Write(bs); err != nil { | |||||
return err | |||||
} | |||||
repoPath := g.gitPath() | |||||
if err := os.MkdirAll(repoPath, os.ModePerm); err != nil { | |||||
return err | |||||
} | |||||
migrateTimeout := 2 * time.Hour | |||||
remoteAddr, err := g.setURLToken(repo.CloneURL) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
err = git.Clone(remoteAddr, repoPath, git.CloneRepoOptions{ | |||||
Mirror: true, | |||||
Quiet: true, | |||||
Timeout: migrateTimeout, | |||||
}) | |||||
if err != nil { | |||||
return fmt.Errorf("Clone: %v", err) | |||||
} | |||||
if opts.Wiki { | |||||
wikiPath := g.wikiPath() | |||||
wikiRemotePath := repository.WikiRemoteURL(remoteAddr) | |||||
if len(wikiRemotePath) > 0 { | |||||
if err := os.MkdirAll(wikiPath, os.ModePerm); err != nil { | |||||
return fmt.Errorf("Failed to remove %s: %v", wikiPath, err) | |||||
} | |||||
if err := git.Clone(wikiRemotePath, wikiPath, git.CloneRepoOptions{ | |||||
Mirror: true, | |||||
Quiet: true, | |||||
Timeout: migrateTimeout, | |||||
Branch: "master", | |||||
}); err != nil { | |||||
log.Warn("Clone wiki: %v", err) | |||||
if err := os.RemoveAll(wikiPath); err != nil { | |||||
return fmt.Errorf("Failed to remove %s: %v", wikiPath, err) | |||||
} | |||||
} | |||||
} | |||||
} | |||||
g.gitRepo, err = git.OpenRepository(g.gitPath()) | |||||
return err | |||||
} | |||||
// Close closes this uploader | |||||
func (g *RepositoryDumper) Close() { | |||||
if g.gitRepo != nil { | |||||
g.gitRepo.Close() | |||||
} | |||||
if g.milestoneFile != nil { | |||||
g.milestoneFile.Close() | |||||
} | |||||
if g.labelFile != nil { | |||||
g.labelFile.Close() | |||||
} | |||||
if g.releaseFile != nil { | |||||
g.releaseFile.Close() | |||||
} | |||||
if g.issueFile != nil { | |||||
g.issueFile.Close() | |||||
} | |||||
for _, f := range g.commentFiles { | |||||
f.Close() | |||||
} | |||||
if g.pullrequestFile != nil { | |||||
g.pullrequestFile.Close() | |||||
} | |||||
for _, f := range g.reviewFiles { | |||||
f.Close() | |||||
} | |||||
} | |||||
// CreateTopics creates topics | |||||
func (g *RepositoryDumper) CreateTopics(topics ...string) error { | |||||
f, err := os.Create(filepath.Join(g.baseDir, "topic.yml")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer f.Close() | |||||
bs, err := yaml.Marshal(map[string]interface{}{ | |||||
"topics": topics, | |||||
}) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if _, err := f.Write(bs); err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
// CreateMilestones creates milestones | |||||
func (g *RepositoryDumper) CreateMilestones(milestones ...*base.Milestone) error { | |||||
var err error | |||||
if g.milestoneFile == nil { | |||||
g.milestoneFile, err = os.Create(filepath.Join(g.baseDir, "milestone.yml")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
bs, err := yaml.Marshal(milestones) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if _, err := g.milestoneFile.Write(bs); err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
// CreateLabels creates labels | |||||
func (g *RepositoryDumper) CreateLabels(labels ...*base.Label) error { | |||||
var err error | |||||
if g.labelFile == nil { | |||||
g.labelFile, err = os.Create(filepath.Join(g.baseDir, "label.yml")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
bs, err := yaml.Marshal(labels) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if _, err := g.labelFile.Write(bs); err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
// CreateReleases creates releases | |||||
func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error { | |||||
if g.opts.ReleaseAssets { | |||||
for _, release := range releases { | |||||
attachDir := filepath.Join("release_assets", release.TagName) | |||||
if err := os.MkdirAll(filepath.Join(g.baseDir, attachDir), os.ModePerm); err != nil { | |||||
return err | |||||
} | |||||
for _, asset := range release.Assets { | |||||
attachLocalPath := filepath.Join(attachDir, asset.Name) | |||||
// download attachment | |||||
err := func(attachPath string) error { | |||||
var rc io.ReadCloser | |||||
var err error | |||||
if asset.DownloadURL == nil { | |||||
rc, err = asset.DownloadFunc() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} else { | |||||
resp, err := http.Get(*asset.DownloadURL) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
rc = resp.Body | |||||
} | |||||
defer rc.Close() | |||||
fw, err := os.Create(attachPath) | |||||
if err != nil { | |||||
return fmt.Errorf("Create: %v", err) | |||||
} | |||||
defer fw.Close() | |||||
_, err = io.Copy(fw, rc) | |||||
return err | |||||
}(filepath.Join(g.baseDir, attachLocalPath)) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
asset.DownloadURL = &attachLocalPath // to save the filepath on the yml file, change the source | |||||
} | |||||
} | |||||
} | |||||
var err error | |||||
if g.releaseFile == nil { | |||||
g.releaseFile, err = os.Create(filepath.Join(g.baseDir, "release.yml")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
bs, err := yaml.Marshal(releases) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if _, err := g.releaseFile.Write(bs); err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
// SyncTags syncs releases with tags in the database | |||||
func (g *RepositoryDumper) SyncTags() error { | |||||
return nil | |||||
} | |||||
// CreateIssues creates issues | |||||
func (g *RepositoryDumper) CreateIssues(issues ...*base.Issue) error { | |||||
var err error | |||||
if g.issueFile == nil { | |||||
g.issueFile, err = os.Create(filepath.Join(g.baseDir, "issue.yml")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
bs, err := yaml.Marshal(issues) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if _, err := g.issueFile.Write(bs); err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (g *RepositoryDumper) createItems(dir string, itemFiles map[int64]*os.File, itemsMap map[int64][]interface{}) error { | |||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil { | |||||
return err | |||||
} | |||||
for number, items := range itemsMap { | |||||
var err error | |||||
itemFile := itemFiles[number] | |||||
if itemFile == nil { | |||||
itemFile, err = os.Create(filepath.Join(dir, fmt.Sprintf("%d.yml", number))) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
itemFiles[number] = itemFile | |||||
} | |||||
bs, err := yaml.Marshal(items) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if _, err := itemFile.Write(bs); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
return nil | |||||
} | |||||
// CreateComments creates comments of issues | |||||
func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error { | |||||
var commentsMap = make(map[int64][]interface{}, len(comments)) | |||||
for _, comment := range comments { | |||||
commentsMap[comment.IssueIndex] = append(commentsMap[comment.IssueIndex], comment) | |||||
} | |||||
return g.createItems(g.commentDir(), g.commentFiles, commentsMap) | |||||
} | |||||
// CreatePullRequests creates pull requests | |||||
func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error { | |||||
for _, pr := range prs { | |||||
// download patch file | |||||
err := func() error { | |||||
u, err := g.setURLToken(pr.PatchURL) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
resp, err := http.Get(u) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer resp.Body.Close() | |||||
pullDir := filepath.Join(g.gitPath(), "pulls") | |||||
if err = os.MkdirAll(pullDir, os.ModePerm); err != nil { | |||||
return err | |||||
} | |||||
fPath := filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number)) | |||||
f, err := os.Create(fPath) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
defer f.Close() | |||||
if _, err = io.Copy(f, resp.Body); err != nil { | |||||
return err | |||||
} | |||||
pr.PatchURL = "git/pulls/" + fmt.Sprintf("%d.patch", pr.Number) | |||||
return nil | |||||
}() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// set head information | |||||
pullHead := filepath.Join(g.gitPath(), "refs", "pull", fmt.Sprintf("%d", pr.Number)) | |||||
if err := os.MkdirAll(pullHead, os.ModePerm); err != nil { | |||||
return err | |||||
} | |||||
p, err := os.Create(filepath.Join(pullHead, "head")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
_, err = p.WriteString(pr.Head.SHA) | |||||
p.Close() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if pr.IsForkPullRequest() && pr.State != "closed" { | |||||
if pr.Head.OwnerName != "" { | |||||
remote := pr.Head.OwnerName | |||||
_, ok := g.prHeadCache[remote] | |||||
if !ok { | |||||
// git remote add | |||||
// TODO: how to handle private CloneURL? | |||||
err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true) | |||||
if err != nil { | |||||
log.Error("AddRemote failed: %s", err) | |||||
} else { | |||||
g.prHeadCache[remote] = struct{}{} | |||||
ok = true | |||||
} | |||||
} | |||||
if ok { | |||||
_, err = git.NewCommand("fetch", remote, pr.Head.Ref).RunInDir(g.gitPath()) | |||||
if err != nil { | |||||
log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err) | |||||
} else { | |||||
headBranch := filepath.Join(g.gitPath(), "refs", "heads", pr.Head.OwnerName, pr.Head.Ref) | |||||
if err := os.MkdirAll(filepath.Dir(headBranch), os.ModePerm); err != nil { | |||||
return err | |||||
} | |||||
b, err := os.Create(headBranch) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
_, err = b.WriteString(pr.Head.SHA) | |||||
b.Close() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
var err error | |||||
if g.pullrequestFile == nil { | |||||
if err := os.MkdirAll(g.baseDir, os.ModePerm); err != nil { | |||||
return err | |||||
} | |||||
g.pullrequestFile, err = os.Create(filepath.Join(g.baseDir, "pull_request.yml")) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
} | |||||
bs, err := yaml.Marshal(prs) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if _, err := g.pullrequestFile.Write(bs); err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
// CreateReviews create pull request reviews | |||||
func (g *RepositoryDumper) CreateReviews(reviews ...*base.Review) error { | |||||
var reviewsMap = make(map[int64][]interface{}, len(reviews)) | |||||
for _, review := range reviews { | |||||
reviewsMap[review.IssueIndex] = append(reviewsMap[review.IssueIndex], review) | |||||
} | |||||
return g.createItems(g.reviewDir(), g.reviewFiles, reviewsMap) | |||||
} | |||||
// Rollback when migrating failed, this will rollback all the changes. | |||||
func (g *RepositoryDumper) Rollback() error { | |||||
g.Close() | |||||
return os.RemoveAll(g.baseDir) | |||||
} | |||||
// Finish when migrating succeed, this will update something. | |||||
func (g *RepositoryDumper) Finish() error { | |||||
return nil | |||||
} | |||||
// DumpRepository dump repository according MigrateOptions to a local directory | |||||
func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error { | |||||
downloader, err := newDownloader(ctx, ownerName, opts) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
uploader, err := NewRepositoryDumper(ctx, baseDir, ownerName, opts.RepoName, opts) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if err := migrateRepository(downloader, uploader, opts); err != nil { | |||||
if err1 := uploader.Rollback(); err1 != nil { | |||||
log.Error("rollback failed: %v", err1) | |||||
} | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
// RestoreRepository restore a repository from the disk directory | |||||
func RestoreRepository(ctx context.Context, baseDir string, ownerName, repoName string) error { | |||||
doer, err := models.GetAdminUser() | |||||
if err != nil { | |||||
return err | |||||
} | |||||
var uploader = NewGiteaLocalUploader(ctx, doer, ownerName, repoName) | |||||
downloader, err := NewRepositoryRestorer(ctx, baseDir, ownerName, repoName) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if err = migrateRepository(downloader, uploader, base.MigrateOptions{ | |||||
Wiki: true, | |||||
Issues: true, | |||||
Milestones: true, | |||||
Labels: true, | |||||
Releases: true, | |||||
Comments: true, | |||||
PullRequests: true, | |||||
ReleaseAssets: true, | |||||
}); err != nil { | |||||
if err1 := uploader.Rollback(); err1 != nil { | |||||
log.Error("rollback failed: %v", err1) | |||||
} | |||||
return err | |||||
} | |||||
return nil | |||||
} |
@@ -14,6 +14,9 @@ import ( | |||||
var ( | var ( | ||||
// ErrNotSupported returns the error not supported | // ErrNotSupported returns the error not supported | ||||
ErrNotSupported = errors.New("not supported") | ErrNotSupported = errors.New("not supported") | ||||
// ErrRepoNotCreated returns the error that repository not created | |||||
ErrRepoNotCreated = errors.New("repository is not created yet") | |||||
) | ) | ||||
// IsRateLimitError returns true if the err is github.RateLimitError | // IsRateLimitError returns true if the err is github.RateLimitError | ||||
@@ -6,7 +6,6 @@ package migrations | |||||
import ( | import ( | ||||
"context" | "context" | ||||
"io" | |||||
"code.gitea.io/gitea/modules/migrations/base" | "code.gitea.io/gitea/modules/migrations/base" | ||||
) | ) | ||||
@@ -65,11 +64,6 @@ func (g *PlainGitDownloader) GetReleases() ([]*base.Release, error) { | |||||
return nil, ErrNotSupported | return nil, ErrNotSupported | ||||
} | } | ||||
// GetAsset returns an asset | |||||
func (g *PlainGitDownloader) GetAsset(_ string, _, _ int64) (io.ReadCloser, error) { | |||||
return nil, ErrNotSupported | |||||
} | |||||
// GetIssues returns issues according page and perPage | // GetIssues returns issues according page and perPage | ||||
func (g *PlainGitDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | func (g *PlainGitDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | ||||
return nil, false, ErrNotSupported | return nil, false, ErrNotSupported | ||||
@@ -268,13 +268,27 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele | |||||
for _, asset := range rel.Attachments { | for _, asset := range rel.Attachments { | ||||
size := int(asset.Size) | size := int(asset.Size) | ||||
dlCount := int(asset.DownloadCount) | dlCount := int(asset.DownloadCount) | ||||
r.Assets = append(r.Assets, base.ReleaseAsset{ | |||||
r.Assets = append(r.Assets, &base.ReleaseAsset{ | |||||
ID: asset.ID, | ID: asset.ID, | ||||
Name: asset.Name, | Name: asset.Name, | ||||
Size: &size, | Size: &size, | ||||
DownloadCount: &dlCount, | DownloadCount: &dlCount, | ||||
Created: asset.Created, | Created: asset.Created, | ||||
DownloadURL: &asset.DownloadURL, | DownloadURL: &asset.DownloadURL, | ||||
DownloadFunc: func() (io.ReadCloser, error) { | |||||
asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, asset.ID) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
// FIXME: for a private download? | |||||
resp, err := http.Get(asset.DownloadURL) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
// resp.Body is closed by the uploader | |||||
return resp.Body, nil | |||||
}, | |||||
}) | }) | ||||
} | } | ||||
return r | return r | ||||
@@ -310,21 +324,6 @@ func (g *GiteaDownloader) GetReleases() ([]*base.Release, error) { | |||||
return releases, nil | return releases, nil | ||||
} | } | ||||
// GetAsset returns an asset | |||||
func (g *GiteaDownloader) GetAsset(_ string, relID, id int64) (io.ReadCloser, error) { | |||||
asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, relID, id) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
resp, err := http.Get(asset.DownloadURL) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
// resp.Body is closed by the uploader | |||||
return resp.Body, nil | |||||
} | |||||
func (g *GiteaDownloader) getIssueReactions(index int64) ([]*base.Reaction, error) { | func (g *GiteaDownloader) getIssueReactions(index int64) ([]*base.Reaction, error) { | ||||
var reactions []*base.Reaction | var reactions []*base.Reaction | ||||
if err := g.client.CheckServerVersionConstraint(">=1.11"); err != nil { | if err := g.client.CheckServerVersionConstraint(">=1.11"); err != nil { | ||||
@@ -10,7 +10,6 @@ import ( | |||||
"context" | "context" | ||||
"fmt" | "fmt" | ||||
"io" | "io" | ||||
"net/http" | |||||
"net/url" | "net/url" | ||||
"os" | "os" | ||||
"path/filepath" | "path/filepath" | ||||
@@ -28,6 +27,7 @@ import ( | |||||
"code.gitea.io/gitea/modules/storage" | "code.gitea.io/gitea/modules/storage" | ||||
"code.gitea.io/gitea/modules/structs" | "code.gitea.io/gitea/modules/structs" | ||||
"code.gitea.io/gitea/modules/timeutil" | "code.gitea.io/gitea/modules/timeutil" | ||||
"code.gitea.io/gitea/modules/uri" | |||||
"code.gitea.io/gitea/services/pull" | "code.gitea.io/gitea/services/pull" | ||||
gouuid "github.com/google/uuid" | gouuid "github.com/google/uuid" | ||||
@@ -86,26 +86,33 @@ func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int { | |||||
return 10 | return 10 | ||||
} | } | ||||
// CreateRepo creates a repository | |||||
func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error { | |||||
owner, err := models.GetUserByName(g.repoOwner) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
var remoteAddr = repo.CloneURL | |||||
func fullURL(opts base.MigrateOptions, remoteAddr string) (string, error) { | |||||
var fullRemoteAddr = remoteAddr | |||||
if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 { | if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 { | ||||
u, err := url.Parse(repo.CloneURL) | |||||
u, err := url.Parse(remoteAddr) | |||||
if err != nil { | if err != nil { | ||||
return err | |||||
return "", err | |||||
} | } | ||||
u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword) | u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword) | ||||
if len(opts.AuthToken) > 0 { | if len(opts.AuthToken) > 0 { | ||||
u.User = url.UserPassword("oauth2", opts.AuthToken) | u.User = url.UserPassword("oauth2", opts.AuthToken) | ||||
} | } | ||||
remoteAddr = u.String() | |||||
fullRemoteAddr = u.String() | |||||
} | |||||
return fullRemoteAddr, nil | |||||
} | |||||
// CreateRepo creates a repository | |||||
func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error { | |||||
owner, err := models.GetUserByName(g.repoOwner) | |||||
if err != nil { | |||||
return err | |||||
} | } | ||||
remoteAddr, err := fullURL(opts, repo.CloneURL) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
var r *models.Repository | var r *models.Repository | ||||
if opts.MigrateToRepoID <= 0 { | if opts.MigrateToRepoID <= 0 { | ||||
r, err = repo_module.CreateRepository(g.doer, owner, models.CreateRepoOptions{ | r, err = repo_module.CreateRepository(g.doer, owner, models.CreateRepoOptions{ | ||||
@@ -224,7 +231,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { | |||||
} | } | ||||
// CreateReleases creates releases | // CreateReleases creates releases | ||||
func (g *GiteaLocalUploader) CreateReleases(downloader base.Downloader, releases ...*base.Release) error { | |||||
func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { | |||||
var rels = make([]*models.Release, 0, len(releases)) | var rels = make([]*models.Release, 0, len(releases)) | ||||
for _, release := range releases { | for _, release := range releases { | ||||
var rel = models.Release{ | var rel = models.Release{ | ||||
@@ -283,25 +290,27 @@ func (g *GiteaLocalUploader) CreateReleases(downloader base.Downloader, releases | |||||
// download attachment | // download attachment | ||||
err = func() error { | err = func() error { | ||||
// asset.DownloadURL maybe a local file | |||||
var rc io.ReadCloser | var rc io.ReadCloser | ||||
if asset.DownloadURL == nil { | if asset.DownloadURL == nil { | ||||
rc, err = downloader.GetAsset(rel.TagName, rel.ID, asset.ID) | |||||
rc, err = asset.DownloadFunc() | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
} else { | } else { | ||||
resp, err := http.Get(*asset.DownloadURL) | |||||
rc, err = uri.Open(*asset.DownloadURL) | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
rc = resp.Body | |||||
} | } | ||||
defer rc.Close() | |||||
_, err = storage.Attachments.Save(attach.RelativePath(), rc) | _, err = storage.Attachments.Save(attach.RelativePath(), rc) | ||||
return err | return err | ||||
}() | }() | ||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
rel.Attachments = append(rel.Attachments, &attach) | rel.Attachments = append(rel.Attachments, &attach) | ||||
} | } | ||||
@@ -559,11 +568,12 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR | |||||
// download patch file | // download patch file | ||||
err := func() error { | err := func() error { | ||||
resp, err := http.Get(pr.PatchURL) | |||||
// pr.PatchURL maybe a local file | |||||
ret, err := uri.Open(pr.PatchURL) | |||||
if err != nil { | if err != nil { | ||||
return err | return err | ||||
} | } | ||||
defer resp.Body.Close() | |||||
defer ret.Close() | |||||
pullDir := filepath.Join(g.repo.RepoPath(), "pulls") | pullDir := filepath.Join(g.repo.RepoPath(), "pulls") | ||||
if err = os.MkdirAll(pullDir, os.ModePerm); err != nil { | if err = os.MkdirAll(pullDir, os.ModePerm); err != nil { | ||||
return err | return err | ||||
@@ -573,7 +583,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR | |||||
return err | return err | ||||
} | } | ||||
defer f.Close() | defer f.Close() | ||||
_, err = io.Copy(f, resp.Body) | |||||
_, err = io.Copy(f, ret) | |||||
return err | return err | ||||
}() | }() | ||||
if err != nil { | if err != nil { | ||||
@@ -859,3 +869,13 @@ func (g *GiteaLocalUploader) Rollback() error { | |||||
} | } | ||||
return nil | return nil | ||||
} | } | ||||
// Finish when migrating success, this will do some status update things. | |||||
func (g *GiteaLocalUploader) Finish() error { | |||||
if g.repo == nil || g.repo.ID <= 0 { | |||||
return ErrRepoNotCreated | |||||
} | |||||
g.repo.Status = models.RepositoryReady | |||||
return models.UpdateRepositoryCols(g.repo, "status") | |||||
} |
@@ -52,6 +52,7 @@ func TestGiteaUploadRepo(t *testing.T) { | |||||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{OwnerID: user.ID, Name: repoName}).(*models.Repository) | repo := models.AssertExistsAndLoadBean(t, &models.Repository{OwnerID: user.ID, Name: repoName}).(*models.Repository) | ||||
assert.True(t, repo.HasWiki()) | assert.True(t, repo.HasWiki()) | ||||
assert.EqualValues(t, models.RepositoryReady, repo.Status) | |||||
milestones, err := models.GetMilestones(models.GetMilestonesOption{ | milestones, err := models.GetMilestones(models.GetMilestonesOption{ | ||||
RepoID: repo.ID, | RepoID: repo.ID, | ||||
@@ -291,7 +291,7 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) | |||||
} | } | ||||
for _, asset := range rel.Assets { | for _, asset := range rel.Assets { | ||||
r.Assets = append(r.Assets, base.ReleaseAsset{ | |||||
r.Assets = append(r.Assets, &base.ReleaseAsset{ | |||||
ID: *asset.ID, | ID: *asset.ID, | ||||
Name: *asset.Name, | Name: *asset.Name, | ||||
ContentType: asset.ContentType, | ContentType: asset.ContentType, | ||||
@@ -299,6 +299,16 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) | |||||
DownloadCount: asset.DownloadCount, | DownloadCount: asset.DownloadCount, | ||||
Created: asset.CreatedAt.Time, | Created: asset.CreatedAt.Time, | ||||
Updated: asset.UpdatedAt.Time, | Updated: asset.UpdatedAt.Time, | ||||
DownloadFunc: func() (io.ReadCloser, error) { | |||||
asset, redir, err := g.client.Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, *asset.ID, http.DefaultClient) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if asset == nil { | |||||
return ioutil.NopCloser(bytes.NewBufferString(redir)), nil | |||||
} | |||||
return asset, nil | |||||
}, | |||||
}) | }) | ||||
} | } | ||||
return r | return r | ||||
@@ -330,18 +340,6 @@ func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) { | |||||
return releases, nil | return releases, nil | ||||
} | } | ||||
// GetAsset returns an asset | |||||
func (g *GithubDownloaderV3) GetAsset(_ string, _, id int64) (io.ReadCloser, error) { | |||||
asset, redir, err := g.client.Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, id, http.DefaultClient) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if asset == nil { | |||||
return ioutil.NopCloser(bytes.NewBufferString(redir)), nil | |||||
} | |||||
return asset, nil | |||||
} | |||||
// GetIssues returns issues according start and limit | // GetIssues returns issues according start and limit | ||||
func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | ||||
if perPage > g.maxPerPage { | if perPage > g.maxPerPage { | ||||
@@ -363,6 +361,7 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, | |||||
if err != nil { | if err != nil { | ||||
return nil, false, fmt.Errorf("error while listing repos: %v", err) | return nil, false, fmt.Errorf("error while listing repos: %v", err) | ||||
} | } | ||||
log.Trace("Request get issues %d/%d, but in fact get %d", perPage, page, len(issues)) | |||||
g.rate = &resp.Rate | g.rate = &resp.Rate | ||||
for _, issue := range issues { | for _, issue := range issues { | ||||
if issue.IsPullRequest() { | if issue.IsPullRequest() { | ||||
@@ -295,12 +295,32 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea | |||||
} | } | ||||
for k, asset := range rel.Assets.Links { | for k, asset := range rel.Assets.Links { | ||||
r.Assets = append(r.Assets, base.ReleaseAsset{ | |||||
r.Assets = append(r.Assets, &base.ReleaseAsset{ | |||||
ID: int64(asset.ID), | ID: int64(asset.ID), | ||||
Name: asset.Name, | Name: asset.Name, | ||||
ContentType: &rel.Assets.Sources[k].Format, | ContentType: &rel.Assets.Sources[k].Format, | ||||
Size: &zero, | Size: &zero, | ||||
DownloadCount: &zero, | DownloadCount: &zero, | ||||
DownloadFunc: func() (io.ReadCloser, error) { | |||||
link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, asset.ID, gitlab.WithContext(g.ctx)) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
req, err := http.NewRequest("GET", link.URL, nil) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
req = req.WithContext(g.ctx) | |||||
resp, err := http.DefaultClient.Do(req) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
// resp.Body is closed by the uploader | |||||
return resp.Body, nil | |||||
}, | |||||
}) | }) | ||||
} | } | ||||
return r | return r | ||||
@@ -329,28 +349,6 @@ func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) { | |||||
return releases, nil | return releases, nil | ||||
} | } | ||||
// GetAsset returns an asset | |||||
func (g *GitlabDownloader) GetAsset(tag string, _, id int64) (io.ReadCloser, error) { | |||||
link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, tag, int(id), gitlab.WithContext(g.ctx)) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
req, err := http.NewRequest("GET", link.URL, nil) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
req = req.WithContext(g.ctx) | |||||
resp, err := http.DefaultClient.Do(req) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
// resp.Body is closed by the uploader | |||||
return resp.Body, nil | |||||
} | |||||
// GetIssues returns issues according start and limit | // GetIssues returns issues according start and limit | ||||
// Note: issue label description and colors are not supported by the go-gitlab library at this time | // Note: issue label description and colors are not supported by the go-gitlab library at this time | ||||
func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | ||||
@@ -73,10 +73,30 @@ func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, | |||||
if err != nil { | if err != nil { | ||||
return nil, err | return nil, err | ||||
} | } | ||||
downloader, err := newDownloader(ctx, ownerName, opts) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
var uploader = NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName) | |||||
uploader.gitServiceType = opts.GitServiceType | |||||
if err := migrateRepository(downloader, uploader, opts); err != nil { | |||||
if err1 := uploader.Rollback(); err1 != nil { | |||||
log.Error("rollback failed: %v", err1) | |||||
} | |||||
if err2 := models.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil { | |||||
log.Error("create respotiry notice failed: ", err2) | |||||
} | |||||
return nil, err | |||||
} | |||||
return uploader.repo, nil | |||||
} | |||||
func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptions) (base.Downloader, error) { | |||||
var ( | var ( | ||||
downloader base.Downloader | downloader base.Downloader | ||||
uploader = NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName) | |||||
err error | |||||
) | ) | ||||
for _, factory := range factories { | for _, factory := range factories { | ||||
@@ -101,24 +121,10 @@ func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, | |||||
log.Trace("Will migrate from git: %s", opts.OriginalURL) | log.Trace("Will migrate from git: %s", opts.OriginalURL) | ||||
} | } | ||||
uploader.gitServiceType = opts.GitServiceType | |||||
if setting.Migrations.MaxAttempts > 1 { | if setting.Migrations.MaxAttempts > 1 { | ||||
downloader = base.NewRetryDownloader(ctx, downloader, setting.Migrations.MaxAttempts, setting.Migrations.RetryBackoff) | downloader = base.NewRetryDownloader(ctx, downloader, setting.Migrations.MaxAttempts, setting.Migrations.RetryBackoff) | ||||
} | } | ||||
if err := migrateRepository(downloader, uploader, opts); err != nil { | |||||
if err1 := uploader.Rollback(); err1 != nil { | |||||
log.Error("rollback failed: %v", err1) | |||||
} | |||||
if err2 := models.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil { | |||||
log.Error("create repository notice failed: ", err2) | |||||
} | |||||
return nil, err | |||||
} | |||||
return uploader.repo, nil | |||||
return downloader, nil | |||||
} | } | ||||
// migrateRepository will download information and then upload it to Uploader, this is a simple | // migrateRepository will download information and then upload it to Uploader, this is a simple | ||||
@@ -204,7 +210,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts | |||||
relBatchSize = len(releases) | relBatchSize = len(releases) | ||||
} | } | ||||
if err := uploader.CreateReleases(downloader, releases[:relBatchSize]...); err != nil { | |||||
if err := uploader.CreateReleases(releases[:relBatchSize]...); err != nil { | |||||
return err | return err | ||||
} | } | ||||
releases = releases[relBatchSize:] | releases = releases[relBatchSize:] | ||||
@@ -235,31 +241,30 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts | |||||
return err | return err | ||||
} | } | ||||
if !opts.Comments { | |||||
continue | |||||
} | |||||
if opts.Comments { | |||||
var allComments = make([]*base.Comment, 0, commentBatchSize) | |||||
for _, issue := range issues { | |||||
log.Trace("migrating issue %d's comments", issue.Number) | |||||
comments, err := downloader.GetComments(issue.Number) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
var allComments = make([]*base.Comment, 0, commentBatchSize) | |||||
for _, issue := range issues { | |||||
comments, err := downloader.GetComments(issue.Number) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
allComments = append(allComments, comments...) | |||||
allComments = append(allComments, comments...) | |||||
if len(allComments) >= commentBatchSize { | |||||
if err := uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { | |||||
return err | |||||
} | |||||
if len(allComments) >= commentBatchSize { | |||||
if err := uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { | |||||
return err | |||||
allComments = allComments[commentBatchSize:] | |||||
} | } | ||||
allComments = allComments[commentBatchSize:] | |||||
} | } | ||||
} | |||||
if len(allComments) > 0 { | |||||
if err := uploader.CreateComments(allComments...); err != nil { | |||||
return err | |||||
if len(allComments) > 0 { | |||||
if err := uploader.CreateComments(allComments...); err != nil { | |||||
return err | |||||
} | |||||
} | } | ||||
} | } | ||||
@@ -282,65 +287,64 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts | |||||
return err | return err | ||||
} | } | ||||
if !opts.Comments { | |||||
continue | |||||
} | |||||
// plain comments | |||||
var allComments = make([]*base.Comment, 0, commentBatchSize) | |||||
for _, pr := range prs { | |||||
comments, err := downloader.GetComments(pr.Number) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if opts.Comments { | |||||
// plain comments | |||||
var allComments = make([]*base.Comment, 0, commentBatchSize) | |||||
for _, pr := range prs { | |||||
log.Trace("migrating pull request %d's comments", pr.Number) | |||||
comments, err := downloader.GetComments(pr.Number) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
allComments = append(allComments, comments...) | |||||
allComments = append(allComments, comments...) | |||||
if len(allComments) >= commentBatchSize { | |||||
if err := uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { | |||||
return err | |||||
if len(allComments) >= commentBatchSize { | |||||
if err := uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { | |||||
return err | |||||
} | |||||
allComments = allComments[commentBatchSize:] | |||||
} | } | ||||
allComments = allComments[commentBatchSize:] | |||||
} | } | ||||
} | |||||
if len(allComments) > 0 { | |||||
if err := uploader.CreateComments(allComments...); err != nil { | |||||
return err | |||||
if len(allComments) > 0 { | |||||
if err := uploader.CreateComments(allComments...); err != nil { | |||||
return err | |||||
} | |||||
} | } | ||||
} | |||||
// migrate reviews | |||||
var allReviews = make([]*base.Review, 0, reviewBatchSize) | |||||
for _, pr := range prs { | |||||
number := pr.Number | |||||
// migrate reviews | |||||
var allReviews = make([]*base.Review, 0, reviewBatchSize) | |||||
for _, pr := range prs { | |||||
number := pr.Number | |||||
// on gitlab migrations pull number change | |||||
if pr.OriginalNumber > 0 { | |||||
number = pr.OriginalNumber | |||||
} | |||||
// on gitlab migrations pull number change | |||||
if pr.OriginalNumber > 0 { | |||||
number = pr.OriginalNumber | |||||
} | |||||
reviews, err := downloader.GetReviews(number) | |||||
if pr.OriginalNumber > 0 { | |||||
for i := range reviews { | |||||
reviews[i].IssueIndex = pr.Number | |||||
reviews, err := downloader.GetReviews(number) | |||||
if pr.OriginalNumber > 0 { | |||||
for i := range reviews { | |||||
reviews[i].IssueIndex = pr.Number | |||||
} | |||||
} | |||||
if err != nil { | |||||
return err | |||||
} | } | ||||
} | |||||
if err != nil { | |||||
return err | |||||
} | |||||
allReviews = append(allReviews, reviews...) | |||||
allReviews = append(allReviews, reviews...) | |||||
if len(allReviews) >= reviewBatchSize { | |||||
if err := uploader.CreateReviews(allReviews[:reviewBatchSize]...); err != nil { | |||||
return err | |||||
if len(allReviews) >= reviewBatchSize { | |||||
if err := uploader.CreateReviews(allReviews[:reviewBatchSize]...); err != nil { | |||||
return err | |||||
} | |||||
allReviews = allReviews[reviewBatchSize:] | |||||
} | } | ||||
allReviews = allReviews[reviewBatchSize:] | |||||
} | } | ||||
} | |||||
if len(allReviews) > 0 { | |||||
if err := uploader.CreateReviews(allReviews...); err != nil { | |||||
return err | |||||
if len(allReviews) > 0 { | |||||
if err := uploader.CreateReviews(allReviews...); err != nil { | |||||
return err | |||||
} | |||||
} | } | ||||
} | } | ||||
@@ -350,7 +354,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts | |||||
} | } | ||||
} | } | ||||
return nil | |||||
return uploader.Finish() | |||||
} | } | ||||
// Init migrations service | // Init migrations service | ||||
@@ -0,0 +1,276 @@ | |||||
// Copyright 2020 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" | |||||
"io/ioutil" | |||||
"os" | |||||
"path/filepath" | |||||
"strconv" | |||||
"code.gitea.io/gitea/modules/migrations/base" | |||||
"gopkg.in/yaml.v2" | |||||
) | |||||
// RepositoryRestorer implements an Downloader from the local directory | |||||
type RepositoryRestorer struct { | |||||
ctx context.Context | |||||
baseDir string | |||||
repoOwner string | |||||
repoName string | |||||
} | |||||
// NewRepositoryRestorer creates a repository restorer which could restore repository from a dumped folder | |||||
func NewRepositoryRestorer(ctx context.Context, baseDir string, owner, repoName string) (*RepositoryRestorer, error) { | |||||
baseDir, err := filepath.Abs(baseDir) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return &RepositoryRestorer{ | |||||
ctx: ctx, | |||||
baseDir: baseDir, | |||||
repoOwner: owner, | |||||
repoName: repoName, | |||||
}, nil | |||||
} | |||||
func (r *RepositoryRestorer) commentDir() string { | |||||
return filepath.Join(r.baseDir, "comments") | |||||
} | |||||
func (r *RepositoryRestorer) reviewDir() string { | |||||
return filepath.Join(r.baseDir, "reviews") | |||||
} | |||||
// SetContext set context | |||||
func (r *RepositoryRestorer) SetContext(ctx context.Context) { | |||||
r.ctx = ctx | |||||
} | |||||
// GetRepoInfo returns a repository information | |||||
func (r *RepositoryRestorer) GetRepoInfo() (*base.Repository, error) { | |||||
p := filepath.Join(r.baseDir, "repo.yml") | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
var opts = make(map[string]string) | |||||
err = yaml.Unmarshal(bs, &opts) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
isPrivate, _ := strconv.ParseBool(opts["is_private"]) | |||||
return &base.Repository{ | |||||
Owner: r.repoOwner, | |||||
Name: r.repoName, | |||||
IsPrivate: isPrivate, | |||||
Description: opts["description"], | |||||
OriginalURL: opts["original_url"], | |||||
CloneURL: opts["clone_addr"], | |||||
DefaultBranch: opts["default_branch"], | |||||
}, nil | |||||
} | |||||
// GetTopics return github topics | |||||
func (r *RepositoryRestorer) GetTopics() ([]string, error) { | |||||
p := filepath.Join(r.baseDir, "topic.yml") | |||||
var topics = struct { | |||||
Topics []string `yaml:"topics"` | |||||
}{} | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
err = yaml.Unmarshal(bs, &topics) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return topics.Topics, nil | |||||
} | |||||
// GetMilestones returns milestones | |||||
func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) { | |||||
var milestones = make([]*base.Milestone, 0, 10) | |||||
p := filepath.Join(r.baseDir, "milestone.yml") | |||||
_, err := os.Stat(p) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return nil, nil | |||||
} | |||||
return nil, err | |||||
} | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
err = yaml.Unmarshal(bs, &milestones) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return milestones, nil | |||||
} | |||||
// GetReleases returns releases | |||||
func (r *RepositoryRestorer) GetReleases() ([]*base.Release, error) { | |||||
var releases = make([]*base.Release, 0, 10) | |||||
p := filepath.Join(r.baseDir, "release.yml") | |||||
_, err := os.Stat(p) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return nil, nil | |||||
} | |||||
return nil, err | |||||
} | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
err = yaml.Unmarshal(bs, &releases) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
for _, rel := range releases { | |||||
for _, asset := range rel.Assets { | |||||
*asset.DownloadURL = "file://" + filepath.Join(r.baseDir, *asset.DownloadURL) | |||||
} | |||||
} | |||||
return releases, nil | |||||
} | |||||
// GetLabels returns labels | |||||
func (r *RepositoryRestorer) GetLabels() ([]*base.Label, error) { | |||||
var labels = make([]*base.Label, 0, 10) | |||||
p := filepath.Join(r.baseDir, "label.yml") | |||||
_, err := os.Stat(p) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return nil, nil | |||||
} | |||||
return nil, err | |||||
} | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
err = yaml.Unmarshal(bs, &labels) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return labels, nil | |||||
} | |||||
// GetIssues returns issues according start and limit | |||||
func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { | |||||
var issues = make([]*base.Issue, 0, 10) | |||||
p := filepath.Join(r.baseDir, "issue.yml") | |||||
_, err := os.Stat(p) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return nil, true, nil | |||||
} | |||||
return nil, false, err | |||||
} | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, false, err | |||||
} | |||||
err = yaml.Unmarshal(bs, &issues) | |||||
if err != nil { | |||||
return nil, false, err | |||||
} | |||||
return issues, true, nil | |||||
} | |||||
// GetComments returns comments according issueNumber | |||||
func (r *RepositoryRestorer) GetComments(issueNumber int64) ([]*base.Comment, error) { | |||||
var comments = make([]*base.Comment, 0, 10) | |||||
p := filepath.Join(r.commentDir(), fmt.Sprintf("%d.yml", issueNumber)) | |||||
_, err := os.Stat(p) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return nil, nil | |||||
} | |||||
return nil, err | |||||
} | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
err = yaml.Unmarshal(bs, &comments) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return comments, nil | |||||
} | |||||
// GetPullRequests returns pull requests according page and perPage | |||||
func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { | |||||
var pulls = make([]*base.PullRequest, 0, 10) | |||||
p := filepath.Join(r.baseDir, "pull_request.yml") | |||||
_, err := os.Stat(p) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return nil, true, nil | |||||
} | |||||
return nil, false, err | |||||
} | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, false, err | |||||
} | |||||
err = yaml.Unmarshal(bs, &pulls) | |||||
if err != nil { | |||||
return nil, false, err | |||||
} | |||||
for _, pr := range pulls { | |||||
pr.PatchURL = "file://" + filepath.Join(r.baseDir, pr.PatchURL) | |||||
} | |||||
return pulls, true, nil | |||||
} | |||||
// GetReviews returns pull requests review | |||||
func (r *RepositoryRestorer) GetReviews(pullRequestNumber int64) ([]*base.Review, error) { | |||||
var reviews = make([]*base.Review, 0, 10) | |||||
p := filepath.Join(r.reviewDir(), fmt.Sprintf("%d.yml", pullRequestNumber)) | |||||
_, err := os.Stat(p) | |||||
if err != nil { | |||||
if os.IsNotExist(err) { | |||||
return nil, nil | |||||
} | |||||
return nil, err | |||||
} | |||||
bs, err := ioutil.ReadFile(p) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
err = yaml.Unmarshal(bs, &reviews) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
return reviews, nil | |||||
} |
@@ -0,0 +1,40 @@ | |||||
// Copyright 2020 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 uri | |||||
import ( | |||||
"fmt" | |||||
"io" | |||||
"net/http" | |||||
"net/url" | |||||
"os" | |||||
"strings" | |||||
) | |||||
// ErrURISchemeNotSupported represents a scheme error | |||||
type ErrURISchemeNotSupported struct { | |||||
Scheme string | |||||
} | |||||
func (e ErrURISchemeNotSupported) Error() string { | |||||
return fmt.Sprintf("Unsupported scheme: %v", e.Scheme) | |||||
} | |||||
// Open open a local file or a remote file | |||||
func Open(uriStr string) (io.ReadCloser, error) { | |||||
u, err := url.Parse(uriStr) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
switch strings.ToLower(u.Scheme) { | |||||
case "http", "https": | |||||
f, err := http.Get(uriStr) | |||||
return f.Body, err | |||||
case "file": | |||||
return os.Open(u.Path) | |||||
default: | |||||
return nil, ErrURISchemeNotSupported{Scheme: u.Scheme} | |||||
} | |||||
} |
@@ -0,0 +1,20 @@ | |||||
// Copyright 2020 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 uri | |||||
import ( | |||||
"path/filepath" | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
) | |||||
func TestReadURI(t *testing.T) { | |||||
p, err := filepath.Abs("./uri.go") | |||||
assert.NoError(t, err) | |||||
f, err := Open("file://" + p) | |||||
assert.NoError(t, err) | |||||
defer f.Close() | |||||
} |
@@ -176,11 +176,8 @@ func Migrate(ctx *context.APIContext, form api.MigrateRepoOptions) { | |||||
} | } | ||||
if err == nil { | if err == nil { | ||||
repo.Status = models.RepositoryReady | |||||
if err := models.UpdateRepositoryCols(repo, "status"); err == nil { | |||||
notification.NotifyMigrateRepository(ctx.User, repoOwner, repo) | |||||
return | |||||
} | |||||
notification.NotifyMigrateRepository(ctx.User, repoOwner, repo) | |||||
return | |||||
} | } | ||||
if repo != nil { | if repo != nil { | ||||