Skip to content

Commit 440039c

Browse files
authored
Add push to remote mirror repository (#15157)
* Added push mirror model. * Integrated push mirror into queue. * Moved methods into own file. * Added basic implementation. * Mirror wiki too. * Removed duplicated method. * Get url for different remotes. * Added migration. * Unified remote url access. * Add/Remove push mirror remotes. * Prevent hangs with missing credentials. * Moved code between files. * Changed sanitizer interface. * Added push mirror backend methods. * Only update the mirror remote. * Limit refs on push. * Added UI part. * Added missing table. * Delete mirror if repository gets removed. * Changed signature. Handle object errors. * Added upload method. * Added "upload" unit tests. * Added transfer adapter unit tests. * Send correct headers. * Added pushing of LFS objects. * Added more logging. * Simpler body handling. * Process files in batches to reduce HTTP calls. * Added created timestamp. * Fixed invalid column name. * Changed name to prevent xorm auto setting. * Remove table header im empty. * Strip exit code from error message. * Added docs page about mirroring. * Fixed date. * Fixed merge errors. * Moved test to integrations. * Added push mirror test. * Added test.
1 parent 5d113bd commit 440039c

39 files changed

+2458
-875
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
date: "2021-05-13T00:00:00-00:00"
3+
title: "Repository Mirror"
4+
slug: "repo-mirror"
5+
weight: 45
6+
toc: false
7+
draft: false
8+
menu:
9+
sidebar:
10+
parent: "advanced"
11+
name: "Repository Mirror"
12+
weight: 45
13+
identifier: "repo-mirror"
14+
---
15+
16+
# Repository Mirror
17+
18+
Repository mirroring allows for the mirroring of repositories to and from external sources. You can use it to mirror branches, tags, and commits between repositories.
19+
20+
**Table of Contents**
21+
22+
{{< toc >}}
23+
24+
## Use cases
25+
26+
The following are some possible use cases for repository mirroring:
27+
28+
- You migrated to Gitea but still need to keep your project in another source. In that case, you can simply set it up to mirror to Gitea (pull) and all the essential history of commits, tags, and branches are available in your Gitea instance.
29+
- You have old projects in another source that you don’t use actively anymore, but don’t want to remove for archiving purposes. In that case, you can create a push mirror so that your active Gitea repository can push its changes to the old location.
30+
31+
## Pulling from a remote repository
32+
33+
For an existing remote repository, you can set up pull mirroring as follows:
34+
35+
1. Select **New Migration** in the **Create...** menu on the top right.
36+
2. Select the remote repository service.
37+
3. Enter a repository URL.
38+
4. If the repository needs authentication fill in your authentication information.
39+
5. Check the box **This repository will be a mirror**.
40+
5. Select **Migrate repository** to save the configuration.
41+
42+
The repository now gets mirrored periodically from the remote repository. You can force a sync by selecting **Synchronize Now** in the repository settings.
43+
44+
## Pushing to a remote repository
45+
46+
For an existing repository, you can set up push mirroring as follows:
47+
48+
1. In your repository, go to **Settings** > **Repository**, and then the **Mirror Settings** section.
49+
2. Enter a repository URL.
50+
3. If the repository needs authentication expand the **Authorization** section and fill in your authentication information.
51+
4. Select **Add Push Mirror** to save the configuration.
52+
53+
The repository now gets mirrored periodically to the remote repository. You can force a sync by selecting **Synchronize Now**. In case of an error a message displayed to help you resolve it.
54+
55+
:exclamation::exclamation: **NOTE:** This will force push to the remote repository. This will overwrite any changes in the remote repository! :exclamation::exclamation:
56+
57+
### Setting up a push mirror from Gitea to GitHub
58+
59+
To set up a mirror from Gitea to GitHub, you need to follow these steps:
60+
61+
1. Create a [GitHub personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with the *public_repo* box checked.
62+
2. Fill in the **Git Remote Repository URL**: `https://github.com/<your_github_group>/<your_github_project>.git`.
63+
3. Fill in the **Authorization** fields with your GitHub username and the personal access token.
64+
4. Select **Add Push Mirror** to save the configuration.
65+
66+
The repository pushes shortly thereafter. To force a push, select the **Synchronize Now** button.
67+
68+
### Setting up a push mirror from Gitea to GitLab
69+
70+
To set up a mirror from Gitea to GitLab, you need to follow these steps:
71+
72+
1. Create a [GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) with *write_repository* scope.
73+
2. Fill in the **Git Remote Repository URL**: `https://<destination host>/<your_gitlab_group_or_name>/<your_gitlab_project>.git`.
74+
3. Fill in the **Authorization** fields with `oauth2` as **Username** and your GitLab personal access token as **Password**.
75+
4. Select **Add Push Mirror** to save the configuration.
76+
77+
The repository pushes shortly thereafter. To force a push, select the **Synchronize Now** button.
78+
79+
### Setting up a push mirror from Gitea to Bitbucket
80+
81+
To set up a mirror from Gitea to Bitbucket, you need to follow these steps:
82+
83+
1. Create a [Bitbucket app password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) with the *Repository Write* box checked.
84+
2. Fill in the **Git Remote Repository URL**: `https://bitbucket.org/<your_bitbucket_group_or_name>/<your_bitbucket_project>.git`.
85+
3. Fill in the **Authorization** fields with your Bitbucket username and the app password as **Password**.
86+
4. Select **Add Push Mirror** to save the configuration.
87+
88+
The repository pushes shortly thereafter. To force a push, select the **Synchronize Now** button.

services/mirror/mirror_test.go renamed to integrations/mirror_pull_test.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,24 @@
22
// Use of this source code is governed by a MIT-style
33
// license that can be found in the LICENSE file.
44

5-
package mirror
5+
package integrations
66

77
import (
88
"context"
9-
"path/filepath"
109
"testing"
1110

1211
"code.gitea.io/gitea/models"
1312
"code.gitea.io/gitea/modules/git"
1413
migration "code.gitea.io/gitea/modules/migrations/base"
1514
"code.gitea.io/gitea/modules/repository"
15+
mirror_service "code.gitea.io/gitea/services/mirror"
1616
release_service "code.gitea.io/gitea/services/release"
1717

1818
"github.com/stretchr/testify/assert"
1919
)
2020

21-
func TestMain(m *testing.M) {
22-
models.MainTest(m, filepath.Join("..", ".."))
23-
}
24-
25-
func TestRelease_MirrorDelete(t *testing.T) {
26-
assert.NoError(t, models.PrepareTestDatabase())
21+
func TestMirrorPull(t *testing.T) {
22+
defer prepareTestEnv(t)()
2723

2824
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
2925
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
@@ -76,7 +72,7 @@ func TestRelease_MirrorDelete(t *testing.T) {
7672
err = mirror.GetMirror()
7773
assert.NoError(t, err)
7874

79-
_, ok := runSync(ctx, mirror.Mirror)
75+
ok := mirror_service.SyncPullMirror(ctx, mirror.ID)
8076
assert.True(t, ok)
8177

8278
count, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions)
@@ -87,7 +83,7 @@ func TestRelease_MirrorDelete(t *testing.T) {
8783
assert.NoError(t, err)
8884
assert.NoError(t, release_service.DeleteReleaseByID(release.ID, user, true))
8985

90-
_, ok = runSync(ctx, mirror.Mirror)
86+
ok = mirror_service.SyncPullMirror(ctx, mirror.ID)
9187
assert.True(t, ok)
9288

9389
count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions)

integrations/mirror_push_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2021 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 integrations
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"net/http"
11+
"net/url"
12+
"testing"
13+
14+
"code.gitea.io/gitea/models"
15+
"code.gitea.io/gitea/modules/git"
16+
"code.gitea.io/gitea/modules/repository"
17+
"code.gitea.io/gitea/modules/setting"
18+
mirror_service "code.gitea.io/gitea/services/mirror"
19+
20+
"github.com/stretchr/testify/assert"
21+
)
22+
23+
func TestMirrorPush(t *testing.T) {
24+
onGiteaRun(t, testMirrorPush)
25+
}
26+
27+
func testMirrorPush(t *testing.T, u *url.URL) {
28+
defer prepareTestEnv(t)()
29+
30+
setting.Migrations.AllowLocalNetworks = true
31+
32+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
33+
srcRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
34+
35+
mirrorRepo, err := repository.CreateRepository(user, user, models.CreateRepoOptions{
36+
Name: "test-push-mirror",
37+
})
38+
assert.NoError(t, err)
39+
40+
ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name)
41+
42+
doCreatePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword)(t)
43+
44+
mirrors, err := models.GetPushMirrorsByRepoID(srcRepo.ID)
45+
assert.NoError(t, err)
46+
assert.Len(t, mirrors, 1)
47+
48+
ok := mirror_service.SyncPushMirror(context.Background(), mirrors[0].ID)
49+
assert.True(t, ok)
50+
51+
srcGitRepo, err := git.OpenRepository(srcRepo.RepoPath())
52+
assert.NoError(t, err)
53+
defer srcGitRepo.Close()
54+
55+
srcCommit, err := srcGitRepo.GetBranchCommit("master")
56+
assert.NoError(t, err)
57+
58+
mirrorGitRepo, err := git.OpenRepository(mirrorRepo.RepoPath())
59+
assert.NoError(t, err)
60+
defer mirrorGitRepo.Close()
61+
62+
mirrorCommit, err := mirrorGitRepo.GetBranchCommit("master")
63+
assert.NoError(t, err)
64+
65+
assert.Equal(t, srcCommit.ID, mirrorCommit.ID)
66+
}
67+
68+
func doCreatePushMirror(ctx APITestContext, address, username, password string) func(t *testing.T) {
69+
return func(t *testing.T) {
70+
csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
71+
72+
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
73+
"_csrf": csrf,
74+
"action": "push-mirror-add",
75+
"push_mirror_address": address,
76+
"push_mirror_username": username,
77+
"push_mirror_password": password,
78+
"push_mirror_interval": "0",
79+
})
80+
ctx.Session.MakeRequest(t, req, http.StatusFound)
81+
82+
flashCookie := ctx.Session.GetCookie("macaron_flash")
83+
assert.NotNil(t, flashCookie)
84+
assert.Contains(t, flashCookie.Value, "success")
85+
}
86+
}

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ var migrations = []Migration{
315315
NewMigration("Always save primary email on email address table", addPrimaryEmail2EmailAddress),
316316
// v182 -> v183
317317
NewMigration("Add issue resource index table", addIssueResourceIndexTable),
318+
// v183 -> v184
319+
NewMigration("Create PushMirror table", createPushMirrorTable),
318320
}
319321

