Skip to content

Commit 08069dc

Browse files
authored
Improve migrations to support migrating milestones/labels/issues/comments/pullrequests (#6290)
* add migrations * fix package dependency * fix lints * implements migrations except pull requests * add releases * migrating releases * fix bug * fix lint * fix migrate releases * fix tests * add rollback * pull request migtations * fix import * fix go module vendor * add tests for upload to gitea * more migrate options * fix swagger-check * fix misspell * add options on migration UI * fix log error * improve UI options on migrating * add support for username password when migrating from github * fix tests * remove comments and fix migrate limitation * improve error handles * migrate API will also support migrate milestones/labels/issues/pulls/releases * fix tests and remove unused codes * add DownloaderFactory and docs about how to create a new Downloader * fix misspell * fix migration docs * Add hints about migrate options on migration page * fix tests
1 parent 1c7c739 commit 08069dc

File tree

128 files changed

+33540
-75
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+33540
-75
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ coverage.all
6666
/integrations/mssql.ini
6767
/node_modules
6868
/modules/indexer/issues/indexers
69-
69+
routers/repo/authorized_keys
7070

7171
# Snapcraft
7272
snap/.snapcraft/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
date: "2019-04-15T17:29:00+08:00"
3+
title: "Advanced: Migrations Interfaces"
4+
slug: "migrations-interfaces"
5+
weight: 30
6+
toc: true
7+
draft: false
8+
menu:
9+
sidebar:
10+
parent: "advanced"
11+
name: "Migrations Interfaces"
12+
weight: 55
13+
identifier: "migrations-interfaces"
14+
---
15+
16+
# Migration Features
17+
18+
The new migration features were introduced in Gitea 1.9.0. It defines two interfaces to support migrating
19+
repositories data from other git host platforms to gitea or, in the future migrating gitea data to other
20+
git host platforms. Currently, only the migrations from github via APIv3 to Gitea is implemented.
21+
22+
First of all, Gitea defines some standard objects in packages `modules/migrations/base`. They are
23+
`Repository`, `Milestone`, `Release`, `Label`, `Issue`, `Comment`, `PullRequest`.
24+
25+
## Downloader Interfaces
26+
27+
To migrate from a new git host platform, there are two steps to be updated.
28+
29+
- You should implement a `Downloader` which will get all kinds of repository informations.
30+
- You should implement a `DownloaderFactory` which is used to detect if the URL matches and
31+
create a Downloader.
32+
- You'll need to register the `DownloaderFactory` via `RegisterDownloaderFactory` on init.
33+
34+
```Go
35+
type Downloader interface {
36+
GetRepoInfo() (*Repository, error)
37+
GetMilestones() ([]*Milestone, error)
38+
GetReleases() ([]*Release, error)
39+
GetLabels() ([]*Label, error)
40+
GetIssues(start, limit int) ([]*Issue, error)
41+
GetComments(issueNumber int64) ([]*Comment, error)
42+
GetPullRequests(start, limit int) ([]*PullRequest, error)
43+
}
44+
```
45+
46+
```Go
47+
type DownloaderFactory interface {
48+
Match(opts MigrateOptions) (bool, error)
49+
New(opts MigrateOptions) (Downloader, error)
50+
}
51+
```
52+
53+
## Uploader Interface
54+
55+
Currently, only a `GiteaLocalUploader` is implemented, so we only save downloaded
56+
data via this `Uploader` on the local Gitea instance. Other uploaders are not supported
57+
and will be implemented in future.
58+
59+
```Go
60+
// Uploader uploads all the informations
61+
type Uploader interface {
62+
CreateRepo(repo *Repository, includeWiki bool) error
63+
CreateMilestone(milestone *Milestone) error
64+
CreateRelease(release *Release) error
65+
CreateLabel(label *Label) error
66+
CreateIssue(issue *Issue) error
67+
CreateComment(issueNumber int64, comment *Comment) error
68+
CreatePullRequest(pr *PullRequest) error
69+
Rollback() error
70+
}
71+
72+
```

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ require (
6262
github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561
6363
github.com/gogits/cron v0.0.0-20160810035002-7f3990acf183
6464
github.com/gogo/protobuf v1.2.1 // indirect
65+
github.com/google/go-github/v24 v24.0.1
6566
github.com/gorilla/context v1.1.1
6667
github.com/issue9/assert v1.3.2 // indirect
6768
github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c
@@ -115,7 +116,7 @@ require (
115116
go.etcd.io/bbolt v1.3.2 // indirect
116117
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793
117118
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519
118-
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759 // indirect
119+
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759
119120
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223
120121
golang.org/x/text v0.3.0
121122
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect

go.sum

+11
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pO
142142
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
143143
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
144144
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
145+
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
146+
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
147+
github.com/google/go-github/v24 v24.0.1 h1:KCt1LjMJEey1qvPXxa9SjaWxwTsCWSq6p2Ju57UR4Q4=
148+
github.com/google/go-github/v24 v24.0.1/go.mod h1:CRqaW1Uns1TCkP0wqTpxYyRxRjxwvKU/XSS44u6X74M=
149+
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
150+
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
145151
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
146152
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
147153
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
@@ -309,24 +315,29 @@ github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
309315
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
310316
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
311317
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
318+
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
312319
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
313320
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
314321
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
322+
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
315323
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
316324
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM=
317325
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
318326
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
327+
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
319328
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759 h1:TMrx+Qdx7uJAeUbv15N72h5Hmyb5+VDjEiMufAEAM04=
320329
golang.org/x/oauth2 v0.0.0-20181101160152-c453e0c75759/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
321330
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
322331
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
332+
golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
323333
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
324334
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
325335
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
326336
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
327337
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
328338
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
329339
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
340+
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
330341
google.golang.org/appengine v1.2.0 h1:S0iUepdCWODXRvtE+gcRDd15L+k+k1AiHlMiMjefH24=
331342
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
332343
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=

models/migrate.go

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import "github.com/go-xorm/xorm"
8+
9+
// InsertIssue insert one issue to database
10+
func InsertIssue(issue *Issue, labelIDs []int64) error {
11+
sess := x.NewSession()
12+
if err := sess.Begin(); err != nil {
13+
return err
14+
}
15+
16+
if err := insertIssue(sess, issue, labelIDs); err != nil {
17+
return err
18+
}
19+
return sess.Commit()
20+
}
21+
22+
func insertIssue(sess *xorm.Session, issue *Issue, labelIDs []int64) error {
23+
if issue.MilestoneID > 0 {
24+
sess.Incr("num_issues")
25+
if issue.IsClosed {
26+
sess.Incr("num_closed_issues")
27+
}
28+
if _, err := sess.ID(issue.MilestoneID).NoAutoTime().Update(new(Milestone)); err != nil {
29+
return err
30+
}
31+
}
32+
if _, err := sess.NoAutoTime().Insert(issue); err != nil {
33+
return err
34+
}
35+
var issueLabels = make([]IssueLabel, 0, len(labelIDs))
36+
for _, labelID := range labelIDs {
37+
issueLabels = append(issueLabels, IssueLabel{
38+
IssueID: issue.ID,
39+
LabelID: labelID,
40+
})
41+
}
42+
if _, err := sess.Insert(issueLabels); err != nil {
43+
return err
44+
}
45+
if !issue.IsPull {
46+
sess.ID(issue.RepoID).Incr("num_issues")
47+
if issue.IsClosed {
48+
sess.Incr("num_closed_issues")
49+
}
50+
} else {
51+
sess.ID(issue.RepoID).Incr("num_pulls")
52+
if issue.IsClosed {
53+
sess.Incr("num_closed_pulls")
54+
}
55+
}
56+
if _, err := sess.NoAutoTime().Update(issue.Repo); err != nil {
57+
return err
58+
}
59+
60+
sess.Incr("num_issues")
61+
if issue.IsClosed {
62+
sess.Incr("num_closed_issues")
63+
}
64+
if _, err := sess.In("id", labelIDs).Update(new(Label)); err != nil {
65+
return err
66+
}
67+
68+
if issue.MilestoneID > 0 {
69+
if _, err := sess.ID(issue.MilestoneID).SetExpr("completeness", "num_closed_issues * 100 / num_issues").Update(new(Milestone)); err != nil {
70+
return err
71+
}
72+
}
73+
74+
return nil
75+
}
76+
77+
// InsertComment inserted a comment
78+
func InsertComment(comment *Comment) error {
79+
sess := x.NewSession()
80+
defer sess.Close()
81+
if err := sess.Begin(); err != nil {
82+
return err
83+
}
84+
if _, err := sess.NoAutoTime().Insert(comment); err != nil {
85+
return err
86+
}
87+
if _, err := sess.ID(comment.IssueID).Incr("num_comments").Update(new(Issue)); err != nil {
88+
return err
89+
}
90+
return sess.Commit()
91+
}
92+
93+
// InsertPullRequest inserted a pull request
94+
func InsertPullRequest(pr *PullRequest, labelIDs []int64) error {
95+
sess := x.NewSession()
96+
defer sess.Close()
97+
if err := sess.Begin(); err != nil {
98+
return err
99+
}
100+
if err := insertIssue(sess, pr.Issue, labelIDs); err != nil {
101+
return err
102+
}
103+
pr.IssueID = pr.Issue.ID
104+
if _, err := sess.NoAutoTime().Insert(pr); err != nil {
105+
return err
106+
}
107+
return sess.Commit()
108+
}
109+
110+
// MigrateRelease migrates release
111+
func MigrateRelease(rel *Release) error {
112+
sess := x.NewSession()
113+
if err := sess.Begin(); err != nil {
114+
return err
115+
}
116+
117+
var oriRel = Release{
118+
RepoID: rel.RepoID,
119+
TagName: rel.TagName,
120+
}
121+
exist, err := sess.Get(&oriRel)
122+
if err != nil {
123+
return err
124+
}
125+
if !exist {
126+
if _, err := sess.NoAutoTime().Insert(rel); err != nil {
127+
return err
128+
}
129+
} else {
130+
rel.ID = oriRel.ID
131+
if _, err := sess.ID(rel.ID).Cols("target, title, note, is_tag, num_commits").Update(rel); err != nil {
132+
return err
133+
}
134+
}
135+
136+
for i := 0; i < len(rel.Attachments); i++ {
137+
rel.Attachments[i].ReleaseID = rel.ID
138+
}
139+
140+
if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil {
141+
return err
142+
}
143+
144+
return sess.Commit()
145+
}

models/release_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func TestRelease_MirrorDelete(t *testing.T) {
107107
IsPrivate: false,
108108
IsMirror: true,
109109
RemoteAddr: repoPath,
110+
Wiki: true,
110111
}
111112
mirror, err := MigrateRepository(user, user, migrationOptions)
112113
assert.NoError(t, err)

models/repo.go

+18-15
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,7 @@ type MigrateRepoOptions struct {
896896
IsPrivate bool
897897
IsMirror bool
898898
RemoteAddr string
899+
Wiki bool // include wiki repository
899900
}
900901

901902
/*
@@ -917,7 +918,7 @@ func wikiRemoteURL(remote string) string {
917918
return ""
918919
}
919920

920-
// MigrateRepository migrates a existing repository from other project hosting.
921+
// MigrateRepository migrates an existing repository from other project hosting.
921922
func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, error) {
922923
repo, err := CreateRepository(doer, u, CreateRepoOptions{
923924
Name: opts.Name,
@@ -930,7 +931,6 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err
930931
}
931932

932933
repoPath := RepoPath(u.Name, opts.Name)
933-
wikiPath := WikiPath(u.Name, opts.Name)
934934

935935
if u.IsOrganization() {
936936
t, err := u.GetOwnerTeam()
@@ -956,22 +956,25 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err
956956
return repo, fmt.Errorf("Clone: %v", err)
957957
}
958958

959-
wikiRemotePath := wikiRemoteURL(opts.RemoteAddr)
960-
if len(wikiRemotePath) > 0 {
961-
if err := os.RemoveAll(wikiPath); err != nil {
962-
return repo, fmt.Errorf("Failed to remove %s: %v", wikiPath, err)
963-
}
964-
965-
if err = git.Clone(wikiRemotePath, wikiPath, git.CloneRepoOptions{
966-
Mirror: true,
967-
Quiet: true,
968-
Timeout: migrateTimeout,
969-
Branch: "master",
970-
}); err != nil {
971-
log.Warn("Clone wiki: %v", err)
959+
if opts.Wiki {
960+
wikiPath := WikiPath(u.Name, opts.Name)
961+
wikiRemotePath := wikiRemoteURL(opts.RemoteAddr)
962+
if len(wikiRemotePath) > 0 {
972963
if err := os.RemoveAll(wikiPath); err != nil {
973964
return repo, fmt.Errorf("Failed to remove %s: %v", wikiPath, err)
974965
}
966+
967+
if err = git.Clone(wikiRemotePath, wikiPath, git.CloneRepoOptions{
968+
Mirror: true,
969+
Quiet: true,
970+
Timeout: migrateTimeout,
971+
Branch: "master",
972+
}); err != nil {
973+
log.Warn("Clone wiki: %v", err)
974+
if err := os.RemoveAll(wikiPath); err != nil {
975+
return repo, fmt.Errorf("Failed to remove %s: %v", wikiPath, err)
976+
}
977+
}
975978
}
976979
}
977980

modules/auth/repo_form.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,16 @@ type MigrateRepoForm struct {
5151
// required: true
5252
UID int64 `json:"uid" binding:"Required"`
5353
// required: true
54-
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
55-
Mirror bool `json:"mirror"`
56-
Private bool `json:"private"`
57-
Description string `json:"description" binding:"MaxSize(255)"`
54+
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
55+
Mirror bool `json:"mirror"`
56+
Private bool `json:"private"`
57+
Description string `json:"description" binding:"MaxSize(255)"`
58+
Wiki bool `json:"wiki"`
59+
Milestones bool `json:"milestones"`
60+
Labels bool `json:"labels"`
61+
Issues bool `json:"issues"`
62+
PullRequests bool `json:"pull_requests"`
63+
Releases bool `json:"releases"`
5864
}
5965

6066
// Validate validates the fields

modules/migrations/base/comment.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2019 The Gitea Authors. All rights reserved.
2+
// Copyright 2018 Jonas Franz. All rights reserved.
3+
// Use of this source code is governed by a MIT-style
4+
// license that can be found in the LICENSE file.
5+
6+
package base
7+
8+
import "time"
9+
10+
// Comment is a standard comment information
11+
type Comment struct {
12+
PosterName string
13+
PosterEmail string
14+
Created time.Time
15+
Content string
16+
Reactions *Reactions
17+
}

0 commit comments

Comments
 (0)