Skip to content

Commit

Permalink
Add push to remote mirror repository (#15157)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
KN4CK3R committed Jun 14, 2021
1 parent 5d113bd commit 440039c
Show file tree
Hide file tree
Showing 39 changed files with 2,458 additions and 875 deletions.
88 changes: 88 additions & 0 deletions docs/content/doc/advanced/repo-mirror.en-us.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
date: "2021-05-13T00:00:00-00:00"
title: "Repository Mirror"
slug: "repo-mirror"
weight: 45
toc: false
draft: false
menu:
sidebar:
parent: "advanced"
name: "Repository Mirror"
weight: 45
identifier: "repo-mirror"
---

# Repository Mirror

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.

**Table of Contents**

{{< toc >}}

## Use cases

The following are some possible use cases for repository mirroring:

- 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.
- 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.

## Pulling from a remote repository

For an existing remote repository, you can set up pull mirroring as follows:

1. Select **New Migration** in the **Create...** menu on the top right.
2. Select the remote repository service.
3. Enter a repository URL.
4. If the repository needs authentication fill in your authentication information.
5. Check the box **This repository will be a mirror**.
5. Select **Migrate repository** to save the configuration.

The repository now gets mirrored periodically from the remote repository. You can force a sync by selecting **Synchronize Now** in the repository settings.

## Pushing to a remote repository

For an existing repository, you can set up push mirroring as follows:

1. In your repository, go to **Settings** > **Repository**, and then the **Mirror Settings** section.
2. Enter a repository URL.
3. If the repository needs authentication expand the **Authorization** section and fill in your authentication information.
4. Select **Add Push Mirror** to save the configuration.

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.

:exclamation::exclamation: **NOTE:** This will force push to the remote repository. This will overwrite any changes in the remote repository! :exclamation::exclamation:

### Setting up a push mirror from Gitea to GitHub

To set up a mirror from Gitea to GitHub, you need to follow these steps:

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.
2. Fill in the **Git Remote Repository URL**: `https://github.com/<your_github_group>/<your_github_project>.git`.
3. Fill in the **Authorization** fields with your GitHub username and the personal access token.
4. Select **Add Push Mirror** to save the configuration.

The repository pushes shortly thereafter. To force a push, select the **Synchronize Now** button.

### Setting up a push mirror from Gitea to GitLab

To set up a mirror from Gitea to GitLab, you need to follow these steps:

1. Create a [GitLab personal access token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) with *write_repository* scope.
2. Fill in the **Git Remote Repository URL**: `https://<destination host>/<your_gitlab_group_or_name>/<your_gitlab_project>.git`.
3. Fill in the **Authorization** fields with `oauth2` as **Username** and your GitLab personal access token as **Password**.
4. Select **Add Push Mirror** to save the configuration.

The repository pushes shortly thereafter. To force a push, select the **Synchronize Now** button.

### Setting up a push mirror from Gitea to Bitbucket

To set up a mirror from Gitea to Bitbucket, you need to follow these steps:

1. Create a [Bitbucket app password](https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/) with the *Repository Write* box checked.
2. Fill in the **Git Remote Repository URL**: `https://bitbucket.org/<your_bitbucket_group_or_name>/<your_bitbucket_project>.git`.
3. Fill in the **Authorization** fields with your Bitbucket username and the app password as **Password**.
4. Select **Add Push Mirror** to save the configuration.

The repository pushes shortly thereafter. To force a push, select the **Synchronize Now** button.
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,24 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package mirror
package integrations

import (
"context"
"path/filepath"
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
migration "code.gitea.io/gitea/modules/migrations/base"
"code.gitea.io/gitea/modules/repository"
mirror_service "code.gitea.io/gitea/services/mirror"
release_service "code.gitea.io/gitea/services/release"

"github.com/stretchr/testify/assert"
)

func TestMain(m *testing.M) {
models.MainTest(m, filepath.Join("..", ".."))
}

func TestRelease_MirrorDelete(t *testing.T) {
assert.NoError(t, models.PrepareTestDatabase())
func TestMirrorPull(t *testing.T) {
defer prepareTestEnv(t)()

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

_, ok := runSync(ctx, mirror.Mirror)
ok := mirror_service.SyncPullMirror(ctx, mirror.ID)
assert.True(t, ok)

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

_, ok = runSync(ctx, mirror.Mirror)
ok = mirror_service.SyncPullMirror(ctx, mirror.ID)
assert.True(t, ok)

count, err = models.GetReleaseCountByRepoID(mirror.ID, findOptions)
Expand Down
86 changes: 86 additions & 0 deletions integrations/mirror_push_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package integrations

import (
"context"
"fmt"
"net/http"
"net/url"
"testing"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
mirror_service "code.gitea.io/gitea/services/mirror"

"github.com/stretchr/testify/assert"
)

func TestMirrorPush(t *testing.T) {
onGiteaRun(t, testMirrorPush)
}

func testMirrorPush(t *testing.T, u *url.URL) {
defer prepareTestEnv(t)()

setting.Migrations.AllowLocalNetworks = true

user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
srcRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)

mirrorRepo, err := repository.CreateRepository(user, user, models.CreateRepoOptions{
Name: "test-push-mirror",
})
assert.NoError(t, err)

ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name)

doCreatePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword)(t)

mirrors, err := models.GetPushMirrorsByRepoID(srcRepo.ID)
assert.NoError(t, err)
assert.Len(t, mirrors, 1)

ok := mirror_service.SyncPushMirror(context.Background(), mirrors[0].ID)
assert.True(t, ok)

srcGitRepo, err := git.OpenRepository(srcRepo.RepoPath())
assert.NoError(t, err)
defer srcGitRepo.Close()

srcCommit, err := srcGitRepo.GetBranchCommit("master")
assert.NoError(t, err)

mirrorGitRepo, err := git.OpenRepository(mirrorRepo.RepoPath())
assert.NoError(t, err)
defer mirrorGitRepo.Close()

mirrorCommit, err := mirrorGitRepo.GetBranchCommit("master")
assert.NoError(t, err)

assert.Equal(t, srcCommit.ID, mirrorCommit.ID)
}