320322
// GetCurrentDBVersion returns the current db version

models/migrations/v180.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func removeCredentials(payload string) (string, error) {
6464

6565
opts.AuthPassword = ""
6666
opts.AuthToken = ""
67-
opts.CloneAddr = util.SanitizeURLCredentials(opts.CloneAddr, true)
67+
opts.CloneAddr = util.NewStringURLSanitizer(opts.CloneAddr, true).Replace(opts.CloneAddr)
6868

6969
confBytes, err := json.Marshal(opts)
7070
if err != nil {

models/migrations/v183.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2021 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 migrations
6+
7+
import (
8+
"fmt"
9+
"time"
10+
11+
"code.gitea.io/gitea/modules/timeutil"
12+
13+
"xorm.io/xorm"
14+
)
15+
16+
func createPushMirrorTable(x *xorm.Engine) error {
17+
type PushMirror struct {
18+
ID int64 `xorm:"pk autoincr"`
19+
RepoID int64 `xorm:"INDEX"`
20+
RemoteName string
21+
22+
Interval time.Duration
23+
CreatedUnix timeutil.TimeStamp `xorm:"created"`
24+
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
25+
LastError string `xorm:"text"`
26+
}
27+
28+
sess := x.NewSession()
29+
defer sess.Close()
30+
if err := sess.Begin(); err != nil {
31+
return err
32+
}
33+
34+
if err := sess.Sync2(new(PushMirror)); err != nil {
35+
return fmt.Errorf("Sync2: %v", err)
36+
}
37+
38+
return sess.Commit()
39+
}

models/models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ func init() {
135135
new(Session),
136136
new(RepoTransfer),
137137
new(IssueIndex),
138+
new(PushMirror),
138139
)
139140

140141
gonicNames := []string{"SSL", "UID"}

models/repo.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,13 @@ type Repository struct {
216216
NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"`
217217
NumOpenProjects int `xorm:"-"`
218218

219-
IsPrivate bool `xorm:"INDEX"`
220-
IsEmpty bool `xorm:"INDEX"`
221-
IsArchived bool `xorm:"INDEX"`
222-
IsMirror bool `xorm:"INDEX"`
223-
*Mirror `xorm:"-"`
224-
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
219+
IsPrivate bool `xorm:"INDEX"`
220+
IsEmpty bool `xorm:"INDEX"`
221+
IsArchived bool `xorm:"INDEX"`
222+
IsMirror bool `xorm:"INDEX"`
223+
*Mirror `xorm:"-"`
224+
PushMirrors []*PushMirror `xorm:"-"`
225+
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
225226

226227
RenderingMetas map[string]string `xorm:"-"`
227228
DocumentRenderingMetas map[string]string `xorm:"-"`
@@ -255,7 +256,12 @@ func (repo *Repository) SanitizedOriginalURL() string {
255256
if repo.OriginalURL == "" {
256257
return ""
257258
}
258-
return util.SanitizeURLCredentials(repo.OriginalURL, false)
259+
u, err := url.Parse(repo.OriginalURL)
260+
if err != nil {
261+
return ""
262+
}
263+
u.User = nil
264+
return u.String()
259265
}
260266

261267
// ColorFormat returns a colored string to represent this repo
@@ -657,6 +663,12 @@ func (repo *Repository) GetMirror() (err error) {
657663
return err
658664
}
659665

666+
// LoadPushMirrors populates the repository push mirrors.
667+
func (repo *Repository) LoadPushMirrors() (err error) {
668+
repo.PushMirrors, err = GetPushMirrorsByRepoID(repo.ID)
669+
return err
670+
}
671+
660672
// GetBaseRepo populates repo.BaseRepo for a fork repository and
661673
// returns an error on failure (NOTE: no error is returned for
662674
// non-fork repositories, and BaseRepo will be left untouched)
@@ -1487,6 +1499,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
14871499
&Notification{RepoID: repoID},
14881500
&ProtectedBranch{RepoID: repoID},
14891501
&PullRequest{BaseRepoID: repoID},
1502+
&PushMirror{RepoID: repoID},
14901503
&Release{RepoID: repoID},
14911504
&RepoIndexerStatus{RepoID: repoID},
14921505
&RepoRedirect{RedirectRepoID: repoID},

models/repo_mirror.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import (
1414
"xorm.io/xorm"
1515
)
1616

17+
// RemoteMirrorer defines base methods for pull/push mirrors.
18+
type RemoteMirrorer interface {
19+
GetRepository() *Repository
20+
GetRemoteName() string
21+
}
22+
1723
// Mirror represents mirror information of a repository.
1824
type Mirror struct {
1925
ID int64 `xorm:"pk autoincr"`
@@ -52,6 +58,16 @@ func (m *Mirror) AfterLoad(session *xorm.Session) {
5258
}
5359
}
5460

61+
// GetRepository returns the repository.
62+
func (m *Mirror) GetRepository() *Repository {
63+
return m.Repo
64+
}
65+
66+
// GetRemoteName returns the name of the remote.
67+
func (m *Mirror) GetRemoteName() string {
68+
return "origin"
69+
}
70+
5571
// ScheduleNextUpdate calculates and sets next update time.
5672
func (m *Mirror) ScheduleNextUpdate() {
5773
if m.Interval != 0 {

0 commit comments

Comments
 (0)