func doCreatePushMirror(ctx APITestContext, address, username, password string) func(t *testing.T) {
return func(t *testing.T) {
csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))

req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{
"_csrf": csrf,
"action": "push-mirror-add",
"push_mirror_address": address,
"push_mirror_username": username,
"push_mirror_password": password,
"push_mirror_interval": "0",
})
ctx.Session.MakeRequest(t, req, http.StatusFound)

flashCookie := ctx.Session.GetCookie("macaron_flash")
assert.NotNil(t, flashCookie)
assert.Contains(t, flashCookie.Value, "success")
}
}
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ var migrations = []Migration{
NewMigration("Always save primary email on email address table", addPrimaryEmail2EmailAddress),
// v182 -> v183
NewMigration("Add issue resource index table", addIssueResourceIndexTable),
// v183 -> v184
NewMigration("Create PushMirror table", createPushMirrorTable),
}

// GetCurrentDBVersion returns the current db version
Expand Down
2 changes: 1 addition & 1 deletion models/migrations/v180.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func removeCredentials(payload string) (string, error) {

opts.AuthPassword = ""
opts.AuthToken = ""
opts.CloneAddr = util.SanitizeURLCredentials(opts.CloneAddr, true)
opts.CloneAddr = util.NewStringURLSanitizer(opts.CloneAddr, true).Replace(opts.CloneAddr)

confBytes, err := json.Marshal(opts)
if err != nil {
Expand Down
39 changes: 39 additions & 0 deletions models/migrations/v183.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
"fmt"
"time"

"code.gitea.io/gitea/modules/timeutil"

"xorm.io/xorm"
)

func createPushMirrorTable(x *xorm.Engine) error {
type PushMirror struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
RemoteName string

Interval time.Duration
CreatedUnix timeutil.TimeStamp `xorm:"created"`
LastUpdateUnix timeutil.TimeStamp `xorm:"INDEX last_update"`
LastError string `xorm:"text"`
}

sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}

if err := sess.Sync2(new(PushMirror)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}

return sess.Commit()
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ func init() {
new(Session),
new(RepoTransfer),
new(IssueIndex),
new(PushMirror),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
27 changes: 20 additions & 7 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,13 @@ type Repository struct {
NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"`
NumOpenProjects int `xorm:"-"`

IsPrivate bool `xorm:"INDEX"`
IsEmpty bool `xorm:"INDEX"`
IsArchived bool `xorm:"INDEX"`
IsMirror bool `xorm:"INDEX"`
*Mirror `xorm:"-"`
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`
IsPrivate bool `xorm:"INDEX"`
IsEmpty bool `xorm:"INDEX"`
IsArchived bool `xorm:"INDEX"`
IsMirror bool `xorm:"INDEX"`
*Mirror `xorm:"-"`
PushMirrors []*PushMirror `xorm:"-"`
Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"`

RenderingMetas map[string]string `xorm:"-"`
DocumentRenderingMetas map[string]string `xorm:"-"`
Expand Down Expand Up @@ -255,7 +256,12 @@ func (repo *Repository) SanitizedOriginalURL() string {
if repo.OriginalURL == "" {
return ""
}
return util.SanitizeURLCredentials(repo.OriginalURL, false)
u, err := url.Parse(repo.OriginalURL)
if err != nil {
return ""
}
u.User = nil
return u.String()
}

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

// LoadPushMirrors populates the repository push mirrors.
func (repo *Repository) LoadPushMirrors() (err error) {
repo.PushMirrors, err = GetPushMirrorsByRepoID(repo.ID)
return err
}

// GetBaseRepo populates repo.BaseRepo for a fork repository and
// returns an error on failure (NOTE: no error is returned for
// non-fork repositories, and BaseRepo will be left untouched)
Expand Down Expand Up @@ -1487,6 +1499,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
&Notification{RepoID: repoID},
&ProtectedBranch{RepoID: repoID},
&PullRequest{BaseRepoID: repoID},
&PushMirror{RepoID: repoID},
&Release{RepoID: repoID},
&RepoIndexerStatus{RepoID: repoID},
&RepoRedirect{RedirectRepoID: repoID},
Expand Down
16 changes: 16 additions & 0 deletions models/repo_mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import (
"xorm.io/xorm"
)

// RemoteMirrorer defines base methods for pull/push mirrors.
type RemoteMirrorer interface {
GetRepository() *Repository
GetRemoteName() string
}

// Mirror represents mirror information of a repository.
type Mirror struct {
ID int64 `xorm:"pk autoincr"`
Expand Down Expand Up @@ -52,6 +58,16 @@ func (m *Mirror) AfterLoad(session *xorm.Session) {
}
}

// GetRepository returns the repository.
func (m *Mirror) GetRepository() *Repository {
return m.Repo
}

// GetRemoteName returns the name of the remote.
func (m *Mirror) GetRemoteName() string {
return "origin"
}

// ScheduleNextUpdate calculates and sets next update time.
func (m *Mirror) ScheduleNextUpdate() {
if m.Interval != 0 {
Expand Down
Loading

0 comments on commit 440039c

Please sign in to comment.